Mochileiro T.I
Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors

Debug no Neovim: finalizando a IDE

Nessa série de artigos, aprenda a transformar do zero este poderoso editor em uma IDE completa e recheada de recursos. Neste terceiro e último artigo, veja como instalar depuradores de código e recursos extras como autocomplete e quickrun (execução rápida de código)

Depurar código é parte indispensável da rotina de qualquer desenvolvedor de software. O Neovim, no entanto, não vem com recurso de debug nativamente incorporado ao editor.

Mas isso não é problema, pois podemos adicionar suporte a depuradores para as mais diversas linguagens de programação com a utilização de um único plugin – o nvim-dap.

Se você é novo por aqui, seja bem-vindo! Este artigo é a terceira e última parte da série Neovim IDE e a não ser que você queira apenas aprender sobre depuradores, recomendo a leitura dos artigos anteriores:

Nesses artigos adicionamos muitas coisas interessantes ao editor, tais como:

  • Gerenciador de Plugins com Lazy.nvim
  • Temas (Catppuccin)
  • Explorador de arquivos com Neotree
  • Buscador inteligente com Telescope
  • Analisador de Sintaxe com Treesitter
  • Barra de status com Lualine
  • Servidores de linguagem com Mason
  • Integração dos servidores com Mason-lspconfig
  • Configuração e inicialização dos servidores com Neovim-lspconfig
  • Linters e Formatadores com Mason
  • Integração e configuração dos linters e formatadores com None-ls

Entretanto ainda faltam alguns recursos importantes para consolidar a transformação de um mero editor de textos para uma IDE prática e muito rápida.

Neste artigo iremos incorporar ferramentas de debug no Neovim, como por exemplo o nvim-dap – plugin que implementa um cliente para o DAP (Debug Adapter Protocol).

Além disso, instalaremos uma interface gráfica completa de depuração de código, nvim-dap-ui, que utiliza o nvim-dap para enviar comandos ao depurador e exibir informações na tela.

Por fim, instalaremos os depuradores, programas externos que se conectam ao Neovim via protocolo DAP fazendo o trabalho de depuração propriamente dito.

E para encerrar esta série de artigos, iremos incluir um plugin para exibir informações de autocomplete e outro para execução rápida de código (quickrun).

Portanto, fique atento e boa leitura!

Debug no Neovim com nvim-dap

O plugin nvim-dap implementa o DAP (Debug Adapter Protocol) para o Neovim, fornecendo a infraestrutura necessária para a comunicação com depuradores externos.

Em outras palavras, o nvim-dap comunica-se com depuradores externos (que veremos mais adiante) para executar a depuração do código e fornecer os resultados de modo que o Neovim os exiba em tela.

Portanto, é função dele gerenciar sessões de depuração, controlar pontos de interrupção (breakpoints), inspecionar variáveis, pilhas de chamada, etc e fornecer comandos para interagir com a depuração.

Importante ressaltar que o DAP é um padrão de comunicação entre editores e depuradores. Dessa forma, tenha em mente que ao utilizar o nvim-dap, os depuradores também devem implementar este padrão.

Sendo assim, vamos instalar o plugin DAP no nosso ambiente. Crie um arquivo de spec novo para receber as configurações de debug do Neovim.

Eu criei um arquivo chamado debugging.lua, mas fique à vontade para nomeá-lo como quiser. Dentro dele, por enquanto, vamos apenas definir o nvim-dap e alguns atalhos de depuração:

return {
  'mfussenegger/nvim-dap',
  keys = {
    { '<F5>', function() require('dap').continue() end, desc = 'Debug Continue' },
    { '<F10>', function() require('dap').step_over() end, desc = 'Step Over' },
    { '<F11>', function() require('dap').step_into() end, desc = 'Step Into'},
    { '<F12>', function() require('dap').step_out() end, desc = 'Step Out'},
    { '<leader>b', function() require('dap').toggle_breakpoint() end, desc = 'Toggle Breakpoint'},
    { '<leader>B', function() require('dap').set_breakpoint() end, desc = 'Set Breakpoint'},
  },
}

Como o habitual, salve, saia e entre novamente no Neovim para que o Lazy.nvim realize seu trabalho.

