Compartilhar via


Adicionar uma extensão do Language Server Protocol

O LSP (Language Server Protocol) é um protocolo comum, na forma de JSON RPC v2.0, usado para fornecer recursos de serviços de linguagem para vários editores de código. Com o uso do protocolo, os desenvolvedores podem escrever um único servidor de linguagem para fornecer recursos de serviços de linguagem como IntelliSense, diagnóstico de erros, localizar todas as referências, entre outros, para vários editores de código que oferecem suporte ao LSP. Tradicionalmente, os serviços de linguagem no Visual Studio podem ser adicionados com o uso de arquivos de gramática TextMate para fornecer funcionalidades básicas, como realce sintático, ou com a escrita de serviços de linguagem personalizados que usam todo o conjunto de APIs de extensibilidade do Visual Studio para fornecer dados mais valiosos. Com o suporte do Visual Studio para LSP, há ainda uma terceira opção.

language server protocol service in Visual Studio

Para garantir a melhor experiência de usuário possível, implemente também a Configuração de linguagem, que faz o processamento local de muitas das mesmas operações e, portanto, pode melhorar o desempenho das operações de editor específicas da linguagem que o LSP aceita.

Language Server Protocol

language server protocol implementation

Este artigo descreve como criar uma extensão do Visual Studio que usa um servidor de linguagem baseado em LSP. Ele pressupõe que você já tenha desenvolvido um servidor de linguagem baseado em LSP e que queira apenas integrá-lo ao Visual Studio.

Para obter suporte dentro do Visual Studio, os servidores de linguagem podem se comunicar com o cliente (Visual Studio) por meio de qualquer mecanismo de transmissão baseado em fluxo, como, por exemplo:

  • Fluxos de entrada/saída padrão
  • Pipes nomeados
  • Soquetes (somente TCP)

