org.nvim

Language server

Stability: experimental (org-extensions-stability)

lsp runs a language server for org buffers inside Neovim: no program to install, the server is Lua in the same process (vim.lsp.start() with a cmd function). Everything that speaks LSP then works in org files: the lsp-defaults keys, vim.diagnostic, and pickers, outline and breadcrumb plugins.

require("org").setup({ extensions = { lsp = {} } })

The server attaches to every org buffer that is a file (a FileType autocmd). One client serves them all; its root is org_directory. It reads the documents from their buffers, unsaved changes included, and other files from disk. Needs Neovim 0.11 or later (tested on 0.11, 0.12 and nightly). File names are compared with symlinks resolved, so a file opened through a symlink gets the edits of a rename in its own buffer.

What it does

  documentSymbol   the outline: each headline with its subtree as range,
                   TODO keyword, priority and tags as detail, and the
                   kind from symbol_kinds (TODO and done headlines get
                   their own); named src blocks and tables are children
                   of their entry (document_symbols). Try
                   :lua vim.lsp.buf.document_symbol().
  workspace/symbol headlines of the workspace files (below) whose title,
                   TODO keyword and tags contain every word of the query,
                   ignoring case (vim.lsp.buf.workspace_symbol()).
  diagnostics      org-lint reports, updated diagnostics.debounce ms
                   (500) after the last change; high-trust reports are
                   errors and low-trust ones warnings (severity), the
                   checker name is the diagnostic's code. See
                   "Performance" below for large buffers.
  hover            on a timestamp: the full date, how far it is ("in 3
                   days, Friday", "2 weeks ago, Monday"), its time range
                   or date range, what its repeater does, the next
                   occurrence and its warning period; on a link: a
                   preview of the target (the headline and its first
                   lines, the file's first lines); on a CLOCK line: the
                   duration; on a footnote reference: its definition; on
                   a headline: TODO state, tags, planning, and for a
                   headline with an ID or CUSTOM_ID its backlink count.
                   A link to a file that isn't Org shows its lines with
                   their filetype.
  definition       follows id:, file: (with :: search options),
                   [[#custom-id]], [[*Heading]] and fuzzy links (a
                   <<target>>, #+NAME: or headline), radio links and
                   footnotes, without moving the cursor or prompting.
                   id: links reach file-level IDs (org-roam file nodes)
                   too, and IDs missing from the ID files are looked for
                   in the workspace files. With the code extension on,
                   code: links go to their file and symbol (found by a
                   text search: no buffer is loaded, no LSP asked).
  references       every link to the headline, CUSTOM_ID, ID, target,
                   #+NAME:, radio target or footnote under the cursor
                   (or to the target of the link under the cursor),
                   across the workspace files.
  rename           see below.
  codeAction       quick fixes for some org-lint reports, line
                   conversions and entry commands (below).
  foldingRange     subtrees, blocks and drawers (for
                   vim.lsp.foldexpr()).
  documentLink     web links and links whose target resolves.

Completion is not provided: org.nvim's own completion (org-completion) already covers org buffers.

Rename

vim.lsp.buf.rename() renames what is under the cursor and rewrites every link that spells its name, in every workspace file:

  headline title   [[*Title]], [[Title]] and [[file:x.org::*Title]];
                   id: links keep their target, but a description that
                   spells the title (org-roam's links) follows it
  :CUSTOM_ID:    [[#id]] and [[file:x.org::#id]]
  :ID:           id: links, bracketed or plain; the ID database
                   (org-id) forgets the old ID and records the new one.
                   A file-level :ID: (an org-roam file node) too
  <<target>>     [[target]] and [[file:x.org::target]]
  <<<radio>>>    the radio target and its occurrences in the text
  #+NAME:        fuzzy links to the name
  footnote label   the definition and its references

On a link, the rename applies to its target: [[#id]] renames the CUSTOM_ID, [[*Title]] the title, id: the ID. A link's description is rewritten too when it equals the old name (rename.update_descriptions). Links inside src, example and comment blocks, drawers and verbatim text are not links and are left alone. Only links that really resolve to the renamed thing change: [[Title]] is left alone when a <<Title>> target wins. A name that would make links point elsewhere (another target, #+NAME: or CUSTOM_ID with it, or a headline with the same title when links spell the title) is refused with a message, as are blanks in IDs, CUSTOM_IDs and footnote labels. Links whose path or description spans lines are found and rewritten like the others. Files the rename edits that were not loaded are written once Neovim has loaded them to apply the edit (rename.write_unloaded, like VS Code's refactoring auto-save); buffers that were already open are left modified for you to check and :write. Emacs has no counterpart.

Code actions

vim.lsp.buf.code_action() offers, depending on the line:

  • quick fixes: remove spurious colons from tags, make an inactive SCHEDULED or DEADLINE timestamp active, replace an obsolete affiliated keyword (#+SRCNAME: -> #+NAME:), turn #+BEGIN_HTML (and the other backends) into #+BEGIN_EXPORT html, unindent a diary sexp, add a keyword's missing colon, remove a special property (TODO, SCHEDULED, ...) from a properties drawer, turn obsolete INCLUDE markup into export html, replace percent escapes in a link with backslash escapes, add the [@N] counter org-lint suggests for a mismatched item number, and point a broken [[#id]] or fuzzy link at the closest existing CUSTOM_ID, target or headline. The other org-lint reports need a decision and have no quick fix;
  • "Convert line to heading" (toggle_heading) and "Convert line to checkbox item" / "Convert item to checkbox";
  • in an entry: cycle or change the TODO state, set the priority, schedule, set a deadline, refile and archive. These run the usual actions at that position (they may prompt) through the server's org.action command. code_actions.entry takes a list of { action, title } pairs instead.

Workspace

References, rename, backlink counts and workspace symbols look at the agenda files, the .org files under org_directory (with subdirectories), workspace.extra and every loaded org buffer. Set workspace.files (a list of files and globs, or a function returning one) to replace the first three, and workspace.max_files (2000) to cap it. The list is kept for a few seconds and made again when an org file is written. Files are parsed once per change (by mtime, or changedtick for buffers); after the server starts they are parsed in the background, a few milliseconds at a time (workspace.preload), so the first references or backlink count doesn't wait for all of them.

Performance

Measured on a 5,000-heading (40,000-line) file plus 200 workspace files of 50 headings: document symbols and folding take about 30 ms, references, rename and backlink counts 15-70 ms once the workspace is parsed (about 0.6 s the first time, spread by workspace.preload). Linting parses the buffer (about 20 ms per 1,000 lines) and then runs the checkers in slices of about 12 ms, starting over if you type meanwhile. Buffers longer than diagnostics.max_lines (3000) are linted when opened and written only; set it to 0 to lint them while typing too.

Actions

  lsp_start    attach the server to the current buffer (with
               autostart = false this is how it starts)
  lsp_stop     stop the server
  lsp_restart  stop it and attach every org buffer again

There are no default keys: the LSP keys of lsp-defaults (K, grn, gra, grr, ...) and vim.diagnostic apply. gO stays org's own outline (the imenu action); document symbols are :lua vim.lsp.buf.document_symbol(). Use on_attach to add your own.

Options

  autostart               attach to org buffers (true)
  filter                  function(bufnr) choosing buffers (file
                            buffers with a name)
  on_attach               function(client, bufnr)
  features                `{ document_symbols, workspace_symbols,
                            diagnostics, hover, definition, references,
                            rename, code_actions, folding, document_links }`
                            (all true)
  diagnostics             `{ debounce = 500, max_lines = 3000,
                            checkers = nil (org-lint's default set),
                            exclude = {}, severity = { high = "Error",
                            low = "Warning", checkers = {} } }
  hover                   { preview_lines = 8,
                            backlinks_all_headings = false }
  document_symbols        { src_blocks = true, tables = true }
  symbol_kinds            { heading = "Namespace", todo = "Event",
                            done = "Constant", src_block = "Function",
                            table = "Struct" }
  workspace               { agenda_files = true, org_directory = true,
                            recursive = true, extra = {}, files = nil,
                            max_files = 2000, preload = true }
  workspace_symbol_limit  1000
  rename                  { update_descriptions = true,
                            write_unloaded = true }
  code_actions            { entry = true, conversions = true }`

Notes

  • The server uses UTF-8 positions (positionEncoding), so columns are byte offsets.
  • Links spanning more than 5 lines (or a blank line) are not seen.
  • Text the transclusion extension inserted into a buffer (org-extensions-transclusion) belongs to its source file: it is not linted, not listed in the symbols, its links are no references, and a rename inside it is refused (rename it in the source). A link of the buffer's own text can still resolve to a headline in it.
  • The ID database is updated when the rename is computed; if the edit is then not applied, the next lookup of either ID repairs it.
  • Files a rename loaded are written only if Neovim loads them within 10 seconds of the rename (it does so while applying the edit).
  • :checkhealth org shows the running client, the number of workspace files and of org-lint checkers.