Skip to main content
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:
npm install -g eslint_d
Biome linting is handled via the Biome LSP server when a biome.json file exists. ESLint is used for projects with ESLint configuration.

Go

-- 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:
npm install -g jsonlint

Shell

-- From lua/plugins/linting.lua:61
sh = { "shellcheck" },
bash = { "shellcheck" },
zsh = { "shellcheck" },
Installation:
brew install shellcheck

Docker

-- From lua/plugins/linting.lua:64
dockerfile = { "hadolint" }
Installation:
brew install hadolint

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:
brew install cppcheck

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:
LanguageLinterInstallation
JavaScript/TypeScripteslint_dnpm install -g eslint_d
Gogolangcilintbrew install golangci-lint
JSON/JSONCjsonlintnpm install -g jsonlint
Shell (sh/bash/zsh)shellcheckbrew install shellcheck
Dockerfilehadolintbrew install hadolint
YAMLyamllintpip install yamllint
C/C++cppcheckbrew install cppcheck
Markdownmarkdownlint-cli2npm install -g markdownlint-cli2
TOMLtombicargo install --locked tombi-cli
Protobufbuf_lintbrew 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

Debugging linters

If a linter isn’t working:
  1. Check if it’s installed: which <linter-name>
  2. Verify configuration in lua/plugins/linting.lua
  3. Check nvim-lint documentation for the specific linter
  4. Look at linter output in :messages

Next steps

Formatting

Configure code formatters

Go setup

Go-specific configuration

TypeScript

TypeScript-specific configuration

Build docs developers (and LLMs) love