Este é o primeiro e mais simples passo para incorporar ao Neovim o recurso de depuração de código.

Ainda precisamos de depuradores, adaptadores e a uma interface de usuário (UI) para depurar o código diretamente no editor.

Instalação de Depuradores e Adaptadores

Essa talvez seja a parte mais chata do processo de instalação do recurso de debug no Neovim.

Uma vez que o protocolo DAP é padrão e genérico, cada linguagem ou depurador tem sua própria maneira de funcionar.

Portanto, para cada depurador, precisamos ter um adaptador que abstraia essas diferenças, traduzindo os comandos enviados pelo Neovim (via DAP) em ações que o depurador entenda.

Para nossa sorte, o GitHub do nvim-dap disponibiliza uma documentação que serve de guia de instalação e configuração dessa infraestrutura para várias linguagens de programação.

A lista contendo todas as linguagens suportadas pelo plugin está disponível em https://codeberg.org/mfussenegger/nvim-dap/wiki/Debug-Adapter-installation.

O guia do link acima (codeberg.org) fornece informações de como instalar os programas externos de depuração (depuradores) bem como o código Lua correspondente para criar o adaptador.

Como exemplo, demonstrarei como instalar um depurador para a linguagem Lua chamado local-lua-debugger-vscode e seu respectivo adaptador.

A instalação dos demais depuradores segue a mesma ideia que apresentarei logo abaixo, bastando apenas consultar a documentação pertinente e seguir suas orientações.

Instalação do Depurador Lua

Você deve ter notado que o depurador carrega em seu nome o sufixo -vscode e deve estar se perguntando o que o VSCode tem a ver com isso.

O local-lua-debugger-vscode foi originalmente criado para o VSCode. No entanto, como ele implementa o DAP, ele funciona com qualquer cliente que também implemente o mesmo protocolo.

Portanto, não só o Neovim como qualquer outro editor que também o faça pode utilizá-lo! Essa é a principal vantagem da padronização.

Assim sendo, vamos instalá-lo em nosso sistema operacional de modo que o nvim-dap possa acessá-lo.

Primeiramente, para organizar, eu prefiro baixar os depuradores dentro de ~/.config/nvim/debuggers, mas fique à vontade para deixá-los onde preferir. Lembre-se apenas de onde os colocou.

Então baixe o local-lua-debugger-vscode conforme abaixo:

cd ~/.config/nvim/debuggers
git clone https://github.com/tomblind/local-lua-debugger-vscode

Como esse depurador foi construído em Node.js, você precisará tê-lo em seu sistema. Caso ainda não tenha, basta instalá-lo via package manager, no meu caso apt:

sudo apt install nodejs npm

Em seguida vamos instalar o local-lua-debugger-vscode através dos seguintes passos:

cd local-lua-debugger-vscode
npm install
npm run build

Pronto! Agora precisamos de um intermediário que faça a ponte entre o nvim-dap e o depurador. Aqui entram os adaptadores, conforme veremos a seguir.

Instalação do Adaptador para o Depurador Lua

Para o Neovim, um adaptador nada mais é que um trecho de código Lua que, entre outras coisas, define onde está o depurador, o interpretador (se for o caso) e o arquivo a ser depurado.

Esse pedaço de código pode ficar dentro do debugging.lua, no entanto, prefiro separá-lo para melhorar a legibilidade. Assim sendo, criei uma pasta adapters dentro de ~/.config/nvim/lua:

mkdir ~/.config/nvim/lua/adapters

Esta pasta abrigará um arquivo Lua para cada adaptador. Entretanto, caso prefira, coloque seus adaptadores onde melhor for.

Seguindo essa lógica, vamos criar um arquivo chamado local-lua.lua:

nvim ~/.config/nvim/lua/adapters/local-lua.lua

Em seguida basta colar o conteúdo abaixo. Note que, apesar de não ser obrigatório, ele está em formato de “classe” para facilitar seu uso no arquivo debugging.lua:

local M = {}

