Skip to main content

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.

1. conform.nvim — The Modern Formatter

conform.nvim is the recommended replacement for none-ls formatting. It's faster, more reliable, and supports fallback formatters:

lua/plugins/formatting.lua
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

nvim-lint runs linters asynchronously and reports results as Neovim diagnostics:

lua/plugins/linting.lua
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

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

CommandShows
Trouble diagnosticsAll workspace diagnostics
Trouble diagnostics filter.buf=0Current buffer only
Trouble symbolsDocument symbols (like outline)
Trouble lspLSP definitions, references, implementations
Trouble qflistQuickfix list
Trouble loclistLocation list

4. Aerial.nvim — Code Outline / Symbol Navigator

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

What's Next