自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

你不知道的 VSCode 代碼高亮原理

開發(fā) 前端
本文將概要介紹兩種方案的工作過程與特點,各自完成什么工作,互相這么寫作,并結(jié)合實際案例一步步揭開 vscode 代碼高亮功能的實現(xiàn)原理。

[[403401]]

 Vscode 的代碼高亮、代碼補(bǔ)齊、錯誤診斷、跳轉(zhuǎn)定義等語言功能由兩種擴(kuò)展方案協(xié)同實現(xiàn),包括:

  • 基于詞法分析技術(shù),識別分詞 token 并應(yīng)用高亮樣式
  • 基于可編程語言特性接口,識別代碼語義并應(yīng)用高亮樣式,此外還能實現(xiàn)錯誤診斷、智能提示、格式化等功能

兩種方案的功能范疇逐級遞增,相應(yīng)地技術(shù)復(fù)雜度與實現(xiàn)成本也逐級升高,本文將概要介紹兩種方案的工作過程與特點,各自完成什么工作,互相這么寫作,并結(jié)合實際案例一步步揭開 vscode 代碼高亮功能的實現(xiàn)原理:

Vscode 插件基礎(chǔ)

介紹 vscode 代碼高亮原理之前,有必要先熟悉一下 vscode 的底層架構(gòu)。與 Webpack 相似,vscode 本身只是實現(xiàn)了一套架子,架子內(nèi)部的命令、樣式、狀態(tài)、調(diào)試等功能都以插件形式提供,vscode 對外提供了五種拓展能力:

其中,代碼高亮功能由 「語言擴(kuò)展」 類插件實現(xiàn),根據(jù)實現(xiàn)方式又可以細(xì)分為:

  • 「聲明式」 :以特定 JSON 結(jié)構(gòu)聲明一堆匹配詞法的正則,無需編寫邏輯代碼即可添加如塊級匹配、自動縮進(jìn)、語法高亮等語言特性,vscode 內(nèi)置的 extendsions/css、extendsions/html 等插件都是基于聲明式接口實現(xiàn)的
  • 「編程式」 :vscode 運(yùn)行過程中會監(jiān)聽用戶行為,在特定行為發(fā)生后觸發(fā)事件回調(diào),編程式語言擴(kuò)展需要監(jiān)聽這些事件,動態(tài)分析文本內(nèi)容并按特定格式返回代碼信息

聲明式性能高,能力弱;編程式性能低,能力強(qiáng)。語言插件開發(fā)者通常可以混用,用聲明式接口在最短時間內(nèi)識別出詞法 token,提供基本的語法高亮功能;之后用編程式接口動態(tài)分析內(nèi)容,提供更高級特性比如錯誤診斷、智能提示等。

Vscode 中的聲明式語言擴(kuò)展基于 TextMate 詞法分析引擎實現(xiàn);編程式語言擴(kuò)展則基于語義分析接口、vscode.language.* 接口、Language Server Protocol 協(xié)議三種方式實現(xiàn),下面展開介紹每種技術(shù)方案的基本邏輯。

詞法高亮

「詞法分析(Lexical Analysis)」 是計算機(jī)學(xué)科中將字符序列轉(zhuǎn)換為 「標(biāo)記(token)」序列的過程,而 「標(biāo)記(token)」 是構(gòu)成源代碼的最小單位,詞法分析技術(shù)在編譯、IDE等領(lǐng)域有非常廣泛的應(yīng)用。

比如 vscode 的詞法引擎分析出 token 序列后再根據(jù) token 的類型應(yīng)用高亮樣式,這個過程可以簡單劃分為分詞、樣式應(yīng)用兩個步驟。

  • 參考資料:
  • https://macromates.com/manual/en/language_grammars
  • https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide

分詞

分詞過程本質(zhì)上將一長串代碼遞歸地拆解為具有特定含義、分類的字符串片段,比如 +-*/%等操作符;var/const 等關(guān)鍵字;1234 或 "tecvan" 類型的常量值等,簡單說就是從一段文本中識別出,什么地方有一個什么詞。

Vscode 的詞法分析基于 TextMate 引擎實現(xiàn),功能比較復(fù)雜,可以簡單劃分為三個方面:基于正則的分詞、復(fù)合分詞規(guī)則、嵌套分詞規(guī)則。

