- Lua 78.4%
- TypeScript 21.6%
| lua/hedgedoc | ||
| plugin | ||
| sidecar | ||
| .gitignore | ||
| AGENTS.md | ||
| README.md | ||
nvim-hedgedoc
Edit HedgeDoc (v1) notes in NeoVim with real-time collaboration via Operational Transformation (OT).
Status
Warning
This project was written entirely by an AI agent and has not been thoroughly reviewed for correctness, security, or edge cases. Use at your own risk. Bug reports and pull requests are welcome.
HedgeDoc 1.x only. HedgeDoc 2.x (Yjs/CRDT over raw WebSocket) is not compatible. HedgeDoc versions before 1.10.4 use Socket.IO v2 which is also unsupported.
Requirements
- NeoVim >= 0.9
- Node.js >= 18
Installation
lazy.nvim
{
'https://git.ultrakalteseis.de/lsteffen/nvim-hedgedoc',
build = 'cd sidecar && npm install && npm run build',
opts = {
default_server = 'https://md.example.com',
},
}
opts is automatically passed to require('hedgedoc').setup(). If you define a custom config function, call setup(opts) manually:
{
'https://git.ultrakalteseis.de/lsteffen/nvim-hedgedoc',
build = 'cd sidecar && npm install && npm run build',
opts = { default_server = 'https://md.example.com' },
config = function(_, opts)
require('hedgedoc').setup(opts)
-- additional setup...
end,
}
packer.nvim
use {
'https://git.ultrakalteseis.de/lsteffen/nvim-hedgedoc',
run = 'cd sidecar && npm install && npm run build',
config = function()
require('hedgedoc').setup({ default_server = 'https://md.example.com' })
end,
}
Manual (vim-plug)
Plug 'https://git.ultrakalteseis.de/lsteffen/nvim-hedgedoc'
Then run:
cd sidecar && npm install && npm run build
A setup() call is optional — defaults are used when not called. To customize:
require('hedgedoc').setup({
default_server = 'https://md.example.com',
})
Usage
Authentication
Authenticate once per server — the session cookie is saved to disk and reused automatically.
:HedgeDocAuth https://md.example.com
You'll be prompted for email and password. The cookie is stored at ~/.local/share/nvim/hedgedoc/cookies.json with 0600 permissions.
Supported auth methods (via hedgesync):
- Email/password (
loginWithEmail) - LDAP (
loginWithLDAP)
Open a note
:HedgeDocOpen https://md.example.com/abc123
Opens the note in a new scratch buffer. Remote changes appear in real time; local edits are synced immediately.
Create a new note
:HedgeDocNew https://md.example.com
Creates a note via the HedgeDoc REST API and opens it.
If default_server is configured, you can omit the URL:
:HedgeDocNew
:HedgeDocSync also uses default_server when no server is specified.
Close
:HedgeDocClose
Closes the current HedgeDoc buffer and disconnects.
Logout
:HedgeDocLogout https://md.example.com
Removes the saved cookie for the server.
Status
:HedgeDocStatus
Shows connection status, server, and note ID for the current buffer.
Health check
:checkhealth hedgedoc
Verifies that the sidecar script exists, Node.js is available, and the cookie directory is set up.
Save (:w)
HedgeDoc buffers use buftype=acwrite. :w is handled by BufWriteCmd, which currently shows a message that the note is auto-synced (no disk write occurs — HedgeDoc is the single source of truth).
How it works
┌────────────┐ JSON lines ┌──────────┐ Socket.IO ┌──────────┐
│ NeoVim │ ◄──────────────► │ Sidecar │ ◄───────────► │ HedgeDoc │
│ (Lua) │ stdin/stdout │ (Node) │ + OT.js │ Server │
└────────────┘ └──────────┘ └──────────┘
Architecture
- Lua layer: Manages buffers, detects local changes via
TextChanged/TextChangedI, computes character-level diffs, applies remote changes vianvim_buf_set_text(), and tracks session state. - Sidecar process (Node.js): Runs hedgesync — handles Socket.IO connection negotiation, the OT.js state machine (
Synchronized→AwaitingConfirm→AwaitingWithBuffer), and reconnection with exponential backoff. - Protocol: Newline-delimited JSON over the sidecar's stdin/stdout.
Change detection
prev_contentis tracked per buffer.- On
TextChanged, the new content is compared toprev_contentby finding the common prefix and suffix. - The middle section is the change — sent as
insert,delete, orreplaceto the sidecar. - The sidecar constructs a
TextOperationand runs it through the OT state machine. - Remote changes arrive as OT ops; the Lua layer applies them minimally with
nvim_buf_set_text()using a guard flag to suppress local change detection.
Authentication flow
:HedgeDocAuthspawns a helper process that runshedgesync'sloginWithEmail().- The password is sent over a stdin pipe (never on the command line).
- The resulting cookie is saved to
stdpath('data')/hedgedoc/cookies.json. :HedgeDocOpenreads the cookie and passes it to the sidecar over stdin.
Configuration
require('hedgedoc').setup({
default_server = 'https://md.example.com', -- optional, for :HedgeDocNew and :HedgeDocSync without args
history_max_entries = 200, -- max entries in local history file
})
sidecar_path is auto-detected relative to the plugin directory. Override if needed:
require('hedgedoc').setup({
sidecar_path = '/custom/path/sidecar/dist/index.js',
})
Development
cd sidecar
npm install
npm run build # compile TypeScript
npm run dev # watch mode
Run :checkhealth hedgedoc in NeoVim after changes to verify the setup.
Project structure
nvim-hedgedoc/
├── plugin/hedgedoc.lua # Registers Ex commands
├── lua/hedgedoc/
│ ├── init.lua # Entry point, VimLeavePre cleanup
│ ├── config.lua # Configuration management
│ ├── sidecar.lua # Process spawn, JSON-line protocol, change detection
│ ├── buffer.lua # Scratch buffer creation, autocmds
│ ├── commands.lua # :HedgeDoc* command implementations
│ ├── auth.lua # Cookie file read/write
│ ├── history.lua # Local note history persistence
│ └── health.lua # :checkhealth implementation
└── sidecar/
├── package.json # Dependencies (hedgesync ^1.4.1)
├── tsconfig.json
└── src/
├── index.ts # stdio JSON-line loop, process lifecycle
├── bridge.ts # Maps messages → HedgeDocClient calls, history sync
├── cmd-login.ts # Login helper (reads password from stdin)
└── cmd-create-note.ts # Create-note helper
Commands
| Command | Description |
|---|---|
:HedgeDocOpen <url> |
Open a HedgeDoc note |
:HedgeDocFind |
Interactive note picker from local history (uses vim.ui.select) |
:HedgeDocSync [server] |
Sync notes from HedgeDoc server's /history and update local cache with titles |
:HedgeDocPin [url] |
Pin a note in history (top of picker, from URL or current buffer) |
:HedgeDocUnpin [url] |
Unpin a note |
:HedgeDocNew [server] |
Create and open a new note |
:HedgeDocClose |
Close current HedgeDoc buffer |
:HedgeDocAuth <server> |
Login to a HedgeDoc server |
:HedgeDocLogout <server> |
Remove saved cookie |
:HedgeDocStatus |
Show connection status |
:HedgeDocClearHistory |
Clear all notes from local history |
Note picker (:HedgeDocFind)
Shows notes from ~/.local/share/nvim/hedgedoc/history.json via vim.ui.select. Supports any fuzzy finder provider (fzf-lua, telescope-fzf-native, mini.pick, etc.). Entries show pinned notes first, then by last-accessed date. If a title is available it is displayed; otherwise the note ID is shown.
Server sync (:HedgeDocSync)
Queries the HedgeDoc server's /history endpoint for recent notes and updates the local history cache, including fetching titles from each note's info. If default_server is configured, it is used automatically; otherwise you are prompted to choose from servers in the local history.
Pin/Unpin (:HedgeDocPin / :HedgeDocUnpin)
Pinned notes appear at the top of the note picker. Accepts a full note URL as an optional argument, otherwise operates on the current buffer's note.