org-lsp

Org in Neovim

The Neovim plugin is the repository itself (plugin/, lua/), for Neovim 0.12 or newer. It sets its keys in Org buffers, so it is meant to be used instead of nvim-orgmode for them, not next to it.

Installing#

Install kristijanhusak/org-lsp with Neovim's own plugin manager, vim.pack (:help vim.pack), in your init.lua:

-- Download the server again whenever vim.pack updates the plugin.
vim.api.nvim_create_autocmd('PackChanged', {
  callback = function(ev)
    if ev.data.spec.name == 'org-lsp' and ev.data.kind == 'update' then
      if not ev.data.active then
        vim.cmd.packadd('org-lsp')
      end
      vim.cmd('OrgLspDownload')
    end
  end,
})

vim.g.org_lsp = { settings = { org_agenda_files = { '~/org' } } }
vim.pack.add({ 'https://github.com/kristijanhusak/org-lsp' })

vim.pack.update() updates it. The autocommand comes before vim.pack.add(), which loads the plugin, and vim.g.org_lsp is set before it too. Another plugin manager works as well; with lazy.nvim, the same is { 'kristijanhusak/org-lsp', build = ':OrgLspDownload', init = function() vim.g.org_lsp = { … } end }.

Opening an Org file the first time downloads the server for your platform from the nightly release (with curl and tar; on Windows, Windows' own tar, not one Git put first on the PATH) and starts it; :OrgLspDownload downloads it again, to update it, and the autocommand above does that on each update of the plugin. release = 'v0.2.0' picks a release instead of the nightly build, and download = false turns the download off. A release that needs a GitHub login, as one of a private repository, is downloaded with the GitHub CLI (gh) when it is installed and logged in.

To build the server instead (with cargo), run :OrgLspBuild, in the autocommand in place of OrgLspDownload to build it on each update. A build in the plugin's directory and org-lsp on PATH come before the download. :Org version says which server runs, with the commit it was built from (org-lsp version 0.1.0-f7e7b68; -dev after it for a server built by hand, not by the release workflow).

:checkhealth org-lsp reports the server the plugin found, where it came from, and the agenda files.

Setting it up#

Set vim.g.org_lsp before the plugin loads (before vim.pack.add(), as above), or call setup() at any time. The server's options go in settings, the plugin's own beside it. The server reads its options when it starts: once it runs, settings changed by setup() take effect after :lsp restart.

require('org-lsp').setup({
  -- The server's options: Org's variables, named with `_`.
  settings = {
    org_agenda_files = { '~/org', '~/notes/**/*.org' },
    org_todo_keywords = { 'TODO(t)', 'NEXT(n)', 'WAIT(w@/!)', '|', 'DONE(d!)', 'CANCELLED(c@)' },
    org_log_done = 'time',
    org_capture_templates = {
      { key = 't', description = 'Task', target = { 'file+headline', '~/org/inbox.org', 'Tasks' }, template = '* TODO %?\n  %U' },
    },
    org_agenda_custom_commands = {
      {
        key = 'd',
        description = 'Today, then what is waiting',
        blocks = {
          { type = 'agenda', settings = { org_agenda_span = 'day' } },
          { type = 'todo', match = 'WAIT', header = 'Waiting for' },
        },
      },
    },
  },
  -- The plugin's own.
  win_split_mode = 'vsplit',
  calendar_win_mode = 'cursor',
})

The server also reads the shared configuration file, ~/.config/org-lsp/config.toml, which every editor shares; settings goes on top of it. configuration.org has everything else: the plugin's options (windows, the calendar, the statusline, highlighting, images) and every option of the server with its default.

The plugin's syntax for Org (syntax/org.vim) colors what a pattern can tell, as the server does and in the same colors: headline stars and titles by level, timestamps, planning and clock lines, links, emphasis, code, lists, tables, drawers, blocks, keywords and comments. TODO keywords and the headlines they start, keywords and tags with faces of their own (org_todo_keyword_faces, org_tag_faces) and the options of how headlines look it colors as the server tells it for each file; before it has, from the file's #+TODO: lines. It shows a file before the server's highlighting arrives, and what you type before the server has answered. What only the server can tell (priorities, entities, LaTeX, what other options change) waits for it. Keep :syntax on (Neovim's default); replace_syntax = true turns the syntax off once the server's highlighting has come.

Keys#

The plugin binds nvim-orgmode's default keys: cit changes the TODO state, ciR and cir the priority, <C-a> and <C-x> the part of the timestamp under the cursor, <Leader>ois schedules, <Leader>or refiles, <Leader>oc captures, <Tab> and <S-Tab> cycle folds, and <Leader>oa opens the agenda from anywhere. g? shows the keys of the buffer at hand.

The agenda has the keys it has in every editor: Emacs's (f and b, t, + and -, <CR>, <C-c><C-s>), with J for a date and the views under v (vd, vw), so that no motion is taken; q closes it. Counts work, and the editing keys repeat with .. Dates are asked for with Emacs's calendar, or typed as at Org's prompt (fri 10:00, +2d, 2/15, ++1w). Keys that a mapping or a macro gives after a key that asks wait for the question and answer it, as in Emacs; keys pasted a few milliseconds after <Leader>ois, before the calendar is there, may reach the Org file instead.

keys/neovim.org lists every key and how to change it.

What works#

FeatureParityNeovimWhat does not work, and why
Highlighting100%✅
Folding and visibility91%❓Cycling the whole file goes between overview and everything: it has no contents state, which needs folds of a headline's own text that Neovim's fold levels do not make. <Tab> changes the level of a new entry in Normal mode; in Insert mode it is a tab.
Headings and structure96%✅
Lists and checkboxes93%✅
TODO keywords100%✅
Priorities100%✅
Tags88%✅
Properties and column view100%✅
Dates and times100%✅
Agenda97%✅
Clocking100%✅
Capture100%✅
org-protocol86%✅
Notes (org-roam)94%✅
A journal (org-journal)93%✅
Refile and archive100%✅
Links96%✅
Footnotes100%✅
Tables100%✅
Table formulas100%✅
Code blocks (Babel)100%✅
Export and publishing100%✅
Citations82%✅
Diagnostics (org-lint)100%✅
Indentation and formatting100%❓The lines a long line wraps into line up with its own indentation, a headline's title or an item's text ('breakindent'), without the prefix of virtual indentation before them: Neovim repeats no inline virtual text on them, and 'breakindentopt' shifts all the lines of a window alike. A line with a long link wraps as if the link's hidden target took room: Neovim counts concealed text when it wraps; the org-roam buffer, whose links are long, does not wrap.
Notes and logging100%✅
Completion and editing helpers100%✅Completion needs an LSP completion plugin (or vim.lsp.completion).
Timers100%✅
Attachments100%✅
Encryption100%✅
Inline images and LaTeX previews100%❓From Neovim 0.13 (vim.ui.img), in a terminal with the Kitty graphics protocol: an image shows below its line, not over the link; images other than PNG need ImageMagick.
All of Org97%

✅ all that org-lsp does works there, ❓ some does not (the last column says what and why), ❌ none does, — nothing to support yet. Parity is the share of what Emacs Org does in that area that org-lsp does; parity.org lists both.

TESTING.org lists what to try by hand.

Developing#

scripts/test-nvim.sh runs the plugin's tests headless against a fresh debug build of the server (NVIM names the Neovim to use).