Skip to content

feat: add Markdown Annotator (:MarkdownAnnotate)#743

Open
coderdevin wants to merge 52 commits intoiamcco:masterfrom
coderdevin:master
Open

feat: add Markdown Annotator (:MarkdownAnnotate)#743
coderdevin wants to merge 52 commits intoiamcco:masterfrom
coderdevin:master

Conversation

@coderdevin
Copy link
Copy Markdown

Summary

Adds a Markdown Annotator feature — a browser-based review interface for markdown documents with three annotation types:

  • Modify (M) — select text + note what to change
  • Add (A) — select anchor text + note what to insert after it
  • Delete (D) — mark text for removal with optional reason

The annotator opens via :MarkdownAnnotate (single step — starts the server if not running) and embeds the live preview in an iframe (same-origin). Annotations persist in localStorage and sync with live Vim edits.

Key features

  • Warm editorial UI (Source Serif 4 + DM Sans, cream/terracotta palette)
  • Floating popover for note editing (not browser prompt())
  • Keyboard shortcuts: M, A, D when toolbar is visible
  • Copy All exports AI-friendly structured format with [MODIFY]/[ADD]/[DELETE] tags — designed for the workflow: annotate → copy → paste to AI
  • Custom confirmation dialogs (no native confirm())
  • Cross-element selections handled safely (no layout corruption)
  • Click to edit existing annotation notes

