<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 27329字,需瀏覽 55分鐘

           ·

          2021-08-12 18:50

          全文5000字,解讀 vscode 背后的代碼高亮實(shí)現(xiàn)原理,歡迎點(diǎn)贊關(guān)注轉(zhuǎn)發(fā)。

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

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

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

          Vscode 插件基礎(chǔ)

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

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

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

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

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

          詞法高亮

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

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

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

          分詞

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

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

          基本規(guī)則

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

          {
              "patterns": [
                  {
                      "name""keyword.control",
                      "match""\b(if|while|for|return)\b"
                  }
              ]
          }

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

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

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

          復(fù)合分詞

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

          {
              "name""Vue",
              "scopeName""source.vue",
              "patterns": [
                  {
                    "begin""(<)(style)(?![^/>]*/>\\s*$)",
                    // 虛構(gòu)字段,方便解釋
                    "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 用于匹配 <style> 語(yǔ)句,end 用于匹配 </style> 語(yǔ)句,且 <style></style> 整個(gè)語(yǔ)句被賦予 scope 為 tag.style.vue 。此外,語(yǔ)句中字符被 beginCapturesendCaptures 屬性分配成不同的 scope 類型:

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

          規(guī)則嵌套

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

          {
              "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"
          }

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

          lng`awesome tecvan`

          可識(shí)別出分詞:

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

          TextMate 還支持語(yǔ)言級(jí)別的嵌套,例如:

          {
              "name""lng",
              "patterns": [
                  {
                      "begin""^lng`",
                      "end""`",
                      "name""tecvan.lng.outline",
                      "contentName""source.js"
                  }
              ],
              "scopeName""tecvan"
          }

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

          樣式

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

          {
              "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.lngtecvan.lng.prefix 等子類型的 token
          • 后代選擇,例如 scope = text.html source.js 用于匹配 html 文檔中的 JavaScript 代碼
          • 分組選擇,例如 scope = string, comment 用于匹配字符串或備注

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

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

          實(shí)例解析

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

          上圖中,左邊是沒(méi)有啟動(dòng) vscode-json5 的效果,右邊是啟動(dòng)后的效果。

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

          • package.json 文件中聲明插件的 contributes 屬性,可以理解為插件的入口:
            "contributes": {
              // 語(yǔ)言配置
              "languages": [{
                "id""json5",
                "aliases": ["JSON5""json5"],
                "extensions": [".json5"],
                "configuration""./json5.configuration.json"
              }],
              // 語(yǔ)法配置
              "grammars": [{
                "language""json5",
                "scopeName""source.json5",
                "path""./syntaxes/json5.json"
              }]
            }
          • 在語(yǔ)法配置文件 ./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,結(jié)束了,沒(méi)了,就是這么簡(jiǎn)單,之后 vscode 就可以根據(jù)這份配置適配 json5 的語(yǔ)法高亮規(guī)則。

          調(diào)試工具

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

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

          編程式語(yǔ)言擴(kuò)展

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

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

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

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

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

          考資料:
          • 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 分詞

          簡(jiǎn)介

          「Sematic Tokens Provider」 是 vscode 內(nèi)置的一種對(duì)象協(xié)議,它需要自行掃描代碼文件內(nèi)容,然后以整數(shù)數(shù)組形式返回語(yǔ)義 token 序列,告訴 vscode 在文件的哪一行、那一列、多長(zhǎng)的區(qū)間內(nèi)是一個(gè)什么類型的 token。

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

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

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

          我們來(lái)看個(gè)完整的示例:

          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(03), new vscode.Position(08)),
                tokenTypes[0],
                [tokenModifiers[0]]
              );
              return tokensBuilder.build();
            }
          };

          const selector = { language'javascript'scheme'file' };

          vscode.languages.registerDocumentSemanticTokensProvider(selector, provider, legend);

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

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

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

          • 5 * i 位,token 所在行相對(duì)于上一個(gè) token 的偏移
          • 5 * i + 1 位,token 所在列相對(duì)于上一個(gè) token 的偏移
          • 5 * i + 2 位,token 長(zhǎng)度
          • 5 * i + 3 位,token 的 type 值
          • 5 * i + 4 位,token 的 modifier 值

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

          const name as

          假如只是簡(jiǎn)單地按空格分割,那么這里可以解析出三個(gè) token:constnameas ,對(duì)應(yīng)的描述數(shù)組為:

          [
          // 對(duì)應(yīng)第一個(gè) token:const
          005, x, x,
          // 對(duì)應(yīng)第二個(gè) token:name
          064, x, x,
          // 第三個(gè) token:as
          052, x, x
          ]

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

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

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

          const tokenTypes = ['class''interface''enum''function''variable'];
          const tokenModifiers = ['declaration''documentation'];
          const legend = new vscode.SemanticTokensLegend(tokenTypes, tokenModifiers);

          // ...

          vscode.languages.registerDocumentSemanticTokensProvider(selector, provider, legend);

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

          語(yǔ)義分析

          上例中 provider 的主要作用就是遍歷分析文件內(nèi)容,返回符合上述規(guī)則的整數(shù)數(shù)組,vscode 對(duì)具體的分析方法并沒(méi)有做限定,只是提供了用于構(gòu)建 token 描述數(shù)組的工具 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(03), new vscode.Position(08)),
                tokenTypes[0],
                [tokenModifiers[0]]
              );
              return tokensBuilder.build();
            }
          };

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

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

          小結(jié)

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

          Language API

          簡(jiǎn)介

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

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

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

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

          Hover 示例

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

          {
              ...
              "main""out/extensions.js",
              "capabilities" : {
                  "hoverProvider" : "true",
                  ...
              }
          }

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

          export function activate(ctx: vscode.ExtensionContext): void {
              ...
              vscode.languages.registerHoverProvider('language name', {
                  provideHover(document, position, token) {
                      return { contents: ['aweome tecvan'] };
                  }
              });
              ...
          }

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

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

          Language Server Protocol

          簡(jiǎn)介

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

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

          增加 LSP 層帶來(lái)兩個(gè)好處:

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

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

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

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

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

          簡(jiǎn)單示例

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

          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 入口文件

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

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

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

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

          入口配置

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

          {
              "activationEvents": [
                  "onLanguage:plaintext"
              ],
              "main""./client/out/extension",
          }

          其中:

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

          Client 樣例

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

          export function activate(context: ExtensionContext{
              // Server 配置信息
              const serverOptions: ServerOptions = {
                  run: { 
                      // Server 模塊的入口文件
                      module: context.asAbsolutePath(
                          path.join('server''out''server.js')
                      ), 
                      // 通訊協(xié)議,支持 stdio、ipc、pipe、socket
                      transport: TransportKind.ipc 
                  },
              };

              // Client 配置
              const clientOptions: LanguageClientOptions = {
                  // 與 packages.json 文件的 activationEvents 類似
                  // 插件的激活條件
                  documentSelector: [{ scheme'file'language'plaintext' }],
                  // ...
              };

              // 使用 Server、Client 配置創(chuàng)建代理對(duì)象
              const client = new LanguageClient(
                  'languageServerExample',
                  'Language Server Example',
                  serverOptions,
                  clientOptions
              );

              client.start();
          }

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

          Server 樣例

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

          // Server 層所有通訊都使用 createConnection 創(chuàng)建的 connection 對(duì)象實(shí)現(xiàn)
          const connection = createConnection(ProposedFeatures.all);

          // 文檔對(duì)象管理器,提供文檔操作、監(jiān)聽(tīng)接口
          // 匹配 Client 激活規(guī)則的文檔對(duì)象都會(huì)自動(dòng)添加到 documents 對(duì)象中
          const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);

          // 監(jiān)聽(tīng)文檔內(nèi)容變更事件
          documents.onDidChangeContent(change => {
              validateTextDocument(change.document);
          });

          // 校驗(yàn)
          async function validateTextDocument(textDocument: TextDocument): Promise<void{
              const text = textDocument.getText();
              // 匹配全大寫的單詞
              const pattern = /\b[A-Z]{2,}\b/g;
              let m: RegExpExecArray | null;

              // 這里判斷,如果一個(gè)單詞里面全都是大寫字符,則報(bào)錯(cuò)
              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);
              }

              // 發(fā)送錯(cuò)誤診斷信息
              // vscode 會(huì)自動(dòng)完成錯(cuò)誤提示渲染
              connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
          }

          LSP Server 代碼的主要流程:

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

          運(yùn)行效果:

          小結(jié)

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

          總結(jié)

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

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

          瀏覽 48
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久操av在线 | 奇米色一区二区三区 | 天天日天天操天天 | 级毛片内射视频 | 亚洲黄色小电影 |