基本規(guī)則

Vscode 底層的 TextMate 引擎基于 正則 匹配實現(xiàn)分詞功能,運(yùn)行時逐行掃描文本內(nèi)容,用預(yù)定義的 rule 集合測試文本行中是否包含匹配特定正則的內(nèi)容,例如對于下面的規(guī)則配置:

  1.     "patterns": [ 
  2.         { 
  3.             "name""keyword.control"
  4.             "match""\b(if|while|for|return)\b" 
  5.         } 
  6.     ] 

示例中,patterns 用于定義規(guī)則集合, match 屬性定于用于匹配 token 的正則,name 屬性聲明該 token 的分類(scope),TextMate 分詞過程遇到匹配 match 正則的內(nèi)容時,會將其看作單獨(dú) token 處理并分類為 name 聲明的 keyword.control 類型。

上述示例會將 if/while/for/return 關(guān)鍵詞識別為 keyword.control 類型,但無法識別其它關(guān)鍵字:

在 TextMate 語境中,scope 是一種 . 分割的層級結(jié)構(gòu),例如 keyword 與 keyword.control 形成父子層級,這種層級結(jié)構(gòu)在樣式處理邏輯中能實現(xiàn)一種類似 css 選擇器的匹配,后面會講到細(xì)節(jié)。

復(fù)合分詞

上述示例配置對象在 TextMate 語境下被稱作 Language Rule,除了 match 用于匹配單行內(nèi)容,還可以使用 begin + end 屬性對匹配更復(fù)雜的跨行場景。從 begin 到 end 所識別到的范圍內(nèi),都認(rèn)為是 name 類型的 token,比如在 vuejs/vetur 插件的 syntaxes/vue.tmLanguage.json 文件中有這么一段配置:

  1.     "name""Vue"
  2.     "scopeName""source.vue"
  3.     "patterns": [ 
  4.         { 
  5.           "begin""(<)(style)(?![^/>]*/>\\s*$)"
  6.           // 虛構(gòu)字段,方便解釋 
  7.           "name""tag.style.vue"
  8.           "beginCaptures": { 
  9.             "1": { 
  10.               "name""punctuation.definition.tag.begin.html" 
  11.             }, 
  12.             "2": { 
  13.               "name""entity.name.tag.style.html" 
  14.             } 
  15.           }, 
  16.           "end""(</)(style)(>)"
  17.           "endCaptures": { 
  18.             "1": { 
  19.               "name""punctuation.definition.tag.begin.html" 
  20.             }, 
  21.             "2": { 
  22.               "name""entity.name.tag.style.html" 
  23.             }, 
  24.             "3": { 
  25.               "name""punctuation.definition.tag.end.html" 
  26.             } 
  27.           } 
  28.         } 
  29.     ] 

 配置中,begin 用于匹配 <style> 語句,end 用于匹配 </style> 語句,且 <style></style> 整個語句被賦予 scope 為 tag.style.vue 。此外,語句中字符被 beginCaptures 、endCaptures 屬性分配成不同的 scope 類型:

這里從 begin 到 beginCaptures ,從 end 到 endCaptures 形成了某種程度的復(fù)合結(jié)構(gòu),從而實現(xiàn)一次匹配多行內(nèi)容。

規(guī)則嵌套

在上述 begin + end 基礎(chǔ)上,TextMate 還支持以子 patterns 方式定義嵌套的語言規(guī)則,例如:

  1.     "name""lng"
  2.     "patterns": [ 
  3.         { 
  4.             "begin""^lng`"
  5.             "end""`"
  6.             "name""tecvan.lng.outline"
  7.             "patterns": [ 
  8.                 { 
  9.                     "match""tec"
  10.                     "name""tecvan.lng.prefix" 
  11.                 }, 
  12.                 { 
  13.                     "match""van"
  14.                     "name""tecvan.lng.name" 
  15.                 } 
  16.             ] 
  17.         } 
  18.     ], 
  19.     "scopeName""tecvan" 

配置識別 lng` 到 ` 之間的字符串,并分類為 tecvan.lng.outline 。之后,遞歸處理兩者之間的內(nèi)容并按照子 patterns 規(guī)則匹配出更具體的 token ,例如對于:

  1. lng`awesome tecvan` 

