新增語言伺服器通訊協定延伸模組

語言伺服器通訊協定 (LSP) 是以 JSON RPC v2.0 的形式,用來提供語言服務功能給各種程式碼編輯器的通用通訊協定。 開發人員可以使用通訊協定撰寫單一語言伺服器,以提供 IntelliSense、錯誤診斷、尋找所有參考等語言服務功能給支援 LSP 的各種程式碼編輯器。 傳統上,您可以使用 TextMate 文法檔案來新增 Visual Studio 中的語言服務,以提供基本功能,例如語法醒目提示,或撰寫使用完整 Visual Studio 擴充性 API 集合的自定義語言服務來提供更豐富的數據。 透過 Visual Studio 對 LSP 的支援,有第三個選項。

language server protocol service in Visual Studio

若要確保最佳的用戶體驗,請考慮實 作語言設定,該設定可提供許多相同作業的本機處理,因此可以改善 LSP 所支援之許多語言特定編輯器作業的效能。

語言伺服器通訊協定

language server protocol implementation

本文說明如何建立使用 LSP 語言伺服器的 Visual Studio 延伸模組。 它假設您已經開發 LSP 型語言伺服器,並只想將其整合到 Visual Studio 中。

如需 Visual Studio 內的支援,語言伺服器可以透過任何以數據流為基礎的傳輸機制與客戶端通訊,例如:

  • 標準輸入/輸出數據流
  • 具名管道
  • 套接字 (僅限 TCP)

