[[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_linkineditors/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)
- Rendered as links in the rich editor:
[[Target]]shows asTarget(or the alias after|, with#headingkept), 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 createTarget.mdnext to the current note (or in the folder set in Settings → Links → new note location). - 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. [[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.- Source editor: links get a decoration (underline, resolved/unresolved colour) and Ctrl+click follows; hover shows the target path.
- Hover preview (both editors): a small popover with the first lines of the target — the index has the text.
- 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. - Embeds
![[Note]]/![[image.png]]: render the target inline in the rich editor (read-only block); pictures need 008 first. - 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 (twoNotes.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.mjsextended — 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).