Tutorial do Protocolo do Servidor de Idiomas: Do VSCode ao Vim

Publicados: 2022-03-11

O principal artefato de todo o seu trabalho é provavelmente arquivos de texto simples. Então, por que você não usa o Bloco de Notas para criá-los?

O realce de sintaxe e a formatação automática são apenas a ponta do iceberg. E quanto ao linting, conclusão de código e refatoração semiautomática? Todas essas são boas razões para usar um editor de código “real”. Estes são vitais para o nosso dia-a-dia, mas será que entendemos como funcionam?

Neste tutorial do Language Server Protocol, exploraremos um pouco essas questões e descobriremos o que faz com que nossos editores de texto funcionem. No final, juntos implementaremos um servidor de linguagem básico junto com clientes de exemplo para VSCode, Sublime Text 3 e Vim.

Compiladores vs. Serviços de Linguagem

Vamos pular o realce e a formatação de sintaxe por enquanto, que é tratado com análise estática — um tópico interessante por si só — e focar nos principais comentários que recebemos dessas ferramentas. Existem duas categorias principais: compiladores e serviços de linguagem.

Os compiladores pegam seu código-fonte e cospem uma forma diferente. Se o código não seguir as regras da linguagem, o compilador retornará erros. Estes são bastante familiares. O problema com isso é que geralmente é bastante lento e limitado em escopo. Que tal oferecer assistência enquanto você ainda está criando o código?

É isso que os serviços linguísticos oferecem. Eles podem fornecer informações sobre sua base de código enquanto ela ainda está em andamento e provavelmente muito mais rápido do que compilar todo o projeto.

O escopo desses serviços é variado. Pode ser algo tão simples como retornar uma lista de todos os símbolos do projeto ou algo complexo como retornar etapas para refatorar o código. Esses serviços são a principal razão pela qual usamos nossos editores de código. Se quiséssemos apenas compilar e ver erros, poderíamos fazer isso com algumas teclas. Os serviços linguísticos fornecem-nos mais informações e muito rapidamente.

Apostando em um editor de texto para programação

Observe que ainda não chamamos editores de texto específicos. Vamos explicar o porquê com um exemplo.

Digamos que você desenvolveu uma nova linguagem de programação chamada Lapine. É uma linguagem bonita e o compilador fornece mensagens de erro fantásticas do tipo Elm. Além disso, você pode fornecer conclusão de código, referências, ajuda de refatoração e diagnósticos.

Qual editor de código/texto você suporta primeiro? E depois disso? Você tem uma batalha difícil lutando para levar as pessoas a adotá-lo, então você quer tornar isso o mais fácil possível. Você não quer escolher o editor errado e perder usuários. E se você mantiver distância dos editores de código e se concentrar em sua especialidade – a linguagem e seus recursos?

Servidores de idiomas

Digite os servidores de idiomas . São ferramentas que conversam com clientes de idiomas e fornecem os insights que mencionamos. Eles são independentes dos editores de texto pelas razões que acabamos de descrever com nossa situação hipotética.

Como de costume, outra camada de abstração é exatamente o que precisamos. Eles prometem quebrar o forte acoplamento de ferramentas de linguagem e editores de código. Os criadores de linguagem podem agrupar seus recursos em um servidor uma vez, e os editores de código/texto podem adicionar pequenas extensões para se transformarem em clientes. É uma vitória para todos. Para facilitar isso, porém, precisamos concordar sobre como esses clientes e servidores se comunicarão.

Para nossa sorte, isso não é hipotético. A Microsoft já começou definindo o Language Server Protocol.

Tal como acontece com a maioria das grandes ideias, surgiu mais por necessidade do que por previsão. Muitos editores de código já começaram a adicionar suporte para vários recursos de linguagem; alguns recursos terceirizados para ferramentas de terceiros, alguns feitos sob o capô dentro dos editores. Surgiram problemas de escalabilidade e a Microsoft assumiu a liderança na divisão das coisas. Sim, a Microsoft abriu o caminho para remover esses recursos dos editores de código, em vez de acumulá-los no VSCode. Eles poderiam ter continuado construindo seu editor, bloqueando os usuários, mas os libertaram.

Protocolo do servidor de idiomas

O Language Server Protocol (LSP) foi definido em 2016 para ajudar a separar ferramentas e editores de linguagem. Ainda existem muitas impressões digitais do VSCode nele, mas é um passo importante na direção do agnosticismo do editor. Vamos examinar um pouco o protocolo.