可識別出分詞:

  • lng`awesome tecvan` ,scope 為 tecvan.lng.outline
  • tec ,scope 為 tecvan.lng.prefix
  • van ,scope 為 tecvan.lng.name

TextMate 還支持語言級別的嵌套,例如:

  1.     "name""lng"
  2.     "patterns": [ 
  3.         { 
  4.             "begin""^lng`"
  5.             "end""`"
  6.             "name""tecvan.lng.outline"
  7.             "contentName""source.js" 
  8.         } 
  9.     ], 
  10.     "scopeName""tecvan" 

基于上述配置, lng` 到 ` 之間的內(nèi)容都會識別為 contentName 指定的 source.js 語句。

樣式

詞法高亮本質(zhì)上就是先按上述規(guī)則將原始文本拆解成多個具類的 token 序列,之后按照 token 的類型適配不同的樣式。TextMate 在分詞基礎(chǔ)上提供了一套按照 token 類型字段 scope 配置樣式的功能結(jié)構(gòu),例如:

  1.     "tokenColors": [ 
  2.         { 
  3.             "scope""tecvan"
  4.             "settings": { 
  5.                 "foreground""#eee" 
  6.             } 
  7.         }, 
  8.         { 
  9.             "scope""tecvan.lng.prefix"
  10.             "settings": { 
  11.                 "foreground""#F44747" 
  12.             } 
  13.         }, 
  14.         { 
  15.             "scope""tecvan.lng.name"
  16.             "settings": { 
  17.                 "foreground""#007acc"
  18.             } 
  19.         } 
  20.     ] 

示例中,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 用于匹配字符串或備注

插件開發(fā)者可以自定義 scope 也可以選擇復(fù)用 TextMate 內(nèi)置的許多 scope ,包括 comment、constant、entity、invalid、keyword 等,完整列表請查閱 官網(wǎng)。

settings 屬性則用于設(shè)置該 token 的表現(xiàn)樣式,支持foreground、background、bold、italic、underline 等樣式屬性。

實例解析

看完原理我們來拆解一個實際案例:https://github.com/mrmlnc/vscode-json5 ,json5 是 JSON 擴(kuò)展協(xié)議,旨在使人類更易于手動編寫和維護(hù),支持備注、單引號、十六進(jìn)制數(shù)字等特性,這些拓展特性需要使用 vscode-json5 插件實現(xiàn)高亮效果:

上圖中,左邊是沒有啟動 vscode-json5 的效果,右邊是啟動后的效果。

