NORMAL ~/wkd/p/lsp/help :set skin=modern utf-8

lsp.nvim.txt

One roof for the whole LSP setup — lsp.nvim

doc/lsp.nvim.txt — rendered from the plugin's own vimdoc

*lsp.nvim.txt*                          One roof for the whole LSP setup
                                                                    *lsp.nvim*

CONTENTS *lsp.nvim-contents*

  1. Introduction ............................. |lsp.nvim-introduction|
  2. Requirements ............................. |lsp.nvim-requirements|
  3. Setup .................................... |lsp.nvim-setup|
  4. Configuration ............................ |lsp.nvim-config|
  5. Keymaps .................................. |lsp.nvim-keymaps|
  6. Commands ................................. |lsp.nvim-commands|
  7. Health ................................... |lsp.nvim-health|
  8. Architecture ............................. |lsp.nvim-architecture|
  9. Status and roadmap ....................... |lsp.nvim-status|

Note on the file name: this is doc/lsp.nvim.txt, not doc/lsp.txt. Neovim's
own runtime ships a doc/lsp.txt (|lsp|), and a second file of that name makes
:help lsp.txt ambiguous. Every tag here is prefixed lsp.nvim- for the same
reason.

1. INTRODUCTION *lsp.nvim-introduction*

lsp.nvim is the umbrella for everything LSP-related in a Neovim config:

  - the own subsystem -- server registry, attach handling, capabilities,
    formatter and workspace-diagnostics toggles, the doctor;
  - the LSP-adjacent third-party plugins -- trouble.nvim, conform.nvim,
    lazydev.nvim, mason.nvim, the completion engine;
  - every LSP and diagnostics keymap, in one catalogue instead of five files.

A config's lua/lsp/** is a stateful subsystem, not a set of declarative
options, which is why it belongs in its own plugin -- the same reasoning that
produced dap.nvim for the debug protocol.