Clientes e servidores – pense em editores de código e ferramentas de linguagem – se comunicam em mensagens de texto simples. Essas mensagens têm cabeçalhos semelhantes a HTTP, conteúdo JSON-RPC e podem se originar do cliente ou do servidor. O protocolo JSON-RPC define solicitações, respostas e notificações e algumas regras básicas em torno delas. Uma característica chave é que ele foi projetado para funcionar de forma assíncrona, de modo que clientes/servidores possam lidar com mensagens fora de ordem e com um grau de paralelismo.

Resumindo, o JSON-RPC permite que um cliente solicite a outro programa que execute um método com parâmetros e retorne um resultado ou um erro. O LSP se baseia nisso e define os métodos disponíveis, as estruturas de dados esperadas e mais algumas regras em torno das transações. Por exemplo, há um processo de handshake quando o cliente inicia o servidor.

O servidor é stateful e destina-se apenas a lidar com um único cliente por vez. No entanto, não há restrições explícitas na comunicação, portanto, um servidor de linguagem pode ser executado em uma máquina diferente da do cliente. Na prática, isso seria muito lento para feedback em tempo real. Servidores e clientes de idiomas trabalham com os mesmos arquivos e são bastante falantes.

O LSP tem uma quantidade razoável de documentação quando você sabe o que procurar. Como mencionado, muito disso é escrito no contexto do VSCode, embora as ideias tenham uma aplicação muito mais ampla. Por exemplo, a especificação do protocolo é toda escrita em TypeScript. Para ajudar os exploradores não familiarizados com VSCode e TypeScript, aqui está uma cartilha.

Tipos de mensagens LSP

Existem muitos grupos de mensagens definidos no Language Server Protocol. Eles podem ser divididos em “admin” e “recursos de idioma”. As mensagens do administrador contêm aquelas usadas no handshake cliente/servidor, abertura/alteração de arquivos, etc. Importante, é aqui que clientes e servidores compartilham quais recursos eles manipulam. Certamente, linguagens e ferramentas diferentes oferecem recursos diferentes. Isso também permite a adoção incremental. Langserver.org nomeia meia dúzia de recursos-chave que clientes e servidores devem oferecer suporte, pelo menos um dos quais é necessário para fazer a lista.

Os recursos de linguagem são o que mais nos interessa. Destes, há um para destacar especificamente: a mensagem de diagnóstico. O diagnóstico é um dos principais recursos. Quando você abre um arquivo, geralmente é assumido que ele será executado. Seu editor deve informar se há algo errado com o arquivo. A maneira como isso acontece com o LSP é:

  1. O cliente abre o arquivo e envia textDocument/didOpen para o servidor.
  2. O servidor analisa o arquivo e envia a notificação textDocument/publishDiagnostics .
  3. O cliente analisa os resultados e exibe indicadores de erro no editor.

Essa é uma maneira passiva de obter insights de seus serviços de idiomas. Um exemplo mais ativo seria encontrar todas as referências para o símbolo sob o cursor. Isso seria algo como:

  1. O cliente envia textDocument/references ao servidor, especificando um local em um arquivo.
  2. O servidor descobre o símbolo, localiza referências neste e em outros arquivos e responde com uma lista.
  3. O cliente exibe as referências para o usuário.

Uma ferramenta de lista negra

Certamente poderíamos nos aprofundar nas especificidades do Language Server Protocol, mas vamos deixar isso para os implementadores de clientes. Para consolidar a ideia de separação entre editor e ferramenta de linguagem, desempenharemos o papel de criador de ferramentas.

Vamos mantê-lo simples e, em vez de criar uma nova linguagem e recursos, vamos nos ater ao diagnóstico. Os diagnósticos são adequados: são apenas avisos sobre o conteúdo de um arquivo. Um linter retorna o diagnóstico. Faremos algo parecido.

Faremos uma ferramenta para nos notificar de palavras que gostaríamos de evitar. Em seguida, forneceremos essa funcionalidade para alguns editores de texto diferentes.

O servidor de idiomas

Primeiro, a ferramenta. Vamos preparar isso direto para um servidor de idiomas. Para simplificar, este será um aplicativo Node.js, embora possamos fazê-lo com qualquer tecnologia capaz de usar fluxos para leitura e gravação.

