LSP e o PyLS

Código Python

Uma das virtudes do VS Code é como ele integra-se facilmente com diferentes linguagens. O esforço é pequeno, geralmente resumindo-se à instalação de um único plugin. Essa característica o torna uma ferramenta extremamente produtiva, e que vem conquistando o coração dos desenvolvedores ao redor do mundo.

O curioso é que o VS Code utiliza um protocolo aberto para ter esse "jeitão" de IDE, chamado Language Server Protocol, que entre outras funcionalidades traz ao editor a possibilidade de autocomplete, go to definition, diagnosis e documentation.

É... Microsoft popularizando protocolos abertos. Tempos interessantes esses que vivemos, não?

LSP - Language Server Protocol

Durante o desenvolvimento do VS Code, a Microsoft introduziu o Language Server Protocol. O langserver.org o define da seguinte forma:

The Language Server protocol is used between a tool (the client) and a language smartness provider (the server) to integrate features like auto complete, go to definition, find all references and alike into the tool.

Em outras palavras, é um protocolo que descreve a comunicação entre um cliente (um editor de textos, por exemplo) e um servidor (ligado à uma determinada linguagem), com a motivação de proporcionar e enriquecer ferramentas de desenvolvimento.

Ilustração de como funciona a comunicação entre cliente e servidor (kieranbamforth.me)

Ou seja, podemos ter uma solução única para Python, que traga as mesmas funcionalidades para diferentes editores ou IDEs. Em contrapartida, podemos ter um único editor que suporte diferentes linguagens e tais funcionalidades avançadas. O langserver.org chama esse problema de "The Matrix Problem":

Com LSP, temos Go funcionando para diferentes editores, seguindo os mesmos princípios (langserver.org)

Como afirma o site oficial do projeto:

LSP is a win for both language providers and tooling vendors!

Como funciona?

Na prática, o que acontece é uma comunicação inter-processos entre o language server e a ferramenta de desenvolvimento. O protocolo utilizado para tal operação é o JSON-RPC, um protocolo simples, e com um formato de dados extremamente popular.

Exemplo de como funciona a comunicação entre cliente e servidor (microsoft.github.io)

Nem toda linguagem pode suportar todas as funcionalidade definidas pelo protocolo (o mesmo vale para as ferramentas de desenvolvimento), mas isso não impede a sua adoção. O protocolo permite que tanto cliente como servidor "anunciem" suas capacidades, portanto, as duas pontas podem ter um comportamento preventivo quando determinada capacidade (como go to definition) não é suportada.

PyLS - Python Language Server

Para Python, além do language server implementado pela própria Microsoft e usado no VS Code, temos o Python Language Server. Ele conta com as seguintes funcionalidades:

  • Auto completion
  • Code linting
  • Signature help
  • Go to definition
  • Hover
  • Find references
  • Document symbols
  • Document formatting

O que acho mais interessante em relação ao projeto é que ele não reinventa a roda. Por exemplo, ele não implementa uma nova ferramenta de linting, pelo contrário, ele utiliza ferramentas que já são bem conhecidas pela comunidade:

Há um certo grau de customização, como por exemplo usar o Black ao invés de YAPF. Discutiremos mais sobre esse tópico a seguir.

Clientes LSP

Segundo o langserver.org, os principais IDEs e editores já possuem alguma forma de cliente implementado. Seja através de extensões (como JetBrains IDE, Vim, Sublime, Atom e Emacs), ou através de suporte built-in (como VS Code e Oni).

Sauron faz um anel para juntar a todos. Sem querer comparar Microsoft com Sauron, mas espero que isso não acabe em uma guerra (lego-lord-of-the-rings.wikia.com)

Para Vim, uma opção que tenho usado com sucesso é o LanguageClient-neovim, que embora possa parecer exclusivo para o Neovim, possui suporte para Vim 8.x.

Na prática

Configurando o servidor

