Часть 2: Реализация языковых служб, автозаполнения, синтаксической и семантической проверки и автоматического форматирования.

Здравствуйте и добро пожаловать во вторую часть моей статьи о том, как создать собственный веб-редактор с помощью Typescript, React, ANTLR, и Монако-редактор. Если не читали первую часть, вот ссылка.

В этой статье я покажу вам, как реализовать языковую службу, которая возьмет на себя тяжелую работу по синтаксическому анализу текущего набранного текста в редакторе. Затем мы используем сгенерированное абстрактное синтаксическое дерево (AST), возвращаемое анализатором, для обнаружения любых синтаксических или семантических ошибок, форматирования набранного текста или предложения пользователю определенных TODOS. как только они начнут печатать (автозаполнение, я не собираюсь его реализовывать. Я просто подскажу, как это сделать и какой API).

По сути, этот сервис будет предоставлять три функции:

  • format(code: string): string
  • validate(code: string): Errors[]
  • autoComplete(code: string, currentPosition: Position): string[]

Итак, приступим.

Добавить ANTLER, сгенерировать лексер и синтаксический анализатор из грамматики

Я собираюсь добавить библиотеку ANTLR и скрипт для создания парсера и лексера из нашегоTODOLang.g4 файла грамматики.

Итак, во-первых, добавьте необходимые библиотеки: antlr4ts и antlr4ts-cli.

antlr4ts - это библиотека времени выполнения для ANTLR в typescript, antlr4ts-cli С другой стороны, как следует из названия, это интерфейс командной строки, который мы будем использовать для создания синтаксического анализатора и лексера для языка.

npm add antlr4ts
npm add -D antlr4ts-cli

Теперь добавьте в корневой каталог следующий файл, содержащий TodoLang грамматические правила:

Теперь мы добавляем сценарий в package.json файл для генерации парсера и лексера для нас с помощью antlr-cli:

"antlr4ts": "antlr4ts ./TodoLangGrammar.g4 -o ./src/ANTLR"

Файлы будут созданы в каталоге ./src/ANTLR.

Запустим скрипт antlr4ts и посмотрим на сгенерированные файлы:

npm run antlr4ts

Как видим, есть лексер и парсер. Если вы проверите файл парсера, вы обнаружите, что он экспортировал класс TodoLangGrammarParser; у этого класса есть конструктор constructor(input: TokenStream), который принимает в качестве аргумента TokenStream, которыйTodoLangGrammarLexer генерирует для данного кода.

TodoLangGrammarLexer имеет конструктор constructor(input: CharStream), который принимает код в качестве параметра.

Парсер содержит метод public todoExpressions(): TodoExpressionsContext, который возвращает контекст всех TodoExpressions, определенных в коде. Угадайте, откуда взялось название TodoExpressions? это из названия первого правила в наших правилах грамматики:

todoExpressions : (addExpression)* (completeExpression)*;

TodoExpressionsContext - это корень нашего AST; каждый узел внутри него - это другой контекст для другого правила. Существуют терминалы и контексты узлов, терминалы содержат последний токен (может быть, токен ADD, токен TODO или токен имя задачи).

TodoExpressionsContext содержит addExpressions и completeExpressions, что вытекает из следующих трех правил:

todoExpressions : (addExpression)* (completeExpression)*; addExpression : ADD TODO STRING;
completeExpression : COMPLETE TODO STRING;

С другой стороны, каждый из классов контекста содержит Терминальные узлы, которые в основном содержат текст (части или токены кода, такие как «ДОБАВИТЬ», «ЗАВЕРШЕНО», «Строка, представляющая TODO»). Сложность AST зависит от правил вашей грамматики; в нашем случае это очень просто.

Как мы видим в TodoExpressionsContext, он содержит ADD, TODO и STRING оконечные узлы, которые соответствуют этим правилам:

addExpression : ADD TODO STRING;

Конечный узел строки содержит текст Todo, который мы пытаемся добавить.

Давайте проанализируем простой TodoLang код, чтобы увидеть, как выглядит AST.