Files changed

  • app/_static/annotator.htmlnew self-contained annotator page (served by existing /_static/* route)
  • app/server.jsopenAnnotator() + buildUrl()/launchInBrowser() helpers
  • app/lib/attach/index.jsopen_annotator RPC handler
  • autoload/mkdp/rpc.vimmkdp#rpc#open_annotator()
  • autoload/mkdp/util.vimmkdp#util#open_annotator_page()
  • plugin/mkdp.vim:MarkdownAnnotate command + <Plug>MarkdownAnnotate
  • README.md — annotator documentation

Copy All output format (for AI)

# Document Review: filename

## Annotations

### 1. [MODIFY]
> original text
**Change to:** reviewer's note

### 2. [ADD]
> anchor text
**Insert after:** content to add

### 3. [DELETE]
> ~~text to remove~~
**Reason:** why

---
Please review these annotations and suggest improvements to the document.

Test plan

  • Open a markdown file, run :MarkdownAnnotate — server starts + annotator opens
  • Select text → toolbar shows Modify / Add / Delete with keyboard hints
  • Press M → popover opens, add note, save → annotation in sidebar
  • Press A → popover with "What to add?" placeholder
  • Press D → immediate strikethrough highlight + toast
  • Click annotation note text → popover opens for editing
  • Copy All → structured AI-friendly format in clipboard
  • Clear All → custom confirm dialog, not native confirm()
  • Select across list items → no layout corruption
  • Reload → annotations persist from localStorage
  • :MarkdownPreview still works independently

🤖 Generated with Claude Code

Devin Yang and others added 30 commits March 21, 2026 16:08
Add a single-step :MarkdownAnnotate command that starts the mkdp server
(if not running) and opens an annotator page in the browser. The annotator
embeds the live preview in an iframe (same-origin) and allows highlighting
text with optional notes. Annotations persist in localStorage and sync
with live edits via the existing Socket.io infrastructure.

- Add annotator.html to app/_static/ (served by existing /_static/* route)
- Add open_annotator RPC notification through the full chain
  (plugin → rpc → attach → server)
- Extract buildUrl/launchInBrowser helpers in server.js to share
  browser-launching logic between openBrowser and openAnnotator
- Add <Plug>MarkdownAnnotate mapping and g:mkdp_open_annotator_on_start flag
- Update README with annotator documentation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace dark navy theme with warm cream/beige palette and terracotta
accents. Replace browser prompt() with inline note editor panel.
Add toast notifications, annotation count badge, entrance animations,
typographic curly quotes, and progressive disclosure on hover.

Fonts: Source Serif 4 for annotation text, DM Sans for UI elements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move note editor from sidebar bottom to a floating popover that
  appears near the selected text or annotation item
- Click on any annotation's note text to re-edit it in the popover
- Annotations without notes show a "Click to add a note" hint on hover
- Overlay backdrop to dismiss popover by clicking outside
- Enter to save, Escape to cancel, Shift+Enter for newline

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…yboard shortcuts

- Toolbar now shows Annotate (H) + Delete (D) with keyboard hints
- Delete marks text with red strikethrough in preview + sidebar
- Popover wider (420px), taller textarea, larger preview area
- Copy All outputs structured AI-friendly format with numbered
  annotations, [HIGHLIGHT]/[DELETE] tags, blockquotes, and
  "Please review" prompt footer
- Copy All button shows count: "Copy All (3)"
- Type badges (Highlight/Delete) on each sidebar annotation
- Keyboard shortcuts H/D work when toolbar visible (in both
  parent and iframe contexts)
- Empty state updated with keyboard shortcut hints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix bug: keyboard shortcuts (H/D) used stale DOM event for position;
  now uses lastSelectionPos saved during showToolbar
- Extract TYPE_HIGHLIGHT/TYPE_DELETE constants, replacing 12+ raw strings
- Deduplicate keyboard handler into shared handleToolbarKeydown function
  used by both parent and iframe document listeners
- Fix TreeWalker using parent document instead of iframe document
- Remove duplicate const doc declaration in findAndHighlight
- Add change guard to updateCount to skip no-op DOM writes
- Break long HIGHLIGHT_CSS string into readable array join

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reload button was calling contentWindow.location.reload() which could
fail, falling back to urlInput value which may be stale or incorrect.
Now always resets iframe src to the known /page/{bufnr} path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three annotation types: Modify (M), Add (A), Delete (D)
- Modify: select text + note what to change → [MODIFY] in copy output
- Add: select anchor text + note what to insert after → [ADD]
- Delete: strikethrough + optional reason → [DELETE]

Toolbar shows all three with keyboard shortcuts.
Copy All uses AI-friendly labels: "Change to:", "Insert after:", "Reason:"
Green highlight/badge for Add type, terracotta for Modify, red for Delete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace ugly native confirm() popup with a styled confirmation dialog
matching the warm editorial aesthetic. Includes backdrop overlay,
slide-in animation, and red "Clear All" danger button.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a selection spans multiple DOM elements (e.g. list items),
surroundContents and the deleteContents fallback would break the
HTML structure causing empty bullet points and layout corruption.

Now wraps individual text nodes within the range instead of trying
to wrap the entire range in one <mark>. Each text node gets its own
<mark> with the same data-ann-id, and unwrapAllMarks removes all of
them on annotation deletion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add fork notice linking to upstream iamcco/markdown-preview.nvim
- Update all install examples to use coderdevin/markdown-preview.nvim
- Rewrite annotator section with Modify/Add/Delete types, keyboard
  shortcuts, and AI-friendly Copy All output format example

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Load and Reload did the same thing since the URL is auto-filled
from the plugin. Simplify to just Reload.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Click on any text in the annotator preview and edit it directly.
Changes auto-save to the Vim buffer after 500ms pause or on blur.

- Add update_lines Socket.io handler in server.js that receives
  line-level text replacements and writes them back to Vim buffer
  via buffer.setLines()
- Enable contentEditable on iframe's .markdown-body
- Snapshot text content per data-source-line element on load
- Detect changes by diffing current text against snapshot
- Emit changes via iframe's existing Socket.io connection
- Green flash animation on saved elements
- Edit counter in topbar ("3 edits")
- Suppress MutationObserver refresh during active editing
- Both inline editing and annotations work simultaneously

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix buffer.setLines() to pass [newLine] array instead of bare string
  (nvim_buf_set_lines expects a list)
- Use WeakMap keyed by DOM element for text snapshots, fixing key
  collisions when multiple elements share a data-source-line
- Remove trim() from oldText/newText to preserve meaningful whitespace
  (indented code blocks, nested list items)
- Remove dead saveFlash keyframes from parent CSS (only used in iframe)
- Simplify isEditing flag — clear immediately after save, no setTimeout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…arch

Root cause: collectChanges sent full element textContent as oldText
(e.g. "Hello world") but the markdown source has formatting markers
(e.g. "Hello **world**"), so server's includes() check failed silently.

Fix: compute minimal character-level diff between old and new text,
sending only the changed substring (e.g. "wrold" → "world"). This
matches the markdown source regardless of surrounding formatting.

Also: server now searches up to 10 lines from data-source-line start,
handling elements that span multiple markdown lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r API

The buffer.getLines/setLines approach was unreliable — textContent
from rendered HTML doesn't match markdown source with formatting
markers. Now reads/writes the .md file directly via fs, which is
simpler and always works. Calls checktime to tell Vim to reload.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace readFileSync/writeFileSync with fs.promises.readFile/writeFile
to avoid blocking the Node event loop. Remove TOCTOU existsSync check.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace vague "Please review" prompt with explicit instructions:
- Tell AI exactly what each annotation type means
- Ask for the complete revised document, not suggestions
- "Do not summarize or skip sections" prevents lazy AI output

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Source pane is now hidden by default. A "Source" button in the topbar
toggles it on/off. When hidden, preview takes full width. Button
highlights with accent color when source pane is active.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix pre-existing bug: disconnect handler used .map() which replaced
  socket objects with booleans, breaking emitRefreshContent broadcasts
- Remove second loadSourceFromServer at 1200ms that could wipe user
  edits typed between 500ms and 1200ms; guard first load with
  sourceContentSnapshot check
- Replace inline style toggle with .btn-ghost.active CSS class

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rename "Source" button to "Edit Source" for clarity
- Source only loads when user clicks "Edit Source", not on page load
- Add retry logic (up to 5 retries, 1s apart) if socket isn't ready
- Remove eager load from iframe load handler

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Change from "insert the new content after the quoted anchor text"
to "write the new content according to the Insert after instruction"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The label "Insert after" implied a specific position. Changed to "Add"
which lets the AI interpret the instruction freely. Also updated
popover placeholder from "What to add after this text?" to
"What to add here?"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
write_source and update_lines were calling emitRefreshContent before
reply(), which could trigger an iframe reload that destroys the socket
reference — causing the client's socketRequest callback to never fire
(5s timeout → "save failed"). Now reply first, then refresh.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Devin Yang and others added 22 commits March 21, 2026 21:05
- Add aria-label to all unlabeled inputs/textareas and aria-hidden to decorative SVGs
- Add focus-visible styles, focus traps for dialogs, aria-live regions
- Replace hardcoded colors (#fff, #fcfbf9, #2b2f36, #a34845) with CSS custom properties
- Add --bg-surface, --bg-surface-warm, --danger-hover, --font-mono tokens
- Add responsive breakpoints (768px/480px) and touch target sizing (pointer:coarse)
- Add prefers-reduced-motion media query
- Fix layout thrashing: single reflow per batch instead of per-element
- Upgrade easing to cubic-bezier(0.25, 0, 0, 1) for smoother deceleration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…l Notes

- Add copy icon button on each annotation item (appears on hover)
- Extract shared formatAnnotation() and copyToClipboard() helpers
- Rename "Copy All" to "Copy All Notes" for clarity

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add document-level annotation type for whole-document directives
(e.g. "translate to Chinese", "fix grammar") without requiring text
selection. Includes always-visible input with preset chips, G keyboard
shortcut, purple/indigo visual identity, and structured Copy All output
with Document Instructions section before selection annotations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The small inline input box for document instructions was cramped and hard
to use. Replace it with a clickable label that opens a centered modal
dialog with a textarea, preset chips, and proper cancel/save actions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Allow switching between markdown files in the same directory without
re-running :MarkdownAnnotate. Adds a collapsible tree panel with
keyboard shortcut (F) that lists .md files recursively.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
badd only creates the buffer without reading the file. Added bufload
to ensure content is available when the preview iframe connects.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Convert --- frontmatter to fenced yaml code block in server before
sending to preview, so it renders with syntax highlighting instead
of being hidden or displayed as raw text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…open

badd with command escaping and bufnr path matching was fragile.
bufadd() returns the buffer number directly and handles paths reliably.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
buildRefreshData now returns null when content is empty, so the
retry loop keeps trying until the buffer is fully loaded after
bufadd/bufload.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Files in subdirectories failed to preview because bufadd/bufload
didn't reliably make content available. New preview_file event reads
the file directly from disk and emits refresh_content to the existing
socket connection, avoiding iframe navigation entirely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extract convertFrontmatter() to deduplicate frontmatter logic
- Remove dead open_md_file handler and navigateToPreviewBufnr
- Move state mutations after async success in switchToFile
- Parallelize getVar calls with Promise.all in preview_file
- Add path validation (absolute + .md extension) to preview_file

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…d of literal replacements

MODIFY, ADD, and DELETE annotations now treat user notes as direction/suggestions
for the LLM rather than exact replacement text. DELETE also now opens a popover
for user input instead of committing immediately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat(annotator): annotation notes as rewrite guidance
doDeleteAction was bypassing the popover save flow by immediately
clearing currentSelection before commitFromPopover could run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant