This configuration uses nvim-lint for code linting. Unlike formatters, linting is triggered manually with <leader>ll.
Manual linting
Linting is not automatic - you trigger it when needed:
-- From lua/plugins/linting.lua:9
{
"<leader>ll" ,
function ()
local lint = require ( "lint" )
lint . try_lint ()
local ft = vim . bo . filetype
local linters = lint . linters_by_ft [ ft ] or {}
if # linters > 0 then
vim . notify ( "Linting triggered: " .. table.concat ( linters , ", " ), vim . log . levels . INFO )
else
vim . notify ( "No linters configured for filetype: " .. ft , vim . log . levels . WARN )
end
end ,
desc = "Trigger linting for current file" ,
}
Press <leader>ll to lint the current buffer.
Manual linting gives you control over when linters run, avoiding performance issues with slow linters.
Why manual linting?
Performance - Some linters (like golangci-lint) can be CPU-intensive
Control - Run linters when you need them, not on every file change
LSP integration - Many diagnostics already come from LSP servers
Biome integration - Biome linting is handled via LSP for JS/TS projects with biome.json
Linters by language
JavaScript/TypeScript
-- From lua/plugins/linting.lua:74
javascript = { "eslint_d" },
javascriptreact = { "eslint_d" },
typescript = { "eslint_d" },
typescriptreact = { "eslint_d" },
Installation:
Biome linting is handled via the Biome LSP server when a biome.json file exists. ESLint is used for projects with ESLint configuration.
-- From lua/plugins/linting.lua:60
go = { "golangcilint" }
The golangci-lint configuration is customized for better performance:
-- From lua/plugins/linting.lua:28
local golangcilint = lint . linters . golangcilint
golangcilint . ignore_exitcode = true
-- golangci-lint v2 uses new output format flags
golangcilint . args = {
"run" ,
"--output.json.path=stdout" ,
"--output.text.path=" ,
"--show-stats=false" ,
function ()
-- Find .golangci.yaml or .golangci.yml in project root
local config_patterns =
{ ".golangci.yaml" , ".golangci.yml" , ".golangci.toml" , ".golangci.json" }
for _ , pattern in ipairs ( config_patterns ) do
local config_file = vim . fs . find ( pattern , {
upward = true ,
path = vim . fn . expand ( "%:p:h" ),
stop = vim . env . HOME ,
})[ 1 ]
if config_file then
return "-c=" .. config_file
end
end
return nil
end ,
function ()
return vim . fn . fnamemodify ( vim . api . nvim_buf_get_name ( 0 ), ":h" )
end ,
}
This configuration:
Uses golangci-lint v2 output format
Automatically finds config files (.golangci.yaml, .golangci.yml, etc.)
Runs linting on the current directory
Installation:
brew install golangci-lint
# or see https://golangci-lint.run/welcome/install/
JSON
-- From lua/plugins/linting.lua:58
json = { "jsonlint" },
jsonc = { "jsonlint" },
Installation:
Shell
-- From lua/plugins/linting.lua:61
sh = { "shellcheck" },
bash = { "shellcheck" },
zsh = { "shellcheck" },
Installation:
Docker
-- From lua/plugins/linting.lua:64
dockerfile = { "hadolint" }
Installation:
YAML
-- From lua/plugins/linting.lua:65
yaml = { "yamllint" },
yml = { "yamllint" },
Installation:
pip install yamllint
# or
brew install yamllint
C/C++
-- From lua/plugins/linting.lua:67
c = { "cppcheck" },
cpp = { "cppcheck" },
Installation:
Markdown
-- From lua/plugins/linting.lua:69
markdown = { "markdownlint-cli2" }
Installation:
npm install -g markdownlint-cli2
TOML
-- From lua/plugins/linting.lua:70
toml = { "tombi" }
Installation:
cargo install --locked tombi-cli
Protobuf
-- From lua/plugins/linting.lua:71
proto = { "buf_lint" }
Installation:
brew install bufbuild/buf/buf
Complete linter list
All linters configured:
Language Linter Installation JavaScript/TypeScript eslint_d npm install -g eslint_dGo golangcilint brew install golangci-lintJSON/JSONC jsonlint npm install -g jsonlintShell (sh/bash/zsh) shellcheck brew install shellcheckDockerfile hadolint brew install hadolintYAML yamllint pip install yamllintC/C++ cppcheck brew install cppcheckMarkdown markdownlint-cli2 npm install -g markdownlint-cli2TOML tombi cargo install --locked tombi-cliProtobuf buf_lint brew install bufbuild/buf/buf
LSP vs linting
Many diagnostics come from LSP servers, not linters:
LSP diagnostics
Always active
Fast, incremental
Type checking
Syntax errors
Linter diagnostics
Manual trigger
More thorough
Style/best practices
Security issues
Example: Go
For Go projects:
gopls provides real-time type checking and basic diagnostics
golangci-lint (triggered with <leader>ll) runs comprehensive linting with multiple linters
This is why gopls has staticcheck disabled:
-- From after/lsp/gopls.lua:49
staticcheck = false , -- Disable to save CPU/memory (use <leader>ll for linting)
Quick install
npm install -g eslint_d jsonlint markdownlint-cli2
brew install golangci-lint shellcheck cppcheck hadolint yamllint bufbuild/buf/buf
cargo install --locked tombi-cli
Debugging linters
If a linter isn’t working:
Check if it’s installed: which <linter-name>
Verify configuration in lua/plugins/linting.lua
Check nvim-lint documentation for the specific linter
Look at linter output in :messages
Next steps
Formatting Configure code formatters
Go setup Go-specific configuration
TypeScript TypeScript-specific configuration