В директории ./src/language-service создайте parser.tsфайл со следующим содержанием:

Все, что делает этот файл, - это экспортирует функцию parseAndGetASTRoot(code), которая принимает код TodoLang и генерирует соответствующий AST.

Анализируя следующий TodoLang код:

parseAndGetASTRoot(`
ADD TODO "Create an editor"
COMPLETE TODO "Create an editor"
`)

приведет к этому AST:

Реализация лексической и синтаксической проверки

В этом разделе я расскажу, как добавить проверку синтаксиса в редактор ANTLR, который сразу же генерирует лексические и синтаксические ошибки. Нам просто нужно реализовать ANTLRErrorListner и передать его лексеру и синтаксическому анализатору, чтобы мы могли собирать ошибки, когда ANTLR анализирует код.

Создайте classTodoLangErrorListener, который реализует ANTLRErrorListner в ./src/language-servicedirectory:

Каждый раз, когда ANTLR обнаруживает ошибку во время синтаксического анализа кода, он вызывает этот слушатель, сообщая ему информацию об ошибке.

Мы возвращаем список ошибок, который содержит позицию, в которой ошибка возникает в нашем коде, и сгенерированное сообщение об ошибке.

Давайте теперь добавим слушателя к лексеру и синтаксическому анализатору в parser.ts файле, изменим его содержимое на:

Теперь внутри каталога ./src/language-service создайте файл LanguageService.ts, который экспортирует следующее:

Теперь мы готовы добавить ошибки в редактор. Для этого сначала я собираюсь создать веб-воркера, о котором я говорил в своей предыдущей статье, и добавить наш прокси-сервер службы воркера, который будет использовать языковой сервис для выполнения работы.

Создание веб-воркера:

Сначала давайте создадим TodoLangWorker, который является рабочим, который будет проксироваться monaco. TodoLangWorker будет использовать методы языковой службы для выполнения функций редактора, те методы, которые выполняются в веб-воркер будет проксироваться monaco, поэтому вызов метода внутри веб-воркера - это просто вызов проксированного метода внутри основного потока.

Внутри папки ./src/todo-lang создайте файл TodoLangWorker.ts со следующим содержимым:

Как вы понимаете, мы создаем экземпляр языковой службы и добавляем doValidation() метод, который вызывает проверку в языковой службе. Остальное - просто получить текст документа из редактора. Есть много вещей, которые следует добавить, если вы хотите поддерживать редактирование нескольких файлов.

