實例解析:如何開發(fā) VSCode LSP 服務(wù)
作者:范文杰
來源:SegmentFault 思否社區(qū)
從一張動圖說起:

上圖應(yīng)該大家經(jīng)常使用的 錯誤診斷 功能,它能夠在你編寫代碼的過程中提示,那一塊代碼存在什么類型的問題。
這個看似高大上的功能,從插件開發(fā)者的角度看其實特別簡單,基本上就是用上一篇文章《你不知道的 VSCode 代碼高亮原理》中簡單介紹過的 VSCode 開發(fā)語言特性的三種方案:
基于 Sematic Tokens Provider 協(xié)議的詞法高亮
基于 Language API 的編程式語法高亮
基于 Language Server Protocol 的多進程架構(gòu)語法高亮
示例代碼
# 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)試
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();創(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/nodevscode-languageserver-textdocument
connection.onXXX 或 documents.onXXX 監(jiān)聽各類交互事件,并在事件回調(diào)中返回符合 LSP 協(xié)議的結(jié)果,或者顯式調(diào)用通訊函數(shù)如 connection.sendDiagnostics 發(fā)送交互信息。懸停提示

hoverProvider 特性:connection.onInitialize((params: InitializeParams) => {
return {
capabilities: {
hoverProvider: true
},
};
});
connection.onHover 事件,并在事件回調(diào)中返回提示信息:connection.onHover((params: HoverParams): Promise<Hover> => {
return Promise.resolve({
contents: ["Hover Demo"],
});
});
代碼格式化

documentFormattingProvider 特性:{
...
capabilities : {
documentFormattingProvider: true
...
}
}
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);
}
);

函數(shù)簽名

documentFormattingProvider 特性:{
...
capabilities : {
signatureHelpProvider: {
triggerCharacters: ["("],
}
...
}
}
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,
});
}
);

錯誤提示
首先不需要通過
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 });
});
sendDiagnostics 發(fā)送相應(yīng)的錯誤信息,實現(xiàn)效果:
如何識別事件與響應(yīng)體
https://zjsms.com/egWtqPj/ , VSCode 官網(wǎng)關(guān)于可編程語言特性的說明文檔
https://zjsms.com/egWVTPg/ ,LSP 協(xié)議官網(wǎng)

vscode-languageserver 包提供了非常完善的 Typescript 類型定義,我們完全可以借助 ts + VSCode 的代碼提示找到需要使用的監(jiān)聽函數(shù):
深入理解 LSP
定義 client 與 server 之間的通訊模型,也就是誰、在什么時候、以什么方式向?qū)Ψ桨l(fā)送什么格式的信息,接收方又以什么方式返回響應(yīng)信息
定義通訊信息體,也就是以什么格式、什么字段、什么樣的值表達信息狀態(tài)
編輯器如 VSCode 跟蹤、計算、管理用戶行為模型,在發(fā)生某些特定的行為序列時,以 LSP 協(xié)議規(guī)定的通訊方式向 Language Server 發(fā)送動作與上下文參數(shù)
Language Server 根據(jù)這些參數(shù)異步地返回響應(yīng)信息
編輯器再根據(jù)響應(yīng)信息處理交互反饋

代碼補全 代碼高亮 定義跳轉(zhuǎn) 類型推斷 錯誤檢測 等等
插件開發(fā)者必須復(fù)用 VSCode 本身的開發(fā)語言、環(huán)境,例如 Python 語言插件就必須用 JavaScript 寫
同一個編程語言需要為不同 IDE 重復(fù)開發(fā)相似的擴展插件,重復(fù)投入

確保 UI 進程不卡頓
Node 環(huán)境下,充分利用多核 CPU 能力
由于不再限定 Language Server 的技術(shù)棧,開發(fā)者可以選擇更高性能的語言,例如 Go
總結(jié)