vscode-json5 插件源碼很簡單,兩個關(guān)鍵點:

  • 在 package.json 文件中聲明插件的 contributes 屬性,可以理解為插件的入口:
  1. "contributes": { 
  2.   // 語言配置 
  3.   "languages": [{ 
  4.     "id""json5"
  5.     "aliases": ["JSON5""json5"], 
  6.     "extensions": [".json5"], 
  7.     "configuration""./json5.configuration.json" 
  8.   }], 
  9.   // 語法配置 
  10.   "grammars": [{ 
  11.     "language""json5"
  12.     "scopeName""source.json5"
  13.     "path""./syntaxes/json5.json" 
  14.   }] 
  • 在語法配置文件 ./syntaxes/json5.json 中按照 TextMate 的要求定義 Language Rule:
  1.     "scopeName""source.json5"
  2.     "fileTypes": ["json5"], 
  3.     "name""JSON5"
  4.     "patterns": [ 
  5.         { "include""#array" }, 
  6.         { "include""#constant" } 
  7.         // ... 
  8.     ], 
  9.     "repository": { 
  10.         "array": { 
  11.             "begin""\\["
  12.             "beginCaptures": { 
  13.                 "0": { "name""punctuation.definition.array.begin.json5" } 
  14.             }, 
  15.             "end""\\]"
  16.             "endCaptures": { 
  17.                 "0": { "name""punctuation.definition.array.end.json5" } 
  18.             }, 
  19.             "name""meta.structure.array.json5" 
  20.             // ... 
  21.         }, 
  22.         "constant": { 
  23.             "match""\\b(?:true|false|null|Infinity|NaN)\\b"
  24.             "name""constant.language.json5" 
  25.         }  
  26.         // ... 
  27.     } 

OK,結(jié)束了,沒了,就是這么簡單,之后 vscode 就可以根據(jù)這份配置適配 json5 的語法高亮規(guī)則。

調(diào)試工具

Vscode 內(nèi)置了一套 scope inspect 工具,用于調(diào)試 TextMate 檢測出的 token、scope 信息,使用時只需要將編輯器光標(biāo) focus 到特定 token 上,快捷鍵 ctrl + shift + p 打開 vscode 命令面板后輸出 Developer: Inspect Editor Tokens and Scopes 命令并回車:

圖片

命令運(yùn)行后就可以看到分詞 token 的語言、scope、樣式等信息。

編程式語言擴(kuò)展

詞法分析引擎 TextMate 本質(zhì)上是一種基于正則的靜態(tài)詞法分析器,優(yōu)點是接入方式標(biāo)準(zhǔn)化,成本低且運(yùn)行效率較高,缺點是靜態(tài)代碼分析很難實現(xiàn)某些上下文相關(guān)的 IDE 功能,例如對于下面的代碼:

注意代碼第一行函數(shù)參數(shù) languageModes 與第二行函數(shù)體內(nèi)的 languageModes 是同一實體但是沒有實現(xiàn)相同的樣式,視覺上沒有形成聯(lián)動。

為此,vscode 在 TextMate 引擎之外提供了三種更強(qiáng)大也更復(fù)雜的語言特性擴(kuò)展機(jī)制:

  • 使用 DocumentSemanticTokensProvider 實現(xiàn)可編程的語義分析
  • 使用 vscode.languages.* 下的接口監(jiān)聽各類編程行為事件,在特定時間節(jié)點實現(xiàn)語義分析
  • 根據(jù) Language Server Protocol 協(xié)議實現(xiàn)一套完備的語言特性分析服務(wù)器

相比于上面介紹的聲明式的詞法高亮,語言特性接口更靈活,能夠?qū)崿F(xiàn)諸如錯誤診斷、候選詞、智能提示、定義跳轉(zhuǎn)等高級功能。

  • 參考資料:
  • 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 內(nèi)置的一種對象協(xié)議,它需要自行掃描代碼文件內(nèi)容,然后以整數(shù)數(shù)組形式返回語義 token 序列,告訴 vscode 在文件的哪一行、那一列、多長的區(qū)間內(nèi)是一個什么類型的 token。

注意區(qū)分一下,TextMate 中的掃描是引擎驅(qū)動的,逐行匹配正則,而 「Sematic Tokens Provider」 場景下掃描規(guī)則、匹配規(guī)則都交由插件開發(fā)者自行實現(xiàn),靈活性增強(qiáng)但相對的開發(fā)成本也會更高。

實現(xiàn)上,「Sematic Tokens Provider」 以 vscode.DocumentSemanticTokensProvider 接口定義,開發(fā)者可以按需實現(xiàn)兩個方法:

  • provideDocumentSemanticTokens :全量分析代碼文件語義
  • provideDocumentSemanticTokensEdits :增量分析正在編輯模塊的語義

我們來看個完整的示例:

  1. import * as vscode from 'vscode'
  2.  
  3. const tokenTypes = ['class''interface''enum''function''variable']; 
  4. const tokenModifiers = ['declaration''documentation']; 
  5. const legend = new vscode.SemanticTokensLegend(tokenTypes, tokenModifiers); 
  6.  
  7. const provider: vscode.DocumentSemanticTokensProvider = { 
  8.   provideDocumentSemanticTokens( 
  9.     document: vscode.TextDocument 
  10.   ): vscode.ProviderResult<vscode.SemanticTokens> { 
  11.     const tokensBuilder = new vscode.SemanticTokensBuilder(legend); 
  12.     tokensBuilder.push(       
  13.       new vscode.Range(new vscode.Position(0, 3), new vscode.Position(0, 8)), 
  14.       tokenTypes[0], 
  15.       [tokenModifiers[0]] 
  16.     ); 
  17.     return tokensBuilder.build(); 
  18.   } 
  19. }; 
  20.  
  21. const selector = { language: 'javascript', scheme: 'file' }; 
  22.  
  23. vscode.languages.registerDocumentSemanticTokensProvider(selector, provider, legend); 

相信大多數(shù)讀者對這段代碼都會覺得陌生,我想了很久,覺得還是從函數(shù)輸出的角度開始講起比較容易理解,也就是上例代碼第 17 行 tokensBuilder.build()。