_ctx: IWorkerContext - это контекст редактора, он содержит модели (открытые файлы…

Теперь давайте создадим файл web worker filetodolang.worker.ts в каталоге ./src/todo-lang со следующим содержанием:

Мы использовали встроенный worker.initialize для инициализации нашего воркера и создания необходимых прокси методов из TodoLangWorker.

Это веб-воркер, поэтому мы должны указать webpack, что нужно объединить собственный файл. Перейдите вправо к файлу конфигурации веб-пакета и добавьте следующее:

Мы назвали наш рабочий файл todoLangWorker.js.

Теперь нам нужно перейти к функции настройки редактора и добавить следующее:

Таким образом monaco получит URL-адрес веб-исполнителя. Обратите внимание, что если меткой воркера является идентификатор TodoLang, мы возвращаем то же имя файла, которое мы использовали для объединения воркера в webpack.

Если вы создадите проект сейчас, вы можете обнаружить, что есть файл с именем todoLangWorker.js (или в dev-tools вы найдете в разделе потоков обоих рабочих).

Теперь давайте создадим WorkerManager, который управляет созданием воркера и помогает нам получить клиента нашего прокси-воркера, чтобы мы могли использовать его позже для выполнения вызовов.

Мы использовалиcreateWebWorker для создания или запуска веб-воркера, если он еще не создан. В противном случае получаем и возвращаем прокси-клиента.

Мы можем использовать workerClientProxy для вызова прокси-методов.

Давайте создадим DiagnosticsAdapter класс, который будет адаптировать ошибки, возвращаемые языковым сервисом, к ошибкам, которые monaco необходимо отмечать в редакторе.

Что я сделал там, так это добавил onDidChangeContent слушателя для каждого изменения, которое вносит пользователь. Мы отклоняем изменения в течение 500 мс, а затем вызываем воркера для проверки кода и добавления маркеров после их адаптации.

API onDidCreateModel вызывается при создании файла (модели), поэтому в этот момент мы добавляем прослушиватель изменений.

setModelMarkers указывает monaco добавить маркеры ошибок или, проще говоря, подчеркивает указанные ошибки.

Чтобы применить эти проверки, обязательно вызовите их в функции настройки и обратите внимание, что мы используем WorkerManager для получения прокси-воркера.

Теперь все должно работать нормально. Запустите проект и начните набирать плохой TodoLang код; вы должны увидеть, что ошибки подчеркнуты.

Вот проект на данный момент:



Реализация семантической проверки

Давайте теперь добавим в наш редактор семантическую проверку. Помните два семантических правила, о которых я упоминал в предыдущей статье.

  • Если TODO определяется с помощью ADD TODO инструкции, мы не можем добавить его повторно.
  • Инструкция COMPLETE не должна применяться в TODO, который не был объявлен с использованием ADD TODO

Чтобы проверить, определено ли TODO, все, что нам нужно сделать, это перебрать AST, чтобы получить каждое выражение ADD и поместить их в список. Затем мы проверяем наличие TODO в заданном списке TODO. Если он существует, это семантическая ошибка, поэтому получите позицию ошибки из контекста выражения ADD и поместите ошибку в массив. То же самое и со вторым правилом.

Теперь вызовите эту функцию и объедините семантические ошибки с синтаксическими ошибками в функции проверки.

Теперь у нас есть редактор, поддерживающий семантическую проверку.



Реализация автоформатирования

Для автоматического форматирования вам необходимо предоставить и зарегистрировать поставщик форматирования для Монако, вызвав API registerDocumentFormattingEditProvider. Подробности смотрите в документации. Вызов и повторение AST предоставят вам всю информацию, необходимую для переписывания кода в красивом формате.

Вот метод форматирования в LanguageService. Он берет код и проверяет, есть ли в нем ошибки, и возвращает отформатированный код:

Теперь давайте добавим поставщика форматирования в monaco и воспользуемся этой услугой. Здесь я добавил метод format к todoLangWorker:

Теперь давайте создадим classTodoLangFomattingProvider, который будет реализовывать интерфейс DocumentFormattingEditProvider.

Все, что он делает, - это получает код и форматирует его с помощью рабочего, а затем предоставляет monaco отформатированный код и диапазон кода, который мы хотим заменить, в нашем случае весь код. Вы можете изменить код для поддержки частичного форматирования.

Теперь перейдите к функции настройки и зарегистрируйте поставщика форматирования с помощью registerDocumentFormattingEditProvider API.

monaco.languages.registerDocumentFormattingEditProvider(languageID, new TodoLangFormattingProvider(worker));

Если вы запустите приложение сейчас, вы увидите, что оно поддерживает форматирование.

Попробуйте нажать Форматировать документ или Shift + Alt + F, вы должны получить следующий результат:



Реализация автозаполнения

Чтобы автоматическое завершение поддерживало определенные TODO, все, что вам нужно сделать, это получить все определенные TODO из AST и предоставить их в поставщике завершения, вызвав registerCompletionItemProvider в настройке. Провайдер предоставляет вам код и текущую позицию курсора, поэтому вы можете проверить контекст, в котором пользователь вводит текст, если он вводит TODO в полном выражении, тогда вы можете предложить заранее определенные TO DO. Имейте в виду, что по умолчанию редактор Monaco поддерживает автозаполнение для предварительно определенных токенов в вашем коде, вы можете отключить это и реализовать свой собственный, чтобы сделать его более интеллектуальным и контекстным.

Вот проект:



Не стесняйтесь обращаться ко мне, если у вас есть какие-либо вопросы, предложения или отзывы.

Большое спасибо за уделенное время.