[[links]] are in the markdown files and the index understands them (index/extract/wikilinks.rs parses [[target#heading|alias]], resolves by path or by stem, and the Links panel shows backlinks), but in the editors they do not behave like in Obsidian.

Today

  • Rich editor (Milkdown): [[Target]] is plain text — remark escapes the brackets (un-escaped on the way back, unescapeWiki), nothing is rendered as a link, and the only interaction is Ctrl/Cmd+click on the text, which finds the brackets around the caret (wikiLinkAt) and opens the target through the index (open_wiki_link in editors/markdown/src/rich.rs).
  • Source editor (CodeMirror): plain text, no link, no click, no completion.
  • No [[ autocompletion, no alias/heading rendering, no distinction between resolved and unresolved targets, no “create on click”, and renaming a note does not update the links pointing at it.

Wanted (Obsidian behaviour)

  1. Rendered as links in the rich editor: [[Target]] shows as Target (or the alias after |, with #heading kept), styled as a link; plain click follows (Ctrl+click opens in a new tab / split); unresolved targets are styled differently (dashed/muted) and a click offers to create Target.md next to the current note (or in the folder set in Settings → Links → new note location).
  2. Round trip is exact: the markdown on disk keeps [[Target#heading|alias]] byte-for-byte — the link is a ProseMirror inline node (a Milkdown plugin: [[…]] ↔ node) so the escaping hack goes away.
  3. [[ autocompletion in both editors: typing [[ opens a dropdown of pages from the index (fuzzy on title/stem/path, most recently opened first), # after a target lists that note’s headings, | starts an alias; Enter inserts and closes the brackets. The source editor uses the existing completion channel (Milestone 7); the rich editor its own popup. Same ranking as Quick Open.
  4. Source editor: links get a decoration (underline, resolved/unresolved colour) and Ctrl+click follows; hover shows the target path.
  5. Hover preview (both editors): a small popover with the first lines of the target — the index has the text.
  6. Rename updates links: renaming or moving a note (Explorer inline edit / Op::Rename) rewrites [[old]] → [[new]] in every file that links to it (the index knows the backlinks; the same replace path as Replace in Workspace, shown as a preview with a count first). Aliases are kept; path-style links are rewritten to the new relative path.
  7. Embeds ![[Note]] / ![[image.png]]: render the target inline in the rich editor (read-only block); pictures need 008 first.
  8. Links panel: unresolved links listed with a Create button; clicking a backlink opens the source at the line.

Notes

  • Resolution rules stay the index’s (resolve: path-like → relative then root; otherwise by stem; Obsidian’s “shortest path when unique” is what stem matching gives). Ambiguous stems (two Notes.md) should offer both in the completion and mark the link as ambiguous.
  • All of this is Rust-owned data: the JS plugin only renders a node and asks Rust (completion, resolution, preview) — rule of packages/js.
  • E2E: links.mjs extended — click follows, unresolved click creates, [[ completion inserts, rename rewrites a backlink.

Done 2026-09-20 (items 1–4, 6, 8; 5 and 7 deferred). Rust owns it: moonkale_ext_api::wiki — spans() parser, Workspace::{resolve_wiki, wiki_spans, wiki_candidates, follow_wiki, wiki_backlinks, rewrite_wiki_links} over the index’s page list (path-like → relative then root; else by stem, same directory first, then shortest path; ambiguous stems complete as paths). Rich editor: a ProseMirror plugin (packages/js/milkdown/src/wiki.ts) decorates the literal [[…]] text — brackets (and the target when an alias is shown) hidden unless the caret is inside, unresolved dashed — so the markdown round trip is byte-exact; plain click follows; [[ opens a popup fed by Rust (wikiQuery → wikiCandidates), closed on blur, never inside an already closed link. Source editor: setWikiLinks marks (UTF-16 offsets, mapped through edits), Ctrl/Cmd+click follows, [[ is a second completion source. Follow creates Target.md next to the note (# Target heading) when unresolved; the Links panel’s phantom rows get Create. Rename (Workspace::rename_node) asks the index for backlinks first, then rewrites [[old]], [[old|, [[old# (stem and path forms) in every linking file — open documents get the edit as an unsaved change, closed files are written. Status re-checked 300 ms after the last edit and on every index change. Suite wiki.mjs (8 steps). Deferred: hover preview (5), embeds ![[…]] (7, needs 008).