The module root is lsp on purpose: Neovim occupies only vim.lsp and
nvim-lspconfig only lspconfig, so an existing require("lsp.…") path in a
config keeps resolving once the code moves here. The flip side is that a config
which still carries its own lua/lsp/** shadows this plugin on the
'runtimepath' -- the two are meant to swap, not to coexist.

2. REQUIREMENTS *lsp.nvim-requirements*

  - Neovim 0.11 or newer.
  - lib.nvim (https://github.com/StefanBartl/lib.nvim), a HARD dependency:
    the |:Lsp| command is built on lib.nvim.bindings.usercmd.composer, and
    lsp/init.lua requires lib.nvim.notify at its top. Without lib.nvim
    require("lsp") itself raises -- there is no degraded mode. It is never
    optional, in the code or in the docs.
  - ui.nvim (https://github.com/StefanBartl/ui.nvim), a SOFT dependency with
    teeth: setup() survives without it, but lsp/lspdoctor/init.lua requires
    ui.kit at its top, so |:LspDoctor| is not registered at all and
    tools.ts_type_lookup fails -- both recorded as setup warnings. The
    pickers behind |:Lsp-root| pick/add and |:Lsp-info|'s viewer go the
    same way. Everything else runs.

3. SETUP *lsp.nvim-setup*

                                                            *lsp.nvim.setup()*
    require("lsp").setup()
Calling setup() merges your options over the defaults, binds the configured
keymap preset and registers |:Lsp|, then bootstraps the LSP core: handlers,
diagnostics, capabilities, attach, formatter, the command family, the language
modules, the configured servers, and the extra tools. It is safe to call once;
a second call is refused with a warning rather than doing all of that twice.

Every bootstrap step is wrapped. One broken server module or tool does not take
the rest of the setup with it -- it is recorded and shown by |:Lsp-status| and
|:checkhealth-lsp|. setup() returns true when at least one server was set up.

With lazy.nvim:
    {
      "StefanBartl/lsp.nvim",
      import = "lsp.pack",
      dependencies = { "StefanBartl/lib.nvim" },
      event = { "BufReadPre", "BufNewFile" },
      opts = {},
    }
                                                            *lsp.nvim-pack*
import = "lsp.pack" additionally installs and configures the ecosystem, in
four spec modules: core (conform, lazydev, workspace-diagnostics), ui
(trouble, lensline, inc-rename) and a completion engine -- blink.cmp
by default, nvim-cmp when pack.completion = "cmp", neither on false. Drop
the import to bring your own: the plugin then wires up whatever of those is
installed and reports the rest in |:checkhealth-lsp|.

Which of them gets installed is a SEPARATE channel from opts, and has to be:
lazy evaluates import while it is still collecting specs, long before
setup(opts) exists to be read. Set it before require("lazy").setup():
    vim.g.lsp_nvim = {
      pack = {
        core = true,          -- conform, lazydev, workspace-diagnostics
        ui = true,            -- trouble, lensline, inc-rename
        completion = "blink", -- "cmp" | "blink" | false (default: blink)
        disable = { "lensline.nvim" },
      },
    }
vim.g decides WHETHER a plugin is installed, opts decides HOW everything is
configured. Note that import names a directory: lazy requires every module
under lua/lsp/pack/, so the selection above is applied per spec through
enabled, not by importing conditionally.
                                                           *lsp.nvim.status()*
    require("lsp").status()
Returns a snapshot of what the plugin currently is: whether setup() has run,
the resolved config, the keymaps actually bound, whether |:Lsp| registered, the
servers that were set up, the attached LSP clients, and every warning collected
during setup. |:Lsp-status| and |:checkhealth-lsp| both read this, so the two
cannot disagree.

4. CONFIGURATION *lsp.nvim-config*

The options are resolved from four layers, lowest to highest:

    1. lua/lsp/config/DEFAULTS.lua     the documented values
    2. lua/lsp/config/PRESETS.lua      selected by preset -- how much of
                                       this should run on this machine
    3. the |lsp.nvim.setup()| options    what you wrote
    4. .nvim-lsp.json                  what this checkout needs

A preset sits below your options because it moves the floor rather than
overruling you; the project file sits above them because "here, not globally"
is the one thing it is for. Layers 1-3 are resolved first, since they are where
project.enable and project.file come from -- a project file cannot decide
whether project files are read.

Defaults (excerpt -- the full table is lua/lsp/config/DEFAULTS.lua):
    {
      preset = "default",  -- "default" | "lean" | "full"
      project = { enable = true, file = ".nvim-lsp.json" },
      servers = { "bashls", "lua_ls", "gopls", "marksman", "html",
                  "ts_ls", "tailwindcss", "csharp" },
      diagnostics = {   -- these two keys only; see the note below
        ui = "auto",   -- "auto" | "native" | "trouble" -- ]d/[d's sink
        debounce_ms = 150,  -- publishDiagnostics throttle; 0 = off
      },
      formatter = { on_save = false, timeout_ms = 1500 },
      workspace = {
        markers = { ".git", "go.work", "go.mod", "package.json", ... },
        containers = { "packages", "apps", "services", ... },
      },
      inlay_hints = { enable = false, filetypes = {} },
      lightbulb = {   -- the code-action indicator
        enable = true, filetypes = {},
        kinds = { "quickfix", "source" },  -- {} = unfiltered
        render = "sign",  -- or "virtual_text"
        text = "󰌵",
        debounce_ms = 150, priority = 20,
      },
      winbar = {   -- the LSP breadcrumb: folder > file > Class > method
        enable = true, filetypes = {},
        show_file = true, folder_level = 1, separator = " › ",
        chips = true, align = "left",  -- "right"/"center" = right edge/centred
        max_symbols = { markdown = 1 },  -- symbols allowed after the file
        debounce_ms = 60, refresh_ms = 300,
      },
      peek = {   -- floating, editable definition (lsp, lsT, :Lsp peek)
        width = 0.7, height = 0.5, border = "rounded", beacon = true,
        keys = { close = "q", edit = "<C-o>", vsplit = "<C-v>",
                 split = "<C-x>", tabedit = "<C-t>" },
      },
      implement = {   -- implementation markers; off unless you turn it on
        enable = false, filetypes = {},
        kinds = { Interface = true }, text = " %d impl",
        debounce_ms = 600, max_requests = 20,
      },
      code_actions = { picker = "auto", gitsigns = false },
      finder = { references = true, implementations = true,
                 definitions = true, declarations = false, typedefs = false },
      auto_restart = {   -- bring a crashed server back
        enable = true, max_attempts = 4,
        initial_delay_ms = 1000, max_delay_ms = 30000,
        reset_after_ms = 60000,
      },
      attach = {
        use_workspace_diagnostics = true,
        workspace_diagnostics_projects = {},
        use_lazydev = true,
      },
      mason = { ensure_install = false, overrides = { ... } },
      lspdoctor = { use_notify = false, list_limit = 8, ... },
      tools = {
        eslint_prettier = { enable = true, filetypes = { ... } },
        lsp_signature = { enable = true },
        ts_type_lookup = { enable = true },
        deprecated_help = { enable = true },
      },
      languages = { enable = true, env_links = true },
      completion = {
        personal_names = { enable = true, labels = nil },
      },
      rename = { provider = "auto" },  -- "auto"|"inc_rename"|"native"
      keymaps = { enable = true, preset = "default", map = {} },
      usrcmds = { enable = true, legacy_aliases = true },
      which_key = { enable = true },
      menu = { enable = true },
      integrations = { ui_menu = true },
    }
diagnostics really is those two keys and nothing else. The look --
update_in_insert, severity_sort, virtual_text, float, signs,
underline -- lives in lsp.core.diagnostics.baseline(), not here, and that
is a correctness point rather than tidiness: this table is merged LAST into
the single |vim.diagnostic.config()| call, after anything another plugin
contributed, so that what you write always wins. Leaving lsp.nvim's own
defaults in it would have meant a plugin's contribution being overruled by
lsp.nvim's default rather than by anything you asked for. Anything you add
here still merges last and still wins.

Field reference

preset                ("default"|"lean"|"full")
                                  Option profile the rest of the table starts
                                  from -- one word instead of roughly twenty
                                  fields. lean turns down the work paid per
                                  keystroke, per attach and per redraw
                                  (virtual text, the signatureHelp round
                                  trip, the workspace scan on attach, the ~25
                                  legacy command registrations); on-demand
                                  actions such as gd, hover and rename are
                                  untouched. full is the inverse trade.
                                  Anything you name explicitly still wins over
                                  the preset. No preset ever sets
                                  mason.ensure_install or
                                  formatter.on_save -- a profile is a
                                  performance dial, not consent to install
                                  software or rewrite your files. Not
                                  keymaps.preset, which picks a set of keys;
                                  this picks a set of options, one of which is
                                  keymaps.preset.
project.enable        (boolean)   Look for a per-project override file at all.
project.file          (string)    Its name. Found once, at setup() time, by
                                  walking upward from the working directory;
                                  the first hit wins. Read once because that
                                  is when servers are enabled and tools are
                                  set up -- re-reading after a :cd would
                                  report a config that is not the one running.
                                  The file is JSON, not Lua: cloning a
                                  repository must not be enough to run its
                                  code, and JSON cannot express a function.
                                  Only servers, diagnostics, formatter,
                                  inlay_hints, lightbulb, attach,
                                  workspace, tools and languages are
                                  accepted -- the keys the repository knows the
                                  answer to.
                                  Keymaps, :Lsp registration and mason are
                                  yours; anything else in the file is dropped
                                  with a warning. |:Lsp| status and
                                  |:checkhealth-lsp| name the file that was
                                  used.
servers               (string[])  Server names to set up and enable. Each
                                  resolves to lsp.servers.<name>, with
                                  lsp.servers.webdev.<name> tried as a
                                  fallback for names without a dot. This used
                                  to be a hardcoded ACTIVE list inside
                                  core/registry.lua, so turning a server on
                                  or off meant editing the plugin. An empty or
                                  malformed list falls back to the defaults --
                                  "no language server at all" looks exactly
                                  like a broken install and is never what a
                                  typo should produce.
diagnostics           (table)     Merged last into the one
                                  |vim.diagnostic.config()| call, made after
                                  the servers are enabled, so neither a server
                                  config nor another plugin's contribution can
                                  overwrite it -- except ui and
                                  debounce_ms, which are this plugin's own
                                  and are stripped before that call. Only
                                  those two have defaults here; the rest of
                                  the table is whatever you add.
diagnostics.debounce_ms (integer) Throttle window for
                                  textDocument/publishDiagnostics, in
                                  milliseconds. A chatty server (ts_ls is
                                  the reference case) publishes several times
                                  per keystroke pause and every push
                                  re-renders. The window is leading-edge: the
                                  first push of a burst goes through
                                  immediately and only the ones inside the
                                  window are collapsed to the newest, so the
                                  push a user waits for is never the one
                                  delayed. Coalescing keeps the newest payload
                                  and never merges -- a diagnostics list
                                  replaces a file's diagnostics wholesale, so
                                  a merge would resurrect entries the server
                                  had just cleared. 0 turns the throttle off.
formatter.on_save     (boolean)   Format on write at startup. The runtime
                                  toggle (|:Lsp-format| toggle, or the
                                  :LspFormatToggle alias) owns it from there;
                                  this is only the starting position. The hook
                                  is a BufWritePre autocommand in the
                                  LspFormatOnSave group -- an in-editor
                                  format, so it belongs before the write. The
                                  shell-based tools.eslint_prettier runs on
                                  BufWritePost instead, for the opposite
                                  reason: a child process needs the file on
                                  disk to already be the buffer.
formatter.timeout_ms  (integer)   Upper bound for one format request.
workspace.markers     (string[])  A directory holding one of these is offered
                                  as a workspace folder by |:Lsp| root add.
                                  Broader than any one server's
                                  root_markers on purpose: the question is
                                  "could a language server sensibly be
                                  pointed here", not "is this that server's
                                  root". Replaces the default list rather
                                  than merging into it, and an explicitly
                                  empty list is honoured -- offering nothing
                                  but the client roots and the cwd is a
                                  coherent wish.
workspace.containers  (string[])  Directory names that hold projects rather
                                  than being one. After walking upward, the
                                  candidate search reads the outermost
                                  project's children and descends exactly one
                                  level through these names -- that is where
                                  a monorepo's sibling package lives, and an
                                  upward walk never looks sideways. Bounded
                                  at one readdir per name; an unbounded
                                  descent would stat a whole repository to
                                  fill a picker.
inlay_hints.enable    (boolean)   Global startup default for Neovim's native
                                  inlay hints (|vim.lsp.inlay_hint|). The
                                  runtime toggle (|:Lsp| hints, <leader>th)
                                  owns it from there.
inlay_hints.filetypes (table<string, boolean>)
                                  Per-filetype override of that default. An
                                  absent key inherits enable; false is an
                                  explicit "off here". Absent and false are
                                  deliberately different -- that is why this is
                                  a map and not a list, and a list is rejected
                                  with a warning rather than silently
                                  overriding nothing.
lightbulb.enable      (boolean)   Global startup default for the code-action
                                  indicator: a mark in the line when
                                  textDocument/codeAction has something to
                                  offer there. The runtime toggle (|:Lsp|
                                  lightbulb, <leader>tb) owns it from there.
lightbulb.filetypes   (table<string, boolean>)
                                  Per-filetype override, resolved exactly as
                                  inlay_hints.filetypes is.
lightbulb.kinds       (string[])  CodeActionKind prefixes that light the
                                  indicator; a kind matches exactly or as a
                                  dotted child. This is why enable can
                                  default to on: unfiltered, the indicator is
                                  lit permanently under servers that offer
                                  refactors everywhere, and one that is always
                                  on carries no information. An action with no
                                  kind always counts -- kind is optional in
                                  the protocol. {} turns the filter off.
lightbulb.render      ("sign"|"virtual_text")
                                  Where it draws. Both obvious places are
                                  taken -- the sign column carries diagnostic
                                  signs, virtual_text sits at end of line --
                                  so sign borrows the sign column on the
                                  cursor line only, above the diagnostic signs,
                                  and virtual_text draws at the window edge.
lightbulb.text        (string)    The indicator itself. Truncated to two
                                  display cells when rendered as a sign.
lightbulb.debounce_ms (integer)   Window between the last cursor movement and
                                  the request -- what the feature costs. One
                                  request per cursor position, sent only to
                                  clients advertising codeActionProvider,
                                  and never in insert mode.
lightbulb.priority    (integer)   Extmark priority. Above |vim.diagnostic|'s
                                  sign priority (10) by default, which is the
                                  point: on a line that has both, the
                                  actionable mark is the one worth seeing.
winbar.enable          (boolean)   Global startup default for the LSP breadcrumb
                                  in the window bar: the file's path, then
                                  every symbol containing the cursor. Replaces
                                  lspsaga's symbol_in_winbar. The runtime
                                  toggle (|:Lsp-winbar|, <leader>tW) owns it
                                  from there. Off under preset = "lean".
winbar.filetypes      (table<string, boolean>)
                                  Per-filetype override, resolved exactly as
                                  inlay_hints.filetypes is.
winbar.show_file      (boolean)   Draw the path in front of the symbols.
winbar.folder_level   (integer)   Directories shown before the file name.
winbar.separator      (string)    Between parts.
winbar.chips          (boolean)   Rounded, coloured chips; false is one flat
                                  string coloured by linked highlight groups.
winbar.align          ("left"|"right"|"center")
                                  "left" (default), "right" or "center": the
                                  latter two wrap the breadcrumb in the
                                  'statusline'/'winbar' built-in %= item,
                                  pushing it to the window's right edge or
                                  splitting it evenly between both. No
                                  padding is computed -- %= does that.
                                  A colorscheme's own WinBar underline is
                                  unaffected by this option; `:hi WinBar
                                  gui=NONE` removes it if it bothers you.
winbar.max_symbols    (table<string, integer|false>)
                                  How many symbols may follow the file, per
                                  filetype. { markdown = 1 } by default,
                                  because marksman reports headings as a
                                  nested outline and a cursor in an H3 would
                                  otherwise draw file > H1 > H2 > H3. A map,
                                  so your entries merge over the default;
                                  markdown = false lifts the cap.
winbar.debounce_ms    (integer)   Between the last cursor movement and the
                                  repaint. Reads a cache; sends nothing.
winbar.refresh_ms     (integer)   Between the last edit and the next
                                  textDocument/documentSymbol request.
peek.width            (number)    Width of the peek float: a fraction of the
                                  editor up to 1, cells above.
peek.height           (number)    Height, the same way.
peek.border           (string|string[])
                                  nvim_open_win border.
peek.beacon           (boolean)   Flash the target line when a peek is taken
                                  into a real window.
peek.keys             (table<string, string|false>)
                                  Keys inside the peek, by action: close,
                                  edit, vsplit, split, tabedit.
                                  Buffer-local to the peeked buffer and given
                                  back when the last peek over it closes.
                                  false unbinds an action.
implement.enable      (boolean)   Implementation markers: a count at the end
                                  of the line of an interface that something
                                  implements. Off by default -- one
                                  textDocument/implementation request per
                                  marked symbol per edit pause.
implement.filetypes   (table<string, boolean>)
                                  Per-filetype override.
implement.kinds       (table<string, boolean>)
                                  SymbolKind names that get a marker.
                                  { Interface = true } by default. A map,
                                  not a list: lists merge index by index.
implement.text        (string)    Marker text; %d is the count.
implement.debounce_ms (integer)   Between the last edit and the requests.
implement.max_requests (integer)  Cap on requests per round.
code_actions.picker   ("auto"|"fzf-lua"|"native")
                                  What lsa opens. auto is fzf-lua's
                                  picker, with the edit previewed as a diff,
                                  when fzf-lua is installed, and
                                  vim.lsp.buf.code_action when it is not.
code_actions.gitsigns (boolean)   Offer gitsigns' hunk actions (stage, reset,
                                  preview) in the same list. Runs a small
                                  in-process language server, which shows up
                                  in |:Lsp| servers -- hence off by default.
                                  The winbar and |:Lsp| stop / restart look
                                  past it; other plugins that list
                                  vim.lsp.get_clients() will see it.
finder.references     (boolean)   Sources of lsf. A map of switches rather
finder.implementations (boolean)  than a list, because lists merge index by
finder.definitions    (boolean)   index. References, implementations and
finder.declarations   (boolean)   definitions are on by default.
finder.typedefs       (boolean)
auto_restart.enable   (boolean)   Bring a language server back when it dies
                                  mid-session. A crashed server is otherwise
                                  invisible -- hover stops answering,
                                  completion goes empty, diagnostics freeze --
                                  and it reads as slowness until someone types
                                  |:Lsp| restart.

                                  Four exits are deliberately not crashes: one
                                  this plugin asked for (a force-stop is a
                                  SIGTERM and would otherwise be
                                  indistinguishable from a kill, so every
                                  deliberate stop declares itself first), a
                                  clean exit nobody asked for, an exit during
                                  |:qa|, and a client that died before it ever
                                  attached -- the last is where a retry loop
                                  would be a hazard, and |:Lsp| recover owns
                                  it.
auto_restart.max_attempts (integer)
                                  Consecutive attempts before it gives up and
                                  says so. The counter survives the giving-up
                                  so |:LspDoctor| startup can report how bad it
                                  got.
auto_restart.initial_delay_ms (integer)
                                  Wait before the first attempt. Doubles from
                                  there: 1s, 2s, 4s, 8s at the defaults.
auto_restart.max_delay_ms (integer)
                                  Cap on that doubling. A value below
                                  initial_delay_ms would make the backoff
                                  shrink instead of grow and is raised to it
                                  with a warning.
auto_restart.reset_after_ms (integer)
                                  How long a relaunched client must stay alive
                                  before the attempt counter clears. Survival
                                  rather than attach: clearing it on attach
                                  would let a server that crashes two seconds
                                  after every attach restart forever, since
                                  each attach would forgive the previous crash.
attach.use_workspace_diagnostics (boolean)
                                  Populate workspace-wide diagnostics on
                                  attach. On by default: the module measures
                                  workspace size itself and refuses above its
                                  own max_files gate.
attach.workspace_diagnostics_projects (table)
                                  project folder -> boolean, a per-project
                                  override of the switch above. ~ and $VAR
                                  expand; the most specific folder wins. false
                                  skips the populate (and its max_files
                                  warning) AND holds back the pushes a server
                                  sends for files you have not opened -- marksman
                                  publishes those on its own. Open files are
                                  unaffected. :Lsp workspace changes it at
                                  runtime. A .nvim-lsp.json may set it for
                                  its own folders only (., a relative path,
                                  or an absolute one inside it); other keys
                                  are dropped with a warning. An entry that is
                                  not absolute once expanded (a $VAR that is
                                  not set here; on Windows also /x) is
                                  ignored with a warning, also listed in
                                  :checkhealth lsp.
attach.use_lazydev    (boolean)   Wire lazydev into lua_ls attaches.
mason.ensure_install  (boolean)   Install missing LSP/linter/formatter
                                  packages on setup. Off by default --
                                  installing software is a side effect a
                                  plugin should not perform unasked.
mason.overrides       (table)     Per-category force-on/off, keyed lsp,
                                  dap, linters, formatters.
lspdoctor             (table)     Forwarded to lsp.lspdoctor.setup().
lspdoctor.formatter_priority (string[])
                                  Order in which |:LspDoctor|'s report ranks
                                  the LSP clients that could format the
                                  buffer. **Report only** -- it chooses
                                  nothing, which is why it sits under
                                  lspdoctor and not under formatter. What
                                  actually formats is conform's chain for the
                                  filetype, with LSP as the fallback conform
                                  falls back to; on every filetype conform
                                  covers, no LSP client formats at all. The
                                  report names conform's answer first and this
                                  ranking second.
lspdoctor.probe_timeout (integer, ms)
                                  How long :LspDoctor probe waits for
                                  diagnostics to come back from a buffer of
                                  deliberately broken content. Default 5000.
                                  Generous on purpose: the answer that matters
                                  is "nothing arrived", and a timeout too short
                                  for a busy server produces that answer for a
                                  pipeline that works.
tools.<name>.enable   (boolean)   Master switch per extra tool:
                                  eslint_prettier, lsp_signature,
                                  ts_type_lookup, deprecated_help.
tools.eslint_prettier.filetypes (string[])
                                  Filetypes that tool attaches to.
languages.enable      (boolean)   Apply the filetype-specific setup under
                                  lsp/languages/** before the servers are
                                  registered.
languages.env_links   (boolean)   Resolve $VAR/..., ${VAR}/... and ~/...
                                  Markdown link targets, which marksman cannot:
                                  definition and hover through an in-process
                                  lsp.nvim-envlinks client, which also
                                  warns about env links whose file or
                                  #heading is missing (marksman reports no
                                  link that carries a #fragment), and
                                  completes the directories such a target
                                  names (/, $, { trigger it).
                                  Resolved through gopath.nvim's
                                  resolve_text when installed, built-in
                                  otherwise. On by default.
completion.personal_names.enable (boolean)
                                  Register the hand-written personal-names
                                  completion source. Engine-neutral: it is set
                                  up from |lsp.nvim.setup()| rather than from
                                  nvim-cmp's opts, which is what stopped a
                                  switch to blink silently dropping it.
completion.personal_names.labels (fun(): (string|{name: string})[])|nil
                                  A reader, not a list. The names are the host
                                  config's data, so the config hands them over
                                  instead of this plugin reaching into it.
                                  Without a reader the source falls back to
                                  completion/personal_names/extra.lua alone.
rename.provider       ("auto"|"inc_rename"|"native")
                                  Backend for the rename action, which both
                                  bound rename keys go through. "auto" prefers
                                  inc-rename when it is installed and falls
                                  back to |vim.lsp.buf.rename()|. The two keys
                                  used to run different renames; routing both
                                  through one option is what stopped them
                                  drifting apart.
keymaps.enable        (boolean)   Master switch. false = the plugin binds no
                                  keys at all.
keymaps.preset        ("default"|"minimal"|"none")
                                  Which entry of the keymap catalogue is bound.
                                  "default" is the full set, "minimal" only the
                                  keys with no plausible native equivalent,
                                  "none" binds nothing while leaving the
                                  catalogue available. An unknown value falls
                                  back to "default" and is reported by
                                  |:checkhealth-lsp|.
keymaps.map           (table<string, string|false>)
                                  Per-action override, keyed by the catalogue's
                                  action name. A string replaces that action's
                                  left-hand side; false drops the mapping; an
                                  absent key keeps the preset's default.
usrcmds.enable        (boolean)   Register the |:Lsp| verb on setup, and
                                  nothing else. false removes exactly one
                                  command, :Lsp itself. It does NOT remove
                                  the 25 flat aliases below -- switching those
                                  off is legacy_aliases, and the two are
                                  independent in both directions. Turning this
                                  off while leaving the aliases on leaves the
                                  whole flat family working with no verb over
                                  it, which is a coherent wish and the reason
                                  the two are separate switches.
usrcmds.legacy_aliases (boolean)  Register the 25 flat :Lsp*/:Diag*
                                  commands as aliases onto the same functions
                                  the |:Lsp| routes call. On by default:
                                  muscle memory beats tidiness and an alias
                                  costs a line. Off drops exactly those 25 and
                                  nothing else. :LspDoctor, :LspMdHints,
                                  :EslintFix and the :TypeDef* family are
                                  not aliases and are registered either way.