輸出結(jié)構(gòu)

provideDocumentSemanticTokens 函數(shù)要求返回一個整數(shù)數(shù)組,數(shù)組項按 5 位為一組分別表示:

  • 第 5 * i 位,token 所在行相對于上一個 token 的偏移
  • 第 5 * i + 1 位,token 所在列相對于上一個 token 的偏移
  • 第 5 * i + 2 位,token 長度
  • 第 5 * i + 3 位,token 的 type 值
  • 第 5 * i + 4 位,token 的 modifier 值

我們需要理解這是一個位置強(qiáng)相關(guān)的整數(shù)數(shù)組,數(shù)組中每 5 個項描述一個 token 的位置、類型。token 位置由所在行、列、長度三個數(shù)字組成,而為了壓縮數(shù)據(jù)的大小 vscode 有意設(shè)計成相對位移的形式,例如對于這樣的代碼:

  1. const name as 

假如只是簡單地按空格分割,那么這里可以解析出三個 token:const 、 name 、as,對應(yīng)的描述數(shù)組為:

  1. // 對應(yīng)第一個 token:const 
  2. 0, 0, 5, x, x, 
  3. // 對應(yīng)第二個 token:name 
  4. 0, 6, 4, x, x, 
  5. // 第三個 token:as 
  6. 0, 5, 2, x, x 

注意這里是以相對前一個 token 位置的形式描述的,比如 as 字符對應(yīng)的 5 個數(shù)字的語義為:相對前一個 token 偏移 0 行、5 列,長度為 2 ,類型為 xx。

剩下的第 5 * i + 3 位與第 5 * i + 4 位分別描述 token 的 type 與 modifier,其中 type 指示 token 的類型,例如 comment、class、function、namespace 等等;modifier 是類型基礎(chǔ)上的修飾器,可以近似理解為子類型,比如對于 class 有可能是 abstract 的,也有可能是從標(biāo)準(zhǔn)庫導(dǎo)出 defaultLibrary。

type、modifier 的具體數(shù)值需要開發(fā)者自行定義,例如上例中:

  1. const tokenTypes = ['class''interface''enum''function''variable']; 
  2. const tokenModifiers = ['declaration''documentation']; 
  3. const legend = new vscode.SemanticTokensLegend(tokenTypes, tokenModifiers); 
  4.  
  5. // ... 
  6.  
  7. vscode.languages.registerDocumentSemanticTokensProvider(selector, provider, legend); 

首先通過 vscode. SemanticTokensLegend 類構(gòu)建 type、modifier 的內(nèi)部表示 legend 對象,之后使用 vscode.languages.registerDocumentSemanticTokensProvider 接口與 provider 一起注冊到 vscode 中。

語義分析

上例中 provider 的主要作用就是遍歷分析文件內(nèi)容,返回符合上述規(guī)則的整數(shù)數(shù)組,vscode 對具體的分析方法并沒有做限定,只是提供了用于構(gòu)建 token 描述數(shù)組的工具 SemanticTokensBuilder,例如上例中:

  1. const provider: vscode.DocumentSemanticTokensProvider = { 
  2.   provideDocumentSemanticTokens( 
  3.     document: vscode.TextDocument 
  4.   ): vscode.ProviderResult<vscode.SemanticTokens> { 
  5.     const tokensBuilder = new vscode.SemanticTokensBuilder(legend); 
  6.     tokensBuilder.push(       
  7.       new vscode.Range(new vscode.Position(0, 3), new vscode.Position(0, 8)), 
  8.       tokenTypes[0], 
  9.       [tokenModifiers[0]] 
  10.     ); 
  11.     return tokensBuilder.build(); 
  12.   } 
  13. }; 

代碼使用 SemanticTokensBuilder 接口構(gòu)建并返回了一個 [0, 3, 5, 0, 0] 的數(shù)組,即第 0 行,第 3 列,長度為 5 的字符串,type =0,modifier = 0,運(yùn)行效果:

除了這一段被識別出的 token 外,其它字符都被認(rèn)為不可識別。

小結(jié)

本質(zhì)上,DocumentSemanticTokensProvider 只是提供了一套粗糙的 IOC 接口,開發(fā)者能做的事情比較有限,所以現(xiàn)在大多數(shù)插件都沒有采用這種方案,讀者理解即可,不必深究。

