Code Quality Plugins
Code quality plugins handle formatting, linting, error display, and code structure navigation. These are the tools that turn Neovim into an IDE-grade development environment.
LazyVim bundles: conform.nvim (🟢 formatter), nvim-lint (🟢 linter), trouble.nvim (🟢). NOT bundled: null-ls (⚪ deprecated anyway), editorconfig depends on setup.
1. conform.nvim — The Modern Formatter (🟢 LazyVim included)
conform.nvim is the recommended replacement for none-ls formatting. It's faster, more reliable, and supports fallback formatters:
return {
{
"stevearc/conform.nvim",
event = { "BufWritePre" },
cmd = { "ConformInfo" },
keys = {
{
"<leader>lf",
function()
require("conform").format({ async = true, lsp_format = "fallback" })
end,
desc = "Format file",
},
},
opts = {
-- Format on save
format_on_save = function(bufnr)
-- Disable for certain filetypes
local disable_filetypes = { c = true, cpp = true }
if disable_filetypes[vim.bo[bufnr].filetype] then
return
end
return { timeout_ms = 500, lsp_format = "fallback" }
end,
-- Formatter definitions
formatters_by_ft = {
lua = { "stylua" },
python = { "isort", "black" }, -- run isort THEN black
javascript = { "prettier", stop_after_first = true },
typescript = { "prettier", stop_after_first = true },
javascriptreact = { "prettier" },
typescriptreact = { "prettier" },
json = { "prettier" },
jsonc = { "prettier" },
yaml = { "prettier" },
html = { "prettier" },
css = { "prettier" },
scss = { "prettier" },
markdown = { "prettier", "markdownlint-cli2" },
php = { "php_cs_fixer" },
go = { "gofumpt", "goimports" },
rust = { "rustfmt" },
sh = { "shfmt" },
bash = { "shfmt" },
toml = { "taplo" },
sql = { "sql_formatter" },
-- Use LSP for everything else
["_"] = { "trim_whitespace", "trim_newlines" },
},
-- Customize individual formatters
formatters = {
shfmt = {
prepend_args = { "-i", "2", "-ci" }, -- 2-space indent, case indent
},
black = {
prepend_args = { "--line-length", "88" },
},
prettier = {
prepend_args = { "--tab-width", "2", "--single-quote" },
},
},
-- Notify on format errors
notify_on_error = true,
},
},
}
Checking Formatter Status
:ConformInfo → shows available and active formatters per filetype
Installing Formatters
# Python
pip install black isort
# JS/TS
npm install -g prettier
# Lua
cargo install stylua
# PHP
composer global require friendsofphp/php-cs-fixer
# Shell
brew install shfmt # or: go install mvdan.cc/sh/v3/cmd/shfmt@latest
# SQL
npm install -g sql-formatter
2. nvim-lint — The Modern Linter (🟢 LazyVim included)
nvim-lint runs linters asynchronously and reports results as Neovim diagnostics:
return {
{
"mfussenegger/nvim-lint",
event = { "BufReadPost", "BufWritePost", "BufNewFile" },
config = function()
local lint = require("lint")
lint.linters_by_ft = {
javascript = { "eslint_d" },
typescript = { "eslint_d" },
javascriptreact = { "eslint_d" },
typescriptreact = { "eslint_d" },
python = { "flake8", "mypy" },
php = { "phpcs" },
lua = { "luacheck" },
bash = { "shellcheck" },
sh = { "shellcheck" },
markdown = { "markdownlint" },
dockerfile = { "hadolint" },
yaml = { "yamllint" },
json = { "jsonlint" },
css = { "stylelint" },
scss = { "stylelint" },
go = { "golangcilint" },
}
-- Customize linter arguments
lint.linters.phpcs = {
cmd = "phpcs",
args = {
"--report=json",
"--standard=PSR12", -- or WordPress
"-",
},
stdin = true,
stream = "stdout",
ignore_exitcode = true,
parser = lint.linters.phpcs.parser,
}
-- Auto-lint on these events
local lint_augroup = vim.api.nvim_create_augroup("lint", { clear = true })
vim.api.nvim_create_autocmd({ "BufEnter", "BufWritePost", "InsertLeave" }, {
group = lint_augroup,
callback = function()
-- Only lint if file is saved (not scratch)
if vim.opt.modifiable:get() then
lint.try_lint()
end
end,
})
-- Manual lint keymap
vim.keymap.set("n", "<leader>ll", function()
lint.try_lint()
end, { desc = "Trigger linting" })
end,
},
}
Installing Linters
# JS/TS (eslint_d is the fast daemon version)
npm install -g eslint_d
# Python
pip install flake8 mypy
# PHP
composer global require squizlabs/php_codesniffer
# Shell
sudo apt install shellcheck
# Markdown
npm install -g markdownlint-cli2
# Dockerfile
brew install hadolint # or: docker pull hadolint/hadolint
# YAML
pip install yamllint
# Lua
luarocks install luacheck
3. Trouble.nvim — Diagnostics List Panel (🟢 LazyVim included)
Trouble shows errors, warnings, LSP references, and quickfix lists in a clean floating/split panel:
{
"folke/trouble.nvim",
dependencies = { "nvim-tree/nvim-web-devicons" },
cmd = "Trouble",
keys = {
{ "<leader>xx", "<cmd>Trouble diagnostics toggle<cr>", desc = "Diagnostics (Trouble)" },
{ "<leader>xX", "<cmd>Trouble diagnostics toggle filter.buf=0<cr>", desc = "Buffer diagnostics" },
{ "<leader>cs", "<cmd>Trouble symbols toggle focus=false<cr>", desc = "Symbols (Trouble)" },
{ "<leader>cl", "<cmd>Trouble lsp toggle focus=false win.position=right<cr>", desc = "LSP definitions (Trouble)" },
{ "<leader>xL", "<cmd>Trouble loclist toggle<cr>", desc = "Location list" },
{ "<leader>xQ", "<cmd>Trouble qflist toggle<cr>", desc = "Quickfix list" },
{
"[q",
function()
local trouble = require("trouble")
if trouble.is_open() then trouble.prev({ skip_groups = true, jump = true })
else pcall(vim.cmd.cprev) end
end,
desc = "Prev trouble/quickfix",
},
{
"]q",
function()
local trouble = require("trouble")
if trouble.is_open() then trouble.next({ skip_groups = true, jump = true })
else pcall(vim.cmd.cnext) end
end,
desc = "Next trouble/quickfix",
},
},
opts = {
position = "bottom",
height = 10,
icons = true,
mode = "workspace_diagnostics",
severity = nil,
fold_open = "",
fold_closed = "",
group = true,
padding = true,
cycle_results = true,
action_keys = {
close = "q",
cancel = "<esc>",
refresh = "r",
jump = { "<cr>", "<tab>" },
open_split = { "<c-x>" },
open_vsplit = { "<c-v>" },
open_tab = { "<c-t>" },
jump_close = { "o" },
toggle_mode = "m",
switch_severity = "s",
toggle_preview = "P",
hover = "K",
preview = "p",
fold = { "zA", "za" },
close_folds = { "zM", "zm" },
open_folds = { "zR", "zr" },
toggle_fold = "zz",
previous = "k",
next = "j",
},
indent_lines = true,
auto_open = false,
auto_close = false,
auto_preview = true,
auto_fold = false,
auto_jump = { "lsp_definitions" },
signs = {
error = "",
warning = "",
hint = "",
information = "",
other = "",
},
use_diagnostic_signs = false,
},
},
Trouble Modes (🟢 LazyVim included)
| Command | Shows |
|---|---|
| Trouble diagnostics | All workspace diagnostics |
| Trouble diagnostics filter.buf=0 | Current buffer only |
| Trouble symbols | Document symbols (like outline) |
| Trouble lsp | LSP definitions, references, implementations |
| Trouble qflist | Quickfix list |
| Trouble loclist | Location list |
4. Aerial.nvim — Code Outline / Symbol Navigator (⚪ Manual install)
Aerial shows the document symbols (functions, classes, methods) in a sidebar or telescope picker:
{
"stevearc/aerial.nvim",
event = { "BufReadPost", "BufNewFile" },
dependencies = {
"nvim-treesitter/nvim-treesitter",
"nvim-tree/nvim-web-devicons",
},
keys = {
{ "<leader>co", "<cmd>AerialToggle<cr>", desc = "Toggle outline (Aerial)" },
{ "<leader>cO", "<cmd>AerialNavToggle<cr>", desc = "Aerial nav float" },
{ "<leader>fa", "<cmd>Telescope aerial<cr>", desc = "Find symbol (Aerial)" },
{ "{", function() require("aerial").prev() end, desc = "Aerial prev symbol" },
{ "}", function() require("aerial").next() end, desc = "Aerial next symbol" },
{ "[[", function() require("aerial").prev_up() end, desc = "Aerial prev class" },
{ "]]", function() require("aerial").next_up() end, desc = "Aerial next class" },
},
opts = {
backends = { "treesitter", "lsp", "markdown", "asciidoc", "man" },
layout = {
max_width = { 40, 0.2 },
width = nil,
min_width = 20,
default_direction = "right",
placement = "edge",
},
attach_mode = "window",
close_automatic_events = {},
keymaps = {
["?"] = "actions.show_help",
["g?"] = "actions.show_help",
["<CR>"] = "actions.jump",
["<2-LeftMouse>"] = "actions.jump",
["<C-v>"] = "actions.jump_vsplit",
["<C-s>"] = "actions.jump_split",
["p"] = "actions.scroll",
["<C-j>"] = "actions.down_and_scroll",
["<C-k>"] = "actions.up_and_scroll",
["{"] = "actions.prev",
["}"] = "actions.next",
["[["] = "actions.prev_up",
["]]"] = "actions.next_up",
["q"] = "actions.close",
["o"] = "actions.tree_toggle",
["za"] = "actions.tree_toggle",
["O"] = "actions.tree_toggle_recursive",
["zA"] = "actions.tree_toggle_recursive",
["l"] = "actions.tree_open",
["zo"] = "actions.tree_open",
["L"] = "actions.tree_open_recursive",
["zO"] = "actions.tree_open_recursive",
["h"] = "actions.tree_close",
["zc"] = "actions.tree_close",
["H"] = "actions.tree_close_recursive",
["zC"] = "actions.tree_close_recursive",
["zr"] = "actions.tree_increase_fold_level",
["zm"] = "actions.tree_decrease_fold_level",
["zx"] = "actions.tree_set_fold_level",
["zX"] = "actions.tree_set_fold_level",
["<2-LeftMouse>"] = "actions.jump",
},
show_guides = true,
guides = {
mid_item = "├─ ",
last_item = "└─ ",
nested_top = "│ ",
whitespace = " ",
},
filter_kind = {
"Class", "Constructor", "Enum", "Function",
"Interface", "Module", "Method", "Struct",
},
icons = {},
highlight_mode = "split_width",
highlight_closest = true,
highlight_on_hover = true,
highlight_on_jump = 300,
autojump = false,
link_folds_to_tree = false,
link_tree_to_folds = true,
manage_folds = false,
open_automatic = false,
on_attach = function(bufnr)
-- Jump with { } when aerial is open
end,
},
},
5. nvim-spectre — Find and Replace Across Project
Spectre is a search-and-replace tool with preview across all project files:
{
"nvim-pack/nvim-spectre",
build = false,
cmd = "Spectre",
dependencies = { "nvim-lua/plenary.nvim" },
keys = {
{ "<leader>sr", function() require("spectre").open() end, desc = "Replace in files (Spectre)" },
{ "<leader>sw", function() require("spectre").open_visual({ select_word = true }) end, desc = "Search word (Spectre)" },
{ "<leader>sf", function() require("spectre").open_file_search({ select_word = true }) end, desc = "Search in file (Spectre)" },
},
opts = {
open_cmd = "noswapfile vnew",
live_update = false,
is_insert_mode = false,
mapping = {
["toggle_line"] = { map = "dd", desc = "Toggle item" },
["enter_file"] = { map = "<cr>", desc = "Go to file" },
["send_to_qf"] = { map = "<leader>q", desc = "Send to quickfix" },
["replace_cmd"] = { map = "<leader>c", desc = "Exec replacement" },
["show_option_menu"] = { map = "<leader>o", desc = "Options" },
["run_current_replace"] = { map = "<leader>rc", desc = "Replace current" },
["run_replace"] = { map = "<leader>R", desc = "Replace all" },
["change_view_mode"] = { map = "<leader>v", desc = "Change view" },
},
},
},
Spectre Workflow
1. <leader>sr → open Spectre
2. Enter search pattern, Enter replacement
3. Press <leader>R to replace all in project
4. Or toggle specific lines with dd before replacing
In LazyVim
LazyVim automates the setup for most of the popular plugins discussed here. By leveraging lazy.nvim, it provides a deeply integrated out-of-the-box experience where core plugins (like Telescope, Neo-tree, and Lualine) are pre-configured with sensible defaults and keymaps, while others can be easily enabled via :LazyExtras.
| Aspect | Vanilla Neovim | LazyVim |
|---|---|---|
| Plugin Installation | Manual via lazy.nvim or packer | Pre-bundled or enabled via :LazyExtras |
| Keymaps | Manually defined | Unified mappings under <leader> |
| Configuration | Manual setup() calls | Auto-configured, override via opts |