which_key.enable      (boolean)   Label the bound key prefixes as which-key
                                  groups. which-key is a soft dependency: when
                                  it is absent this does nothing and no mapping
                                  is affected.
integrations.ui_menu  (boolean)   Let ui.nvim's right-click menu (ui.menu)
                                  compose this plugin's fly-outs. false
                                  hides them there only; items() still
                                  serves any other host. Default true.
menu.enable           (boolean)   Contribute this plugin's entries to the
                                  right-click context menu (nvzone/menu, a
                                  soft dependency). The entries mirror the
                                  resolved keymap catalogue, so
                                  keymaps.preset and keymaps.map decide
                                  what appears. With nvzone/menu absent this
                                  only gates whether lsp.integrations.menu
                                  returns entries at all.

An out-of-range value never raises. It degrades to the documented default and
is recorded as a warning, visible in |:Lsp-status| and |:checkhealth-lsp| -- a
typo in a config should cost a feature, not a startup. Each warning also names
the layer the value came from: (from setup()), (from preset "lean"), `(from
.nvim-lsp.json)`.

Every enum is checked this way, and so is every numeric field, every list and
every filetype map. The numeric rule has no exceptions: the set is derived from
DEFAULTS rather than listed by hand, so a field cannot be added without one.
Six of the twelve used to have no check and reached :Lsp status unaltered --
lightbulb.priority, formatter.timeout_ms, lspdoctor.list_limit,
lspdoctor.probe_timeout, lspdoctor.scratch_threshold and
lspdoctor.semantic_tokens_timeout. Nothing raised, because the consumers
defend themselves, but the report then showed what you typed rather than what
was running, which is the one thing a status report must not do.

