After finishing my keyboard build, I experienced how rewarding and fun it was to have your tools tailor-made specifically for your taste and needs, and the next stop had to be my editor. Neovim was the cool kid on the block, without me realizing back then why, but I had to give it a try.
Initially, I was lost and overwhelmed. I was watching influencers building their configurations from scratch and starting from scratch with Neovim is just daunting. You need to configure everything: syntax highlighting, file explorer, status line, buffer management, and many more. This could take weeks of tweaking before you even have something usable. This has now changed, since you can start a configuration from scratch using AI tooling, but even in this case, you need to really know what you need, and you just don’t know when you start. Then I discovered pre-made configurations that you could customize, and I started with NvChad. I fell in love with it, because it was minimal which is my overall aesthetic and taste. Also because it was guiding you to discover it. It had plugins that were guiding you when typing shortcuts and a cheatsheet page with all the available commands. Then I got familiar with the vim modes and keybindings and everything clicked! I realized how fun and productive the otherwise mundane task of editing text files had suddenly become. I had to get more out of it.
I started with blindly stitching Lua code from other configurations and guides and ended up supporting all the languages I was using. To my surprise back then, you could have all the features of an IDE, like autocompletion, formatting, linting and even debugging. All these thanks to VSCode which introduced protocols for all these IDE components which allowed the interoperability of tools across different IDEs, making it possible for any editor to leverage the same language intelligence that powers modern IDEs.
After many months of working on my config, and eventually my vim config for simple text editing over ssh, I started to understand why Neovim was the cool kid on the block. Neovim is a modern fork of Vim that brings Lua as the scripting interface for configuration, built-in Language Server Protocol (LSP) support, and an extensible architecture that makes it a joy to customize. You will never appreciate Lua, unless you write Vimscript. Also, when compared to traditional IDEs, Neovim is lightweight, runs in the terminal, and can be tailored to fit exactly how you work. And of course, the “Edit Text at the Speed of Thought” mantra, which you can’t comprehend unless you experience it.
If you are thinking to build your own config, here’s a chart I wish I had when starting:
The init.lua file is the entry point that bootstraps everything. It loads the
plugin manager, which in turn loads all plugins. Core options and key mappings
are kept separate for maintainability. LSP configurations live in their own
directory, making it easy to customize each language server independently. Last,
Mason is used to manage all the LSP, formatting, linting and debugging
dependencies.
When I started my Neovim journey, the plugin ecosystem was still evolving. Every few months, core plugins were being replaced by new ones, so I was playing cat and mouse for a while, which sometimes was fun, other times it wasn’t. Since then, the ecosystem has stabilized for about a year, so here are the core plugins and a sample setup.
The LSP configuration is where Neovim truly shines. Instead of configuring each
server inline, with Neovim 0.11 you can keep custom configurations in separate
files under lua/lsp/:
-- lua/configs/lspconfig.lua
require("nvchad.configs.lspconfig").defaults()
-- LSP servers to enable
local servers = {
"bashls",
"clangd",
"cssls",
"dockerls",
"gopls",
"html",
"lua_ls",
"marksman",
"pyright",
"ruby_lsp",
"taplo",
"terraformls",
"tflint",
"ts_ls",
"vimls",
"yamlls",
}
-- Enable inlay hints
vim.lsp.inlay_hint.enable(true)
-- Enable native LSP completion
require "configs.completion"
vim.lsp.enable(servers)
This approach keeps the main configuration clean while allowing for server-specific customizations. For example, my Go LSP configuration has the auto-importing feature enabled:
-- lua/lsp/gopls.lua
return {
settings = {
gopls = {
completeUnimported = true,
analyses = {
unusedparams = true,
},
},
},
}
Automatic formatting on save is usually a non-negotiable. conform.nvim handles the formatting for each file type and the formatting on save elegantly:
-- lua/configs/conform.lua
local options = {
formatters_by_ft = {
c = { "clang-format" },
cpp = { "clang-format" },
css = { "prettier" },
go = { "goimports", "gofmt" },
graphql = { "prettier" },
-- ... and more
},
}
require("conform").setup {
formatters_by_ft = options.formatters_by_ft,
format_on_save = function()
if not vim.g.format_on_save then
return
end
return { lsp_format = "fallback", timeout_ms = 3000 }
end,
}
The format_on_save toggle is controlled by a global variable, which I can
quickly toggle with <leader>tf when I need to save without formatting.
While LSPs catch many issues early, dedicated linters often provide deeper analysis. nvim-lint offers linter setup by file type and runs them on save and when leaving insert mode:
-- lua/configs/lint.lua
local lint = require "lint"
lint.linters_by_ft = {
go = { "golangcilint" },
markdown = { "markdownlint" },
python = { "pylint" },
ruby = { "rubocop" },
terraform = { "tflint" },
}
vim.api.nvim_create_autocmd({ "BufEnter", "BufWritePost", "InsertLeave" }, {
group = vim.api.nvim_create_augroup("lint", { clear = true }),
callback = function()
lint.try_lint()
end,
})
Debugging directly in Neovim was a game-changer for me, since I realized you can simply drop the need for an IDE for good. The Debug Adapter Protocol (DAP) setup includes support for Go and Python:
-- lua/plugins/dap-go.lua
return {
"leoluz/nvim-dap-go",
ft = "go",
dependencies = {
"mfussenegger/nvim-dap",
"rcarriga/nvim-dap-ui",
},
config = function(_, opts)
require("dap-go").setup(opts)
end,
}
All LSP servers, formatters, linters, and debuggers are managed through Mason. The configuration ensures all required tools are installed automatically:
-- lua/plugins/mason.lua
return {
"williamboman/mason.nvim",
config = function(_, opts)
require("mason").setup(opts)
local mason_registry = require "mason-registry"
local ensure_installed = opts.ensure_installed or {}
-- Ensure all listed tools are installed
mason_registry.refresh(function()
for _, tool in ipairs(ensure_installed) do
local ok, package = pcall(mason_registry.get_package, tool)
if ok and not package:is_installed() then
package:install()
end
end
end)
end,
opts = {
max_concurrent_installers = 10,
ensure_installed = {
"bash-language-server",
"black",
"clangd",
"clang-format",
"css-lsp",
"debugpy",
"delve",
"dockerfile-language-server",
"goimports",
"golangci-lint",
"gopls",
-- ... and more
},
},
}
This means a fresh installation gets all the tools it needs without manual intervention. There is a caveat I haven’t tackled yet, which is what happens when a project depends on a tool version that is behaving differently than the global tools that Mason has installed. I haven’t run into this yet, so this bridge will be crossed later.
The latest addition to my setup is CodeCompanion, which integrates AI assistance directly into Neovim:
-- lua/plugins/codecompanion.lua
return {
"olimorris/codecompanion.nvim",
cmd = { "CodeCompanion", "CodeCompanionChat", "CodeCompanionActions" },
dependencies = {
"nvim-lua/plenary.nvim",
"nvim-treesitter/nvim-treesitter",
},
opts = {
strategies = {
chat = {
adapter = {
-- ANTHROPIC_API_KEY must be set in your environment
name = "anthropic",
model = "claude-opus-4-5-20251101",
},
roles = {
user = "NvMegaChad Companion",
},
tools = {
opts = {
default_tools = { "full_stack_dev", "cmd_runner", "fetch_webpage" },
},
-- Disable approval for read-only tools
["read_file"] = {
opts = { require_approval_before = false },
},
["file_search"] = {
opts = { require_approval_before = false },
},
["grep_search"] = {
opts = { require_approval_before = false },
},
["list_code_usages"] = {
opts = { require_approval_before = false },
},
["get_changed_files"] = {
opts = { require_approval_before = false },
},
},
},
inline = {
adapter = {
name = "anthropic",
model = "claude-sonnet-4-20250514",
},
},
},
},
}
With <leader>cc, I can open a chat window and ask questions about my codebase,
generate code, or get explanations for complex logic, all without leaving the
editor.
I consider it a done project which has entered maintenance mode. If you’re interested in trying it, it’s the NvMegaChad configuration and you can find it on GitHub. The installation is straightforward, and you’ll have a fully configured environment for polyglot development in seconds. Feel free to use it as-is, fork it, or just cherry-pick the parts that work for you.
Happy editing!