O objetivo do LSP e seu suporte no Visual Studio é integrar serviços de linguagem que não fazem parte do produto Visual Studio. Ele não se destina a estender serviços de linguagem existentes (como C#) no Visual Studio. Para estender as linguagens existentes, consulte o guia de extensibilidade do serviço de linguagem (por exemplo, a plataforma do compilador .NET "Roslyn") ou consulte Estender o editor e os serviços de linguagem.

Para obter mais informações sobre o protocolo em si, consulte a documentação aqui.

Para obter mais informações sobre como criar um servidor de linguagem de exemplo ou como integrar um servidor de linguagem existente no Visual Studio Code, consulte a documentação aqui.

Recursos aceitos pelo Language Server Protocol

As tabelas a seguir mostram quais recursos do LSP têm suporte no Visual Studio:

Mensagem Tem suporte no Visual Studio
Initialize sim
inicializado sim
shutdown sim
exit sim
$/cancelRequest sim
window/showMessage sim
window/showMessageRequest sim
window/logMessage sim
telemetry/event
client/registerCapability
client/unregisterCapability
workspace/didChangeConfiguration sim
workspace/didChangeWatchedFiles sim
workspace/symbol sim
workspace/executeCommand sim
workspace/applyEdit sim
textDocument/publishDiagnostics sim
textDocument/didOpen sim
textDocument/didChange sim
textDocument/willSave
textDocument/willSaveWaitUntil
textDocument/didSave sim
textDocument/didClose sim
textDocument/completion sim
completion/resolve sim
textDocument/hover sim
textDocument/signatureHelp sim
textDocument/references sim
textDocument/documentHighlight sim
textDocument/documentSymbol sim
textDocument/formatting sim
textDocument/rangeFormatting sim
textDocument/onTypeFormatting
textDocument/definition sim
textDocument/codeAction sim
textDocument/codeLens
codeLens/resolve
textDocument/documentLink
documentLink/resolve
textDocument/rename sim

Introdução

Observação

A partir do Visual Studio 2017 versão 15.8, o suporte para o Common Language Server Protocol é incorporado ao Visual Studio. Se você tiver criado extensões do LSP usando a versão Language Server Client VSIX, elas deixarão de funcionar quando você atualizar para a versão 15.8 ou superior. Para que suas extensões LSP voltem a funcionar, você precisará fazer o seguinte:

  1. Desinstale o Microsoft Visual Studio Language Server Protocol Preview VSIX.

    A partir da versão 15.8, sempre que você executa uma atualização no Visual Studio, a versão prévia do VSIX é detectada e removida automaticamente.

  2. Atualize sua referência do Nuget para a versão não prévia mais recente para pacotes LSP.

  3. Remova a dependência do Microsoft Visual Studio Language Server Protocol Preview VSIX em seu manifesto do VSIX.

  4. Verifique se o VSIX especifica o Visual Studio 2017 versão 15.8 Preview 3 como o limite mínimo para o destino de instalação.

  5. Recompilar e reimplantar.

Criar um projeto de VSIX

Para criar uma extensão de serviço de linguagem usando um servidor de linguagem baseado em LSP, primeiro verifique se você tem a carga de trabalho de desenvolvimento de extensão do Visual Studio instalada para sua instância do VS.

Em seguida, crie um projeto do VSIX acessando Arquivo>Novo Projeto>Visual C#>Extensibilidade>VSIX Project:

create vsix project

Servidor de linguagem e instalação de runtime

Por padrão, as extensões criadas para oferecer suporte a servidores de linguagem baseados em LSP no Visual Studio não contêm os próprios servidores de linguagem ou os runtimes necessários para executá-los. Os desenvolvedores de extensões são responsáveis por distribuir os servidores de linguagem e os runtimes necessários. Há várias maneiras de fazer isso:

  • Os servidores de linguagem podem ser incorporados no VSIX como arquivos de conteúdo.
  • Crie um MSI para instalar o servidor de linguagem e/ou os runtimes necessários.
  • Forneça instruções no Marketplace informando aos usuários como obter runtimes e servidores de linguagem.

Arquivos de gramática do TextMate

O LSP não inclui especificação sobre como colorir o texto das linguagens. Para uma colorização personalizada das linguagens no Visual Studio, os desenvolvedores de extensão podem usar um arquivo de gramática do TextMate. Para adicionar arquivos de tema ou gramática personalizada do TextMate, siga estas etapas:

  1. Crie uma pasta chamada "Grammars" dentro da sua extensão (ou pode ser qualquer nome que você escolher).

  2. Dentro da pasta Grammars, inclua qualquer arquivo *.tmlanguage, *.plist, *.tmtheme ou *.json que deseja colorir de forma personalizada.

    Dica

    Um arquivo .tmtheme define como os escopos são mapeados para classificações do Visual Studio (chamadas chaves coloridas). Para obter orientação, você pode fazer referência ao arquivo .tmtheme na versão %ProgramFiles(x86)%\Microsoft Visual Studio\< do diretório>\<SKU>\Common7\IDE\CommonExtensions\Microsoft\TextMate\Starterkit\Themesg.

  3. Crie um arquivo .pkgdef e adicione uma linha semelhante a esta:

    [$RootKey$\TextMate\Repositories]
    "MyLang"="$PackageFolder$\Grammars"
    
  4. Clique com o botão direito do mouse nos arquivos e selecione Propriedades. Altere a ação Build para Content e altere a propriedade Include in VSIX para true.

Depois de concluir as etapas anteriores, uma pasta Grammars é adicionada ao diretório de instalação do pacote como uma fonte de repositório chamada "MyLang" ("MyLang" é apenas um nome para desambiguação, que pode ser qualquer cadeia de caracteres exclusiva). Todos os arquivos de gramática (arquivos .tmlanguage) e de tema (arquivos .tmtheme) neste diretório são escolhidos como potenciais e substituem as gramáticas internas fornecidas pelo TextMate. Se as extensões declaradas do arquivo de gramática corresponderem à extensão do arquivo que está sendo aberto, o TextMate fará intervenção.

Criar um cliente de linguagem simples

Interface principal - ILanguageClient

Depois de criar seu projeto do VSIX, adicione um ou mais dos seguintes pacotes NuGet ao projeto:

Observação

Quando você usa uma dependência no pacote NuGet depois de concluir as etapas anteriores, os pacotes Newtonsoft.Json e StreamJsonRpc também são adicionados ao projeto. Não atualize esses pacotes, a menos que você tenha certeza de que essas novas versões serão instaladas na versão do Visual Studio à qual sua extensão se destina. Os assemblies não serão incluídos no VSIX, mas serão coletados do diretório de instalação do Visual Studio. Se você estiver fazendo referência a uma versão mais recente dos assemblies do que a instalada na máquina de um usuário, sua extensão não funcionará.

Você pode então criar uma nova classe que implementa a interface ILanguageClient, que é a interface principal necessária para clientes de linguagem que se conectam a um servidor de linguagem baseado em LSP.

Veja o exemplo a seguir:

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;
        }
    }
}

