語言伺服器通訊協定

什麼是語言伺服器協定?

支援編輯器或 IDE 中程式設計語言的豐富編輯功能,例如原始程式碼自動完成或移至定義,這在傳統上非常具有挑戰性且耗時。 通常需要以編輯器或 IDE 的程式設計語言撰寫領域模型 (掃描器、剖析器、類型檢查器、建立器等)。 例如,Eclipse CDT 外掛程式提供 Eclipse IDE 中 C/C++ 的支援是以 Java 撰寫,因為 Eclipse IDE 本身是以 Java 撰寫。 遵循這種方法,表示在 TypeScript for Visual Studio Code 中實作 C/C++ 領域模型,以及在 C# for Visual Studio 中實作個別的定義域模型。

如果開發工具可以重複使用現有的語言特定程式庫,建立特定語言的領域模型也會容易許多。 不過,這些程式庫通常會以程式設計語言本身實作 (例如,良好的 C/C++ 領域模型是在 C/C++ 中實作)。 在技術上,將 C/C++ 程式庫整合到以 TypeScript 撰寫的編輯器中是可行的,但很難做到。

語言伺服器

另一種方法是在自己的處理程序中執行程式庫,並使用處理程序間通訊來與其通訊。 來回傳送的訊息即形成通訊協定。 語言伺服器通訊協定 (LSP) 是標準化開發工具和語言伺服器處理程序間交換之訊息的產物。 使用語言伺服器或示範不是新的或新穎的想法。 Vim 和 Emacs 等編輯器已經這麼做了一段時間,以提供語意自動完成支援。 LSP 的目標是簡化這種整合,並提供實用架構,以將語言功能公開至各種工具。

擁有通用通訊協定,可藉由重複使用語言領域模型的現有實作,以最不麻煩的方式將程式語言功能整合到開發工具中。 語言伺服器後端可以用 PHP、Python 或 Java 撰寫,而 LSP 可協助輕鬆整合到各種工具中。 通訊協定可在一般抽象層級運作,讓工具可以提供豐富的語言服務,而無需完全了解特定於底層網域模型的細微差別。

LSP 的運作方式已啟動

LSP 經過一段時間演進後,至今已經是 3.0 版。 從 OmniSharp 挑選語言伺服器的概念開始作業,以提供 C# 的豐富編輯功能。 起初 OmniSharp 使用具有 JSON 承載的 HTTP 通訊協定,並已整合到數個編輯器中,包括 Visual Studio Code

差不多相同時間內,Microsoft 開始在 TypeScript 語言伺服器上工作,並想在 Emacs 和 Sublime Text 等編輯器中支援 TypeScript。 在此實作中,編輯器透過 stdin/stdout 與 TypeScript 伺服器處理程序通訊,並使用受 V8 偵錯工具通訊協定啟發的 JSON 承載來處理要求和回應。 TypeScript 伺服器已整合到 TypeScript Sublime 外掛程式和 VS Code 中,進行豐富的 TypeScript 編輯。

整合兩部不同的語言伺服器後,VS Code 小組開始探索編輯器和 IDE 的通用語言伺服器通訊協定。 通用通訊協定可讓語言提供者建立由不同 IDE 取用的單一語言伺服器。 語言伺服器取用者只需要實作通訊協定的用戶端一次。 這會為語言提供者和語言取用者帶來雙贏。

語言伺服器通訊協定從 TypeScript 伺服器使用的通訊協定開始,透過 VS Code 語言 API 所啟發的更多語言功能進行擴充。 由於通訊協定的簡易性和現有程式庫,因此會利用 JSON-RPC 支援遠端叫用。

VS Code 小組藉由實作數個 linter 語言伺服器來建立通訊協定的原型,以回應對 lint (scan) 檔案的要求,並傳回一組偵測到的警告和錯誤。 目標是在使用者編輯文件時對檔案進行lint,這表示在編輯器工作階段將會有許多 linting 要求。 讓伺服器保持啟動並執行是有原因的,如此就不需要針對每個使用者編輯啟動新的 Linting 程序。 已實作數個 linter 伺服器,包括 VS Code 的 ESLint 和 TSLint 擴充功能。 這兩個 linter 伺服器都在 TypeScript/JavaScript 中實作,並在 Node.js 上執行。 他們會共用程式庫,實作通訊協定的用戶端和伺服器部分。

LSP 的運作方式

