添加语言服务器协议扩展

语言服务器协议 (LSP) 是一种通用协议,采用 JSON RPC v2.0 的形式,用于为各种代码编辑器提供语言服务功能。 使用该协议,开发人员可以写入单一语言服务器,为支持 LSP 的各种代码编辑器提供语言服务功能,例如 IntelliSense、错误诊断、查找所有引用等。 传统上,通过使用 TextMate 语法文件来提供语法突出显示等基本功能,或者通过编写自定义语言服务来使用全套 Visual Studio 扩展性 API 提供更丰富的数据,可以添加 Visual Studio 中的语言服务。 通过 Visual Studio 对 LSP 的支持,有了第三种选择。

language server protocol service in Visual Studio

为了确保获得最佳的用户体验,还可以考虑实施语言配置,语言配置可提供许多相同操作的本地处理,因此可以提高 LSP 支持的许多语言特定编辑器操作的性能。

语言服务器协议

language server protocol implementation

本文介绍如何创建一个使用基于 LSP 的语言服务器的 Visual Studio 扩展。 本文假设您已经开发了基于 LSP 的语言服务器,并且只想将其集成到 Visual Studio 中。

为了在 Visual Studio 中获得支持,语言服务器可以通过任何基于流的传输机制与客户端 (Visual Studio) 进行通信,例如:

  • 标准输入/输出流
  • Named pipes
  • 套接字(仅限 TCP)

LSP 以及在 Visual Studio 中对其予以支持的目的是加入不属于 Visual Studio 产品的语言服务。 它并不用于扩展 Visual Studio 中现有的语言服务(如 C#)。 要扩展现有语言,请参阅语言服务的扩展性指南(例如,"Roslyn" .NET 编译器平台),或参见扩展编辑器和语言服务

有关协议本身的更多信息,请参阅此处的文档。

有关如何创建示例语言服务器或如何将现有语言服务器集成到 Visual Studio Code 的更多信息,请参阅此处的文档。

语言服务器协议支持的功能

下表显示了 Visual Studio 支持哪些 LSP 功能:

Message 在 Visual Studio 中可获得支持
initialize
已初始化
shutdown
exit
$/cancelRequest
window/showMessage
window/showMessageRequest
window/logMessage
telemetry/event
client/registerCapability
client/unregisterCapability
workspace/didChangeConfiguration
workspace/didChangeWatchedFiles
workspace/symbol
workspace/executeCommand
workspace/applyEdit
textDocument/publishDiagnostics
textDocument/didOpen
textDocument/didChange
textDocument/willSave
textDocument/willSaveWaitUntil
textDocument/didSave
textDocument/didClose
textDocument/completion
completion/resolve
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 语言服务器协议预览版 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;
        }
    }
}

需要实现的主要方法有 OnLoadedAsyncActivateAsync。 当 Visual Studio 加载了扩展并且语言服务器准备好启动时,会调用 OnLoadedAsync。 在此方法中,您可以立即调用 StartAsync 委托以发出应启动语言服务器的信号,或者您可以执行其他逻辑并稍后调用 StartAsync要激活语言服务器,您必须在某个时刻调用 StartAsync。

ActivateAsync 是通过调用 StartAsync 委托最终调用的方法。 它包含用于启动语言服务器并与其建立连接的逻辑。 必须返回一个连接对象,此对象包含用于写入服务器和从服务器读取的流。 此处引发的任何异常都会被捕获并通过 Visual Studio 中的 InfoBar 消息显示给用户。

激活

实现语言客户端类后,您需要为其定义两个属性,以定义如何将其加载到 Visual Studio 中并激活:

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

MEF

Visual Studio 使用 MEF(托管扩展性框架)来管理其扩展性点。 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 文件(例如,MockLanguageExtensionSettings.json)添加到包含设置及其默认值的项目中。 例如:

    {
        "foo.maxNumberOfProblems": -1
    }
    
  2. 右键单击 JSON 文件并选择属性。 将生成操作更改为“内容”,并将“包含在 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. 用户在 .vs 文件夹中添加一个名为 VSWorkspaceSettings.json 的文件。

  3. 用户在 VSWorkspaceSettings.json 文件中为服务器提供的设置添加一行。 例如:

    {
        "foo.maxNumberOfProblems": 10
    }
    

启用诊断跟踪

可以启用诊断跟踪以输出客户端和服务器之间的所有消息,这在调试问题时非常有用。 要启用诊断跟踪,请执行以下操作:

  1. 打开或创建工作区设置文件 VSWorkspaceSettings.json(请参阅“用户编辑工作区设置”)。
  2. 在设置 json 文件中添加以下行:
{
    "foo.trace.server": "Off"
}

跟踪详细程度有三个可能的值:

  • “关”:完全关闭跟踪
  • “消息”:跟踪已打开,但仅跟踪方法名称和响应 ID。
  • “详细”:跟踪已打开;跟踪整个 rpc 消息。

打开跟踪后,内容将写入 %temp%\VisualStudio\LSP 目录内的文件中。 日志遵循 [LanguageClientName]-[Datetime Stamp].log 命名格式。 目前,只能对打开的文件夹场景启用跟踪。 打开单个文件来激活语言服务器不具有诊断跟踪支持。

自定义消息

我们部署了一些 API,以帮助向语言服务器传递消息以及从语言服务器接收消息,但这些 API 不属于标准语言服务器协议的一部分。 要处理自定义消息,请在语言客户端类中实现 ILanguageClientCustomMessage2 接口。 VS-StreamJsonRpc 库用于在语言客户端和语言服务器之间传输自定义消息。 由于 LSP 语言客户端扩展与任何其他 Visual Studio 扩展一样,因此,您可以决定通过自定义消息向扩展中的 Visual Studio(使用其他 Visual Studio API)添加(LSP 不支持的)其他功能。

接收自定义消息

若要从语言服务器接收自定义消息,请在 ILanguageClientCustomMessage2 上实现 [CustomMessageTarget]((/dotnet/api/microsoft.visualstudio.languageserver.client.ilanguageclientcustommessage.custommessagetarget) 属性,并返回一个知道如何处理自定义消息的对象。 示例如下:

ILanguageClientCustomMessage2 上的 (/dotnet/api/microsoft.visualstudio.languageserver.client.ilanguageclientcustommessage.custommessagetarget) 属性,并返回一个知道如何处理自定义消息的对象。 示例如下:

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?

请参阅此处的市场说明。