語言伺服器通訊協定
適用于:Visual Studio
Visual Studio for Mac
Visual Studio Code
什麼是語言伺服器通訊協定?
支援編輯器或 IDE 中程式設計語言的原始程式碼自動完成或 移至定義 等豐富編輯功能,傳統上非常具挑戰性且耗時。 通常需要在掃描器、剖析器、類型檢查程式、建立器等程式設計語言中,以編輯器或 IDE 的程式設計語言撰寫領域 (模型) 。 例如,Eclipse CDT 外掛程式提供 Eclipse IDE 中 C/C++ 的支援是以 JAVA 撰寫,因為 Eclipse IDE 本身是以 JAVA 撰寫。 遵循此方法,這表示在 TypeScript 中實作 C/C++ 領域模型,以Visual Studio Code,以及在適用于 Visual Studio 的 C# 中實作個別的定義域模型。
如果開發工具可以重複使用現有的語言特定程式庫,建立特定語言的定義域模型也比較容易。 不過,這些程式庫通常會以程式設計語言本身實作 (,例如,良好的 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 (掃描) 檔案的要求,並傳回一組偵測到的警告和錯誤。 目標是在檔中以使用者編輯的方式 Lint 檔案,這表示編輯器會話期間會有許多 Linting 要求。 讓伺服器保持啟動並執行是合理的做法,因此不需要針對每個使用者編輯啟動新的 Linting 程式。 已實作數個 linter 伺服器,包括 VS Code 的 ESLint 和 TSLint 延伸模組。 這兩個 linter 伺服器都是在 TypeScript/JavaScript 中實作,並在 Node.js 上執行。 他們會共用程式庫,以實作通訊協定的用戶端和伺服器部分。
LSP 的運作方式
語言伺服器會在自己的進程中執行,而 Visual Studio 或 VS Code 之類的工具會透過 JSON-RPC 與伺服器通訊。 在專用進程中運作的語言伺服器的另一個優點是避免與單一進程模型相關的效能問題。 如果用戶端和伺服器都以Node.js撰寫,實際的傳輸通道可以是 stdio、sockets、named pipes 或 node ipc。
以下是工具與語言伺服器在常式編輯會話期間如何通訊的範例:
使用者會在工具中開啟檔案 (稱為檔) :此工具會通知語言伺服器檔開啟 ('textDocument/didOpen') 。 從現在起,檔內容的事實不再位於檔案系統上,但由工具保留在記憶體中。
使用者進行編輯:此工具會通知伺服器檔變更 ('textDocument/didChange') ,而且語言伺服器會更新程式的語意資訊。 發生這種情況時,語言伺服器會分析此資訊,並通知工具偵測到的錯誤和警告, ('textDocument/publishDiagnostics') 。
使用者在編輯器中的符號上執行「Go to Definition」:此工具會傳送具有兩個參數的 'textDocument/definition' 要求: (1) 檔 URI, (2) 從移至定義要求起始到伺服器的文字位置。 伺服器會回應檔 URI,以及符號定義在檔內的位置。
使用者關閉檔 (檔案) :會從工具傳送 'textDocument/didClose' 通知,通知語言伺服器檔現在不再位於記憶體中,而且目前的內容現在已是檔案系統上的最新狀態。
此範例說明通訊協定如何與編輯器層級的語言伺服器通訊,例如「移至定義」、「尋找所有參考」。 通訊協定所使用的資料類型是編輯器或 IDE 'data types',例如目前開啟的文字檔和游標的位置。 資料類型不是程式設計語言領域模型的層級,通常會提供抽象語法樹狀架構和編譯器符號 (,例如解析的類型、命名空間...) 。這可大幅簡化通訊協定。
現在讓我們更詳細地查看 '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 檔案上運作的會話。
功能
並非所有語言伺服器都支援通訊協定所定義的所有功能。 因此,用戶端和伺服器會透過「功能」宣告其支援的功能集。 例如,伺服器會宣告它可以處理 'textDocument/definition' 要求,但可能不會處理 'workspace/symbol' 要求。 同樣地,用戶端可以宣告他們可以在儲存檔之前提供「即將儲存」通知,讓伺服器可以計算文字編輯,以自動格式化已編輯的檔。
整合語言伺服器
語言伺服器與特定工具的實際整合不是由語言伺服器通訊協定所定義,而且會保留給工具實作者。 某些工具透過擁有可啟動和與任何類型的語言伺服器交談的擴充功能,將語言伺服器一般整合。 VS Code 等其他專案會為每個語言伺服器建立自訂延伸模組,讓延伸模組仍能夠提供一些自訂語言功能。
為了簡化語言伺服器和用戶端的實作,用戶端和伺服器元件有程式庫或 SDK。 這些程式庫會針對不同的語言提供。 例如,有 語言用戶端 npm 模組 可讓您輕鬆地將語言伺服器整合到 VS Code 延伸模組,而另一個 語言伺服器 npm 模組 使用 Node.js 來撰寫語言伺服器。 這是目前的支援程式庫 清單 。
在 Visual Studio 中使用語言伺服器通訊協定
- 新增語言伺服器通訊協定延伸模組 - 瞭解如何將語言伺服器整合到 Visual Studio 中。