function M.setup(dap)
  -- Nome do adaptador "local-lua"
  dap.adapters["local-lua"] = {
    type = "executable",
    command = "node",
    args = {
    -- Local do depurador
      vim.fn.stdpath("config") .. "/debuggers/local-lua-debugger-vscode/extension/debugAdapter.js",
    },
    enrich_config = function(config, on_config)
      if not config.extensionPath then
        local c = vim.deepcopy(config)
        -- Diretório do depurador
        c.extensionPath = vim.fn.stdpath("config") .. "/debuggers/local-lua-debugger-vscode/"
        on_config(c)
      else
        on_config(config)
      end
    end,
  }

  dap.configurations.lua = {
    {
      -- Qualquer descrição
      name = "Debug Lua file (local-lua)",
      -- Nome do adaptador
      type = "local-lua",
      request = "launch",
      -- Diretório atual de trabalho
      cwd = vim.fn.getcwd(),
      program = {
        -- Definição do interpretador Lua
        lua = "lua5.1",
        -- Local do arquivo a ser depurado
        file = vim.fn.expand("%:p"),
      },
      args = {},
    },
  }
end

return M

Este arquivo basicamente se resume em duas tabelas Lua contidas no objeto dap que vem do plugin nvim-dap.

Tabela Adapters

A primeira delas, dap.adapters[“local-lua”] contém a definição propriamente dita do adaptador Lua que estamos criando. O nome “local-lua” não é obrigatório; podemos escolher qualquer nome.

Repare que a chave command define como executável o NodeJs. A chave args aponta para o javascript que faz o depurador funcionar.

A chave enrich_config, por sua vez, tem configurações adicionais para garantir que a sessão DAP tenha um caminho válido para o depurador. Não se preocupe em entender exatamente o que ela faz.

Apenas tenha em mente que o caminho atribuído em c.extensionPath precisa estar correto, apontando para o diretório de instalação do depurador.

Tabela Configurations

Já a segunda tabela, dap.configurations.lua, define como iniciar uma sessão de depuração para arquivos do tipo Lua.

Muito importante saber que a chave type precisa conter o mesmo nome do adaptador, ou seja, “local-lua”. Isso porque definimos esse nome entre os colchetes de dap.adapters[“”].

O restante da tabela é praticamente autoexplicativo. As chaves apontam para o diretório atual de trabalho, o interpretador Lua e o caminho para o arquivo a ser depurado.

Tudo pronto! Agora basta fazer a chamada do método setup() no arquivo de spec do nvim-dap (debugging.lua) para tudo funcionar.

Chamando o Método Setup do Adaptador

Para finalizar o assunto dos adaptadores, vamos criar uma rotina que chama a função setup() do nosso adaptador de forma que o nvim-dap possa utilizá-lo.

Abra o debugging.lua e adicione a chave config conforme abaixo:

nvim ~/.config/nvim/lua/plugins/debugging.lua

Veja o arquivo completo:

return {
  'mfussenegger/nvim-dap',
  config = function()
        local dap = require 'dap'
        -- Adaptadores
        require('adapters.local-lua').setup(dap)
        -- outros adaptadores podem ser chamados aqui
  end,
  keys = {
    { '<F5>', function() require('dap').continue() end, desc = 'Debug Continue' },
    { '<F10>', function() require('dap').step_over() end, desc = 'Step Over' },
    { '<F11>', function() require('dap').step_into() end, desc = 'Step Into'},
    { '<F12>', function() require('dap').step_out() end, desc = 'Step Out'},
    { '<leader>b', function() require('dap').toggle_breakpoint() end, desc = 'Toggle Breakpoint'},
    { '<leader>B', function() require('dap').set_breakpoint() end, desc = 'Set Breakpoint'},
  },
}

A função anônima atribuída à chave config obtém o objeto dap do plugin nvim-dap e o passa via parâmetro para a função setup do nosso adaptador.

Como criamos um diretório chamado adapters dentro da pasta lua, fazemos o require desta maneira. Se você optou por deixar o adaptador em outro local, precisará ajustar esse valor.

Com o código organizado dessa forma, caso precise de suporte a debug no Neovim para outras linguagens, simplesmente crie o adaptador e chame-o logo abaixo do último require.