語言伺服器會在自己的處理程序中執行,而 Visual Studio 或 VS Code 等工具會透過 JSON-RPC 與伺服器通訊。 在專用處理程序中運作的語言伺服器的另一個優點是,避免發生與單一處理程序模型相關的效能問題。 如果用戶端和伺服器都以 Node.js 撰寫,實際的傳輸通道可以是 stdio、通訊端、具名管道或節點 ipc。

以下是工具與語言伺服器在例行編輯工作階段通訊的範例:

lsp flow diagram

  • 使用者會在工具中開啟檔案 (稱為文件):此工具會通知語言伺服器文件已開啟 ('textDocument/didOpen')。 從現在起,文件內容的事實已不在檔案系統上,而是由工具保留在記憶體中。

  • 使用者進行編輯:此工具會通知伺服器文件變更 ('textDocument/didChange') ,而程式語意資訊會由語言伺服器更新。 發生這種情況時,語言伺服器會分析這項資訊,並通知工具偵測到的錯誤和警告 ('textDocument/publishDiagnostics')。

  • 使用者會在編輯器中的符號上執行「移至定義」:此工具會傳送具有兩個參數的 'textDocument/definition' 要求:(1) 文件 URI 和 (2) 起始至伺服器的文字位置。 伺服器會以文件 URI 和符號定義在文件內的位置回應。

  • 使用者關閉文件 (檔案):從工具傳送 'textDocument/didClose' 通知,知會語言伺服器文件現在不再處於記憶體中,而且目前的內容在檔案系統上是最新的。

此範例說明通訊協定如何與編輯器功能層級的語言伺服器進行通訊,例如「移至定義」、「尋找所有參考」。 通訊協定所使用的資料類型是編輯器或 IDE「資料類型」,例如目前開啟的文字文件和游標的位置。 資料類型不在程式設計語言領域模型的層級,通常提供抽象語法樹狀架構和編譯器符號 (例如解析的類型、命名空間...)。這大幅簡化了通訊協定。

現在我們來更詳細查看 'textDocument/definition' 要求。 以下是用戶端工具與 C++ 文件中「移至定義」要求的語言伺服器之間的承載。

此為要求:

{
    "jsonrpc": "2.0",
    "id" : 1,
    "method": "textDocument/definition",
    "params": {
        "textDocument": {
            "uri": "file:///p%3A/mseng/VSCode/Playgrounds/cpp/use.cpp"
        },
        "position": {
            "line": 3,
            "character": 12
        }
    }
}

此為回應:

{
    "jsonrpc": "2.0",
    "id": "1",
    "result": {
        "uri": "file:///p%3A/mseng/VSCode/Playgrounds/cpp/provide.cpp",
        "range": {
            "start": {
                "line": 0,
                "character": 4
            },
            "end": {
                "line": 0,
                "character": 11
            }
        }
    }
}

回想一下,描述編輯器層級而不是程式設計語言模型的層級的資料類型,是語言伺服器通訊協定成功的原因之一。 相較於在不同程式設計語言中標準化抽象語法樹狀結構與編譯器符號,將文字文件 URI 或游標位置標準化要簡單得多。

當使用者使用不同的語言時,VS Code 通常會針對每個程式設計語言啟動語言伺服器。 下列範例展示了使用者在 Java 和 SASS 檔案上運作的工作階段。

java and sass

功能

並非所有語言伺服器都支援通訊協定所定義的所有功能。 因此,用戶端和伺服器會透過「功能」宣告其支援的功能集。 例如,伺服器會宣告它可以處理 'textDocument/definition' 要求,但可能不會處理 'workspace/symbol' 要求。 同樣的,用戶端可以宣佈能夠在儲存文件之前提供「即將儲存」通知,讓伺服器可以計算文字編輯,以自動格式化編輯的文件。

整合語言伺服器

語言伺服器與特定工具的實際整合不是由語言伺服器通訊協定所定義,而是留給工具實作者。 某些工具一般都會透過具有可啟動和與任何類型的語言伺服器交談的擴充功能來整合語言伺服器。 其他像 VS Code,會為每個語言伺服器建立自訂擴充功能,讓擴充功能仍能提供一些自訂語言功能。

為了簡化語言伺服器和用戶端的實作,用戶端和伺服器元件都有程式庫或 SDK。 這些程式庫會針對不同的語言提供。 例如,有一種語言用戶端 npm 模組可簡單地將語言伺服器整合到 VS Code 擴充功能和另一個語言伺服器 npm 模組 中,以使用 Node.js 撰寫語言伺服器。 這是目前的支援程式庫清單

在 Visual Studio 中使用語言伺服器通訊協定