譯文來源
歡迎閱讀如何使用 TypeScript, React, ANTLR4, Monaco Editor 建立一個自定義 Web 編輯器系列的第二章節, 在這之前建議您閱讀使用 TypeScript, React, ANTLR4, Monaco Editor 建立一個自定義 Web 編輯器(一)
在本文中, 我将介紹如何實作語言服務, 語言服務在編輯器中主要用來解析鍵入文本的繁重工作, 我們将使用通過Parser生成的抽象文法樹(AST)來查找文法或詞法錯誤, 格式文本, 針對使用者鍵入文本對TODOS文法做隻能提示(本文中我不會實作文法自動完成), 基本上, 語言服務暴露如下函數:
<code>format(code: string): string</code>
<code>validate(code: string): Errors[]</code>
<code>autoComplete(code: string, currentPosition: Position): string[]</code>
我将引入ANTLR庫并增加一個根據<code>TODOLang.g4</code> 文法檔案生Parser和Lexer的腳本, 首先引入兩個必須的庫:antlr4ts 和antlr4ts-cli, antlr4 Typescript 目标生成的解析器對antlr4ts包有運作時依賴, 另一方面, 顧名思義antlr4ts-cli 就是CLI我們将使用它生成該語言的Parser和Lexer
在根路徑建立包含<code>TodoLang</code>文法規則的檔案<code>TodoLangGrammar.g4</code>
現在我們在<code>package.json</code>檔案裡增加通過antlr-cli生成Parser和Lexer的腳本
讓我們執行一下antlr4ts腳本,就可以在<code>./src/ANTLR</code>目錄看到生成的解析器的typescript源碼了