Agora precisamos testar toda essa coisa! Crie um arquivo Lua onde desejar (exceto em home) com este simples trecho de código:

local nome = 'Mochileiro T.I'
if nome == 'Mochileiro T.I' then
   print 'Deu certo'
end

Em seguida, posicione o cursor na linha três e pressione <leader>b (espaço+b no nosso caso). O nvim-dap entra em ação e marca essa linha como ponto de depuração (breakpoint) com a letra B.

Agora, pressione F5 e veja que a linha toda fica marcada. Isso significa que o nvim-dap criou uma sessão de depuração iniciando a execução do código e parando-a na linha três conforme indicamos.

Dessa maneira, podemos inspecionar as variáveis. Para tanto, digite :lua require(‘dap’).repl.open() e perceba que um prompt abrirá dividindo a tela ao meio.

Nele, entre no modo de edição com a tecla i (assim como fazemos com texto simples) e digite nome (variável que contém o valor Mochileiro T.I) seguido de <enter>

Como resultado, você verá o seguinte:

Embora interessante, na minha opinião essa abordagem de depuração não é muito prática. Apesar de podermos melhorá-la com adição de mais atalhos, ela ainda continuaria trabalhosa.

Mudaremos isso radicalmente através de uma interface muito mais amigável!

Instalação da Interface nvim-dap-ui

Este plugin é uma alternativa ao prompt padrão do nvim-dap, oferecendo uma interface visual mais rica e interativa.

Sua instalação e configuração é bem simples, bastando apenas mapear alguns métodos listeners que são responsáveis por abrir as janelas de depuração.

Então vamos ao que interessa. Abra seu arquivo debugging.lua e adicione as modificações de maneira que fique igual ao arquivo abaixo:

return {
   'mfussenegger/nvim-dap',
   dependencies = {
      'rcarriga/nvim-dap-ui',
      'nvim-neotest/nvim-nio',
   },
   config = function()
        local dap, dapui = require 'dap', require 'dapui'
        dapui.setup()

        dap.listeners.before.attach.dapui_config = function()
            dapui.open()
        end
        dap.listeners.before.launch.dapui_config = function()
            dapui.open()
        end
        dap.listeners.before.event_terminated.dapui_config = function()
            dapui.close()
        end
        dap.listeners.before.event_exited.dapui_config = function()
            dapui.close()
        end
        -- Adaptadores
        require('adapters.local-lua-config').setup(dap)

    end,
    keys = {
        { '<F5>', function() require('dap').continue() end, desc = 'Debug Continue' },
        { '<F10>', function() require('dap').step_over() end, desc = 'Step Over' },
        { '<F11>', function() require('dap').step_into() end, desc = 'Step Into'},
        { '<F12>', function() require('dap').step_out() end, desc = 'Step Out'},
        { '<leader>b', function() require('dap').toggle_breakpoint() end, desc = 'Toggle Breakpoint'},
        { '<leader>B', function() require('dap').set_breakpoint() end, desc = 'Set Breakpoint'},
  }
}

Como sempre, salve, saia e entre novamente no Neovim, mas desta vez, abra o arquivo Lua de teste anteriormente criado.

Marque um breakpoint com <leader>b e, em seguida, pressione F5. Veja a diferença:

Bem melhor, não é mesmo? Nada de digitar comandos para abrir prompt nem inspecionar variáveis manualmente.

Como definimos atalhos no nvim-dap, você pode navegar pela sessão de depuração com as teclas F10 à F12 e inspecionar a execução do código. Ou ainda clicar no botões da parte inferior da tela.

Agora sim a função de debug no Neovim está completa! Existem outros plugins que podem melhorar ainda mais a experiência de depuração no editor.

Porém para não alongar demasiadamente o artigo, preferi deixá-los de fora. Caso sinta necessidade, deixo a dica de plugins complementares: nvim-dap-virtual-text e telescope-dap.nvim.

Auto-complete com Blink.cmp

No artigo anterior, instalamos e configuramos toda a infraestrutura necessária para análise de código via servidores de linguagem.

No entanto, propositalmente deixei de lado o recurso de auto-complete, muito importante no desenvolvimento de software.

