語言服務器協議教程:從 VSCode 到 Vim
已發表: 2022-03-11您所有工作的主要工件很可能是純文本文件。 那麼為什麼不使用記事本來創建它們呢?
語法高亮和自動格式化只是冰山一角。 linting、代碼完成和半自動重構呢? 這些都是使用“真正的”代碼編輯器的好理由。 這些對我們的日常生活至關重要,但我們了解它們的工作原理嗎?
在本語言服務器協議教程中,我們將稍微探討一下這些問題,並找出是什麼讓我們的文本編輯器打勾。 最後,我們將一起實現一個基本的語言服務器以及 VSCode、Sublime Text 3 和 Vim 的示例客戶端。
編譯器與語言服務
我們現在將跳過語法突出顯示和格式化,這是通過靜態分析處理的——這本身就是一個有趣的話題——並專注於我們從這些工具中獲得的主要反饋。 有兩個主要類別:編譯器和語言服務。
編譯器接收您的源代碼並輸出不同的形式。 如果代碼不遵循語言規則,編譯器將返回錯誤。 這些都很熟悉。 這樣做的問題是它通常非常緩慢且範圍有限。 在您仍在創建代碼時提供幫助怎麼樣?
這就是語言服務所提供的。 它們可以讓您在代碼庫仍在工作時深入了解它,並且可能比編譯整個項目要快得多。
這些服務的範圍是多種多樣的。 它可以像返回項目中所有符號的列表一樣簡單,也可以像返回重構代碼的步驟那樣複雜。 這些服務是我們使用代碼編輯器的主要原因。 如果我們只是想編譯並查看錯誤,我們可以通過幾次擊鍵來完成。 語言服務為我們提供了更多見解,而且速度非常快。
押注文本編輯器進行編程
請注意,我們尚未調用特定的文本編輯器。 讓我們用一個例子來解釋為什麼。
假設您開發了一種名為 Lapine 的新編程語言。 這是一門漂亮的語言,編譯器會給出非常棒的類似 Elm 的錯誤消息。 此外,您還可以提供代碼完成、參考、重構幫助和診斷。
您首先支持哪個代碼/文本編輯器? 那之後呢? 為了讓人們採用它,你需要進行一場艱苦的戰鬥,所以你想讓它盡可能簡單。 您不想選擇錯誤的編輯器並錯過用戶。 如果您與代碼編輯器保持距離並專注於您的專長——語言及其特性會怎樣?
語言服務器
輸入語言服務器。 這些是與語言客戶交談並提供我們提到的見解的工具。 由於我們剛剛在假設情況中描述的原因,它們獨立於文本編輯器。
像往常一樣,另一層抽象正是我們所需要的。 這些承諾打破語言工具和代碼編輯器的緊密耦合。 語言創建者可以將他們的功能包裝在服務器中一次,代碼/文本編輯器可以添加小的擴展來將自己變成客戶端。 這對每個人來說都是一場胜利。 但是,為了促進這一點,我們需要就這些客戶端和服務器的通信方式達成一致。
幸運的是,這不是假設。 Microsoft 已經開始定義語言服務器協議。
與大多數偉大的想法一樣,它產生於必要性而非遠見。 許多代碼編輯器已經開始添加對各種語言特性的支持; 一些功能外包給第三方工具,一些在編輯器內部完成。 可擴展性問題出現了,微軟率先進行了拆分。 是的,微軟為將這些功能從代碼編輯器中移出而不是將它們囤積在 VSCode 中鋪平了道路。 他們本可以繼續構建他們的編輯器,鎖定用戶——但他們讓他們自由了。
語言服務器協議
語言服務器協議 (LSP) 於 2016 年定義,以幫助分離語言工具和編輯器。 上面還有很多 VSCode 指紋,但這是朝著編輯器不可知論方向邁出的重要一步。 讓我們稍微檢查一下協議。
客戶端和服務器——想想代碼編輯器和語言工具——通過簡單的文本消息進行通信。 這些消息具有類似 HTTP 的標頭、JSON-RPC 內容,並且可能來自客戶端或服務器。 JSON-RPC 協議定義了請求、響應和通知以及圍繞它們的一些基本規則。 一個關鍵特性是它被設計為異步工作,因此客戶端/服務器可以無序地處理消息並具有一定程度的並行性。
簡而言之,JSON-RPC 允許客戶端請求另一個程序運行帶參數的方法並返回結果或錯誤。 LSP 以此為基礎,定義了可用的方法、預期的數據結構以及圍繞事務的更多規則。 例如,客戶端啟動服務器時有一個握手過程。
服務器是有狀態的,並且一次只能處理一個客戶端。 但是,對通信沒有明確的限制,因此語言服務器可以在與客戶端不同的機器上運行。 不過,在實踐中,這對於實時反饋來說會很慢。 語言服務器和客戶端使用相同的文件並且非常健談。
一旦您知道要查找什麼,LSP 就會有大量的文檔。 如前所述,其中大部分是在 VSCode 的上下文中編寫的,儘管這些想法有更廣泛的應用。 例如,協議規範都是用 TypeScript 編寫的。 為了幫助不熟悉 VSCode 和 TypeScript 的探索者,這裡有一個入門指南。
LSP 消息類型
語言服務器協議中定義了許多消息組。 它們可以大致分為“管理”和“語言功能”。 管理消息包含客戶端/服務器握手、打開/更改文件等中使用的消息。重要的是,這是客戶端和服務器共享它們處理的功能的地方。 當然,不同的語言和工具提供不同的功能。 這也允許增量採用。 Langserver.org 列出了客戶端和服務器應支持的六項關鍵特性,其中至少一項是列出該列表所必需的。
語言特性是我們最感興趣的。其中,有一個需要特別指出:診斷信息。 診斷是關鍵功能之一。 當您打開一個文件時,通常假定它會運行。 您的編輯應該告訴您文件是否有問題。 LSP 發生這種情況的方式是:
- 客戶端打開文件並將
textDocument/didOpen
發送到服務器。 - 服務器分析文件並發送
textDocument/publishDiagnostics
通知。 - 客戶端解析結果並在編輯器中顯示錯誤指示符。
這是從您的語言服務中獲取洞察力的被動方式。 一個更活躍的示例是查找光標下符號的所有引用。 這將是這樣的:
- 客戶端將
textDocument/references
發送到服務器,指定文件中的位置。 - 服務器找出符號,在此文件和其他文件中查找引用,並以列表響應。
- 客戶端向用戶顯示引用。
黑名單工具
我們當然可以深入研究語言服務器協議的細節,但讓我們把它留給客戶端實現者。 為了鞏固編輯器和語言工具分離的理念,我們將扮演工具創建者的角色。
我們將保持簡單,而不是創建新的語言和功能,我們將堅持診斷。 診斷非常適合:它們只是關於文件內容的警告。 linter 返回診斷信息。 我們會做類似的東西。
我們將製作一個工具來通知我們想要避免的單詞。 然後,我們將向幾個不同的文本編輯器提供該功能。
語言服務器
首先,工具。 我們將把它直接烘焙到語言服務器中。 為簡單起見,這將是一個 Node.js 應用程序,儘管我們可以使用任何能夠使用流進行讀寫的技術來做到這一點。
這是邏輯。 給定一些文本,此方法返回匹配的列入黑名單的單詞的數組以及找到它們的索引。
const getBlacklisted = (text) => { const blacklist = [ 'foo', 'bar', 'baz', ] const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi') const results = [] while ((matches = regex.exec(text)) && results.length < 100) { results.push({ value: matches[0], index: matches.index, }) } return results }
現在,讓我們讓它成為一個服務器。
const { TextDocuments, createConnection, } = require('vscode-languageserver') const {TextDocument} = require('vscode-languageserver-textdocument') const connection = createConnection() const documents = new TextDocuments(TextDocument) connection.onInitialize(() => ({ capabilities: { textDocumentSync: documents.syncKind, }, })) documents.listen(connection) connection.listen()
在這裡,我們使用vscode-languageserver
。 這個名字有誤導性,因為它當然可以在 VSCode 之外工作。 這是您看到的關於 LSP 起源的眾多“指紋”之一。 vscode-languageserver
負責處理較低級別的協議,並允許您專注於用例。 此片段啟動連接並將其綁定到文檔管理器。 當客戶端連接到服務器時,服務器會告訴它它希望收到有關正在打開的文本文檔的通知。
我們可以在這裡停下來。 這是一個功能齊全但毫無意義的 LSP 服務器。 相反,讓我們使用一些診斷信息來響應文檔更改。
documents.onDidChangeContent(change => { connection.sendDiagnostics({ uri: change.document.uri, diagnostics: getDiagnostics(change.document), }) })
最後,我們將更改的文檔、我們的邏輯和診斷響應之間的點聯繫起來。
const getDiagnostics = (textDocument) => getBlacklisted(textDocument.getText()) .map(blacklistToDiagnostic(textDocument)) const { DiagnosticSeverity, } = require('vscode-languageserver') const blacklistToDiagnostic = (textDocument) => ({ index, value }) => ({ severity: DiagnosticSeverity.Warning, range: { start: textDocument.positionAt(index), end: textDocument.positionAt(index + value.length), }, message: `${value} is blacklisted.`, source: 'Blacklister', })
我們的診斷負載將是通過我們的函數運行文檔文本的結果,然後映射到客戶端期望的格式。
該腳本將為您創建所有這些。
curl -o- https://raw.githubusercontent.com/reergymerej/lsp-article-resources/revision-for-6.0.0/blacklist-server-install.sh | bash
注意:如果您對陌生人向您的機器添加可執行文件感到不舒服,請檢查源代碼。 它會為您創建項目、下載index.js
和npm link
。

完整的服務器源
最終的blacklist-server
來源是:
#!/usr/bin/env node const { DiagnosticSeverity, TextDocuments, createConnection, } = require('vscode-languageserver') const {TextDocument} = require('vscode-languageserver-textdocument') const getBlacklisted = (text) => { const blacklist = [ 'foo', 'bar', 'baz', ] const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi') const results = [] while ((matches = regex.exec(text)) && results.length < 100) { results.push({ value: matches[0], index: matches.index, }) } return results } const blacklistToDiagnostic = (textDocument) => ({ index, value }) => ({ severity: DiagnosticSeverity.Warning, range: { start: textDocument.positionAt(index), end: textDocument.positionAt(index + value.length), }, message: `${value} is blacklisted.`, source: 'Blacklister', }) const getDiagnostics = (textDocument) => getBlacklisted(textDocument.getText()) .map(blacklistToDiagnostic(textDocument)) const connection = createConnection() const documents = new TextDocuments(TextDocument) connection.onInitialize(() => ({ capabilities: { textDocumentSync: documents.syncKind, }, })) documents.onDidChangeContent(change => { connection.sendDiagnostics({ uri: change.document.uri, diagnostics: getDiagnostics(change.document), }) }) documents.listen(connection) connection.listen()
語言服務器協議教程:試駕時間
項目被link
後,嘗試運行服務器,指定stdio
作為傳輸機制:
blacklist-server --stdio
它現在在stdio
上監聽我們之前討論過的 LSP 消息。 我們可以手動提供這些,但讓我們創建一個客戶端。
語言客戶端:VSCode
由於這項技術起源於 VSCode,因此從那裡開始似乎很合適。 我們將創建一個擴展,該擴展將創建一個 LSP 客戶端並將其連接到我們剛剛創建的服務器。
有多種方法可以創建 VSCode 擴展,包括使用 Yeoman 和適當的生成generator-code
。 不過,為簡單起見,讓我們做一個準系統示例。
讓我們克隆樣板並安裝它的依賴項:
git clone [email protected]:reergymerej/standalone-vscode-ext.git blacklist-vscode cd blacklist-vscode npm i # or yarn
在 VSCode 中打開blacklist-vscode
目錄。
按 F5 啟動另一個 VSCode 實例,調試擴展。
在第一個 VSCode 實例的“調試控制台”中,您將看到文本“看,媽媽。 延期!”
我們現在已經有了一個基本的 VSCode 擴展,它不需要所有的花里胡哨。 讓它成為一個 LSP 客戶端。 關閉兩個 VSCode 實例並從blacklist-vscode
目錄中運行:
npm i vscode-languageclient
將 extension.js 替換為:
const { LanguageClient } = require('vscode-languageclient') module.exports = { activate(context) { const executable = { command: 'blacklist-server', args: ['--stdio'], } const serverOptions = { run: executable, debug: executable, } const clientOptions = { documentSelector: [{ scheme: 'file', language: 'plaintext', }], } const client = new LanguageClient( 'blacklist-extension-id', 'Blacklister', serverOptions, clientOptions ) context.subscriptions.push(client.start()) }, }
這使用vscode-languageclient
包在 VSCode 中創建 LSP 客戶端。 與vscode-languageserver
不同,它與 VSCode 緊密耦合。 簡而言之,我們在這個擴展中所做的是創建一個客戶端並告訴它使用我們在前面的步驟中創建的服務器。 回顧 VSCode 擴展細節,我們可以看到我們告訴它使用這個 LSP 客戶端來處理純文本文件。
要測試驅動它,請在 VSCode 中打開blacklist-vscode
目錄。 按 F5 啟動另一個實例,調試擴展。
在新的 VSCode 實例中,創建一個純文本文件並保存。 輸入“foo”或“bar”並稍等片刻。 您將看到這些被列入黑名單的警告。
而已! 我們不必重新創建任何邏輯,只需協調客戶端和服務器。
讓我們為另一個編輯器再做一次,這次是 Sublime Text 3。這個過程將非常相似並且更容易一些。
語言客戶端:Sublime Text 3
首先,打開 ST3 並打開命令面板。 我們需要一個框架來使編輯器成為 LSP 客戶端。 輸入“包控制:安裝包”並按回車鍵。 找到“LSP”包並安裝它。 完成後,我們可以指定 LSP 客戶端。 有很多預設,但我們不會使用它們。 我們已經創建了自己的。
再次打開命令面板。 找到“首選項:LSP 設置”並按 Enter。 這將打開 LSP 包的配置文件LSP.sublime-settings
。 要添加自定義客戶端,請使用以下配置。
{ "clients": { "blacklister": { "command": [ "blacklist-server", "--stdio" ], "enabled": true, "languages": [ { "syntaxes": [ "Plain text" ] } ] } }, "log_debug": true }
這在 VSCode 擴展中可能看起來很熟悉。 我們定義了一個客戶端,告訴它處理純文本文件,並指定語言服務器。
保存設置,然後創建並保存純文本文件。 輸入“foo”或“bar”並等待。 同樣,您會看到這些被列入黑名單的警告。 處理方式——消息在編輯器中的顯示方式——是不同的。 但是,我們的功能是相同的。 這次我們幾乎沒有做任何事情來增加對編輯器的支持。
語言“客戶端”:Vim
如果您仍然不相信這種關注點分離可以輕鬆地在文本編輯器之間共享功能,這裡是通過 Coc 將相同功能添加到 Vim 的步驟。
打開 Vim 並輸入:CocConfig
,然後添加:
"languageserver": { "blacklister": { "command": "blacklist-server", "args": ["--stdio"], "filetypes": ["text"] } }
完畢。
客戶端-服務器分離讓語言和語言服務蓬勃發展
將語言服務的職責與使用它們的文本編輯器分開顯然是一種勝利。 它允許語言功能創建者專注於他們的專業,而編輯器創建者也可以這樣做。 這是一個相當新的想法,但正在普及。
現在您已經有了工作的基礎,也許您可以找到一個項目並幫助推動這個想法。 編輯器的火焰戰爭永遠不會結束,但沒關係。 只要語言能力可以存在於特定編輯器之外,您就可以自由使用您喜歡的任何編輯器。
作為 Microsoft 金牌合作夥伴,Toptal 是您的 Microsoft 專家精英網絡。 與您需要的專家一起建立高績效團隊 - 在您需要的任何時間和地點!