No description
  • Lua 78.4%
  • TypeScript 21.6%
Find a file
2026-06-18 22:23:40 +00:00
lua/hedgedoc fix: move cookie/history to stdpath('data') and normalize trailing slashes in server URLs 2026-06-18 22:21:49 +00:00
plugin feat: add note history, server sync, and interactive picker 2026-06-14 22:47:12 +02:00
sidecar fix: pass cookie over stdin instead of command line in cmd-create-note 2026-06-18 00:23:54 +02:00
.gitignore Implement nvim-hedgedoc plugin: sidecar (Node) + Lua core for real-time HedgeDoc editing 2026-05-29 18:31:34 +02:00
AGENTS.md docs: update paths from stdpath('config') to stdpath('data') for cookies and history 2026-06-18 22:23:40 +00:00
README.md docs: update paths from stdpath('config') to stdpath('data') for cookies and history 2026-06-18 22:23:40 +00:00

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 via nvim_buf_set_text(), and tracks session state.
  • Sidecar process (Node.js): Runs hedgesync — handles Socket.IO connection negotiation, the OT.js state machine (SynchronizedAwaitingConfirmAwaitingWithBuffer), and reconnection with exponential backoff.
  • Protocol: Newline-delimited JSON over the sidecar's stdin/stdout.

Change detection

  1. prev_content is tracked per buffer.
  2. On TextChanged, the new content is compared to prev_content by finding the common prefix and suffix.
  3. The middle section is the change — sent as insert, delete, or replace to the sidecar.
  4. The sidecar constructs a TextOperation and runs it through the OT state machine.
  5. 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

  1. :HedgeDocAuth spawns a helper process that runs hedgesync's loginWithEmail().
  2. The password is sent over a stdin pipe (never on the command line).
  3. The resulting cookie is saved to stdpath('data')/hedgedoc/cookies.json.
  4. :HedgeDocOpen reads 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.

Acknowledgments

  • hedgesync — HedgeDoc Socket.IO + OT client for Node.js
  • HedgeDoc — Collaborative real-time markdown editor