在 Visual Studio 中,LSP 和支援的意圖是將不屬於 Visual Studio 產品的語言服務上線。 它不適合在Visual Studio中擴充現有的語言服務(例如 C#)。 若要擴充現有的語言,請參閱語言服務的擴充性指南(例如 ,“Roslyn” .NET 編譯程式平臺),或請參閱 擴充編輯器和語言服務

如需通訊協定本身的詳細資訊,請參閱這裡的

如需如何建立範例語言伺服器或如何將現有語言伺服器整合到Visual Studio Code 的詳細資訊,請參閱這裡的

語言伺服器通訊協定支援的功能

下表顯示 Visual Studio 支援哪些 LSP 功能:

訊息 在 Visual Studio 中具有支援
初始化
初始化
shutdown
exit
$/cancelRequest
window/showMessage
window/showMessageRequest
window/logMessage
telemetry/event
client/registerCapability
client/unregisterCapability
workspace/didChangeConfiguration
workspace/didChangeWatchedFiles
工作區/符號
workspace/executeCommand
workspace/applyEdit
textDocument/publishDiagnostics
textDocument/didOpen
textDocument/didChange
textDocument/willSave
textDocument/willSaveWaitUntil
textDocument/didSave
textDocument/didClose
textDocument/completion
完成/解決
textDocument/hover
textDocument/signatureHelp
textDocument/references
textDocument/documentHighlight
textDocument/documentSymbol
textDocument/formatting
textDocument/rangeFormatting
textDocument/onTypeFormatting
textDocument/definition
textDocument/codeAction
textDocument/codeLens
codeLens/resolve
textDocument/documentLink
documentLink/resolve
textDocument/rename

開始使用

注意

從 Visual Studio 2017 15.8 版開始,通用語言伺服器通訊協議的支援會內建於 Visual Studio 中。 如果您已使用預覽 語言伺服器用戶端 VSIX 版本建置 LSP 延伸模組,當您升級至 15.8 版或更新版本之後,它們就會停止運作。 您必須執行下列動作,才能讓 LSP 延伸模組再次運作:

  1. 卸載 Microsoft Visual Studio Language Server 通訊協定預覽 VSIX。

    從 15.8 版開始,每次您在 Visual Studio 中執行升級時,都會自動偵測並移除預覽 VSIX。

  2. 將 Nuget 參考更新為 LSP 套件的最新非預覽版本

  3. 拿掉 VSIX 指令清單中 Microsoft Visual Studio 語言伺服器通訊協定預覽 VSIX 的相依性。

  4. 請確定 VSIX 指定 Visual Studio 2017 15.8 版 Preview 3 做為安裝目標的下限。

  5. 重建和重新部署。

建立 VSIX 專案

若要使用以 LSP 為基礎的語言伺服器建立語言服務延伸模組,請先確定您 已安裝 VS 實例的 Visual Studio 延伸模塊開發 工作負載。

接下來,流覽至 [檔案>新專案>Visual C#>擴充性>VSIX 專案],以建立新的 VSIX 專案:

create vsix project

語言伺服器和運行時間安裝

根據預設,在 Visual Studio 中建立以支援 LSP 語言伺服器的延伸模組不會包含執行它們所需的語言伺服器本身或運行時間。 延伸模組開發人員負責散發語言伺服器和所需的運行時間。 有數種方式可以執行此動作:

  • 語言伺服器可以內嵌在 VSIX 中做為內容檔案。
  • 建立 MSI 以安裝語言伺服器和/或所需的運行時間。
  • 提供 Marketplace 的指示,告知使用者如何取得運行時間和語言伺服器。

TextMate 文法檔案

LSP 不包含如何為語言提供文字色彩標示的規格。 若要在 Visual Studio 中提供語言的自訂色彩設定,延伸模塊開發人員可以使用 TextMate 文法檔案。 若要新增自定義 TextMate 文法或主題檔案,請遵循下列步驟:

  1. 在您的延伸模組內建立名為 「Grammars」 的資料夾(或可以是您選擇的任何名稱)。

  2. 在 Grammars 資料夾內,包含任何 *.tmlanguage*.plist*.tmtheme*.json您想要提供自定義色彩化的檔案。

    提示

    .tmtheme 檔案會定義範圍如何對應至 Visual Studio 分類(具名色彩索引鍵)。 如需指引,您可以在 %ProgramFiles(x86)%\Microsoft Visual Studio\<version<>\SKU>\Common7\IDE\CommonExtensions\Microsoft\TextMate\Starterkit\Themesg 目錄中參考全域 .tmtheme 檔案

  3. 建立 .pkgdef 檔案,並新增類似以下的行:

    [$RootKey$\TextMate\Repositories]
    "MyLang"="$PackageFolder$\Grammars"
    
  4. 以滑鼠右鍵按下檔案,然後選取 [ 屬性]。 將 [建置] 動作變更為 [內容],並將 [包含於 VSIX] 屬性變更true

完成上述步驟之後, Grammars 資料夾會新增至套件的安裝目錄,作為名為 『MyLang』 的存放庫來源('MyLang' 只是釐清的名稱,而且可以是任何唯一字符串)。 此目錄中的所有文法 (.tmlanguage 檔案) 和主題檔案 (.tmtheme 檔案) 都會被挑選為潛力,並取代 TextMate 提供的內建文法。 如果文法檔案的宣告延伸模組符合所開啟檔案的擴展名,TextMate 將會逐步執行。

建立簡單的語言用戶端

主要介面 - ILanguageClient

建立 VSIX 項目之後,請將下列 NuGet 套件新增至您的專案:

注意

完成先前步驟之後,當您相依於 NuGet 套件時,也會將 Newtonsoft.Json 和 StreamJsonRpc 套件新增至您的專案。 除非您確定這些新版本會安裝在擴充功能目標Visual Studio版本上,否則請勿更新這些套件。 VSIX 中不會包含元件;而是會從 Visual Studio 安裝目錄挑選它們。 如果您參考的元件版本比使用者計算機上安裝的版本還新,您的擴充功能將無法運作。

然後,您可以建立實作 ILanguageClient 介面的新類別,這是連線到 LSP 型語言伺服器之語言用戶端所需的主要介面。

以下是範例:

namespace MockLanguageExtension
{
    [ContentType("bar")]
    [Export(typeof(ILanguageClient))]
    public class BarLanguageClient : ILanguageClient
    {
        public string Name => "Bar Language Extension";

        public IEnumerable<string> ConfigurationSections => null;

        public object InitializationOptions => null;

        public IEnumerable<string> FilesToWatch => null;

        public event AsyncEventHandler<EventArgs> StartAsync;
        public event AsyncEventHandler<EventArgs> StopAsync;

        public async Task<Connection> ActivateAsync(CancellationToken token)
        {
            await Task.Yield();

            ProcessStartInfo info = new ProcessStartInfo();
            info.FileName = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Server", @"MockLanguageServer.exe");
            info.Arguments = "bar";
            info.RedirectStandardInput = true;
            info.RedirectStandardOutput = true;
            info.UseShellExecute = false;
            info.CreateNoWindow = true;

            Process process = new Process();
            process.StartInfo = info;

            if (process.Start())
            {
                return new Connection(process.StandardOutput.BaseStream, process.StandardInput.BaseStream);
            }

            return null;
        }

        public async Task OnLoadedAsync()
        {
            await StartAsync.InvokeAsync(this, EventArgs.Empty);
        }

        public Task OnServerInitializeFailedAsync(Exception e)
        {
            return Task.CompletedTask;
        }

        public Task OnServerInitializedAsync()
        {
            return Task.CompletedTask;
        }
    }
}

需要實作的主要方法是 OnLoadedAsync 和 ActivateAsync當 Visual Studio 已載入您的延伸模組,且您的語言伺服器已準備好啟動時,就會呼叫 OnLoadedAsync 。 在此方法中,您可以立即叫 用 StartAsync 委派,以發出語言伺服器應該啟動的訊號,或稍後可以執行其他邏輯並叫 用 StartAsync若要啟用您的語言伺服器,您必須在某個時間點呼叫 StartAsync。

ActivateAsync 是最終藉由呼叫 StartAsync 委派來叫用的方法。 其中包含啟動語言伺服器的邏輯,並與其建立連線。 必須傳回連接物件,其中包含寫入伺服器及從伺服器讀取的數據流。 此處擲回的任何例外狀況都會透過Visual Studio中的InfoBar訊息攔截並向用戶顯示。

啟用

實作語言用戶端類別之後,您必須定義兩個屬性,以定義如何將它載入 Visual Studio 並啟動:

  [Export(typeof(ILanguageClient))]
  [ContentType("bar")]

MEF

Visual Studio 使用 MEF (Managed Extensibility Framework) 來管理其擴充點。 Export 屬性會向 Visual Studio 指出此類別應該取成擴充點,並在適當時間載入。

若要使用MEF,您也必須將MEF定義為 VSIX 指令清單中的資產。

開啟 VSIX 指令清單設計工具,並流覽至 [ 資產 ] 索引標籤:

add MEF asset

單擊 [ 新增 ] 以建立新的資產:

define MEF asset

  • 類型:Microsoft.VisualStudio.MefComponent
  • 來源:目前方案中的專案
  • 專案:[您的專案]

內容類型定義

目前,載入 LSP 語言伺服器延伸模組的唯一方式是檔案內容類型。 也就是說,在定義語言用戶端類別時(實作 ILanguageClient),您必須定義開啟時會導致擴充功能載入的檔案類型。 如果未開啟任何符合您定義內容類型的檔案,則不會載入您的延伸模組。

這是透過定義一或多個 ContentTypeDefinition 類別來完成:

namespace MockLanguageExtension
{
    public class BarContentDefinition
    {
        [Export]
        [Name("bar")]
        [BaseDefinition(CodeRemoteContentDefinition.CodeRemoteContentTypeName)]
        internal static ContentTypeDefinition BarContentTypeDefinition;

        [Export]
        [FileExtension(".bar")]
        [ContentType("bar")]
        internal static FileExtensionToContentTypeDefinition BarFileExtensionDefinition;
    }
}

在上一個範例中,會針對以 .bar 擴展名結尾的檔案建立內容類型定義。 內容類型定義會指定名稱 「bar」 而且必須衍生自 CodeRemoteContentTypeName

新增內容類型定義之後,您就可以在語言用戶端類別中定義何時載入語言用戶端延伸模組:

    [ContentType("bar")]
    [Export(typeof(ILanguageClient))]
    public class BarLanguageClient : ILanguageClient
    {
    }

新增 LSP 語言伺服器的支援不需要您在 Visual Studio 中實作自己的項目系統。 客戶可以在 Visual Studio 中開啟單一檔案或資料夾,以開始使用您的語言服務。 事實上,LSP 語言伺服器的支持是設計為只能在開啟的資料夾/檔案案例中運作。 如果實作自定義項目系統,某些功能(例如設定)將無法運作。

進階功能

設定

自定義語言伺服器特定設定的支援可供使用,但仍在改善過程中。 設定 專屬於語言伺服器所支援的內容,而且通常會控制語言伺服器發出數據的方式。 例如,語言伺服器可能會有報告錯誤數目上限的設定。 延伸模組作者會定義預設值,用戶可以針對特定項目變更此值。

請遵循下列步驟,將設定的支援新增至您的 LSP 語言服務延伸模組:

  1. 將 JSON 檔案 (例如 MockLanguageExtension 設定.json) 新增至包含設定及其預設值的專案。 例如:

    {
        "foo.maxNumberOfProblems": -1
    }
    
  2. 以滑鼠右鍵按下 JSON 檔案,然後選取 [ 屬性]。 將 [建置 ] 動作變更為 “Content”,並將 [包含於 VSIX] 屬性變更為 true

  3. 實作 ConfigurationSections 並傳回 JSON 檔案中所定義之設定的前置詞列表(在 Visual Studio Code 中,這會對應至 package.json 中的組態區段名稱):

    public IEnumerable<string> ConfigurationSections
    {
        get
        {
            yield return "foo";
        }
    }
    
  4. 將 .pkgdef 檔案新增至專案(新增文本檔,並將擴展名變更為 .pkgdef)。 pkgdef 檔案應該包含此資訊:

    [$RootKey$\OpenFolder\Settings\VSWorkspaceSettings\[settings-name]]
    @="$PackageFolder$\[settings-file-name].json"
    

    範例:

    [$RootKey$\OpenFolder\Settings\VSWorkspaceSettings\MockLanguageExtension]
    @="$PackageFolder$\MockLanguageExtensionSettings.json"
    
  5. 以滑鼠右鍵按兩下 .pkgdef 檔案,然後選取 [ 屬性]。 將 [建置] 動作變更為 [內容],並將 [包含在 VSIX] 屬性變更為 true

  6. 開啟 source.extension.vsixmanifest 檔案,並在 [資產] 索引標籤中 新增資產

    edit vspackage asset

    • 類型:Microsoft.VisualStudio.VsPackage
    • 來源:檔案系統上的檔案
    • 路徑:[. pkgdef 檔案的路徑]

用戶編輯工作區的設定

  1. 用戶會開啟包含您伺服器擁有檔案的工作區。

  2. 使用者會在名為 VSWorkspace 的 .vs 資料夾中新增檔案 設定.json

  3. 使用者會將一行新增至 VSWorkspace 設定.json 檔案,以供伺服器提供的設定使用。 例如:

    {
        "foo.maxNumberOfProblems": 10
    }
    

啟用診斷追蹤

您可以啟用診斷追蹤來輸出客戶端與伺服器之間的所有訊息,這在偵錯問題時很有用。 若要啟用診斷追蹤,請執行下列動作:

  1. 開啟或建立工作區配置檔 VSWorkspace 設定.json (請參閱使用者編輯工作區的設定]。
  2. 在設定 json 檔案中新增下列這一行:
{
    "foo.trace.server": "Off"
}

追蹤詳細資訊有三個可能的值:

  • “Off”: 追蹤完全關閉
  • 「訊息」:追蹤已開啟,但只會追蹤方法名稱和響應標識符。
  • “Verbose”: 追蹤已開啟;追蹤整個 rpc 訊息。

開啟追蹤時,內容會寫入 %temp%\VisualStudio\LSP 目錄中的檔案。 記錄會遵循命名格式 [LanguageClientName]-[Datetime Stamp].log。 目前,只能針對開啟的資料夾案例啟用追蹤。 開啟單一檔案以啟動語言伺服器並無診斷追蹤支援。

自訂訊息

有 API 可用來協助將訊息傳遞至不屬於標準語言伺服器通訊協定的語言伺服器,以及接收訊息。 若要處理自定義訊息,請在您的語言用戶端類別中實 作 ILanguageClientCustomMessage2 介面。 VS-StreamJsonRpc 連結庫可用來在語言用戶端與語言伺服器之間傳輸自定義訊息。 由於您的 LSP 語言用戶端延伸模組就像任何其他 Visual Studio 延伸模組一樣,因此您可以決定透過自定義訊息將其他功能(LSP 不支援的功能)新增至 Visual Studio(使用其他 Visual Studio API)。

接收自訂訊息

若要從語言伺服器接收自定義訊息,請在 ILanguageClientCustomMessageMessage2實作 [CustomMessageTarget](/dotnet/api/microsoft.visualstudio.languageserver.client.ilanguageclientcusageclientcusagetarget)屬性,並傳回知道如何處理自定義訊息的物件。 範例如下:

(/dotnet/api/microsoft.visualstudio.languageserver.client.ilanguageclientcustommessage.custommessagetarget) 屬性上的 ILanguageClientCustomMessage2 ,並傳回知道如何處理自定義訊息的物件。 範例如下:

internal class MockCustomLanguageClient : MockLanguageClient, ILanguageClientCustomMessage2
{
    private JsonRpc customMessageRpc;

    public MockCustomLanguageClient() : base()
    {
        CustomMessageTarget = new CustomTarget();
    }

    public object CustomMessageTarget
    {
        get;
        set;
    }

    public class CustomTarget
    {
        public void OnCustomNotification(JToken arg)
        {
            // Provide logic on what happens OnCustomNotification is called from the language server
        }

        public string OnCustomRequest(string test)
        {
            // Provide logic on what happens OnCustomRequest is called from the language server
        }
    }
}

傳送自定義訊息

若要將自定義訊息傳送至語言伺服器,請在 ILanguageClientCustomMessage2作 AttachForCustomMessageAsync 方法。 當您的語言伺服器啟動並準備好接收訊息時,會叫用這個方法。 JsonRpc 物件會當做參數傳遞,然後您就可以繼續使用 VS-StreamJsonRpc API 將訊息傳送至語言伺服器。 範例如下:

internal class MockCustomLanguageClient : MockLanguageClient, ILanguageClientCustomMessage2
{
    private JsonRpc customMessageRpc;

    public MockCustomLanguageClient() : base()
    {
        CustomMessageTarget = new CustomTarget();
    }

    public async Task AttachForCustomMessageAsync(JsonRpc rpc)
    {
        await Task.Yield();

        this.customMessageRpc = rpc;
    }

    public async Task SendServerCustomNotification(object arg)
    {
        await this.customMessageRpc.NotifyWithParameterObjectAsync("OnCustomNotification", arg);
    }

    public async Task<string> SendServerCustomMessage(string test)
    {
        return await this.customMessageRpc.InvokeAsync<string>("OnCustomRequest", test);
    }
}

中間層

有時候擴充功能開發人員可能會想要攔截從語言伺服器傳送和接收的 LSP 訊息。 例如,延伸模塊開發人員可能想要改變針對特定 LSP 訊息傳送的訊息參數,或修改從語言伺服器傳回之 LSP 功能的結果(例如完成)。 必要時,延伸模塊開發人員可以使用 MiddleLayer API 來攔截 LSP 訊息。

若要攔截特定訊息,請建立實作 ILanguageClientMiddleLayer 介面的類別。 然後,在語言用戶端類別中實作 ILanguageClientCustomMessage2 介面,並在 MiddleLayer 屬性中傳回對象的實例。 範例如下:

public class MockLanguageClient : ILanguageClient, ILanguageClientCustomMessage2
{
  public object MiddleLayer => DiagnosticsFilterMiddleLayer.Instance;

  private class DiagnosticsFilterMiddleLayer : ILanguageClientMiddleLayer
  {
    internal readonly static DiagnosticsFilterMiddleLayer Instance = new DiagnosticsFilterMiddleLayer();

    private DiagnosticsFilterMiddleLayer() { }

    public bool CanHandle(string methodName)
    {
      return methodName == "textDocument/publishDiagnostics";
    }

    public async Task HandleNotificationAsync(string methodName, JToken methodParam, Func<JToken, Task> sendNotification)
    {
      if (methodName == "textDocument/publishDiagnostics")
      {
        var diagnosticsToFilter = (JArray)methodParam["diagnostics"];
        // ony show diagnostics of severity 1 (error)
        methodParam["diagnostics"] = new JArray(diagnosticsToFilter.Where(diagnostic => diagnostic.Value<int?>("severity") == 1));

      }
      await sendNotification(methodParam);
    }

    public async Task<JToken> HandleRequestAsync(string methodName, JToken methodParam, Func<JToken, Task<JToken>> sendRequest)
    {
      return await sendRequest(methodParam);
    }
  }
}

中介層功能仍在開發中,尚未全面。

範例 LSP 語言伺服器延伸模組

若要查看 Visual Studio 中使用 LSP 用戶端 API 之範例延伸模組的原始程式碼,請參閱 VSSDK-Extensibility-Samples LSP 範例

常見問題集

我想要建置自定義項目系統來補充 LSP 語言伺服器,以在 Visual Studio 中提供更豐富的功能支援,我要如何執行這項操作?

Visual Studio 中以 LSP 為基礎的語言伺服器支援依賴 開啟的資料夾功能 ,其設計目的是不需要自定義項目系統。 您可以依照這裡的指示來建置自己的自定義項目系統,但某些功能,例如設定可能無法運作。 LSP 語言伺服器的預設初始化邏輯是傳入目前開啟之資料夾的根資料夾位置,因此如果您使用自定義項目系統,您可能需要在初始化期間提供自定義邏輯,以確保語言伺服器可以正常啟動。

如何? 新增調試程序支援嗎?

我們將在未來版本中提供常見偵錯通訊協議的支援

如果已安裝 VS 支援的語言服務(例如 JavaScript),我仍然可以安裝提供其他功能(例如 linting)的 LSP 語言伺服器延伸模組嗎?

是,但並非所有功能都能正常運作。 LSP 語言伺服器延伸模組的最終目標是啟用 Visual Studio 原生不支援的語言服務。 您可以建立使用 LSP 語言伺服器提供額外支援的延伸模組,但某些功能(例如 IntelliSense)不會是順暢的體驗。 一般而言,建議使用 LSP 語言伺服器延伸模組來提供新的語言體驗,而不是擴充現有的語言。

我在哪裡發佈已完成的 LSP 語言伺服器 VSIX?

請參閱這裡的 Marketplace 指示