Language API

簡介

相對而言,vscode.languages.* 系列 API 所提供的語言擴(kuò)展能力可能更符合前端開發(fā)者的思維習(xí)慣。vscode.languages.* 托管了一系列用戶交互行為的處理、歸類邏輯,并以事件接口方式開放出來,插件開發(fā)者只需監(jiān)聽這些事件,根據(jù)參數(shù)推斷語言特性,并按規(guī)則返回結(jié)果即可。

Vscode Language API 提供了很多事件接口,比如說:

  • registerCompletionItemProvider:提供代碼補(bǔ)齊提示
圖片
  • registerHoverProvider:光標(biāo)停留在 token 上時觸發(fā)
圖片
  • registerSignatureHelpProvider:提供函數(shù)簽名提示
圖片

完整的列表請查閱 https://code.visualstudio.com/api/language-extensions/programmatic-language-features#show-hovers 一文。

Hover 示例

Hover 功能實現(xiàn)分兩步,首先需要在 package.json 中聲明 hover 特性:

  1.     ... 
  2.     "main""out/extensions.js"
  3.     "capabilities" : { 
  4.         "hoverProvider" : "true"
  5.         ... 
  6.     } 

之后,需要在 activate 函數(shù)中調(diào)用 registerHoverProvider 注冊 hover 回調(diào):

  1. export function activate(ctx: vscode.ExtensionContext): void { 
  2.     ... 
  3.     vscode.languages.registerHoverProvider('language name', { 
  4.         provideHover(document, position, token) { 
  5.             return { contents: ['aweome tecvan'] }; 
  6.         } 
  7.     }); 
  8.     ... 

運(yùn)行結(jié)果:

其它特性功能的寫法與此相似,感興趣的同學(xué)建議到官網(wǎng)自行查閱。

Language Server Protocol

簡介

上述基于語言擴(kuò)展插件的代碼高亮方法有一個相似的問題:難以在編輯器間復(fù)用,同一個語言,需要根據(jù)編輯器環(huán)境、語言重復(fù)編寫功能相似的支持插件,那么對于 n 種語言,m 種編輯器,這里面的開發(fā)成本就是 n * m。

為了解決這個問題,微軟提出了一種叫做 Language Server Protocol 的標(biāo)準(zhǔn)協(xié)議,語言功能插件與編輯器之間不再直接通訊,而是通過 LSP 做一層隔離:

增加 LSP 層帶來兩個好處:

  • LSP 層的開發(fā)語言、環(huán)境等與具體 IDE 所提供的 host 環(huán)境脫耦
  • 語言插件的核心功能只需要編寫一次,就可以復(fù)用到支持 LSP 協(xié)議的 IDE 中

雖然 LSP 與上述 Language API 能力上幾乎相同,但借助這兩個優(yōu)點大大提升了插件的開發(fā)效率,目前很多 vscode 語言類插件都已經(jīng)遷移到 LSP 實現(xiàn),包括 vetur、eslint、Python for VSCode 等知名插件。

Vscode 中的 LSP 架構(gòu)包含兩部分:

  • Language Client: 一個標(biāo)準(zhǔn) vscode 插件,實現(xiàn)與 vscode 環(huán)境的交互,例如 hover 事件首先會傳遞到 client,再由 client 傳遞到背后的 server
  • Language Server: 語言特性的核心實現(xiàn),通過 LSP 協(xié)議與 Language Client 通訊,注意 Server 實例會以單獨(dú)進(jìn)程方式運(yùn)行

做個類比,LSP 就是經(jīng)過架構(gòu)優(yōu)化的 Language API,原來由單個 provider 函數(shù)實現(xiàn)的功能拆解為 Client + Server 兩端跨語言架構(gòu),Client 與 vscode 交互并實現(xiàn)請求轉(zhuǎn)發(fā);Server 執(zhí)行代碼分析動作,并提供高亮、補(bǔ)全、提示等功能,如下圖:

簡單示例

LSP 稍微有一點點復(fù)雜,建議讀者先拉下 vscode 官方示例對比學(xué)習(xí):

  1. git clone https://github.com/microsoft/vscode-extension-samples.git 
  2. cd vscode-extension-samples/lsp-sample 
  3. yarn 
  4. yarn compile 
  5. code . 

vscode-extension-samples/lsp-sample 的主要代碼文件有:

  1. ├── client // Language Client 
  2. │   ├── src 
  3. │   │   └── extension.ts // Language Client 入口文件 
  4. ├── package.json  
  5. └── server // Language Server 
  6.     └── src 
  7.         └── server.ts // Language Server 入口文件 

樣例代碼中有幾個關(guān)鍵點:

  1. 在 package.json 中聲明激活條件與插件入口
  2. 編寫入口文件 client/src/extension.ts,啟動 LSP 服務(wù)
  3. 編寫 LSP 服務(wù)即 server/src/server.ts ,實現(xiàn) LSP 協(xié)議

邏輯上,vscode 會在加載插件時根據(jù) package.json 的配置判斷激活條件,之后加載、運(yùn)行插件入口,啟動 LSP 服務(wù)器。插件啟動后,后續(xù)用戶在 vscode 的交互行為會以標(biāo)準(zhǔn)事件,如 hover、completion、signature help 等方式觸發(fā)插件的 client ,client 再按照 LSP 協(xié)議轉(zhuǎn)發(fā)到 server 層。

下面我們拆開看看三個模塊的細(xì)節(jié)。

入口配置

示例 vscode-extension-samples/lsp-sample 中的 package.json 有兩個關(guān)鍵配置:

  1.     "activationEvents": [ 
  2.         "onLanguage:plaintext" 
  3.     ], 
  4.     "main""./client/out/extension"

其中:

  • activationEvents:聲明插件的激活條件,代碼中的 onLanguage:plaintext意為打開 txt 文本文件時激活
  • main:插件的入口文件

Client 樣例

示例 vscode-extension-samples/lsp-sample 中的 Client 入口代碼,關(guān)鍵部分如下:

  1. export function activate(context: ExtensionContext) { 
  2.     // Server 配置信息 
  3.     const serverOptions: ServerOptions = { 
  4.         run: {  
  5.             // Server 模塊的入口文件 
  6.             module: context.asAbsolutePath( 
  7.                 path.join('server''out''server.js'
  8.             ),  
  9.             // 通訊協(xié)議,支持 stdio、ipc、pipe、socket 
  10.             transport: TransportKind.ipc  
  11.         }, 
  12.     }; 
  13.  
  14.     // Client 配置 
  15.     const clientOptions: LanguageClientOptions = { 
  16.         // 與 packages.json 文件的 activationEvents 類似 
  17.         // 插件的激活條件 
  18.         documentSelector: [{ scheme: 'file', language: 'plaintext' }], 
  19.         // ... 
  20.     }; 
  21.  
  22.     // 使用 Server、Client 配置創(chuàng)建代理對象 
  23.     const client = new LanguageClient( 
  24.         'languageServerExample'
  25.         'Language Server Example'
  26.         serverOptions, 
  27.         clientOptions 
  28.     ); 
  29.  
  30.     client.start(); 

代碼脈絡(luò)很清晰,先是定義 Server、Client 配置對象,之后創(chuàng)建并啟動了 LanguageClient 實例。從實例可以看到,Client 這一層可以做的很薄,在 Node 環(huán)境下大部分轉(zhuǎn)發(fā)邏輯都被封裝在 LanguageClient 類中,開發(fā)者無需關(guān)心細(xì)節(jié)。

Server 樣例

示例 vscode-extension-samples/lsp-sample 中的 Server 代碼實現(xiàn)了錯誤診斷、代碼補(bǔ)全功能,作為學(xué)習(xí)樣例來說稍顯復(fù)雜,所以我只摘抄出錯誤診斷部分的代碼:

  1. // Server 層所有通訊都使用 createConnection 創(chuàng)建的 connection 對象實現(xiàn) 
  2. const connection = createConnection(ProposedFeatures.all); 
  3.  
  4. // 文檔對象管理器,提供文檔操作、監(jiān)聽接口 
  5. // 匹配 Client 激活規(guī)則的文檔對象都會自動添加到 documents 對象中 
  6. const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument); 
  7.  
  8. // 監(jiān)聽文檔內(nèi)容變更事件 
  9. documents.onDidChangeContent(change => { 
  10.     validateTextDocument(change.document); 
  11. }); 
  12.  
  13. // 校驗 
  14. async function validateTextDocument(textDocument: TextDocument): Promise<void> { 
  15.     const text = textDocument.getText(); 
  16.     // 匹配全大寫的單詞 
  17.     const pattern = /\b[A-Z]{2,}\b/g; 
  18.     let m: RegExpExecArray | null
  19.  
  20.     // 這里判斷,如果一個單詞里面全都是大寫字符,則報錯 
  21.     const diagnostics: Diagnostic[] = []; 
  22.     while ((m = pattern.exec(text))) { 
  23.         const diagnostic: Diagnostic = { 
  24.             severity: DiagnosticSeverity.Warning, 
  25.             range: { 
  26.                 start: textDocument.positionAt(m.index), 
  27.                 end: textDocument.positionAt(m.index + m[0].length) 
  28.             }, 
  29.             message: `${m[0]} is all uppercase.`, 
  30.             source: 'ex' 
  31.         }; 
  32.         diagnostics.push(diagnostic); 
  33.     } 
  34.  
  35.     // 發(fā)送錯誤診斷信息 
  36.     // vscode 會自動完成錯誤提示渲染 
  37.     connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); 

LSP Server 代碼的主要流程:

  • 調(diào)用 createConnection 建立與 vscode 主進(jìn)程的通訊鏈路,后續(xù)所有的信息交互都基于 connection 對象實現(xiàn)。
  • 創(chuàng)建 documents 對象,并根據(jù)需要監(jiān)聽文檔事件如上例中的 onDidChangeContent
  • 在事件回調(diào)中分析代碼內(nèi)容,根據(jù)語言規(guī)則返回錯誤診斷信息,例如示例中使用正則判斷單詞是否全部為大寫字母,是的話使用 connection.sendDiagnostics 接口發(fā)送錯誤提示信息

運(yùn)行效果:

圖片

小結(jié)

通覽樣例代碼,LSP 客戶端服務(wù)器之間的通訊過程都已經(jīng)封裝在 LanguageClient 、connection 等對象中,插件開發(fā)者并不需要關(guān)心底層實現(xiàn)細(xì)節(jié),也不需要深入理解 LSP 協(xié)議即可基于這些對象暴露的接口、事件等實現(xiàn)簡單的代碼高亮效果。

總結(jié)

Vscode 用插件方式提供了多種語言擴(kuò)展接口,分聲明式、編程式兩類,在實際項目中通常會混合使用這兩種技術(shù),用基于 TextMate 的聲明式接口迅速識別出代碼中的詞法;再用編程式接口如 LSP 補(bǔ)充提供諸如錯誤提示、代碼補(bǔ)齊、跳轉(zhuǎn)定義等高級功能。

這段時間看了不少開源 vscode 插件,其中 Vue 官方提供的 Vetur 插件學(xué)習(xí)是這方面的典型案例,學(xué)習(xí)價值極高,建議對這方面有興趣的讀者可以自行前往分析學(xué)習(xí) vscode 語言擴(kuò)展類插件的寫法。

 

責(zé)任編輯:姜華 來源: Tecvan
相關(guān)推薦

2021-11-11 06:57:00

架構(gòu)

2020-06-12 09:20:33

前端Blob字符串

2020-07-28 08:26:34

WebSocket瀏覽器

2022-03-24 20:20:31

VS Code項目約束倉庫配置

2011-09-15 17:10:41

2022-10-13 11:48:37

Web共享機(jī)制操作系統(tǒng)

2009-12-10 09:37:43

2021-02-01 23:23:39

FiddlerCharlesWeb

2010-08-23 09:56:09

Java性能監(jiān)控

2018-09-02 15:43:56

Python代碼編程語言

2020-09-15 08:35:57

TypeScript JavaScript類型

2022-11-04 08:19:18

gRPC框架項目

2021-12-29 11:38:59

JS前端沙箱

2021-10-17 13:10:56

函數(shù)TypeScript泛型

2021-12-22 09:08:39

JSON.stringJavaScript字符串

2012-11-23 10:57:44

Shell

2015-06-19 13:54:49

2020-08-11 11:20:49

Linux命令使用技巧

2018-10-17 09:25:22

2012-06-26 15:49:05

點贊
收藏

51CTO技術(shù)棧公眾號