Aqui está a lógica. Dado algum texto, esse método retorna uma matriz das palavras da lista negra correspondentes e os índices onde foram encontradas.

 const getBlacklisted = (text) => { const blacklist = [ 'foo', 'bar', 'baz', ] const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi') const results = [] while ((matches = regex.exec(text)) && results.length < 100) { results.push({ value: matches[0], index: matches.index, }) } return results }

Agora, vamos torná-lo um servidor.

 const { TextDocuments, createConnection, } = require('vscode-languageserver') const {TextDocument} = require('vscode-languageserver-textdocument') const connection = createConnection() const documents = new TextDocuments(TextDocument) connection.onInitialize(() => ({ capabilities: { textDocumentSync: documents.syncKind, }, })) documents.listen(connection) connection.listen()

Aqui, estamos utilizando o vscode-languageserver . O nome é enganoso, pois certamente pode funcionar fora do VSCode. Esta é uma das muitas “impressões digitais” que você vê das origens do LSP. vscode-languageserver cuida do protocolo de nível inferior e permite que você se concentre nos casos de uso. Este trecho inicia uma conexão e a vincula a um gerenciador de documentos. Quando um cliente se conecta ao servidor, o servidor informará que gostaria de ser notificado sobre a abertura de documentos de texto.

Poderíamos parar aqui. Este é um servidor LSP totalmente funcional, embora inútil. Em vez disso, vamos responder às alterações do documento com algumas informações de diagnóstico.

 documents.onDidChangeContent(change => { connection.sendDiagnostics({ uri: change.document.uri, diagnostics: getDiagnostics(change.document), }) })

Por fim, conectamos os pontos entre o documento que mudou, nossa lógica e a resposta do diagnóstico.

 const getDiagnostics = (textDocument) => getBlacklisted(textDocument.getText()) .map(blacklistToDiagnostic(textDocument)) const { DiagnosticSeverity, } = require('vscode-languageserver') const blacklistToDiagnostic = (textDocument) => ({ index, value }) => ({ severity: DiagnosticSeverity.Warning, range: { start: textDocument.positionAt(index), end: textDocument.positionAt(index + value.length), }, message: `${value} is blacklisted.`, source: 'Blacklister', })

Nossa carga de diagnóstico será o resultado da execução do texto do documento por meio de nossa função, depois mapeada para o formato esperado pelo cliente.

Este script criará tudo isso para você.

 curl -o- https://raw.githubusercontent.com/reergymerej/lsp-article-resources/revision-for-6.0.0/blacklist-server-install.sh | bash

Nota: Se você não se sentir à vontade com estranhos adicionando executáveis ​​à sua máquina, verifique a fonte. Ele cria o projeto, baixa o index.js e npm link é para você.

Saída do comando curl acima, instalando o projeto para você.

Fonte completa do servidor

A fonte final blacklist-server é:

 #!/usr/bin/env node const { DiagnosticSeverity, TextDocuments, createConnection, } = require('vscode-languageserver') const {TextDocument} = require('vscode-languageserver-textdocument') const getBlacklisted = (text) => { const blacklist = [ 'foo', 'bar', 'baz', ] const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi') const results = [] while ((matches = regex.exec(text)) && results.length < 100) { results.push({ value: matches[0], index: matches.index, }) } return results } const blacklistToDiagnostic = (textDocument) => ({ index, value }) => ({ severity: DiagnosticSeverity.Warning, range: { start: textDocument.positionAt(index), end: textDocument.positionAt(index + value.length), }, message: `${value} is blacklisted.`, source: 'Blacklister', }) const getDiagnostics = (textDocument) => getBlacklisted(textDocument.getText()) .map(blacklistToDiagnostic(textDocument)) const connection = createConnection() const documents = new TextDocuments(TextDocument) connection.onInitialize(() => ({ capabilities: { textDocumentSync: documents.syncKind, }, })) documents.onDidChangeContent(change => { connection.sendDiagnostics({ uri: change.document.uri, diagnostics: getDiagnostics(change.document), }) }) documents.listen(connection) connection.listen()

Tutorial do Protocolo do Servidor de Idiomas: Hora de um Test Drive

Depois que o projeto for link , tente executar o servidor, especificando stdio como o mecanismo de transporte:

 blacklist-server --stdio

Está ouvindo no stdio agora as mensagens LSP sobre as quais falamos antes. Poderíamos fornecê-los manualmente, mas vamos criar um cliente.

Cliente de idioma: VSCode

Como essa tecnologia se originou no VSCode, parece apropriado começar por aí. Vamos criar uma extensão que irá criar um cliente LSP e conectá-lo ao servidor que acabamos de criar.

Existem várias maneiras de criar uma extensão VSCode, incluindo usar Yeoman e o gerador apropriado, generator-code . Para simplificar, porém, vamos fazer um exemplo básico.

Vamos clonar o clichê e instalar suas dependências:

 git clone [email protected]:reergymerej/standalone-vscode-ext.git blacklist-vscode cd blacklist-vscode npm i # or yarn

Abra o diretório blacklist-vscode no VSCode.

Pressione F5 para iniciar outra instância do VSCode, depurando a extensão.

No “console de depuração” da primeira instância do VSCode, você verá o texto “Olha, ma. Uma extensão!"

Duas instâncias do VSCode. O da esquerda está executando a extensão blacklist-vscode e mostrando sua saída do console de depuração, e o da direita é o host de desenvolvimento da extensão.

Agora temos uma extensão básica do VSCode funcionando sem todos os sinos e assobios. Vamos torná-lo um cliente LSP. Feche as duas instâncias do VSCode e, no diretório blacklist-vscode , execute:

 npm i vscode-languageclient

Substitua extension.js por:

 const { LanguageClient } = require('vscode-languageclient') module.exports = { activate(context) { const executable = { command: 'blacklist-server', args: ['--stdio'], } const serverOptions = { run: executable, debug: executable, } const clientOptions = { documentSelector: [{ scheme: 'file', language: 'plaintext', }], } const client = new LanguageClient( 'blacklist-extension-id', 'Blacklister', serverOptions, clientOptions ) context.subscriptions.push(client.start()) }, }

Isso usa o pacote vscode-languageclient para criar um cliente LSP no VSCode. Ao contrário vscode-languageserver , isso é fortemente acoplado ao VSCode. Resumindo, o que estamos fazendo nesta extensão é criar um cliente e dizer a ele para usar o servidor que criamos nas etapas anteriores. Passando por cima das especificidades da extensão VSCode, podemos ver que estamos dizendo para usar esse cliente LSP para arquivos de texto simples.

Para testá-lo, abra o diretório blacklist-vscode no VSCode. Pressione F5 para iniciar outra instância, depurando a extensão.

Na nova instância do VSCode, crie um arquivo de texto simples e salve-o. Digite “foo” ou “bar” e espere um momento. Você verá avisos de que eles estão na lista negra.

A nova instância do VSCode com test.txt aberto, mostrando "foo" e "bar" com sublinhado de erro e uma mensagem sobre cada um no painel de problemas, dizendo que estão na lista negra.

É isso! Não tivemos que recriar nenhuma lógica, apenas coordenar o cliente e o servidor.

Vamos fazer de novo para outro editor, desta vez Sublime Text 3. O processo será bem parecido e um pouco mais fácil.

Cliente de idioma: Texto sublime 3

Primeiro, abra o ST3 e abra a paleta de comandos. Precisamos de uma estrutura para tornar o editor um cliente LSP. Digite “Package Control: Install Package” e aperte enter. Encontre o pacote “LSP” e instale-o. Depois de concluído, temos a capacidade de especificar clientes LSP. Existem muitas predefinições, mas não vamos usá-las. Nós criamos o nosso próprio.

Novamente, abra a paleta de comandos. Encontre “Preferências: Configurações LSP” e pressione Enter. Isso abrirá o arquivo de configuração, LSP.sublime-settings , para o pacote LSP. Para adicionar um cliente personalizado, use a configuração abaixo.

 { "clients": { "blacklister": { "command": [ "blacklist-server", "--stdio" ], "enabled": true, "languages": [ { "syntaxes": [ "Plain text" ] } ] } }, "log_debug": true }

Isso pode parecer familiar da extensão VSCode. Definimos um cliente, dissemos a ele para funcionar em arquivos de texto simples e especificamos o servidor de idioma.

Salve as configurações e, em seguida, crie e salve um arquivo de texto simples. Digite “foo” ou “bar” e aguarde. Novamente, você verá avisos de que eles estão na lista negra. O tratamento — como as mensagens são exibidas no editor — é diferente. No entanto, nossa funcionalidade é a mesma. Nós quase não fizemos nada desta vez para adicionar suporte ao editor.

Idioma “Cliente”: Vim

Se você ainda não está convencido de que essa separação de interesses facilita o compartilhamento de recursos entre editores de texto, aqui estão as etapas para adicionar a mesma funcionalidade ao Vim via Coc.

Abra o Vim e digite :CocConfig e adicione:

 "languageserver": { "blacklister": { "command": "blacklist-server", "args": ["--stdio"], "filetypes": ["text"] } }

Feito.

A separação cliente-servidor permite que os idiomas e os serviços de idiomas prosperem

Separar a responsabilidade dos serviços linguísticos dos editores de texto em que são usados ​​é claramente uma vitória. Ele permite que os criadores de recursos de linguagem se concentrem em sua especialidade e os criadores de editores façam o mesmo. É uma ideia relativamente nova, mas a adoção está se espalhando.

Agora que você tem uma base para trabalhar, talvez possa encontrar um projeto e ajudar a levar essa ideia adiante. A guerra de chamas do editor nunca vai acabar, mas tudo bem. Contanto que as habilidades linguísticas possam existir fora de editores específicos, você está livre para usar qualquer editor que desejar.


Selo Microsoft Gold Partner.

Como Microsoft Gold Partner, a Toptal é sua rede de elite de especialistas da Microsoft. Construa equipes de alto desempenho com os especialistas que você precisa - em qualquer lugar e exatamente quando você precisar deles!