integrations is the one option in this document's history that never
arrived: nothing reads it, and a default nothing reads is a promise the plugin
does not keep. completion and rename DID arrive -- both are in
DEFAULTS.lua and both are read (lsp/init.lua for the completion source,
bindings/actions.lua for the rename backend) -- and both are documented
above.

5. KEYMAPS *lsp.nvim-keymaps*

Keymaps are data. lua/lsp/config/KEYMAPS.lua holds one entry per action --
left-hand side, mode, action, description -- and bindings/keymaps.lua binds
what is left after your keymaps.map overrides are applied. Adding a mapping
means adding a catalogue entry; nothing is hardcoded at the binding site.
docs/BINDINGS.md is generated from the same table by
scripts/gen_bindings.lua, which CI checks, so the list cannot drift from the
code.

The default preset binds 57 entries; minimal binds the 33 that have no
Neovim 0.11 equivalent (it drops 24: grn, grt, ]d/[d, twelve of the
fourteen prefixless ls* keys, the five <leader>xl* Trouble views, and the
three leader keys added with the navigation features -- <leader>xo,
<leader>xa, <leader>tW); none binds nothing.

minimal keeps two ls* keys, lsc and lsC: call hierarchy is the one
thing in that family Neovim has no default for. So the 'timeoutlen' cost below
is NOT bought back by the preset -- two prefixless ls maps cost exactly what
fourteen do. `keymaps.map = { picker_incoming_calls = false, picker_outgoing_calls
= false }` is what finishes the job.

The navigation keys -- what replaced lspsaga -- are lsp / lsT (peek the
definition / type definition in a floating, editable window), lsa (code
actions with a diff preview; gra in Visual mode for a selection, because an
ls* key there would make every l wait out 'timeoutlen'), <leader>xa
(the quick fix for the diagnostic on this line), lsf (finder), lsh /
lsH (type hierarchy), <leader>xo (outline sidebar) and <leader>tW
(winbar toggle).
docs/FEATURES/NAVIGATION.md has the detail.

Overriding, per action:
    keymaps = {
      map = {
        goto_definition = "gd",   -- a string replaces the left-hand side
        rename_leader = false,    -- false drops the mapping
      },
    }
Two properties of the bound keys are worth knowing:

  - The prefixless ls* family costs every Normal-mode l a 'timeoutlen'
    wait, because Neovim must see whether an s follows. That is the price of
    a prefixless three-character mapping, and it is deliberate.
  - grn and grt collide with Neovim 0.11's own gr* maps -- but those are
    GLOBAL, not buffer-local. $VIMRUNTIME/lua/vim/_core/defaults.lua sets all
    six (grn, gra, grr, gri, grt, gO) at startup, "unconditionally
    to avoid different behavior depending on whether an LSP client is
    attached"; in a --headless --noplugin session with no client at all,
    maparg("grn", "n", false, true).buffer is 0 for every one of them.
    Being global, the catalogue's rename and goto_type_definition_gr
    replace Neovim's outright the moment the binder runs: after setup() under
    the default preset, maparg("grn").desc reads "LSP: Rename symbol" where
    it read "vim.lsp.buf.rename()" before. That is what routing grn through
    rename.provider needs, and it does not need |LspAttach| to get it.
    bindings/autocmds.lua re-binds the two buffer-locally on LspAttach
    anyway; with the collision global on both sides, that hook is belt and
    braces rather than the thing that makes the catalogue win.

