添加语言服务器协议扩展

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

在 Visual Studio语言服务器协议服务

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

语言服务器协议

语言服务器协议实现

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

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

  • 标准输入/输出流
  • 命名管道
  • 套接字(仅限 TCP)

LSP 及其在 Visual Studio 中的支持,旨在引入不属于 Visual Studio 产品的语言服务。 它不打算在 Visual Studio 中扩展现有语言服务(如 C#)。 若要扩展现有语言,请参阅语言服务的扩展性指南(例如,“Roslyn”.NET 编译器平台),或查看 扩展编辑器和语言服务

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

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

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

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

消息 在 Visual Studio 中是否具有支持
initialize 是的
initialized 是的
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 是的
completion/resolve 是的
textDocument/hover 是的
textDocument/signatureHelp 是的
textDocument/references 是的
textDocument/documentHighlight 是的
textDocument/documentSymbol 是的
文本文档/格式化 是的
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 中。 如果已使用预览版生成 LSP 扩展 语言服务器客户端 VSIX 版本,则升级到版本 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 预览版 3 指定为安装目标的下限。

  5. 重建和重新部署。

创建 VSIX 项目

若要使用基于 LSP 的语言服务器创建语言服务扩展,首先请确保为 VS 实例安装了 Visual Studio 扩展开发 工作负荷。

通过以下路径创建新的 VSIX 项目:文件>新建项目>Visual C#>扩展性>VSIX 项目

创建 vsix 项目

语言服务器和运行时安装

默认情况下,为支持 Visual Studio 中基于 LSP 的语言服务器而创建的扩展不包含执行它们所需的语言服务器本身或运行时。 扩展开发人员负责分发语言服务器和所需的运行时。 有多种方法可以执行此操作:

  • 语言服务器可以作为内容文件嵌入 VSIX 中。
  • 创建 MSI 以安装语言服务器和/或所需的运行时。
  • 提供有关应用市场的指南,告知用户如何获取运行时和语言服务器。

TextMate 语法文件

LSP 不包括有关如何为语言提供文本着色的规范。 若要在 Visual Studio 中为语言提供自定义着色,扩展开发人员可以使用 TextMate 语法文件。 若要添加自定义 TextMate 语法或主题文件,请执行以下步骤:

  1. 在扩展中创建名为“Grammars”的文件夹(也可以是你选择的任何名称)。

  2. 语法 文件夹中,放入任何您需要的 *.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”

完成上述步骤后,语法 文件夹作为名为“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,还必须在 VSIX 清单中将 MEF 定义为资产。

打开 VSIX 清单设计器并导航到 资产 选项卡:

添加 MEF 资产

单击“新建”创建新资产

定义 MEF 资产

  • 类型: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 文件,并在 资产 选项卡中添加资产:

    编辑 vspackage 资产

    • 类型:Microsoft.VisualStudio.VsPackage
    • 源:文件系统上的文件
    • 路径:[.pkgdef 文件的路径]

用户编辑工作区的设置

  1. 用户打开一个工作区,其中包含服务器拥有的文件。

  2. 用户在名为 VSWorkspaceSettings.json.vs 文件夹中添加文件。

  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 有助于将消息传递到不属于标准语言服务器协议的语言服务器以及从语言服务器接收消息。 若要处理自定义消息,请在语言客户端类中实现 ILanguageClientCustomMessage2 接口。 VS-StreamJsonRpc 库用于在语言客户端和语言服务器之间传输自定义消息。 由于 LSP 语言客户端扩展就像任何其他 Visual Studio 扩展一样,因此你可以决定通过自定义消息将其他功能(LSP 不支持的功能)添加到 Visual Studio(使用其他 Visual Studio API)。

接收自定义消息

若要从语言服务器接收自定义消息,请在 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?

请参阅此处的市场说明。