A função de auto-complete já é parte integrante dos servidores de linguagem, portanto, precisamos somente informá-los que o Neovim tem suporte a ela.

Fazemos isso diretamente no plugin neovim/nvim-lspconfig, de modo que as sugestões de complemento sejam direcionadas ao Blink.cmp.

Dessa maneira, o Blink pode criar um menu pop-up navegável com todos os itens de que precisamos, tornando a experiência muito mais agradável ao programador.

Vamos instalá-lo adicionando mais algumas linhas ao arquivo de configuração do lsp (lsp-config.lua).

nvim ~/.config/nvim/lua/plugins/lsp-config.lua

Veja como fica o arquivo completo:

return {
    {
        "mason-org/mason.nvim",
        opts = {
        }
    },
    {
        "mason-org/mason-lspconfig.nvim",
        opts = {
            ensure_installed = { "lua_ls" },
        },
        dependencies = {
            { "mason-org/mason.nvim", opts = {} },
            "neovim/nvim-lspconfig"
        },
    },
    {
        "neovim/nvim-lspconfig",
        -- Instalação do Blink.cmp
        dependencies = {
            'saghen/blink.cmp',
        }, 
        opts = {
            servers = {
                -- Adicione servidores de linguagem aqui
                lua_ls = {},
            }
        },
        config = function(_, opts)
            -- Anuncia aos servidores suporte a autocomplete
            local lspconfig = require('lspconfig')
            for server, config in pairs(opts.servers) do
                config.capabilities = require('blink.cmp').get_lsp_capabilities(config.capabilities)
                lspconfig[server].setup(config)
            end
            
            vim.keymap.set('n', 'K', vim.lsp.buf.hover, {})
            vim.keymap.set('n', 'gd', vim.lsp.buf.definition, {})
            vim.keymap.set('n', '<leader>ca', vim.lsp.buf.code_action, {})
            vim.diagnostic.config({
              virtual_text = {
                spacing = 4,  -- Espaço entre o código e a mensagem
              },
              signs = true,            -- Mostra ícones na lateral esquerda
              underline = true,        -- Sublinha o texto com problema
              update_in_insert = false, -- Atualiza apenas fora do modo insert (evita distração)
              severity_sort = true,    -- Ordena por severidade
            })
        end
    },
}

Por fim, salve, saia e entre novamente no Neovim.

Com o intuito de facilitar a explicação, vou replicar abaixo somente o trecho que instala e configura a função do autocomplete:

        "neovim/nvim-lspconfig",
        -- Instalação do Blink.cmp
        dependencies = {
            'saghen/blink.cmp',
        }, 
        opts = {
            servers = {
                -- Adicione servidores de linguagem aqui
                lua_ls = {},
            }
        },
        config = function(_, opts)
            -- Anuncia aos servidores suporte a autocomplete
            local lspconfig = require('lspconfig')
            for server, config in pairs(opts.servers) do
                config.capabilities = require('blink.cmp').get_lsp_capabilities(config.capabilities)
                lspconfig[server].setup(config)
            end

A instalação do Blink.cmp é muito simples; somente o adicionamos como dependência do nvim-lspconfig e o Lazy.nvim faz o resto.

Em contrapartida, o “anúncio” aos servidores de linguagem de que o Neovim tem suporte à autocomplete via Blink.cmp é um pouco mais complicado.

Vamos por partes: primeiramente, dentro de opts, veja que existe uma tabela servers onde declaramos os nomes dos servidores que temos. Neste caso, apenas lua_ls. (linha 9)

A função atribuída à config (linha12), por sua vez, agora recebe como parâmetro a tabela opts e terá a tabela servers percorrida através do loop for (linha 14).

Dessa maneira, por meio do método get_lsp_capabilities() do Blink,cmp (linha 16), a tabela vazia da chave lua_ls é preenchida com as funcionalidades que o Blink espera.

Assim sendo, basta informarmos ao servidor de linguagem o que ele deve fornecer. A chamada ao método setup na linha 17 representa isso.

Como resultado, veja tudo funcionando:

Execução rápida de código com jaq-nvim