正如我們看到的那樣, 這裡有一個Lexer 和 Parser, 如果你檢視Parser檔案, 你會發現它導出 <code>TodoLangGrammarParser</code>類, 該類有個構造函數<code>constructor(input: TokenStream)</code>, 該構造函數将<code>TodoLangGrammarLexer</code>為給定代碼生成的<code>TokenStream</code>作為參數, <code>TodoLangGrammarLexer</code> 有一個以代碼作為入參的構造函數 <code>constructor(input: CharStream)</code>
Parser檔案包含了<code>public todoExpressions(): TodoExpressionsContext</code>方法,該方法會傳回代碼中定義的所有<code>TodoExpressions</code>的上下文對象, 猜想一下<code>TodoExpressions</code>在哪裡可以追蹤到,其實它是源于我們文法規則檔案的第一行文法規則:
<code>TodoExpressionsContext</code>是<code>AST</code>的根基, 其中的每個節點都是另一個規則的另一個上下文, 它包含了終端和節點上下文,終端擁有最終令牌(ADD 令牌, TODO 令牌, todo 事項名稱的令牌)
<code>TodoExpressionsContext</code>包含了<code>addExpressions</code>和<code>completeExpressions</code>表達式清單, 來源于以下三條規則
另一方面, 每個上下文類都包含了終端節點, 它基本包含以下文本(代碼段或者令牌, 例如:ADD, COMPLETE, 代表 TODO 的字元串), AST的複雜度取決于你編寫的文法規則
讓我們來看看TodoExpressionsContext, 它包含了<code>ADD</code>, <code>TODO</code> 和<code>STRING</code>終端節點, 對應的規則如:
<code>STRING</code>終端節點儲存了我們要加的<code>Todo</code>文本内容, 先來解析一個簡單的<code>TodoLang</code>代碼以來了解AST如何工作的,在<code>./src/language-service</code>目錄建一個包含以下内容的檔案<code>parser.ts</code>
<code>parser.ts</code>檔案導出了<code>parseAndGetASTRoot(code)</code>方法, 它接受<code>TodoLang</code>代碼并且生成相應的AST, 解析以下<code>TodoLang</code>代碼:
在本節中, 我将引導您逐漸了解如何向編輯器添加文法驗證, ANTLR開箱即用為我們生成詞彙和文法錯誤, 我們隻需要實作<code>ANTLRErrorListner</code>類并将其提供給Lexer和Parser, 這樣我們就可以在 ANTLR解析代碼時收集錯誤
在<code>./src/language-service</code>目錄下建立<code>TodoLangErrorListener.ts</code>檔案, 檔案導出實作<code>ANTLRErrorListner</code>接口的<code>TodoLangErrorListener</code>類
每次 ANTLR 在代碼解析期間遇到錯誤時, 它将調用此<code>TodoLangErrorListener</code>, 以向其提供有關錯誤的資訊, 該監聽器會傳回包含解析發生錯誤的代碼位置極錯誤資訊, 現在我們嘗試把<code>TodoLangErrorListener</code>綁定到<code>parser.ts</code>的檔案的Lexer和Parser裡, eg:
在<code>./src/language-service</code>目錄下建立<code>LanguageService.ts</code>, 以下是它導出的内容
不錯, 我們實作了編輯器錯誤解析, 為此我将要建立上篇文章讨論過的<code>web worker</code>, 并且添加<code>worker</code>服務代理, 該代理将調用語言服務區完成編輯器的進階功能
首先, 我們調用 monaco.editor.createWebWorker 來使用内置的 ES6 Proxies 建立代理<code>TodoLangWorker</code>, <code>TodoLangWorker</code>将使用語言服務來執行編輯器功能,在<code>web worker</code>中執行的那些方法将由monaco代理,是以在<code>web worker</code>中調用方法僅是在主線程中調用被代理的方法。
在<code>./src/todo-lang</code>檔案夾下建立<code>TodoLangWorker.ts</code>包含以下内容:
我們建立了<code>language service</code>執行個體 并且添加了<code>doValidation</code>方法, 進一步它會調用<code>language service</code>的<code>validate</code>方法, 還添加了<code>getTextDocument</code>方法, 該方法用來擷取編輯器的文本值, <code>TodoLangWorker</code>類還可以擴充很多功能如果你想要支援多檔案編輯等, <code>_ctx: IWorkerContext</code> 是編輯器的上下文對象, 它儲存了檔案的 model 資訊
現在讓我們在<code>./src/todo-lang</code>目錄下建立 web worker 檔案<code>todolang.worker.ts</code>
我們使用内置的<code>worker.initialize</code>初始化我們的 worker,并使用<code>TodoLangWorker</code>進行必要的方法代理
那是一個<code>web worker</code>, 是以我們必須讓<code>webpack</code>輸出對應的<code>worker</code>檔案
我們命名<code>worker</code>檔案為<code>todoLangWorker.js</code>檔案, 現在我們在編輯器啟動函數裡面增加<code>getWorkUrl</code>
這是 monaco 如何擷取<code>web worker</code>的 URL 的方法, 請注意, 如果<code>worker</code>的 label 是<code>TodoLang</code>的 ID, 我們将傳回用于在 Webpack 中打包輸出的同名<code>worker,</code> 如果現在建構項目, 則可能會發現有一個名為<code>todoLangWorker.js</code>的檔案(或者在 dev-tools 中, 您将線上程部分中找到兩個<code>worker</code>)
現在建立一個用來管理<code>worker</code>建立和擷取代理<code>worker</code>用戶端的 <code>WorkerManager</code>
我們使用<code>createWebWorker</code>建立monaco代理的<code>web worker</code>, 其次我們擷取傳回了代理的用戶端對象, 我們使用<code>workerClientProxy</code>調用代理的一些方法, 讓我們建立<code>DiagnosticsAdapter</code>類, 該類用來連接配接 Monaco 标記 Api 和語言服務傳回的 error,為了讓解析的錯誤正确的标記在monaco上
<code>onDidChangeContent</code>監聽器監聽<code>model</code>資訊, 如果<code>model</code>資訊變更, 我們将每隔 500ms 調用<code>webworker</code>去驗證代碼并且增加錯誤标記;<code>setModelMarkers</code>通知monaco增加錯誤标記, 為了使得編輯器文法驗證功能完成,請確定在<code>setup</code>函數中調用它們,并注意我們正在使用WorkerManager來擷取代理<code>worker</code>
現在一切準備就緒, 運作項目并且輸入錯誤的<code>TodoLang</code>代碼, 你會發現錯誤被标記在代碼下面
現在往編輯器增加語義校驗, 記得我在上篇文章提到的兩個語義規則
如果使用 ADD TODO 說明定義了 TODO ,我們可以重新添加它。
在 TODO 中應用中,COMPLETE 指令不應在尚未使用聲明 ADD TODO 前
要檢查是否定義了 TODO,我們要做的就是周遊 AST 以擷取每個 ADD 表達式并将其推入<code>definedTodos</code> .然後我們在<code>definedTodos</code>中檢查 TODO 的存在. 如果存在, 則是語義錯誤, 是以請從 ADD 表達式的上下文中擷取錯誤的位置, 然後将錯誤推送到數組中, 第二條規則也是如此
現在調用<code>checkSemanticRules</code>函數, 在<code>language service</code>的<code>validate</code>方法中将語義和文法錯誤合并傳回, 現在我們編輯器已經支援語義校驗
對于編輯器的自動格式化功能, 您需要通過調用<code>Monaco API registerDocumentFormattingEditProvider</code>提供并注冊 Monaco 的格式化提供程式. 檢視 monaco-editor 文檔以擷取更多詳細資訊. 調用并周遊 AST 将為你展示美化後的代碼
在<code>todoLangWorker</code>中添加<code>format</code>方法, 該<code>format</code>方法會使用<code>language service</code>的<code>format</code>方法
現在建立<code>TodoLangFomattingProvider</code>類去實作``DocumentFormattingEditProvider`接口
<code>TodoLangFormattingProvider</code>通過調用<code>worker</code>提供的<code>format</code>方法, 并借助<code>editor.getValue()</code>作為入參, 并且向monaco提供各式後的代碼及想要替換的代碼範圍, 現在進入<code>setup</code>函數并且使用<code>Monaco registerDocumentFormattingEditProvider</code> API注冊<code>formatting provider</code>, 重跑應用, 你能看到編輯器已支援自動格式化了
嘗試點選Format document 或Shift + Alt + F, 你能看到如圖的效果:
若要使自動完成支援定義的 TODO, 您要做的就是從 AST 擷取所有定義的 TODO, 并提供<code>completion provider</code>通過在<code>setup</code>中調用<code>registerCompletionItemProvider</code>。<code>completion provider</code>為您提供代碼和光标的目前位置,是以您可以檢查使用者正在鍵入的上下文,如果他們在完整的表達式中鍵入 TODO,則可以建議預定義的 TO DOs。 請記住,預設情況下,Monaco-editor 支援對代碼中的預定義标記進行自動補全,您可能需要禁用該功能并實作自己的标記以使其更加智能化和上下文化