Os principais métodos que precisam ser implementados são OnLoadedAsync e ActivateAsync. OnLoadedAsync é chamado quando o Visual Studio carregou a extensão e o servidor de linguagem está pronto para ser iniciado. Nesse método, você pode invocar o delegado StartAsync imediatamente para sinalizar que o servidor de linguagem deve ser iniciado. Ou você pode usar a lógica adicional e invocar StartAsync posteriormente. Para ativar o servidor de linguagem, você deve chamar StartAsync em algum momento.

ActivateAsync é o método muitas vezes invocado ao chamar o delegado StartAsync. Ele contém a lógica para iniciar o servidor de linguagem e estabelecer conexão com ele. Um objeto de conexão que contém fluxos para gravação no servidor e leitura do servidor deve ser retornado. Todas as exceções lançadas aqui são capturadas e exibidas ao usuário por meio de uma mensagem da Barra de Informações que aparece no Visual Studio.

Ativação

Depois que sua classe de cliente de linguagem for implementada, você precisará definir dois atributos para que ela defina como será carregada no Visual Studio e ativada:

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

MEF

O Visual Studio usa MEF (Managed Extensibility Framework) para gerenciar seus pontos de extensibilidade. O atributo Export indica ao Visual Studio que essa classe deve ser selecionada como um ponto de extensão e carregada no momento apropriado.

Para usar o MEF, você também deve defini-lo como um Ativo no manifesto do VSIX.

Abra o designer de manifesto do VSIX e navegue até a guia Ativos:

add MEF asset

Clique em Novo para criar um ativo:

define MEF asset

  • Digite: Microsoft.VisualStudio.MefComponent
  • Fonte: um projeto na solução atual
  • Projeto: [Seu projeto]

Definição de tipo de conteúdo

Atualmente, a única maneira de carregar sua extensão de servidor de linguagem baseada em LSP é por tipo de conteúdo de arquivo. Ou seja, ao definir sua classe de cliente de linguagem (que implementa ILanguageClient), você precisará definir os tipos de arquivos que, quando abertos, farão com que a extensão seja carregada. Se nenhum arquivo que corresponda ao seu tipo de conteúdo definido for aberto, sua extensão não será carregada.

Isso é feito por meio da definição de uma ou mais classes 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;
    }
}

No exemplo anterior, uma definição de tipo de conteúdo é criada para arquivos que terminam com a extensão de arquivo .bar. A definição de tipo de conteúdo recebe o nome "bar" e deve derivar de CodeRemoteContentTypeName.

Depois de adicionar uma definição de tipo de conteúdo, você pode definir quando carregar sua extensão de cliente de linguagem na classe apropriada:

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

Para adicionar suporte para servidores de linguagem LSP, você não precisa implementar seu próprio sistema de projeto no Visual Studio. Os clientes podem abrir um único arquivo ou uma pasta no Visual Studio para começar a usar o serviço de linguagem. Na verdade, o suporte para servidores de linguagem LSP serve para funcionar apenas em cenários de pasta/arquivo abertos. Se um sistema de projeto personalizado for implementado, alguns recursos (como configurações) não funcionarão.

Recursos avançados

Configurações

O suporte para configurações personalizadas específicas do servidor de linguagem está disponível, mas ainda em processo de melhorias. As configurações são específicas ao suporte do servidor de linguagem e geralmente controlam como esse servidor emite dados. Por exemplo, um servidor de linguagem pode ter uma configuração para o número máximo de erros relatados. Os autores da extensão definem um valor padrão, que pode ser alterado pelos usuários para projetos específicos.