6. COMMANDS *lsp.nvim-commands*

One command with eighteen subcommands and <Tab> completion, built with
lib.nvim.bindings.usercmd.composer. The folding is done: the flat command
family (:LspStatus, :LspLog, :LspInfo, :LspRecover, :LspStartHere,
:LspStopHere, :LspRestartHere, :LspForceRestart, :LspFormat*,
:LspWorkspaceDiagnostics*, :Diag*) is now 25 thin aliases onto the same
functions the routes below call, so the two cannot drift apart.
usrcmds.legacy_aliases = false drops them. |:LspDoctor| and :LspMdHints keep
their own verbs and are not aliases, and neither are :EslintFix or the
:TypeDef* family, which come from tools/.

Every subcommand below completes its arguments, and every argument set here is
closed unless it says otherwise.
                                                                       *:Lsp*
    :Lsp status                                              *:Lsp-status*
                    Report what the plugin has set up: resolved config,
                    keymaps bound, whether the command registered, and any
                    configuration warnings.

    :Lsp servers                                            *:Lsp-servers*
                    The servers this plugin set up, plus the LSP clients
                    Neovim currently has attached with their root directory
                    and buffer count. The gap between the two is what one
                    usually wants to see.

    :Lsp health                                              *:Lsp-health*
                    Run |:checkhealth| for this plugin.

    :Lsp info                                                  *:Lsp-info*
                    Detailed LSP information for the current buffer, in
                    ui.nvim's viewer. The :LspInfo alias is the same thing.

    :Lsp start [server]                                       *:Lsp-start*
    :Lsp stop [server]                                         *:Lsp-stop*
    :Lsp restart [server]                                   *:Lsp-restart*
                    Lifecycle for this buffer's clients: all of them with no
                    argument, one by name with one. The server argument
                    completes from the LIVE set rather than a list frozen at
                    setup -- attached clients first, since "restart this one"
                    usually means one of those, then everything servers
                    configures, attached or not.

    :Lsp force-restart {server}                        *:Lsp-force-restart*
                    Restart one server with a full teardown first. Its own
                    subcommand rather than a flag on restart, because a
                    literal word after restart would be ambiguous with a
                    server actually called "force". The server name is
                    required here.

    :Lsp recover                                            *:Lsp-recover*
                    Start the servers that should be running on this buffer
                    and are not. This is what owns the case auto_restart
                    deliberately refuses: a client that died before it ever
                    attached, where an automatic retry loop is a hazard.

    :Lsp format [action]                                     *:Lsp-format*
                    Format once, or control format-on-save. Actions: once
                    (default), on, off, toggle, status, which.
                    which answers which engine would format this buffer --
                    conform's chain for the filetype, or the LSP fallback.

    :Lsp hints [action] [filetype]                            *:Lsp-hints*
                    Neovim's native inlay hints (|vim.lsp.inlay_hint|),
                    globally or for one filetype. Actions: toggle
                    (default), on, off, status, clear. As with
                    |:Lsp-lightbulb|, clear needs a filetype -- dropping
                    "the global override" would be dropping the setting
                    itself, so it is refused with a warning.

    :Lsp diag {action} [list]                                  *:Lsp-diag*
                    Diagnostics into a list, or movement within one. Actions:
                    qf, loc, next, prev -- required, since there is no
                    sensible default for "do something with diagnostics". The
                    optional second argument picks the list next/prev move
                    in, qf or loc, defaulting to loc. Movement is one
                    step per invocation: the keymaps fall back to |v:count1|,
                    which is right for a keypress and wrong for a command,
                    where v:count holds whatever the last keypress left
                    behind.

    :Lsp workspace [action] [project]                     *:Lsp-workspace*
                    Workspace-wide diagnostics. Actions: on, off, toggle,
                    status (default), now, clear, list. now
                    populates immediately rather than waiting for an attach.

                    Without {project} the global switch moves. With one, only
                    that folder's override does. {project} is . (the cwd's
                    project root), a folder name under $REPOS_DIR, or a path;
                    it completes ., the $REPOS_DIR folders and existing
                    overrides. clear drops an override, list shows them.
                    Off also holds back pushes for files you have not opened
                    and clears what a scan already showed; on or clear
                    replays them. Not persisted -- see
                    attach.workspace_diagnostics_projects.

    :Lsp root [action]                                         *:Lsp-root*
                    Roots and workspace folders. Two mechanisms under one
                    word, because from where you sit both answer "what does
                    this server consider my project".

                      show    (default) The active root scope, plus every
                              attached client's resolved root, the workspace
                              folders it holds, and whether it accepts a
                              runtime change.
                      pick    Switch the resolution scope: cwd, git root, or
                              the file's own path. Reaches only the servers
                              whose root_dir is a function. Of the eight
                              servers ship by default that is three:
                              lua_ls, marksman, and csharp -- which
                              registers under the config name omnisharp,
                              so that is what :Lsp root show calls it.
                              bashls, gopls, html, ts_ls and
                              tailwindcss declare root_markers, which
                              Neovim resolves itself with no hook to
                              intercept. A root_dir function is necessary
                              and not sufficient, though: of the three, only
                              lua_ls reads the scope at all
                              (servers/lua_ls/rootresolver.lua) and only it
                              listens for User LspRootScopeChanged to
                              recompute for open buffers. marksman and
                              csharp resolve from their own markers and are
                              unaffected by a pick.
                      add     Pick a directory and add it as a workspace
                              folder on every attached client that accepts
                              one. This is LSP's own multi-root mechanism
                              (workspace/didChangeWorkspaceFolders): it
                              reaches every server that advertises
                              changeNotifications, root_markers ones
                              included, and takes effect without a restart.
                              The monorepo case it exists for: gopls sitting
                              in packages/api, one pick of packages/web,
                              and definitions across the package boundary
                              resolve.
                      remove  Pick one of the held folders and take it off.
                      list    What add would offer, as a report, without
                              opening a picker.

                    Candidates are found by walking upward from the buffer
                    for |lsp.nvim-config| workspace.markers, then reading
                    the outermost project's children and descending one level
                    through workspace.containers -- the sibling package an
                    upward walk can never see. Anything already a workspace
                    folder is left out.

                    A client that declares no workspaceFolders.supported,
                    or that never asked for changeNotifications, is skipped
                    rather than sent a notification it did not ask for.
                    :Lsp root show names it, with the reason.

    :Lsp autorestart [action]                           *:Lsp-autorestart*
                    Whether a crashed server is brought back on its own.
                    Actions: toggle (default), on, off, status.

                    status is the one to read after something went wrong: it
                    names every server with a failed attempt on record, why the
                    last attempt failed, and how far the backoff had got. The
                    same counter is what |:LspDoctor| startup reports.

    :Lsp lightbulb [action] [filetype]                    *:Lsp-lightbulb*
                    The code-action indicator, globally or for one filetype.
                    Actions: toggle (default), on, off, status,
                    clear. With no filetype the global default moves; with
                    one, an override is written for that filetype alone, and
                    clear gives it back to the global.

                    status answers the question that decides whether the
                    indicator is worth having in this buffer: which attached
                    clients advertise codeActionProvider, which
                    CodeActionKinds are on the allowlist, and whether a mark
                    is on screen right now.

    :Lsp winbar [action] [filetype]                                  *:Lsp-winbar*
                    The LSP breadcrumb in the winbar, globally or for one
                    filetype. Actions: toggle (default), on, off,
                    status, clear, with the same meaning as for
                    |:Lsp-lightbulb|. status says which attached client
                    answers textDocument/documentSymbol in this buffer and
                    whether the symbols cached for it are fresh.

    :Lsp implement [action] [filetype]                            *:Lsp-implement*
                    Implementation markers on interfaces, globally or for one
                    filetype. Same actions and arguments as |:Lsp-winbar|.
                    status names the client that would answer
                    textDocument/implementation and how many markers are on
                    screen. Off unless implement.enable says otherwise.

    :Lsp peek [kind]                                                   *:Lsp-peek*
                    Peek a definition in a floating, editable window. kind
                    is definition (default), type_definition,
                    implementation or declaration; the last two have no
                    key. Inside the float q closes, <C-o> takes the buffer
                    into the window you came from, <C-v>/<C-x> into a
                    split and <C-t> into a tab (peek.keys).

    :Lsp log open                                          *:Lsp-log-open*
                    Open Neovim's LSP log file (|vim.lsp.get_log_path()|) in a
                    split. Warns when no log file exists yet.

    :Lsp log level {level}                                *:Lsp-log-level*
                    Set the LSP log level. Completes over trace, debug, info,
                    warn, error and off.

    :LspDoctor [report]                                        *:LspDoctor*
                    Six reports on the LSP state of the current buffer, each
                    answering one question. With ! the report opens in a
                    scratch buffer instead of being printed.

                    Reachable as :Lsp doctor too, over the same six names --
                    the route takes lspdoctor.MODES itself rather than
                    repeating the spellings, so the two cannot come to offer
                    different reports. They differ in one thing only, and
                    deliberately: with no argument :LspDoctor runs all,
                    while :Lsp doctor runs startup. The route always opens
                    a scratch split, the combined report is long, and the
                    question one arrives with is almost always "why is my
                    server not running".

                      startup       Is the server running, and if not, why?
                                    Executable, attempts, last error, and what
                                    to run next.
                      resolve       Where the filetype -> server chain breaks,
                                    in five steps.
                      buffer        Clients, diagnostic counts, provider
                                    conflicts, offset encodings, formatter.
                                    Capped at lspdoctor.list_limit.
                      capabilities  The buffer report uncapped, plus
                                    root_dir, workspace folders and the full
                                    capability set per client.
                      probe         Whether diagnostics arrive at all: hands
                                    the attached clients a buffer they cannot
                                    parse and waits lspdoctor.probe_timeout.
                                    The only report that provokes rather than
                                    observes, and the only one that costs
                                    anything -- which is why all leaves it
                                    out.
                      all           (default) The four observing reports.

                    The older names health, debug, quick and deep
                    (which mapped onto startup, resolve, buffer and
                    capabilities) were accepted but never offered, and were
                    removed on 2026-09-02.

                    lua/lsp/lspdoctor/README.md documents the reports in
                    full.

