01 Jan 2026 . tech . Comments
Table of Contents
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.
So after all, why Neovim?
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.
General Structure of a Neovim Config
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.
Overview of the main modern Neovim plugins
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.
Language Server Protocol 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()
-- All LSP servers to enable
-- Custom configurations are in lua/lsp/<server>.lua
local servers = {
"bashls",
"clangd",
"cssls",
"dockerls",
"gopls",
"html",
"lua_ls",
"marksman",
"pyright",
"ruby_lsp",
"taplo",
"terraformls",
"tflint",
"ts_ls",
"vimls",
"yamlls",
}
-- Load custom configs from lua/lsp/<server>.lua
for _, server in ipairs(servers) do
local ok, custom = pcall(require, "lsp." .. server)
if ok then
vim.lsp.config(server, custom)
end
end
vim.lsp.enable(servers)
This approach keeps the main configuration clean while allowing for server-specific customizations. For example, my Go LSP configuration enables auto-importing and placeholder completion:
-- lua/lsp/gopls.lua
return {
settings = {
gopls = {
completeUnimported = true,
usePlaceholders = true,
analyses = {
unusedparams = true,
},
},
},
}
Formatting with Conform
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.
Linting with nvim-lint
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,
})
Debug Adapter Procotol Setup
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,
}
Mason for Tool Management
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.
AI-Powered Development with CodeCompanion
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 = {
-- Enable tools for exploring the codebase (includes cmd_runner, file_search, grep_search, etc.)
default_tools = { "full_stack_dev", "cmd_runner", "web_search", "fetch_webpage" },
},
},
},
},
},
}
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 my editor.
What’s next?
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!

Panos is an engineering leader at Panther, with over a decade of experience in cybersecurity and engineering leadership. His career includes security research at CERN, security engineering at Microsoft Office 365, and founding Blocktopus, a KYC/AML startup. He holds patents, has published research in security and machine learning, and has helped scale startups from pre-seed through Series B. On this blog, he writes about security, leadership, and developer productivity.