Language Server Protocol
O que é o Language Server Protocol?
O suporte a recursos avançados de edição, como auto-completar código-fonte ou Ir para definição para uma linguagem de programação em um editor ou IDE, é tradicionalmente muito desafiador e demorado. Normalmente, é necessário escrever um modelo de domínio (um scanner, um analisador, um verificador de tipos, um construtor e muito mais) na linguagem de programação do editor ou IDE. Por exemplo, o plug-in Eclipse CDT, que fornece suporte para C/C++ no IDE do Eclipse, é escrito em Java, já que o próprio IDE do Eclipse é escrito em Java. Seguindo essa abordagem, isso significaria implementar um modelo de domínio C/C++ no TypeScript para Visual Studio Code e um modelo de domínio separado em C# para Visual Studio.
A criação de modelos de domínio específicos de linguagem também é muito mais fácil se uma ferramenta de desenvolvimento puder reutilizar bibliotecas específicas de idioma existentes. No entanto, essas bibliotecas são geralmente implementadas na própria linguagem de programação (por exemplo, bons modelos de domínio C/C++ são implementados em C/C++). Integrar uma biblioteca C/C++ em um editor escrito em TypeScript é tecnicamente possível, mas difícil de fazer.
Servidores de idiomas
Outra abordagem é executar a biblioteca em seu próprio processo e usar a comunicação entre processos para falar com ela. As mensagens enviadas de um lado para o outro formam um protocolo. O protocolo de servidor de idiomas (LSP) é o produto da padronização das mensagens trocadas entre uma ferramenta de desenvolvimento e um processo de servidor de idiomas. Usar servidores de linguagem ou demônios não é uma ideia nova ou nova. Editores como Vim e Emacs têm feito isso há algum tempo para fornecer suporte ao preenchimento automático semântico. O objetivo do LSP era simplificar esses tipos de integrações e fornecer uma estrutura útil para expor recursos de linguagem a uma variedade de ferramentas.
Ter um protocolo comum permite a integração de recursos de linguagem de programação em uma ferramenta de desenvolvimento com o mínimo de barulho, reutilizando uma implementação existente do modelo de domínio da linguagem. Um back-end de servidor de linguagem pode ser escrito em PHP, Python ou Java e o LSP permite que ele seja facilmente integrado a uma variedade de ferramentas. O protocolo funciona em um nível comum de abstração para que uma ferramenta possa oferecer serviços de linguagem avançada sem a necessidade de entender completamente as nuances específicas do modelo de domínio subjacente.
Como começou o trabalho no PSL
O LSP evoluiu ao longo do tempo e hoje está na Versão 3.0. Tudo começou quando o conceito de um servidor de linguagem foi adotado pela OmniSharp para fornecer recursos de edição avançados para C#. Inicialmente, o OmniSharp usou o protocolo HTTP com uma carga JSON e foi integrado a vários editores, incluindo o Visual Studio Code.
Na mesma época, a Microsoft começou a trabalhar em um servidor de linguagem TypeScript, com a ideia de suportar o TypeScript em editores como Emacs e Sublime Text. Nessa implementação, um editor se comunica por meio de stdin/stdout com o processo do servidor TypeScript e usa uma carga JSON inspirada no protocolo do depurador V8 para solicitações e respostas. O servidor TypeScript foi integrado ao plugin TypeScript Sublime e ao VS Code para edição avançada do TypeScript.
Depois de ter integrado dois servidores de idiomas diferentes, a equipe do VS Code começou a explorar um protocolo de servidor de linguagem comum para editores e IDEs. Um protocolo comum permite que um provedor de idiomas crie um único servidor de idiomas que pode ser consumido por diferentes IDEs. Um consumidor de servidor de idioma só precisa implementar o lado do cliente do protocolo uma vez. Isso resulta em uma situação vantajosa tanto para o provedor de idiomas quanto para o consumidor de idiomas.
O protocolo do servidor de idiomas começou com o protocolo usado pelo servidor TypeScript, expandindo-o com mais recursos de linguagem inspirados na API da linguagem VS Code. O protocolo é apoiado com JSON-RPC para invocação remota devido à sua simplicidade e bibliotecas existentes.
A equipe do VS Code prototipou o protocolo implementando vários servidores de linguagem linter que respondem a solicitações para lint (varredura) um arquivo e retornam um conjunto de avisos e erros detectados. O objetivo era lint um arquivo como o usuário edita em um documento, o que significa que haverá muitas solicitações de linting durante uma sessão do editor. Fazia sentido manter um servidor em funcionamento para que um novo processo de linting não precisasse ser iniciado para cada edição do usuário. Vários servidores linter foram implementados, incluindo as extensões ESLint e TSLint do VS Code. Esses dois servidores linter são implementados em TypeScript/JavaScript e executados em Node.js. Eles compartilham uma biblioteca que implementa a parte cliente e servidor do protocolo.
Como funciona o PSL
Um servidor de idioma é executado em seu próprio processo, e ferramentas como Visual Studio ou VS Code se comunicam com o servidor usando o protocolo de linguagem sobre JSON-RPC. Outra vantagem do servidor de idiomas operando em um processo dedicado é que problemas de desempenho relacionados a um único modelo de processo são evitados. O canal de transporte real pode ser stdio, soquetes, pipes nomeados ou ipc de nó se o cliente e o servidor estiverem gravados em Node.js.
Abaixo está um exemplo de como uma ferramenta e um servidor de idiomas se comunicam durante uma sessão de edição de rotina:
O usuário abre um arquivo (chamado de documento) na ferramenta: A ferramenta notifica o servidor de idiomas de que um documento está aberto ('textDocument/didOpen'). A partir de agora, a verdade sobre o conteúdo do documento não está mais no sistema de arquivos, mas mantida pela ferramenta na memória.
O usuário faz edições: A ferramenta notifica o servidor sobre a alteração do documento ('textDocument/didChange') e as informações semânticas do programa são atualizadas pelo servidor de idiomas. À medida que isso acontece, o servidor de idiomas analisa essas informações e notifica a ferramenta com os erros e avisos detectados ('textDocument/publishDiagnostics').
O usuário executa "Go to Definition" em um símbolo no editor: A ferramenta envia uma solicitação 'textDocument/definition' com dois parâmetros: (1) o URI do documento e (2) a posição do texto de onde a solicitação Go to Definition foi iniciada para o servidor. O servidor responde com o URI do documento e a posição da definição do símbolo dentro do documento.
O usuário fecha o documento (arquivo): Uma notificação 'textDocument/didClose' é enviada da ferramenta, informando ao servidor de idioma que o documento não está mais na memória e que o conteúdo atual está atualizado no sistema de arquivos.
Este exemplo ilustra como o protocolo se comunica com o servidor de idiomas no nível de recursos do editor como "Ir para definição", "Localizar todas as referências". Os tipos de dados usados pelo protocolo são 'tipos de dados' do editor ou IDE, como o documento de texto aberto no momento e a posição do cursor. Os tipos de dados não estão no nível de um modelo de domínio de linguagem de programação que normalmente forneceria árvores de sintaxe abstratas e símbolos de compilador (por exemplo, tipos resolvidos, namespaces, ...). Isso simplifica significativamente o protocolo.
Agora vamos examinar a solicitação 'textDocument/definition' com mais detalhes. Abaixo estão as cargas úteis que vão entre a ferramenta cliente e o servidor de idioma para a solicitação "Ir para definição" em um documento C++.
Este é o pedido:
{
"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
}
}
}
Esta é a resposta:
{
"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
}
}
}
}
Em retrospectiva, descrever os tipos de dados no nível do editor e não no nível do modelo de linguagem de programação é uma das razões para o sucesso do protocolo do servidor de linguagem. É muito mais simples padronizar um URI de documento de texto ou uma posição do cursor em comparação com a padronização de uma árvore de sintaxe abstrata e símbolos do compilador em diferentes linguagens de programação.
Quando um usuário está trabalhando com linguagens diferentes, o VS Code normalmente inicia um servidor de linguagem para cada linguagem de programação. O exemplo abaixo mostra uma sessão em que o usuário trabalha em arquivos Java e SASS.
Funcionalidades
Nem todo servidor de idioma pode oferecer suporte a todos os recursos definidos pelo protocolo. Portanto, o cliente e o servidor anunciam seu conjunto de recursos suportados por meio de 'recursos'. Como exemplo, um servidor anuncia que pode lidar com a solicitação 'textDocument/definition', mas pode não lidar com a solicitação 'workspace/symbol'. Da mesma forma, os clientes podem anunciar que podem fornecer notificações "prestes a salvar" antes que um documento seja salvo, para que um servidor possa computar edições textuais para formatar automaticamente o documento editado.
Integrando um servidor de idiomas
A integração real de um servidor de idiomas em uma ferramenta específica não é definida pelo protocolo do servidor de idiomas e é deixada para os implementadores da ferramenta. Algumas ferramentas integram servidores de idiomas genericamente por ter uma extensão que pode iniciar e conversar com qualquer tipo de servidor de idiomas. Outros, como o VS Code, criam uma extensão personalizada por servidor de idioma, para que uma extensão ainda possa fornecer alguns recursos de idioma personalizados.
Para simplificar a implementação de servidores de idiomas e clientes, existem bibliotecas ou SDKs para as partes de cliente e servidor. Essas bibliotecas são fornecidas para diferentes idiomas. Por exemplo, há um módulo npm do cliente de idioma para facilitar a integração de um servidor de idiomas em uma extensão VS Code e outro módulo npm do servidor de idiomas para escrever um servidor de idiomas usando Node.js. Esta é a lista atual de bibliotecas de suporte.
Usando o protocolo de servidor de idioma no Visual Studio
- Adicionando uma extensão de protocolo de servidor de idioma - Saiba mais sobre como integrar um servidor de idiomas ao Visual Studio.