Neovim (nvim) é o mesmo vim, só que melhorado, reescrito e tendo atraído para o seu lado a maior parte da comunidade do editor original. Hoje esse projeto ultrapassou de longe o vim original nas estatísticas do github (34 vs 75k estrelas em 2024), então, se por algum motivo você decidiu começar a programar no terminal em 2024 — o neovim é a sua escolha.
Para que isso serve, afinal, quando existem IDEs convenientes e editores modernos como o visual studio code? Normalmente se apresentam dois argumentos:
Velocidade. Em qualquer editor no estilo vim, o grande diferencial são as combinações. E, com a prática necessária, você consegue navegar pelo código e editá-lo bem mais rápido do que pegar o mouse a cada poucos segundos;
Leveza. O Nvim é um utilitário que abre no terminal e consome pouquíssimos recursos. Uma IDE é um programa grande e muito pesado que, num mac de 2022, mal consegue abrir 3-4 projetos ao mesmo tempo.
Com o primeiro argumento eu concordo em parte: nós lemos mais código e pensamos mais no que escrever do que digitamos no teclado. Por isso, se você acha que dominar o nvim vai te dar vantagem sobre um colega com mouse — não, não vai. Mas onde você realmente vai superar o colega com mouse é no conforto. Depois de se acostumar com as combinações do vim, você já não vai conseguir digitar como antes — vai parecer muito desconfortável.
Com o segundo argumento eu concordo totalmente — o nvim é leve e rápido, e a possibilidade de rodar o editor a que estou acostumado em qualquer terminal, em qualquer sistema operacional moderno (e nem tão moderno assim), e em qualquer dispositivo — de um notebook velho até o celular — pessoalmente me agrada.
Este artigo não tem a missão de te vender esse editor — ele definitivamente não é para todos. Primeiro você precisa aprender a trabalhar com ele, e depois precisa de tempo para configurá-lo. Se essa ideia não te agrada — provavelmente é mais fácil comprar uma assinatura da JetBrains ou baixar o visual studio code, abrir e tudo já vai funcionar de cara. Mas se você gosta de linux, gosta de configurar as ferramentas com que trabalha e gosta da ideia de open source — então há uma boa chance de o nvim também te agradar.
No escopo deste artigo eu não vou explicar como trabalhar dentro do vim — existem inúmeros tutoriais e materiais onde isso é descrito. Eu vou mostrar como configurar o nvim do zero e obter, como resultado, um editor muito parecido com uma IDE, tanto na aparência quanto no funcionamento.

Toda a configuração foi feita num MacBook com MacOS 14, e como terminal foi usado o WezTerm. O WezTerm impõe algumas particularidades no trabalho com atalhos, que vou mencionar abaixo, mas o mesmo config pode ser rodado sem problemas em qualquer sistema unix-like, o que será até um pouco mais simples. Vou montar tudo do zero num sistema limpo, para fechar todas as questões.
Para começar, vamos baixar e instalar o WezTerm — na minha opinião, o melhor terminal, que também pode ser personalizado: https://wezfurlong.org/wezterm/installation.html, baixamos e instalamos o pacote da sua OS.
De cara, claro, ele não impressiona muito:

Vamos corrigir isso. Criamos o config:
poltora ~: mkdir -p ~/.config/wezterm/
poltora ~: touch ~/.config/wezterm/wezterm.lua
poltora ~: vim ~/.config/wezterm/wezterm.luaAbre o vim, onde colamos as seguintes configurações:
local wezterm = require 'wezterm'
local config = {}
if wezterm.config_builder then
config = wezterm.config_builder()
end
config.color_scheme = 'Atelierdune (light) (terminal.sexy)'
config.font = wezterm.font('JetBrains Mono', { weight = 'Medium' })
config.font_size = 16
config.hide_tab_bar_if_only_one_tab = true
config.window_background_opacity = 0.95
return configPara o wezterm existem inúmeros temas, você pode encontrar e definir no config acima qualquer um a seu gosto — https://wezfurlong.org/wezterm/config/appearance.html. O mesmo vale para as outras configurações — graças à documentação detalhada, ele é muito fácil e cômodo de personalizar.
Pressionamos :wqa, reiniciamos o terminal e é isto que obtemos:

Agora vamos instalar o neovim. Existem inúmeras formas de instalação, dependendo do sistema e do ambiente, e você pode ler em detalhe sobre cada uma no manual oficial — https://github.com/neovim/neovim/blob/master/INSTALL.md
O mais simples para o MacOS é instalar como pacote:
poltora ~: brew install neovimApós a instalação — iniciamos o nvim:
poltora ~: nvim .Abre o editor padrão que, para dizer o mínimo, não lembra muito uma IDE:

Fechamos com a combinação :q e começamos a configurar. Antes de mais nada, vamos criar o arquivo raiz do config:
mkdir ~/.config/nvim
touch ~/.config/nvim/init.lua
nvim ~/.config/nvim/init.luaO config será composto, em sua maior parte, por configurações de diversos plugins. Para instalar os plugins precisamos de um gerenciador especial, que é o que proponho adicionar agora.
Gerenciador de plugins
Existem muitos gerenciadores; eu uso um dos mais populares — o VimPlug. Instalamos com o comando:
sh -c 'curl -fLo "${XDG_DATA_HOME:-$HOME/.local/share}"/nvim/site/autoload/plug.vim --create-dirs \
https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim'Agora abrimos o arquivo de config ~/.config/nvim/init.lua, criado acima, e adicionamos a ele a seguinte estrutura:
local vim = vim
local Plug = vim.fn['plug#']
vim.call('plug#begin')
vim.call('plug#end')Daqui em diante, todos os plugins vamos adicionar entre as tags vim.call('plug#begin') e vim.call('plug#end'). Salvamos, reiniciamos e, se não houver erros no terminal — o gerenciador foi instalado com sucesso.
Para não nos perdermos nos nossos configs, eu vou colocar todas as configurações de cada plugin em um arquivo lua separado, que ficará ao lado do init.lua. Vamos criar o arquivo common.lua e definir nele uma série de configurações básicas.
common.lua:
-- show line numbers
vim.wo.number = true
-- enable mouse control
vim.g.mouse = 'a'
vim.opt.encoding="utf-8"
-- disable the swap file
vim.opt.swapfile = false
-- set tab and indentation options
vim.opt.scrolloff = 7
vim.opt.tabstop = 4
vim.opt.softtabstop = 4
vim.opt.shiftwidth = 4
vim.opt.autoindent = true
vim.opt.fileformat = "unix"Agora voltamos ao init.lua e conectamos esse arquivo:
local vim = vim
local Plug = vim.fn['plug#']
vim.call('plug#begin')
vim.call('plug#end')
home=os.getenv("HOME")
package.path = home .. "/.config/nvim/?.lua;" .. package.path
require"common"Recarregamos o nvim e, se virmos os números das linhas — está tudo funcionando:

Temas
Primeiro, vamos fazer com que o nvim se pareça visualmente com uma IDE. Existe um site bacana com um catálogo de temas da comunidade — https://vimcolorschemes.com/, onde você pode escolher um tema a seu gosto. Eu estou acostumado a trabalhar com temas claros e, por tentativa e erro, escolhi o que considero o melhor — o kanagawa, que é o que proponho instalar. Criamos um novo arquivo theme.lua, onde adicionamos:
vim.cmd.colorscheme("kanagawa")No init.lua conectamos o novo plugin e o arquivo theme.lua:
...
Plug('rebelot/kanagawa.nvim')
vim.call('plug#end')
...
dofile"theme.lua"Recarregamos o nvim, executamos :PlugInstall, recarregamos mais uma vez e vemos que o tema foi aplicado:

