本篇文章給大家深入解析vscode代碼高亮原理。有一定的參考價值,有需要的朋友可以參考一下,希望對大家有所幫助。
全文5000字,解讀 vscode 背后的代碼高亮實現原理,歡迎點贊關注轉發。
Vscode 的代碼高亮、代碼補齊、錯誤診斷、跳轉定義等語言功能由兩種擴展方案協同實現,包括:
【推薦學習:《vscode》】
兩種方案的功能范疇逐級遞增,相應地技術復雜度與實現成本也逐級升高,本文將概要介紹兩種方案的工作過程與特點,各自完成什么工作,互相這么寫作,并結合實際案例一步步揭開 vscode 代碼高亮功能的實現原理:
Vscode 插件基礎
介紹 vscode 代碼高亮原理之前,有必要先熟悉一下 vscode 的底層架構。與 webpack 相似,vscode 本身只是實現了一套架子,架子內部的命令、樣式、狀態、調試等功能都以插件形式提供,vscode 對外提供了五種拓展能力:
其中,代碼高亮功能由 語言擴展 類插件實現,根據實現方式又可以細分為:
- 聲明式 :以特定 JSON 結構聲明一堆匹配詞法的正則,無需編寫邏輯代碼即可添加如塊級匹配、自動縮進、語法高亮等語言特性,vscode 內置的 extendsions/css、extendsions/html 等插件都是基于聲明式接口實現的
- 編程式 :vscode 運行過程中會監聽用戶行為,在特定行為發生后觸發事件回調,編程式語言擴展需要監聽這些事件,動態分析文本內容并按特定格式返回代碼信息
聲明式性能高,能力弱;編程式性能低,能力強。語言插件開發者通常可以混用,用聲明式接口在最短時間內識別出詞法 token,提供基本的語法高亮功能;之后用編程式接口動態分析內容,提供更高級特性比如錯誤診斷、智能提示等。
Vscode 中的聲明式語言擴展基于 TextMate 詞法分析引擎實現;編程式語言擴展則基于語義分析接口、vscode.language.* 接口、Language Server Protocol 協議三種方式實現,下面展開介紹每種技術方案的基本邏輯。
詞法高亮
詞法分析(Lexical Analysis) 是計算機學科中將字符序列轉換為 標記(token) 序列的過程,而 標記(token) 是構成源代碼的最小單位,詞法分析技術在編譯、ide等領域有非常廣泛的應用。
比如 vscode 的詞法引擎分析出 token 序列后再根據 token 的類型應用高亮樣式,這個過程可以簡單劃分為分詞、樣式應用兩個步驟。
參考資料:
- https://macromates.com/manual/en/language%5C_grammars
https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide
分詞
分詞過程本質上將一長串代碼遞歸地拆解為具有特定含義、分類的字符串片段,比如 +-*/% 等操作符;var/const 等關鍵字;1234 或 “tecvan” 類型的常量值等,簡單說就是從一段文本中識別出,什么地方有一個什么詞。
Vscode 的詞法分析基于 vscode 引擎實現,功能比較復雜,可以簡單劃分為三個方面:基于正則的分詞、復合分詞規則、嵌套分詞規則。
基本規則
Vscode 底層的 vscode 引擎基于 vscode 匹配實現分詞功能,運行時逐行掃描文本內容,用預定義的 vscode 集合測試文本行中是否包含匹配特定正則的內容,例如對于下面的規則配置:
{ ????"patterns":?[ ????????{ ????????????"name":?"keyword.control", ????????????"match":?"(if|while|for|return)" ????????} ????] }
示例中,patterns 用于定義規則集合, match 屬性定于用于匹配 token 的正則,name 屬性聲明該 token 的分類(scope),TextMate 分詞過程遇到匹配 match 正則的內容時,會將其看作單獨 token 處理并分類為 name 聲明的 keyword.control 類型。
上述示例會將 if/while/for/return 關鍵詞識別為 keyword.control 類型,但無法識別其它關鍵字:
在 TextMate 語境中,scope 是一種 . 分割的層級結構,例如 keyword 與 keyword.control 形成父子層級,這種層級結構在樣式處理邏輯中能實現一種類似 css 選擇器的匹配,后面會講到細節。
復合分詞
上述示例配置對象在 TextMate 語境下被稱作 Language Rule,除了 match 用于匹配單行內容,還可以使用 begin + end 屬性對匹配更復雜的跨行場景。從 begin 到 end 所識別到的范圍內,都認為是 name 類型的 token,比如在 vscode 插件的 syntaxes/vue.tmLanguage.json 文件中有這么一段配置:
{ ????"name":?"Vue", ????"scopeName":?"source.vue", ????"patterns":?[ ????????{ ??????????"begin":?"(]*/>s*$)", ??????????//?虛構字段,方便解釋 ??????????"name":?"tag.style.vue", ??????????"beginCaptures":?{ ????????????"1":?{ ??????????????"name":?"punctuation.definition.tag.begin.html" ????????????}, ????????????"2":?{ ??????????????"name":?"entity.name.tag.style.html" ????????????} ??????????}, ??????????"end":?"()(style)(>)", ??????????"endCaptures":?{ ????????????"1":?{ ??????????????"name":?"punctuation.definition.tag.begin.html" ????????????}, ????????????"2":?{ ??????????????"name":?"entity.name.tag.style.html" ????????????}, ????????????"3":?{ ??????????????"name":?"punctuation.definition.tag.end.html" ????????????} ??????????} ????????} ????] }
配置中,begin 用于匹配
語句,且
整個語句被賦予 scope 為 tag.style.vue 。此外,語句中字符被 beginCaptures 、endCaptures 屬性分配成不同的 scope 類型:
這里從 begin 到 beginCaptures ,從 end 到 endCaptures 形成了某種程度的復合結構,從而實現一次匹配多行內容。
規則嵌套
在上述 begin + end 基礎上,TextMate 還支持以子 patterns 方式定義嵌套的語言規則,例如:
{ ????"name":?"lng", ????"patterns":?[ ????????{ ????????????"begin":?"^lng`", ????????????"end":?"`", ????????????"name":?"tecvan.lng.outline", ????????????"patterns":?[ ????????????????{ ????????????????????"match":?"tec", ????????????????????"name":?"tecvan.lng.prefix" ????????????????}, ????????????????{ ????????????????????"match":?"van", ????????????????????"name":?"tecvan.lng.name" ????????????????} ????????????] ????????} ????], ????"scopeName":?"tecvan" }
配置識別 lng` 到 ` 之間的字符串,并分類為 tecvan.lng.outline 。之后,遞歸處理兩者之間的內容并按照子 patterns 規則匹配出更具體的 token ,例如對于:
lng`awesome?tecvan
可識別出分詞:
- lng`awesome tecvan` ,scope 為 tecvan.lng.outline
- tec ,scope 為 tecvan.lng.prefix
- van ,scope 為 tecvan.lng.name
TextMate 還支持語言級別的嵌套,例如:
{ ????"name":?"lng", ????"patterns":?[ ????????{ ????????????"begin":?"^lng`", ????????????"end":?"`", ????????????"name":?"tecvan.lng.outline", ????????????"contentName":?"source.js" ????????} ????], ????"scopeName":?"tecvan" }
基于上述配置, lng` 到 ` 之間的內容都會識別為 contentName 指定的 source.js 語句。
樣式
詞法高亮本質上就是先按上述規則將原始文本拆解成多個具類的 token 序列,之后按照 token 的類型適配不同的樣式。TextMate 在分詞基礎上提供了一套按照 token 類型字段 scope 配置樣式的功能結構,例如:
{ ????"tokenColors":?[ ????????{ ????????????"scope":?"tecvan", ????????????"settings":?{ ????????????????"foreground":?"#eee" ????????????} ????????}, ????????{ ????????????"scope":?"tecvan.lng.prefix", ????????????"settings":?{ ????????????????"foreground":?"#F44747" ????????????} ????????}, ????????{ ????????????"scope":?"tecvan.lng.name", ????????????"settings":?{ ????????????????"foreground":?"#007acc", ????????????} ????????} ????] }
示例中,scope 屬性支持一種被稱作 Scope Selectors 的匹配模式,這種模式與 css 選擇器類似,支持:
- 元素選擇,例如 scope = tecvan.lng.prefix 能夠匹配 tecvan.lng.prefix 類型的token;特別的 scope = tecvan 能夠匹配 tecvan.lng 、tecvan.lng.prefix 等子類型的 token
- 后代選擇,例如 scope = text.html source.js 用于匹配 html 文檔中的 JavaScript 代碼
- 分組選擇,例如 scope = String, comment 用于匹配字符串或備注
插件開發者可以自定義 scope 也可以選擇復用 TextMate 內置的許多 scope ,包括 comment、constant、entity、invalid、keyword 等,完整列表請查閱 vscode。
settings 屬性則用于設置該 token 的表現樣式,支持foreground、background、bold、italic、underline 等樣式屬性。
實例解析
看完原理我們來拆解一個實際案例: vscode ,vscode 是 JSON 擴展協議,旨在使人類更易于手動編寫和維護,支持備注、單引號、十六進制數字等特性,這些拓展特性需要使用 vscode-json5 插件實現高亮效果:
上圖中,左邊是沒有啟動 vscode-json5 的效果,右邊是啟動后的效果。
vscode-json5 插件源碼很簡單,兩個關鍵點:
- 在 package.json 文件中聲明插件的 contributes 屬性,可以理解為插件的入口:
??"contributes":?{ ????//?語言配置 ????"languages":?[{ ??????"id":?"json5", ??????"aliases":?["JSON5",?"json5"], ??????"extensions":?[".json5"], ??????"configuration":?"./json5.configuration.json" ????}], ????//?語法配置 ????"grammars":?[{ ??????"language":?"json5", ??????"scopeName":?"source.json5", ??????"path":?"./syntaxes/json5.json" ????}] ??}
- 在語法配置文件 ./syntaxes/json5.json 中按照 TextMate 的要求定義 Language Rule:
{ ????"scopeName":?"source.json5", ????"fileTypes":?["json5"], ????"name":?"JSON5", ????"patterns":?[ ????????{?"include":?"#array"?}, ????????{?"include":?"#constant"?} ????????//?... ????], ????"repository":?{ ????????"array":?{ ????????????"begin":?"[", ????????????"beginCaptures":?{ ????????????????"0":?{?"name":?"punctuation.definition.array.begin.json5"?} ????????????}, ????????????"end":?"]", ????????????"endCaptures":?{ ????????????????"0":?{?"name":?"punctuation.definition.array.end.json5"?} ????????????}, ????????????"name":?"meta.structure.array.json5" ????????????//?... ????????}, ????????"constant":?{ ????????????"match":?"b(?:true|false|null|Infinity|NaN)b", ????????????"name":?"constant.language.json5" ????????}? ????????//?... ????} }
OK,結束了,沒了,就是這么簡單,之后 vscode 就可以根據這份配置適配 json5 的語法高亮規則。
調試工具
Vscode 內置了一套 scope inspect 工具,用于調試 TextMate 檢測出的 token、scope 信息,使用時只需要將編輯器光標 focus 到特定 token 上,快捷鍵 ctrl + shift + p 打開 vscode 命令面板后輸出 Developer: Inspect Editor Tokens and Scopes 命令并回車:
命令運行后就可以看到分詞 token 的語言、scope、樣式等信息。
編程式語言擴展
詞法分析引擎 TextMate 本質上是一種基于正則的靜態詞法分析器,優點是接入方式標準化,成本低且運行效率較高,缺點是靜態代碼分析很難實現某些上下文相關的 IDE 功能,例如對于下面的代碼:
注意代碼第一行函數參數 languageModes 與第二行函數體內的 languageModes 是同一實體但是沒有實現相同的樣式,視覺上沒有形成聯動。
為此,vscode 在 TextMate 引擎之外提供了三種更強大也更復雜的語言特性擴展機制:
- 使用 DocumentSemanticTokensProvider 實現可編程的語義分析
- 使用 vscode.languages.* 下的接口監聽各類編程行為事件,在特定時間節點實現語義分析
- 根據 vscode 協議實現一套完備的語言特性分析服務器
相比于上面介紹的聲明式的詞法高亮,語言特性接口更靈活,能夠實現諸如錯誤診斷、候選詞、智能提示、定義跳轉等高級功能。
參考資料:
- https://code.visualstudio.com/api/language-extensions/semantic-highlight-guide
- https://code.visualstudio.com/api/language-extensions/programmatic-language-features
- https://code.visualstudio.com/api/language-extensions/language-server-extension-guide
DocumentSemanticTokensProvider 分詞
簡介
Sematic Tokens Provider 是 vscode 內置的一種對象協議,它需要自行掃描代碼文件內容,然后以整數數組形式返回語義 token 序列,告訴 vscode 在文件的哪一行、那一列、多長的區間內是一個什么類型的 token。
注意區分一下,TextMate 中的掃描是引擎驅動的,逐行匹配正則,而 Sematic Tokens Provider 場景下掃描規則、匹配規則都交由插件開發者自行實現,靈活性增強但相對的開發成本也會更高。
實現上,Sematic Tokens Provider 以 vscode.DocumentSemanticTokensProvider 接口定義,開發者可以按需實現兩個方法:
- provideDocumentSemanticTokens :全量分析代碼文件語義
- provideDocumentSemanticTokensEdits :增量分析正在編輯模塊的語義
我們來看個完整的示例:
import?*?as?vscode?from?'vscode'; const?tokenTypes?=?['class',?'interface',?'enum',?'function',?'variable']; const?tokenModifiers?=?['declaration',?'documentation']; const?legend?=?new?vscode.SemanticTokensLegend(tokenTypes,?tokenModifiers); const?provider:?vscode.DocumentSemanticTokensProvider?=?{ ??provideDocumentSemanticTokens( ????document:?vscode.TextDocument ??):?vscode.ProviderResult<vscode.semantictokens>?{ ????const?tokensBuilder?=?new?vscode.SemanticTokensBuilder(legend); ????tokensBuilder.push(?????? ??????new?vscode.Range(new?vscode.Position(0,?3),?new?vscode.Position(0,?8)), ??????tokenTypes[0], ??????[tokenModifiers[0]] ????); ????return?tokensBuilder.build(); ??} }; const?selector?=?{?language:?'javascript',?scheme:?'file'?}; vscode.languages.registerDocumentSemanticTokensProvider(selector,?provider,?legend);</vscode.semantictokens>
相信大多數讀者對這段代碼都會覺得陌生,我想了很久,覺得還是從函數輸出的角度開始講起比較容易理解,也就是上例代碼第 17 行 tokensBuilder.build()。
輸出結構
provideDocumentSemanticTokens 函數要求返回一個整數數組,數組項按 5 位為一組分別表示:
- 第 5 * i 位,token 所在行相對于上一個 token 的偏移
- 第 5 * i + 1 位,token 所在列相對于上一個 token 的偏移
- 第 5 * i + 2 位,token 長度
- 第 5 * i + 3 位,token 的 type 值
- 第 5 * i + 4 位,token 的 modifier 值
我們需要理解這是一個位置強相關的整數數組,數組中每 5 個項描述一個 token 的位置、類型。token 位置由所在行、列、長度三個數字組成,而為了壓縮數據的大小 vscode 有意設計成相對位移的形式,例如對于這樣的代碼:
const?name?as
假如只是簡單地按空格分割,那么這里可以解析出三個 token:const 、 name 、as ,對應的描述數組為:
[ //?對應第一個?token:const 0,?0,?5,?x,?x, //?對應第二個?token:?name 0,?6,?4,?x,?x, //?第三個?token:as 0,?5,?2,?x,?x ]
注意這里是以相對前一個 token 位置的形式描述的,比如 as 字符對應的 5 個數字的語義為:相對前一個 token 偏移 0 行、5 列,長度為 2 ,類型為 xx。
剩下的第 5 * i + 3 位與第 5 * i + 4 位分別描述 token 的 type 與 modifier,其中 type 指示 token 的類型,例如 comment、class、function、Namespace 等等;modifier 是類型基礎上的修飾器,可以近似理解為子類型,比如對于 class 有可能是 abstract 的,也有可能是從標準庫導出 defaultLibrary。
type、modifier 的具體數值需要開發者自行定義,例如上例中:
const?tokenTypes?=?['class',?'interface',?'enum',?'function',?'variable']; const?tokenModifiers?=?['declaration',?'documentation']; const?legend?=?new?vscode.SemanticTokensLegend(tokenTypes,?tokenModifiers); //?... vscode.languages.registerDocumentSemanticTokensProvider(selector,?provider,?legend);
首先通過 vscode. SemanticTokensLegend 類構建 type、modifier 的內部表示 legend 對象,之后使用 vscode.languages.registerDocumentSemanticTokensProvider 接口與 provider 一起注冊到 vscode 中。
語義分析
上例中 provider 的主要作用就是遍歷分析文件內容,返回符合上述規則的整數數組,vscode 對具體的分析方法并沒有做限定,只是提供了用于構建 token 描述數組的工具 SemanticTokensBuilder,例如上例中:
const?provider:?vscode.DocumentSemanticTokensProvider?=?{ ??provideDocumentSemanticTokens( ????document:?vscode.TextDocument ??):?vscode.ProviderResult<vscode.semantictokens>?{ ????const?tokensBuilder?=?new?vscode.SemanticTokensBuilder(legend); ????tokensBuilder.push(?????? ??????new?vscode.Range(new?vscode.Position(0,?3),?new?vscode.Position(0,?8)), ??????tokenTypes[0], ??????[tokenModifiers[0]] ????); ????return?tokensBuilder.build(); ??} };</vscode.semantictokens>
代碼使用 SemanticTokensBuilder 接口構建并返回了一個 [0, 3, 5, 0, 0] 的數組,即第 0 行,第 3 列,長度為 5 的字符串,type =0,modifier = 0,運行效果:
除了這一段被識別出的 token 外,其它字符都被認為不可識別。
小結
本質上,DocumentSemanticTokensProvider 只是提供了一套粗糙的 IOC 接口,開發者能做的事情比較有限,所以現在大多數插件都沒有采用這種方案,讀者理解即可,不必深究。
Language API
簡介
相對而言,vscode.languages.* 系列 API 所提供的語言擴展能力可能更符合前端開發者的思維習慣。vscode.languages.* 托管了一系列用戶交互行為的處理、歸類邏輯,并以事件接口方式開放出來,插件開發者只需監聽這些事件,根據參數推斷語言特性,并按規則返回結果即可。
Vscode Language API 提供了很多事件接口,比如說:
- registerCompletionItemProvider: 提供代碼補齊提示
- registerHoverProvider:光標停留在 token 上時觸發
- registerSignatureHelpProvider:提供函數簽名提示
完整的列表請查閱 https://code.visualstudio.com/api/language-extensions/programmatic-language-features#show-hovers 一文。
Hover 示例
Hover 功能實現分兩步,首先需要在 package.json 中聲明 hover 特性:
{ ????... ????"main":?"out/extensions.js", ????"capabilities"?:?{ ????????"hoverProvider"?:?"true", ????????... ????} }
之后,需要在 activate 函數中調用 registerHoverProvider 注冊 hover 回調:
export?function?activate(ctx:?vscode.ExtensionContext):?void?{ ????... ????vscode.languages.registerHoverProvider('language?name',?{ ????????provideHover(document,?position,?token)?{ ????????????return?{?contents:?['aweome?tecvan']?}; ????????} ????}); ????... }
運行結果:
其它特性功能的寫法與此相似,感興趣的同學建議到官網自行查閱。
Language Server Protocol
簡介
上述基于語言擴展插件的代碼高亮方法有一個相似的問題:難以在編輯器間復用,同一個語言,需要根據編輯器環境、語言重復編寫功能相似的支持插件,那么對于 n 種語言,m 中編輯器,這里面的開發成本就是 n * m。
為了解決這個問題,微軟提出了一種叫做 Language Server Protocol 的標準協議,語言功能插件與編輯器之間不再直接通訊,而是通過 LSP 做一層隔離:
增加 LSP 層帶來兩個好處:
- LSP 層的開發語言、環境等與具體 IDE 所提供的 host 環境脫耦
- 語言插件的核心功能只需要編寫一次,就可以復用到支持 LSP 協議的 IDE 中
雖然 LSP 與上述 Language API 能力上幾乎相同,但借助這兩個優點大大提升了插件的開發效率,目前很多 vscode 語言類插件都已經遷移到 LSP 實現,包括 vetur、eslint、python for VSCode 等知名插件。
Vscode 中的 LSP 架構包含兩部分:
- Language Client: 一個標準 vscode 插件,實現與 vscode 環境的交互,例如 hover 事件首先會傳遞到 client,再由 client 傳遞到背后的 server
- Language Server: 語言特性的核心實現,通過 LSP 協議與 Language Client 通訊,注意 Server 實例會以單獨進程方式運行
做個類比,LSP 就是經過架構優化的 Language API,原來由單個 provider 函數實現的功能拆解為 Client + Server 兩端跨語言架構,Client 與 vscode 交互并實現請求轉發;Server 執行代碼分析動作,并提供高亮、補全、提示等功能,如下圖:
簡單示例
LSP 稍微有一點點復雜,建議讀者先拉下 vscode 官方示例對比學習:
git?clone?https://github.com/microsoft/vscode-extension-samples.git cd?vscode-extension-samples/lsp-sample yarn yarn?compile code?.
vscode-extension-samples/lsp-sample 的主要代碼文件有:
. ├──?client?//?Language?Client │???├──?src │???│???└──?extension.ts?//?Language?Client?入口文件 ├──?package.json? └──?server?//?Language?Server ????└──?src ????????└──?server.ts?//?Language?Server?入口文件
樣例代碼中有幾個關鍵點:
-
在 package.json 中聲明激活條件與插件入口
-
編寫入口文件 client/src/extension.ts,啟動 LSP 服務
-
編寫 LSP 服務即 server/src/server.ts ,實現 LSP 協議
邏輯上,vscode 會在加載插件時根據 package.json 的配置判斷激活條件,之后加載、運行插件入口,啟動 LSP 服務器。插件啟動后,后續用戶在 vscode 的交互行為會以標準事件,如 hover、completion、signature help 等方式觸發插件的 client ,client 再按照 LSP 協議轉發到 server 層。
下面我們拆開看看三個模塊的細節。
入口配置
示例 vscode-extension-samples/lsp-sample 中的 package.json 有兩個關鍵配置:
{ ????"activationEvents":?[ ????????"onLanguage:plaintext" ????], ????"main":?"./client/out/extension", }
其中:
- activationEvents: 聲明插件的激活條件,代碼中的 onLanguage:plaintext 意為打開 txt 文本文件時激活
- main: 插件的入口文件
Client 樣例
示例 vscode-extension-samples/lsp-sample 中的 Client 入口代碼,關鍵部分如下:
export?function?activate(context:?ExtensionContext)?{ ????//?Server?配置信息 ????const?serverOptions:?ServerOptions?=?{ ????????run:?{? ????????????//?Server?模塊的入口文件 ????????????module:?context.asAbsolutePath( ????????????????path.join('server',?'out',?'server.js') ????????????),? ????????????//?通訊協議,支持?stdio、ipc、pipe、socket ????????????transport:?TransportKind.ipc? ????????}, ????}; ????//?Client?配置 ????const?clientOptions:?LanguageClientOptions?=?{ ????????//?與?packages.json?文件的?activationEvents?類似 ????????//?插件的激活條件 ????????documentSelector:?[{?scheme:?'file',?language:?'plaintext'?}], ????????//?... ????}; ????//?使用?Server、Client?配置創建代理對象 ????const?client?=?new?LanguageClient( ????????'languageServerExample', ????????'Language?Server?Example', ????????serverOptions, ????????clientOptions ????); ????client.start(); }
代碼脈絡很清晰,先是定義 Server、Client 配置對象,之后創建并啟動了 LanguageClient 實例。從實例可以看到,Client 這一層可以做的很薄,在 Node 環境下大部分轉發邏輯都被封裝在 LanguageClient 類中,開發者無需關心細節。
Server 樣例
示例 vscode-extension-samples/lsp-sample 中的 Server 代碼實現了錯誤診斷、代碼補全功能,作為學習樣例來說稍顯復雜,所以我只摘抄出錯誤診斷部分的代碼:
//?Server?層所有通訊都使用?createConnection?創建的?connection?對象實現 const?connection?=?createConnection(ProposedFeatures.all); //?文檔對象管理器,提供文檔操作、監聽接口 //?匹配?Client?激活規則的文檔對象都會自動添加到?documents?對象中 const?documents:?TextDocuments<textdocument>?=?new?TextDocuments(TextDocument); //?監聽文檔內容變更事件 documents.onDidChangeContent(change?=>?{ ????validateTextDocument(change.document); }); //?校驗 async?function?validateTextDocument(textDocument:?TextDocument):?Promise<void>?{ ????const?text?=?textDocument.getText(); ????//?匹配全大寫的單詞 ????const?pattern?=?/[A-Z]{2,}/g; ????let?m:?RegExpExecArray?|?null; ????//?這里判斷,如果一個單詞里面全都是大寫字符,則報錯 ????const?diagnostics:?Diagnostic[]?=?[]; ????while?((m?=?pattern.exec(text)))?{ ????????const?diagnostic:?Diagnostic?=?{ ????????????severity:?DiagnosticSeverity.Warning, ????????????range:?{ ????????????????start:?textDocument.positionAt(m.index), ????????????????end:?textDocument.positionAt(m.index?+?m[0].length) ????????????}, ????????????message:?`${m[0]}?is?all?uppercase.`, ????????????source:?'ex' ????????}; ????????diagnostics.push(diagnostic); ????} ????//?發送錯誤診斷信息 ????//?vscode?會自動完成錯誤提示渲染 ????connection.sendDiagnostics({?uri:?textDocument.uri,?diagnostics?}); }</void></textdocument>
LSP Server 代碼的主要流程:
- 調用 createConnection 建立與 vscode 主進程的通訊鏈路,后續所有的信息交互都基于 connection 對象實現。
- 創建 documents 對象,并根據需要監聽文檔事件如上例中的 onDidChangeContent
- 在事件回調中分析代碼內容,根據語言規則返回錯誤診斷信息,例如示例中使用正則判斷單詞是否全部為大寫字母,是的話使用 connection.sendDiagnostics 接口發送錯誤提示信息
運行效果:
小結
通覽樣例代碼,LSP 客戶端服務器之間的通訊過程都已經封裝在 LanguageClient 、connection 等對象中,插件開發者并不需要關心底層實現細節,也不需要深入理解 LSP 協議即可基于這些對象暴露的接口、事件等實現簡單的代碼高亮效果。
總結
Vscode 用插件方式提供了多種語言擴展接口,分聲明式、編程式兩類,在實際項目中通常會混合使用這兩種技術,用基于 TextMate 的聲明式接口迅速識別出代碼中的詞法;再用編程式接口如 LSP 補充提供諸如錯誤提示、代碼補齊、跳轉定義等高級功能。
這段時間看了不少開源 vscode 插件,其中 Vue 官方提供的 Vetur 插件學習是這方面的典型案例,學習價值極高,建議對這方面有興趣的讀者可以自行前往分析學習 vscode 語言擴展類插件的寫法。
更多編程相關知識,請訪問:vscode!!