Vamos para um exemplo prático de como usar LSP e Vim com Python. Mas antes, deixa eu definir algumas constraints do meu ambiente de desenvolvimento:

  • Quero utilizar flake8 ao invés de pycodestyle;
  • Quero utilizar Black ao invés de YAPF ou autopep8;
  • Quero utilizar isort como uma das opções de formatação.

Para começar, vamos instalar o language server. Eu prefiro tê-lo dentro do ambiente virtual do projeto em que estou trabalhando. É trabalhoso e repetitivo se você estiver trabalhando em diferentes projetos e virtualenvs! Uma alternativa mais prática é tê-lo instalado em uma instância global do pyenv. Fica a seu critério o destino da instalação:

$ pip install python-language-server

O próprio servidor identificará quais ferramentas você tem instaladas e habilitará determinada capacidade proporcionada por tal ferramenta (por exemplo, se o Rope estiver instalado, será capaz de fazer renaming). Mas dentre elas o Jedi é fundamental, e por isso é instalada automaticamente como dependência do PyLS.

Nesse momento temos grande parte das funcionalidades cobertas pelo LSP habilitadas pelo servidor. Uma muito útil, que ainda está faltando, é o renaming. Segundo a lista acima, conseguimos tal capacidade ao instalar o Rope:

$ pip install 'python-language-server[rope]'

Se você está interessado em utilizar a configuração padrão do python-language-server, uma opção mais prática é instalar todos os providers de uma só vez:

$ pip install 'python-language-server[all]'

Para fins didáticos estamos resolvendo as dependências manualmente. Há uma outra forma de gerenciar quais providers serão utilizados, que discutiremos a seguir.

Para termos o isort e o Black habilitados como ferramentas de formatação, precisaremos recorrer à plugins de terceiros:

$ pip install pyls-isort pyls-black

Infelizmente, o pyls-black exige que você não tenha o autopep8 instalado para funcionar. Como não há um controle manual para decidir qual será usado, a melhor forma é garantindo que apenas uma das duas extensões esteja instalada.

Temos tudo o que é necessário para o lado do servidor.

Configurando o cliente

A melhor forma de instalarmos o LanguageClient-neovim é através do utilitário Plug:

" ~/.vimrc

Plug 'autozimu/LanguageClient-neovim', {
    \ 'branch': 'next',
    \ 'do': 'bash install.sh',
    \ }

Plug 'junegunn/fzf'

Ele funciona muito bem com o fzf, portanto, é uma boa pedida termos os dois disponíveis.

O próximo passo é informar ao cliente como encontrar o servidor da linguagem:

" ~/.vimrc

(...)

let g:LanguageClient_serverCommands = {
    \ 'python': ['pyls'],
    \ }

Ainda precisamos dizer ao LSP sobre as nossas preferências. O arquivo abaixo está situado na raíz do projeto que estou trabalhando, mais especificamente em ./.vim/settings.json:

{
  "pyls": {
    "configurationSources": ["flake8"],
    "plugins": {
      "yapf": {
        "enabled": false
      }
    }
  }
}

Para fins didáticos adicionei o yapf à configuração. Alguns plugins possuem a flexibilidade de mesmo que instalados, poderem ser desabilitados.

Pronto! O cliente está configurado, e com os seguintes comandos você pode explorar todas as funcionalidades do LSP:

  • Ctrl+x Ctrl+o: Auto completion via Omni completion (o Deoplete é uma boa pedida)
  • :call LanguageClient#textDocument_definition(): Go to definition
  • :call LanguageClient#textDocument_hover(): Hover
  • :call LanguageClient#textDocument_references(): References
  • :call LanguageClient#textDocument_documentSymbol(): Lista de Symbols
  • :call LanguageClient#textDocument_rename(): Renaming
  • :call LanguageClient#textDocument_formatting(): Formatting
  • :call :call LanguageClient_contextMenu(): Menu de contexto mostrando todas as opções disponíveis

Exemplo do LSP funcionando com o Vim

Agora é só configurar os key bindings e correr para o abraço :)

Referências