Siga as etapas abaixo para adicionar suporte para configurações à sua extensão de serviço de linguagem LSP:

  1. Adicione um arquivo JSON (por exemplo, MockLanguageExtensionSettings.json) ao seu projeto, que contém as configurações e valores padrão. Por exemplo:

    {
        "foo.maxNumberOfProblems": -1
    }
    
  2. Clique com o botão direito do mouse no arquivo JSON e selecione Propriedades. Altere a ação Build para "Content" e altere a propriedade "Include in VSIX" para true.

  3. Implemente ConfigurationSections e retorne a lista de prefixos para as configurações definidas no arquivo JSON (no Visual Studio Code, o mapeamento estaria no nome da seção de configuração em package.json):

    public IEnumerable<string> ConfigurationSections
    {
        get
        {
            yield return "foo";
        }
    }
    
  4. Adicione um arquivo .pkgdef ao projeto (adicione um novo arquivo de texto e altere a extensão do arquivo para .pkgdef). O arquivo pkgdef deve conter estas informações:

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

    Amostra:

    [$RootKey$\OpenFolder\Settings\VSWorkspaceSettings\MockLanguageExtension]
    @="$PackageFolder$\MockLanguageExtensionSettings.json"
    
  5. Clique com o botão direito do mouse no arquivo .pkgdef e selecione Propriedades. Altere a ação Build para Content e altere a propriedade Include in VSIX para true.

  6. Abra o arquivo source.extension.vsixmanifest e adicione um ativo na guia Ativo:

    edit vspackage asset

    • Digite: Microsoft.VisualStudio.VsPackage
    • Fonte: arquivo no sistema de arquivos
    • Caminho: [Caminho para o arquivo .pkgdef]

Edição de configurações de um espaço de trabalho pelo usuário

  1. O usuário abre um espaço de trabalho contendo arquivos de propriedade do servidor.

  2. O usuário adiciona um arquivo à pasta .vs chamado VSWorkspaceSettings.json.

  3. O usuário adiciona uma linha ao arquivo VSWorkspaceSettings.json para uma configuração fornecida pelo servidor. Por exemplo:

    {
        "foo.maxNumberOfProblems": 10
    }
    

Habilitar o rastreamento de diagnóstico

O rastreamento de diagnóstico pode ser habilitado para gerar todas as mensagens entre o cliente e o servidor, o que pode ser útil ao depurar problemas. Para habilitar o rastreamento de diagnóstico, faça o seguinte:

  1. Abra ou crie o arquivo de configurações do espaço de trabalho VSWorkspaceSettings.json (consulte "Edição de configurações de um espaço de trabalho pelo usuário").
  2. Adicione a seguinte linha ao arquivo json de configurações:
{
    "foo.trace.server": "Off"
}

Há três valores possíveis para detalhamento do rastreamento:

  • "Off": rastreamento completamente desativado
  • "Messages": rastreamento ativado, mas apenas o nome do método e o ID de resposta são rastreados.
  • "Verbose": rastreamento ativado; a mensagem RPC inteira é rastreada.

Quando o rastreamento está ativado, o conteúdo é gravado em um arquivo no diretório %temp%\VisualStudio\LSP. O log segue o formato de nomenclatura [LanguageClientName]-[Datetime Stamp].log. Atualmente, o rastreamento só pode ser habilitado para cenários de pasta aberta. Abrir um único arquivo para ativar um servidor de linguagem não permite rastreamento de diagnóstico.

Mensagens personalizadas

Existem APIs para facilitar a transmissão e o recebimento de mensagens do servidor de linguagem que não fazem parte do LSP padrão. Para manipular mensagens personalizadas, implemente a interface ILanguageClientCustomMessage2 na classe do cliente de linguagem. A biblioteca VS-StreamJsonRpc é usada para transmitir mensagens personalizadas entre o cliente de linguagem e o servidor de linguagem. Como sua extensão de cliente de linguagem LSP é como qualquer outra extensão do Visual Studio, você pode decidir adicionar outros recursos (que não são aceitos pelo LSP) ao Visual Studio (usando outras APIs do Visual Studio) em sua extensão por meio de mensagens personalizadas.

