<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>

          實例解析:如何開發(fā) VSCode LSP 服務(wù)

          共 10884字,需瀏覽 22分鐘

           ·

          2021-07-15 01:22

          作者:范文杰

          來源:SegmentFault 思否社區(qū)


          從一張動圖說起:

          上圖應(yīng)該大家經(jīng)常使用的 錯誤診斷 功能,它能夠在你編寫代碼的過程中提示,那一塊代碼存在什么類型的問題。

          這個看似高大上的功能,從插件開發(fā)者的角度看其實特別簡單,基本上就是用上一篇文章《你不知道的 VSCode 代碼高亮原理》中簡單介紹過的 VSCode 開發(fā)語言特性的三種方案:

          • 基于 Sematic Tokens Provider 協(xié)議的詞法高亮

          • 基于 Language API 的編程式語法高亮

          • 基于 Language Server Protocol 的多進程架構(gòu)語法高亮


          其中, Language Server Protocol 由于性能與開發(fā)效率上的優(yōu)勢已經(jīng)逐漸成為主流實現(xiàn)方案,本文接下來會基于 LSP 展開介紹各種語言特性的實現(xiàn)細節(jié),解答 LSP 的通訊模型與開發(fā)模式。

          示例代碼

          本文示例均已同步到 github,建議讀者先拉下代碼實際體驗:
          # 1. clone 示例代碼
          git clone [email protected]:Tecvan-fe/vscode-lsp-sample.git
          # 2. 安裝依賴
          npm i # or yarn
          # 3. 使用 vscode 打開示例代碼
          code ./vscode-lsp-sample
          # 4. 在 vscode 中按下 F5 啟動調(diào)試
          順利執(zhí)行完畢后,可以看到插件的調(diào)試窗口:
          核心代碼有:
          • server/src/server.ts:LSP 服務(wù)端代碼,提供代碼補全、錯誤診斷、代碼提示等常見語言功能的示例

          • client/src/extension.ts:提供一系列 LSP 參數(shù),包括 Server 的調(diào)試端口、代碼入口、通訊方式等。

          • packages.json:主要提供了語法插件所需要的配置信息,包括:

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

          • main:插件的入口文件


          其中,
          client/src/extension.ts 與 packages.json 都比較簡單,本文過多介紹,重點在于 server/src/server.ts 文件,接下來我們逐步拆解,解析不同語言特性的實現(xiàn)細節(jié)。

          如何編寫 Language Server

          Server 結(jié)構(gòu)解析

          示例項目的 server/src/server.ts 實現(xiàn)了一個小型但完整的 Language Server 應(yīng)用,核心代碼:
          // 要素1: 初始化 LSP 連接對象
          const connection = createConnection(ProposedFeatures.all);

          // 要素2: 創(chuàng)建文檔集合對象,用于映射到實際文檔
          const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);

          connection.onInitialize((params: InitializeParams) => {
            // 要素3: 顯式聲明插件支持的語言特性
            const result: InitializeResult = {
              capabilities: {
                hoverProvider: true
              },
            };
            return result;
          });

          // 要素4: 將文檔集合對象關(guān)聯(lián)到連接對象
          documents.listen(connection);

          // 要素5: 開始監(jiān)聽連接對象
          connection.listen();
          從示例代碼可以總結(jié)出 Language Server 的 5 個必要步驟:
          • 創(chuàng)建 connection 對象,用于實現(xiàn)客戶端與服務(wù)器之間的信息互通

          • 創(chuàng)建 documents 文檔集合對象,用于映射客戶端正在編輯的文件

          • 在 connection.onInitialize 事件中,顯式聲明插件支持的語法特性,例如上例中返回對象包含 hoverProvider: true 聲明,表示該插件能夠提供代碼懸停提示功能

          • 將 documents 關(guān)聯(lián)到 connection 對象

          • 調(diào)用 connection.listen 函數(shù),開始監(jiān)聽客戶端消息


          上述 connection 、documents 等對象定義在 npm 包:
          • vscode-languageserver/node
          • vscode-languageserver-textdocument
          這是一個基本模板,主要完成了 Language Server 各種初始化操作,后續(xù)就可以使用 connection.onXXX 或 documents.onXXX 監(jiān)聽各類交互事件,并在事件回調(diào)中返回符合 LSP 協(xié)議的結(jié)果,或者顯式調(diào)用通訊函數(shù)如 connection.sendDiagnostics 發(fā)送交互信息。
          接下來我們通過幾個簡單實例,分析各項語言特性的實現(xiàn)邏輯。

          懸停提示

          當(dāng)鼠標(biāo)停留在語言元素如函數(shù)、變量、符號等 token 時,VSCode 會顯示 token 對應(yīng)描述與幫助信息:
          要實現(xiàn)懸停提示功能,首先需要聲明插件支持 hoverProvider 特性:
          connection.onInitialize((params: InitializeParams) => {
            return {
              capabilities: {
                hoverProvider: true
              },
            };
          });
          之后,需要監(jiān)聽 connection.onHover 事件,并在事件回調(diào)中返回提示信息:
          connection.onHover((params: HoverParams): Promise<Hover> => {
            return Promise.resolve({
              contents: ["Hover Demo"],
            });
          });
          OK,這就是一個很簡單的語言特性示例了,本質(zhì)上就是監(jiān)聽事件 + 返回結(jié)果,非常簡單。

          代碼格式化

          代碼格式化是一個特別有用的功能,能夠幫助用戶快速、自動完成代碼的美化處理,實現(xiàn)效果如:
          實現(xiàn)懸停提示功能,首先需要聲明插件支持 documentFormattingProvider 特性:
          {
              ...
              capabilities : {
                  documentFormattingProvider: true
                  ...
              }
          }
          之后,監(jiān)聽 onDocumentFormatting 事件:
          connection.onDocumentFormatting(
            (params: DocumentFormattingParams): Promise<TextEdit[]> => {
              const { textDocument } = params;
              const doc = documents.get(textDocument.uri)!;
              const text = doc.getText();
              const pattern = /\b[A-Z]{3,}\b/g;
              let match;
              const res = [];
              // 查找連續(xù)大寫字符串
              while ((match = pattern.exec(text))) {
                res.push({
                  range: {
                    start: doc.positionAt(match.index),
                    end: doc.positionAt(match.index + match[0].length),
                  },
                  // 將大寫字符串替換為 駝峰風(fēng)格
                  newText: match[0].replace(/(?<=[A-Z])[A-Z]+/, (r) => r.toLowerCase()),
                });
              }

              return Promise.resolve(res);
            }
          );
          示例代碼中,回調(diào)函數(shù)主要實現(xiàn)將連續(xù)大寫字符串格式化為駝峰字符串,效果如圖:

          函數(shù)簽名

          函數(shù)簽名特性在用戶輸入函數(shù)調(diào)用語法時觸發(fā),此時 VSCode 會根據(jù) Language Server 返回的內(nèi)容,顯示該函數(shù)的幫助信息。
          實現(xiàn)函數(shù)簽名功能,需要首先聲明插件支持 documentFormattingProvider 特性:
          {
              ...
              capabilities : {
                  signatureHelpProvider: {
                      triggerCharacters: ["("],
                  }
                  ...
              }
          }
          之后,監(jiān)聽 onSignatureHelp 事件:
          connection.onSignatureHelp(
            (params: SignatureHelpParams): Promise<SignatureHelp> => {
              return Promise.resolve({
                signatures: [
                  {
                    label: "Signature Demo",
                    documentation: "幫助文檔",
                    parameters: [
                      {
                        label: "@p1 first param",
                        documentation: "參數(shù)說明",
                      },
                    ],
                  },
                ],
                activeSignature: 0,
                activeParameter: 0,
              });
            }
          );
          實現(xiàn)效果:

          錯誤提示

          注意,錯誤提示的實現(xiàn)邏輯與上述事件 + 響應(yīng)的模式有一點點不同:
          • 首先不需要通過capabilities 做額外聲明;

          • 監(jiān)聽的是 documents.onDidChangeContent 事件,而不是 connection 對象上的事件

          • 不是在事件回調(diào)中用 return 語句返回錯誤信息,而是調(diào)用 connection.sendDiagnostics 發(fā)送錯誤消息


          完整示例:
          // 增量錯誤診斷
          documents.onDidChangeContent((change) => {
            const textDocument = change.document;

            // The validator creates diagnostics for all uppercase words length 2 and more
            const text = textDocument.getText();
            const pattern = /\b[A-Z]{2,}\b/g;
            let m: RegExpExecArray | null;

            let problems = 0;
            const diagnostics: Diagnostic[] = [];
            while ((m = pattern.exec(text))) {
              problems++;
              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"Diagnostics Demo",
              };
              diagnostics.push(diagnostic);
            }

            // Send the computed diagnostics to VSCode.
            connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
          });
          這段邏輯診斷代碼中是否存在連續(xù)大寫字符串,通過 sendDiagnostics 發(fā)送相應(yīng)的錯誤信息,實現(xiàn)效果:

          如何識別事件與響應(yīng)體

          上述示例,我有意忽略大多數(shù)實現(xiàn)細節(jié),更關(guān)注實現(xiàn)語言特性的基本框架和輸入輸出。授人以魚不如授人以漁,所以接下來我們花一點點時間了解從哪里獲取這些接口、參數(shù)、響應(yīng)體的信息。有兩個非常重要的鏈接:
          • https://zjsms.com/egWtqPj/ , VSCode 官網(wǎng)關(guān)于可編程語言特性的說明文檔

          • https://zjsms.com/egWVTPg/ ,LSP 協(xié)議官網(wǎng)


          這兩個網(wǎng)頁提供了 VSCode 所支持的所有語言特性的詳細介紹,可以在這里找到你想要實現(xiàn)的特性的概念性描述,例如對于代碼補齊:


          嗯,有點復(fù)雜且太過 detail,不過還是很有必要耐心了解下,讓你對即將要做的事情有一個高層概念上的理解。
          此外,如果你選擇使用 TS 編寫 LSP,事情會變得更簡單。vscode-languageserver 包提供了非常完善的 Typescript 類型定義,我們完全可以借助 ts + VSCode 的代碼提示找到需要使用的監(jiān)聽函數(shù):
          之后,根據(jù)函數(shù)簽名找到參數(shù)、結(jié)果的類型定義
          之后,就可以根據(jù)類型定義,有針對性地處理參數(shù),返回對應(yīng)結(jié)構(gòu)的數(shù)據(jù)。

          深入理解 LSP

          看完示例后,我們再反過頭來看看 LSP。LSP —— Language Server Protocol 本質(zhì)上是一種基于 JSON-RPC 的進程間通訊協(xié)議,LSP 本身包含兩大塊內(nèi)容:
          • 定義 client 與 server 之間的通訊模型,也就是誰、在什么時候、以什么方式向?qū)Ψ桨l(fā)送什么格式的信息,接收方又以什么方式返回響應(yīng)信息

          • 定義通訊信息體,也就是以什么格式、什么字段、什么樣的值表達信息狀態(tài)


          作為類比,HTTP 協(xié)議專門用于描述網(wǎng)絡(luò)節(jié)點間如何傳輸、理解超媒體文檔的網(wǎng)絡(luò)通訊協(xié)議;而 LSP 協(xié)議則專門用于描述 IDE 中,用戶行為與響應(yīng)之間的通訊方式與信息結(jié)構(gòu)。
          總結(jié)一下,LSP 架構(gòu)的工作流程如下:
          • 編輯器如 VSCode 跟蹤、計算、管理用戶行為模型,在發(fā)生某些特定的行為序列時,以 LSP 協(xié)議規(guī)定的通訊方式向 Language Server 發(fā)送動作與上下文參數(shù)

          • Language Server 根據(jù)這些參數(shù)異步地返回響應(yīng)信息

          • 編輯器再根據(jù)響應(yīng)信息處理交互反饋


          簡單說,編輯器負責(zé)與用戶直接交互, Language Server 負責(zé)在背后默默計算如何響應(yīng)用戶的交互動作,兩者以進程粒度分離、解耦,在 LSP 協(xié)議框架下各司其職又協(xié)作共生。就好像我們通常開發(fā)的 Web 應(yīng)用中,前端負責(zé)與用戶交互,服務(wù)端負責(zé)管理諸如權(quán)限、業(yè)務(wù)數(shù)據(jù)、業(yè)務(wù)狀態(tài)流轉(zhuǎn)等不可見的部分。
          目前,LSP 協(xié)議已經(jīng)發(fā)展到 3.16 版本,覆蓋大多數(shù)語言特性,包括:
          • 代碼補全
          • 代碼高亮
          • 定義跳轉(zhuǎn)
          • 類型推斷
          • 錯誤檢測
          • 等等
          得益于 LSP 清晰的設(shè)計,這些語言特性的開發(fā)套路都很相似,學(xué)習(xí)曲線很平滑,開發(fā)的時候基本上只需要關(guān)心監(jiān)聽那個函數(shù),返回什么格式的結(jié)構(gòu),可以說掌握上述幾個示例之后就可以很簡單地上手了。
          過去,IDE 對語言特性的支持是集成在 IDE 或者以同構(gòu)插件形式實現(xiàn)的,在 VSCode 中這種同構(gòu)擴展能力以 Language API 或 Sematic Tokens Provider 接口方式提供,這兩種方式在上一篇文章《你不知道的 VSCode 代碼高亮原理》都有過介紹了,雖然架構(gòu)上比較簡單,容易理解,但有一些明顯硬傷:
          • 插件開發(fā)者必須復(fù)用 VSCode 本身的開發(fā)語言、環(huán)境,例如 Python 語言插件就必須用 JavaScript 寫

          • 同一個編程語言需要為不同 IDE 重復(fù)開發(fā)相似的擴展插件,重復(fù)投入



          LSP 最大的優(yōu)勢就是將 IDE 客戶端與實際計算交互特性的服務(wù)端隔離開來,同一個 Language Service 可以重復(fù)應(yīng)用在多個不同 Language Client 中。
          此外,LSP 協(xié)議下客戶端、服務(wù)器分別在各自進程運行,在性能上也會有正向收益:
          • 確保 UI 進程不卡頓

          • Node 環(huán)境下,充分利用多核 CPU 能力

          • 由于不再限定 Language Server 的技術(shù)棧,開發(fā)者可以選擇更高性能的語言,例如 Go


          總的來說,就是很強。

          總結(jié)

          本文介紹了 VSCode 下,開發(fā)一款基于 LSP 的語言插件所需要具備的最最基本的技能,實際開發(fā)的時候通常還會混合另一種技術(shù):嵌入式語法 —— Embedded Languages Server ,實現(xiàn)復(fù)雜的多語言復(fù)合支持,如果有人感興趣,我們下周可以聊聊。


          點擊左下角閱讀原文,到 SegmentFault 思否社區(qū) 和文章作者展開更多互動和交流,掃描下方”二維碼“或在“公眾號后臺回復(fù)“ 入群 ”即可加入我們的技術(shù)交流群,收獲更多的技術(shù)文章~

          - END -

          瀏覽 54
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  亚洲欧洲在线看 | 久久精品熟女 | 亚洲性色网 | 狠狠干免费视频 | 竹菊一区二区 |