Reports open in a scratch split rather than a notification: they are
multi-line, and meant to be read and copied from.

7. HEALTH *lsp.nvim-health*

                                                        *:checkhealth-lsp*
    :checkhealth lsp
Six sections, in this order: Environment (Neovim version, lib.nvim),
lsp.nvim (what setup() registered, including every warning it worked around),
Servers, Ecosystem (the plugins around this one), Diagnostics (who owns
|vim.diagnostic.config()| and what each contributor put there), and
Per-buffer diagnosis, a pointer to |:LspDoctor|.

The servers section reads left to right along four numbers: installed (what
Mason has on disk, whatever servers says), configured, set up, and attached
-- both in total and in the buffer you were in when you opened the report. That
last one is the alternate buffer, not the current one: Neovim creates the
health:// buffer and makes it current before running a single check, so
reading the current buffer would report on the report. The alternate only
survives the first |:checkhealth| of a session; after that the section falls
back to the most recently used file buffer, and says so rather than reporting
a zero it would have invented.

An installed server that nothing attaches to costs nothing. The section warns
about exactly one thing: a server whose cost scales with attached buffers
(ts_ls/tsserver, pyright, jdtls, omnisharp) held open across more
than twenty of them. Never on a count alone -- five buffers on ts_ls is a
working set, and warning about that would train you to skip the section.