Receber mensagens personalizadas

Para receber mensagens personalizadas do servidor de idiomas, implemente a propriedade [CustomMessageTarget]((/dotnet/api/microsoft.visualstudio.languageserver.client.ilanguageclientcustommessage.custommessagetarget) em ILanguageClientCustomMessage2 e retorne um objeto que saiba manipular suas mensagens personalizadas. Exemplo abaixo:

propriedade (/dotnet/api/microsoft.visualstudio.languageserver.client.ilanguageclientcustommessage.custommessagetarget) em ILanguageClientCustomMessage2 e retorna um objeto que sabe manipular suas mensagens personalizadas. Exemplo abaixo:

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
        }
    }
}

Enviar mensagens personalizadas

Para enviar mensagens personalizadas para o servidor de linguagem, implemente o método AttachForCustomMessageAsync em ILanguageClientCustomMessage2. Esse método é chamado quando o servidor de linguagem é iniciado e está pronto para receber mensagens. Um objeto JsonRpc é passado como parâmetro, que você pode manter para enviar mensagens para o servidor de linguagem usando APIs VS-StreamJsonRpc. Exemplo abaixo:

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);
    }
}

Camada intermediária

Às vezes, um desenvolvedor de extensão pode querer interceptar mensagens LSP enviadas e recebidas do servidor de linguagem. Por exemplo, um desenvolvedor de extensão pode querer alterar o parâmetro de mensagem enviado para uma mensagem LSP específica ou modificar os resultados retornados do servidor de linguagem para um recurso LSP (por exemplo, conclusões). Quando isso é necessário, os desenvolvedores de extensão podem usar a API MiddleLayer para interceptar mensagens do LSP.

Para interceptar uma mensagem específica, crie uma classe que implementa a interface ILanguageClientMiddleLayer. Em seguida, implemente a interface ILanguageClientCustomMessage2 em sua classe do cliente de linguagem e retorne uma instância do seu objeto na propriedade MiddleLayer. Exemplo abaixo:

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);
    }
  }
}

O recurso de camada intermediária ainda está em desenvolvimento e ainda não é abrangente.

Exemplo de extensão de servidor de linguagem do LSP

Para ver o código-fonte de uma extensão de exemplo usando a API de cliente do LSP no Visual Studio, consulte o exemplo do LSP VSSDK-Extensibility-Samples.

Perguntas frequentes

Quero criar um sistema de projeto personalizado para complementar meu servidor de linguagem LSP e oferecer suporte a recursos mais avançados no Visual Studio. Como faço isso?

O suporte para servidores de linguagem baseados em LSP no Visual Studio depende do recurso de pasta aberta e foi criado para não exigir um sistema de projeto personalizado. Você pode criar seu próprio sistema de projeto personalizado seguindo as instruções aqui, mas alguns recursos, como configurações, podem não funcionar. A lógica de inicialização padrão para servidores de linguagem LSP é passar o local de pasta raiz da pasta que está sendo aberta no momento. Portanto, se você usar um sistema de projeto personalizado, talvez seja necessário fornecer lógica personalizada durante a inicialização para garantir que o servidor de linguagem seja iniciado corretamente.

Como adiciono o suporte ao depurador?

Vamos oferecer suporte para o protocolo de depuração comum em uma versão futura.

Se já houver um serviço de linguagem compatível com VS instalado (por exemplo, JavaScript), ainda posso instalar uma extensão de servidor de linguagem LSP que ofereça recursos adicionais (como linting)?

Sim, mas nem todos os recursos funcionarão corretamente. O objetivo das extensões de servidor de linguagem LSP é viabilizar serviços de linguagem que não têm suporte nativo do Visual Studio. Para criar extensões que oferecem suporte adicional, use servidores de linguagem LSP, mas alguns recursos (como o IntelliSense) não serão uma experiência simples. Em geral, é aconselhável que as extensões do servidor de linguagem LSP sejam usadas para proporcionar novas experiências de linguagem, não para estender as existentes.

Onde eu publico meu servidor de linguagem LSP VSIX concluído?

Veja as instruções do Marketplace aqui.