Agora vamos fazer com que a árvore de arquivos seja exibida do mesmo jeito a que estamos acostumados numa IDE.
Nvim-Tree - árvore de arquivos
Para exibir a árvore de arquivos, vamos usar o plugin nvim-tree. Criamos o arquivo vimtree.lua e adicionamos nele as seguintes configurações:
-- disable netrw at the very start of your init.lua
vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1
-- optionally enable 24-bit colour
vim.opt.termguicolors = true
-- empty setup using defaults
require("nvim-tree").setup()
-- OR setup with some options
require("nvim-tree").setup({
sort = {
sorter = "case_sensitive",
},
view = {
width = 30,
},
renderer = {
group_empty = true,
},
filters = {
dotfiles = true,
},
})Agora adicionamos o plugin nvim-tree e o plugin de suporte a ícones de arquivos, e ativamos o arquivo com as configurações do nvim-tree no init.lua:
...
Plug('kyazdani42/nvim-tree.lua')
Plug('kyazdani42/nvim-web-devicons')
vim.call('plug#end')
...
require"vimtree"Recarregamos, instalamos o plugin com o comando :PlugInstall, recarregamos mais uma vez e nos abre uma árvore de arquivos parecida com aquela a que estamos acostumados:

BarBar - abas de arquivos
Agora vamos adicionar ao editor as familiares abas de arquivos. Para isso vou usar o plugin barbar. Criamos o arquivo barbar.lua e adicionamos nele as configurações:
require'barbar'.setup {
animation = true,
clickable = true,
exclude_ft = {'javascript'},
exclude_name = {'package.json'},
focus_on_close = 'left',
highlight_inactive_file_icons = false,
highlight_visible = true,
icons = {
buffer_index = false,
buffer_number = false,
button = '',
diagnostics = {
[vim.diagnostic.severity.ERROR] = {enabled = true, icon = 'ff'},
[vim.diagnostic.severity.WARN] = {enabled = false},
[vim.diagnostic.severity.INFO] = {enabled = false},
[vim.diagnostic.severity.HINT] = {enabled = true},
},
gitsigns = {
added = {enabled = true, icon = '+'},
changed = {enabled = true, icon = '~'},
deleted = {enabled = true, icon = '-'},
},
filetype = {
custom_colors = false,
enabled = true,
},
separator = {left = '▎', right = ''},
separator_at_end = true,
modified = {button = '●'},
pinned = {button = '', filename = true},
preset = 'default',
alternate = {filetype = {enabled = false}},
current = {buffer_index = true},
inactive = {button = '×'},
visible = {modified = {buffer_number = false}},
},
insert_at_end = true,
maximum_padding = 1,
minimum_padding = 1,
maximum_length = 30,
minimum_length = 0,
semantic_letters = true,
sidebar_filetypes = {
NvimTree = true,
undotree = {text = 'undotree'},
['neo-tree'] = {event = 'BufWipeout'},
Outline = {event = 'BufWinLeave', text = 'symbols-outline'},
},
letters = 'asdfjkl;ghnmxcvbziowerutyqpASDFJKLGHNMXCVBZIOWERUTYQP',
no_name_title = nil,
}Agora, no init.lua adicionamos o plugin e ativamos o arquivo com as configurações:
...
Plug('romgrk/barbar.nvim')
vim.call('plug#end')
...
require"barbar"E após reiniciar rodamos :PlugInstall, em seguida recarregamos mais uma vez e vemos que as abas chegaram com sucesso:

Lualine - barra de status
A seguir, com a ajuda do plugin lualine, vamos adicionar na parte de baixo uma barra de status, na qual será exibido o vim-mode, o arquivo atual e a linguagem de desenvolvimento. Criamos o arquivo lua_line.lua (é importante que o arquivo tenha underscore, caso contrário, por conflitos de nomes, o nvim não vai enxergá-lo) e adicionamos nele as seguintes configurações:
require('lualine').setup ({
options = {
icons_enabled = true,
theme = 'auto',
component_separators = { left = '', right = ''},
section_separators = { left = '', right = ''},
disabled_filetypes = {
statusline = {},
winbar = {},
},
ignore_focus = {},
always_divide_middle = true,
globalstatus = false,
refresh = {
statusline = 1000,
tabline = 1000,
winbar = 1000,
}
},
sections = {
lualine_a = {'mode'},
lualine_b = {'branch', 'diff', 'diagnostics'},
lualine_c = {'filename'},
lualine_x = {'encoding', 'fileformat', 'filetype'},
lualine_y = {'progress'},
lualine_z = {'location'}
},
inactive_sections = {
lualine_a = {},
lualine_b = {},
lualine_c = {'filename'},
lualine_x = {'location'},
lualine_y = {},
lualine_z = {}
},
tabline = {},
winbar = {},
inactive_winbar = {},
extensions = {}
})init.lua:
...
Plug('nvim-lualine/lualine.nvim')
...
vim.call('plug#end')
...
require"lua_line"
E a variante com tema escuro:

O espaço para personalizar a aparência do editor é infinito. Dá para acoplar literalmente de tudo, desde animações até plugins que permitem gerar temas com a ajuda de IA.
Agora que a aparência básica ficou parecida com uma IDE, é hora de ensinar o nvim a entender o nosso código, dar sugestões e destacar a sintaxe.
TreeSitter - árvore de código
A próxima coisa que queremos fazer é adicionar o destaque de código. O TreeSitter é um utilitário que sabe analisar o código aberto no editor e construir a partir dele uma árvore sintática. A árvore sintática é uma estrutura de dados na qual fica armazenado o código decomposto em pedaços. Editores que trabalham com essa árvore entendem como destacar uma ou outra parte do código, e assim obtemos um destaque de sintaxe completo.
No momento em que este artigo é escrito, o TreeSitter é a forma mais popular de construir uma árvore sintática. Para fazer o nvim e o TreeSitter conversarem, precisamos de um driver, que será o plugin https://github.com/nvim-treesitter/nvim-treesitter. Conectamos o plugin ao config:
local vim = vim
local Plug = vim.fn['plug#']
vim.call('plug#begin')
Plug('nvim-treesitter/nvim-treesitter', {['do'] = ':TSUpdate'})
vim.call('plug#end')Criamos o arquivo treesitter.lua e adicionamos nele a configuração do repositório do plugin:
require'nvim-treesitter.configs'.setup {
ensure_installed = "all",
highlight = { enable = true },
incremental_selection = { enable = true },
textobjects = { enable = true },
}Salvamos, reiniciamos o nvim e executamos o comando :PlugInstall, após o qual será exibida a interface do VimPlug com a mensagem de que o plugin do TreeSitter foi instalado com sucesso. Agora é preciso reiniciar o nvim mais uma vez para que o TreeSitter instale os parsers de linguagem. Agora vamos ver como ficou a sintaxe de um projeto go:

Dá para comparar com a captura acima e avaliar a diferença. Ficou bonito, mas escrever código ainda é desconfortável — não há o autocomplete a que estamos acostumados, nem sugestões, nem destaque de erros, nem descrição de tipos. Vamos corrigir isso.
Servidor LSP
O TreeSitter ensinou o nvim a formatar o código do jeito a que estamos acostumados. Agora precisamos ensinar o nvim a dar sugestões, destacar erros e processar o código da forma que conhecemos. Para isso vamos usar o LSP.
LSP (language server protocol) é um protocolo pelo qual o cliente (editor ou IDE) se comunica com o servidor, que dá instruções ao cliente. Por exemplo, o cliente pode passar ao servidor uma variável, após a qual colocamos um ponto, e o servidor, em resposta, devolve ao cliente todas as opções de autocomplete possíveis para essa variável. Assim, para ensinar o editor a entender o código, precisamos subir um servidor que saiba tudo sobre esse código e sobre a linguagem em que ele foi escrito, e ensinar o editor a conversar com esse servidor.
Para cada linguagem existe o seu próprio servidor LSP, que primeiro precisa ser instalado no sistema e depois, por meio de algum driver, conectado ao editor. Como driver vamos usar o plugin https://github.com/neovim/nvim-lspconfig. O plugin tem uma excelente documentação, onde é possível encontrar como configurar o lsp para inúmeras linguagens.
No escopo deste artigo eu vou configurar o LSP para golang. Para as manipulações seguintes, assumo que você já tem o go instalado.
Para golang existem várias opções de servidores LSP; eu vou usar o mais popular — o gopls:
brew install goplsAgora criamos o arquivo lsp.lua e adicionamos nele as seguintes configurações:
local nvim_lsp = require'lspconfig'
-- Mappings.
-- See `:help vim.diagnostic.*` for documentation on any of the below functions
local opts = { noremap=true, silent=true }
vim.api.nvim_set_keymap('n', '<space>e', '<cmd>lua vim.diagnostic.open_float()<CR>', opts)
vim.api.nvim_set_keymap('n', '[d', '<cmd>lua vim.diagnostic.goto_prev()<CR>', opts)
vim.api.nvim_set_keymap('n', ']d', '<cmd>lua vim.diagnostic.goto_next()<CR>', opts)
vim.api.nvim_set_keymap('n', '<space>q', '<cmd>lua vim.diagnostic.setloclist()<CR>', opts)
-- Use an on_attach function to only map the following keys
-- after the language server attaches to the current buffer
local on_attach = function(client, bufnr)
-- Enable completion triggered by <c-x><c-o>
vim.api.nvim_buf_set_option(bufnr, 'omnifunc', 'v:lua.vim.lsp.omnifunc')
-- Mappings.
-- See `:help vim.lsp.*` for documentation on any of the below functions
vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gD', '<cmd>lua vim.lsp.buf.declaration()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gd', '<cmd>lua vim.lsp.buf.definition()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', 'K', '<cmd>lua vim.lsp.buf.hover()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gi', '<cmd>lua vim.lsp.buf.implementation()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', '<C-k>', '<cmd>lua vim.lsp.buf.signature_help()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>wa', '<cmd>lua vim.lsp.buf.add_workspace_folder()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>wr', '<cmd>lua vim.lsp.buf.remove_workspace_folder()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>wl', '<cmd>lua print(vim.inspect(vim.lsp.buf.list_workspace_folders()))<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>D', '<cmd>lua vim.lsp.buf.type_definition()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>rn', '<cmd>lua vim.lsp.buf.rename()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>ca', '<cmd>lua vim.lsp.buf.code_action()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gr', '<cmd>lua vim.lsp.buf.references()<CR>', opts)
vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>f', '<cmd>lua vim.lsp.buf.formatting()<CR>', opts)
end
-- golang
nvim_lsp.gopls.setup ({
on_attach = on_attach,
flags = {
-- This will be the default in neovim 0.7+
debounce_text_changes = 150,
},
capabilities = {
workspace = {
didChangeWatchedFiles = {
dynamicRegistration = true,
},
},
}
})
vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with(
vim.lsp.diagnostic.on_publish_diagnostics, {
virtual_text = false
}
)A seção com o mapeamento e a inicialização do servidor são padrão, dos exemplos de config do repositório do nvim-lspconfig.
init.lua:
Plug('neovim/nvim-lspconfig')
...
vim.call('plug#end')
...
require"lsp"Reiniciamos, instalamos o plugin com :PlugInstall, reiniciamos mais uma vez e abrimos um exemplo de código go. Aqui é importante notar que o gopls, por padrão, está configurado de modo que só reconhece o código se houver um go.mod na raiz do projeto. Outro ponto importante — verifique se o go está adicionado ao PATH do shell que você usa. No meu caso é o zsh, e no .zshrc eu tenho definido:
export GOPATH=$HOME/golang
export GOROOT=/usr/local/opt/go/libexec
export GOBIN=$GOPATH/bin
export PATH=$PATH:$GOPATH
export PATH=$PATH:$GOROOT/binAgora, tendo aberto o código go e certificado de que não há erros, executamos :LspInfo:

Vemos que temos um cliente lsp ativo — o que significa que o nvim se conectou com sucesso ao servidor lsp. Embora o nvim já saiba trabalhar com o autocomplete, precisamos instalar uma série de plugins para conseguirmos ver esse trabalho. Para visualizar esse trabalho vou usar um conjunto de plugins de https://github.com/hrsh7th. Criamos o arquivo cmp.lua e colamos nele o seguinte config:
vim.o.completeopt="menu,menuone,noselect"
-- Setup nvim-cmp.
local cmp = require'cmp'
cmp.setup({
snippet = {
-- REQUIRED - you must specify a snippet engine
expand = function(args)
vim.fn["vsnip#anonymous"](args.body) -- For `vsnip` users.
-- require('luasnip').lsp_expand(args.body) -- For `luasnip` users.
-- require('snippy').expand_snippet(args.body) -- For `snippy` users.
-- vim.fn["UltiSnips#Anon"](args.body) -- For `ultisnips` users.
end,
},
window = {
-- completion = cmp.config.window.bordered(),
-- documentation = cmp.config.window.bordered(),
},
mapping = cmp.mapping.preset.insert({
['<C-b>'] = cmp.mapping.scroll_docs(-4),
['<C-f>'] = cmp.mapping.scroll_docs(4),
['<C-Space>'] = cmp.mapping.complete(),
['<C-e>'] = cmp.mapping.abort(),
['<CR>'] = cmp.mapping.confirm({ select = true }),
['<Tab>'] = cmp.mapping(function(fallback)
local col = vim.fn.col('.') - 1
if cmp.visible() then
cmp.select_next_item(select_opts)
elseif col == 0 or vim.fn.getline('.'):sub(col, col):match('%s') then
fallback()
else
cmp.complete()
end
end, {'i', 's'}),
['<S-Tab>'] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_prev_item(select_opts)
else
fallback()
end
end, {'i', 's'}),
}),
sources = cmp.config.sources({
{ name = 'nvim_lsp' },
{ name = 'vsnip' }, -- For vsnip users.
{ name = 'nvim_lsp_signature_help' },
}, {
{ name = 'buffer' },
})
})
-- Set configuration for specific filetype.
cmp.setup.filetype('gitcommit', {
sources = cmp.config.sources({
{ name = 'cmp_git' }, -- You can specify the `cmp_git` source if you were installed it.
}, {
{ name = 'buffer' },
})
})
-- Use buffer source for `/` (if you enabled `native_menu`, this won't work anymore).
cmp.setup.cmdline('/', {
mapping = cmp.mapping.preset.cmdline(),
sources = {
{ name = 'buffer' }
}
})
-- Use cmdline & path source for ':' (if you enabled `native_menu`, this won't work anymore).
cmp.setup.cmdline(':', {
mapping = cmp.mapping.preset.cmdline(),
sources = cmp.config.sources({
{ name = 'path' }
}, {
{ name = 'cmdline' }
})
})Agora conectamos os plugins e o config no init.lua:
Plug('hrsh7th/cmp-buffer')
Plug('hrsh7th/cmp-path')
Plug('hrsh7th/cmp-cmdline')
Plug('hrsh7th/nvim-cmp')
Plug('hrsh7th/cmp-nvim-lsp' )
Plug('hrsh7th/cmp-nvim-lsp-signature-help')
vim.call('plug#end')
...
require"cmp_config"Instalamos com :PlugInstall e verificamos:

Nesta etapa, obtivemos um editor que, por fora, lembra o estilo familiar de uma IDE e que sabe trabalhar com o código no formato a que estamos acostumados. Acho que dá para encerrar logicamente o primeiro artigo neste ponto, para que ele não se estique até um tamanho totalmente indecente. Nos próximos episódios vamos configurar a busca familiar por arquivos e por ocorrências, configurar um mapeamento cômodo para a maioria das ações, e eu vou jogar mais uns 5-10 plugins que facilitam muito o desenvolvimento do dia a dia.
O código completo do config pode ser encontrado aqui: https://github.com/itxor/go-rust-nvim-config/tree/master
A versão antiga do config no formato nvim (o config funciona, mas está desatualizado e tem muitas diferenças em relação à atual versão lua): https://github.com/itxor/go-rust-nvim-config/tree/old_master