Severity follows dependency hardness: something the plugin cannot work without
is an error, something it uses when present is information. conform.nvim is
the one ERROR in the ecosystem section -- it is the formatter's primary
engine, and without it formatting falls back to the LSP. Everything else
there, trouble.nvim included, is informational. Note that informational does
not mean unwired: trouble is driven from the keymap catalogue, which binds
thirteen keys for it under the default preset, and diagnostics.ui routes
]d/[d through it. The lsp.nvim section warns separately about keys bound
for a plugin that is not installed, and names them.

8. ARCHITECTURE *lsp.nvim-architecture*

Three layers, each answering a different question:
    pack/          WHAT gets installed  -- LazySpec export, no logic
    integrations/  HOW third-party plugins are wired -- one adapter each
    core/          the own code on vim.lsp.* -- registry, attach, servers, ...
The core does not reach into the integrations: core/attach.lua never requires
lazydev itself, the adapter does. That keeps the core testable without a plugin
manager and makes swapping a completion engine a one-file change.
scripts/gen_map.lua declares the rule so it can be checked rather than merely
intended.

The tree is complete: pack/, integrations/, core/, servers/, languages/,
formatter/, diagnostics/, lspdoctor/, tools/, usercmds/,
completion/, config/, bindings/, @types/, health.lua and init.lua.

The adapters own every third-party require. They are not called by the core;
they hand capability contributors and attach hooks to lsp/init.lua, which
passes them in as plain functions. That is why core/attach.lua no longer
knows lazydev or NvChad exist, and why |lsp.nvim.setup()| is the only place
that composes the two layers.

9. STATUS AND ROADMAP *lsp.nvim-status*

All five migration phases are done. The core lives here, configures the
servers, owns every LSP keymap, reaches third-party plugins only through the
adapters in integrations/, and installs and configures them from pack/.

The last item that used to stand here -- folding the flat :Lsp* commands into
|:Lsp| routes -- is done too. There are eighteen routes, and the 25 flat
commands that remain are aliases onto the same functions, switchable off with
usrcmds.legacy_aliases. See |lsp.nvim-commands|.

What is left is integrations as a configurable option: the adapters exist and
are wired, but nothing reads a user-supplied integrations table, so there is
deliberately no default for one. See the end of |lsp.nvim-config|.

keys

j / k
next / previous line
gg / G
first / last line
⏎
open the line under the cursor
/
search the plugins
:
command line — Tab completes
:help x
vimdoc of a plugin (:e x = plugin page)
:ls · :log · :stack
plugin list · activity stream · dependency graph
:colo x
colorscheme
:set skin=
modern | tui
?
this help
esc
close