Para finalizar nossa extensa lista de recursos, adicionaremos um plugin que acrescenta a capacidade de rodar rapidamente códigos em qualquer linguagem ao pressionarmos a tecla espaço+r.

Estou falando do jaq-nvim, um plugin bastante simples que mapeia uma tecla de atalho qualquer para disparar a execução do código e exibir os resultados em uma janela customizável.

Nesse sentido, vamos criar o último arquivo de spec do Lazy.nvim responsável por instalar e configurar o jaq:

nvim ~/.config/nvim/lua/plugins/jaq-nvim.lua

Seu conteúdo pode ficar dessa maneira:

return {
  'is0n/jaq-nvim',
  opts = {
    cmds = {
      external = {
        -- Adicione aqui os comandos shell que rodam os programas
        lua = 'lua %',
      },
    },
    behavior = {
    -- usar janela flutuante
      default = 'float',
      -- não entrar no modo insert automaticamente
      startinsert = false,
      -- não alternar janelas automaticamente
      wincmd = false,
      -- não salvar automaticamente antes de executar
      autosave = false,
    },
  },
  keys = {
        { '<leader>r', ':Jaq<CR>', desc = 'Rodar Código'},
  }
}

Salve, saia e entre novamente no Neovim mas, desta vez, aproveite para abrir nosso arquivo de testes escrito em Lua.

Pressione <leader>r (espaço+r no nosso caso) e veja como ele se comporta:

Assim como praticamente todos os plugins vistos até aqui, o jaq-nvim é altamente customizável e você pode ver todas as configurações no GitHub do projeto: https://github.com/is0n/jaq-nvim

Montei a configuração do jaq-nvim da forma mais simples possível com o intuito de facilitar seu entendimento. Os comentários já dão uma boa ideia do que cada trecho representa.

Destaco apenas o comentário da linha 6, onde os executáveis de cada linguagem são adicionados. Para acrescentar suporte a outras linguagens, basta incluir o comando shell na tabela external.

No exemplo, o suporte existe apenas para a linguagem Lua. Caso queira incluir Python, por exemplo, acrescente python = ‘python %’ ou python = ‘python3 %’ logo abaixo da chave lua = ‘lua %’.

Conclusão

Conforme vimos, preparar o recurso de debug no Neovim é um tanto longo e pode ficar complexo caso haja necessidade configurações mais customizadas.

No entanto, para a maioria dos casos, se seguirmos os passos mencionados neste artigo, conseguimos com poucos ajustes adicionar depuradores para diversas linguagens.

E o melhor de tudo: com a tradicional alta performance que o Neovim oferece sempre nos oferece!

Em seguida finalizamos o assunto dos servidores de linguagem com chave de ouro, incorporando ao editor a capacidade de sugerir complementos de código através da função de autocomplete.

O Blink.cmp consegue abstrair muita da complexidade que envolve esse assunto, além de fornecer uma bela interface que acelera muito o desenvolvimento de software.

Por fim demos uma pitada a mais de comodidade com o jaq-nvim, responsável por executar nossos programas e exibir seus resultados, sem precisarmos sair do Neovim ou abrir telas de prompt.

Conclusão da série Neovim IDE!

Foi uma longa jornada até aqui, saímos do zero absoluto para um editor completamente modificado, capaz de atuar como uma IDE completa e muito customizável.

Evidentemente que a complexidade que envolve tal modificação é alta, principalmente quando comparada a outras soluções como o excelente VSCode.

Penso que a alternativa construída com o Neovim é voltada para um público muito específico, acostumado com modo texto, que prioriza performance e que gosta de muita customização.

Mesmo assim, pessoas que não tem tanta disposição para aprender e configurar plugins podem usar o conteúdo destes artigos e apenas desfrutar dos benefícios!

Além disso, existem distribuições do Neovim feitas para poupar tempo de configuração e oferecer uma experiência rica e pronta para uso aos usuários, como por exemplo o NvChad e o LazyVim.

A ideia principal desta série de três artigos foi ensinar como tudo funciona nos bastidores, de forma que os usuários sejam capazes de montar ou usar uma distribuição no Neovim.

Espero ter conseguido atingir esse objetivo!

Até a próxima!