fix: link embed edit UX — Enter after label + arrow-to-unwrap source (Obsidian Live Preview feel) - #87
Merged
Merged
Conversation
Rolled-up `[label](/n/id)` links were read-only chips with a destructive edge: Enter at the visual end of a label spliced the markup in half (`- [mobidoo-reply` / `- ](/n/abc)`), and there was no keyboard path to edit a label or a URL at all. Enter now never lands inside link markup. A CommonMark inline link cannot survive a line break, so every interior offset has the same destructive outcome; `stateBrokenOutOfLink` moves the caret past the closing `)` and the break is taken there. Outside a list, `continueMarkup` declines and stock Enter would still run at the original caret, so the command falls back to `insertNewlineAndIndent` on the escaped state. The result is rebased onto the real state, so the caret move and the break stay one transaction — one undo step, one CRDT update. Links also unwrap to full source while being edited, Obsidian Live Preview style. A new `activeLinks` field tracks which links hold a selection endpoint (via `resolveInner`, so caret motion stays O(log n)), and their `hide` ranges are skipped — which drops the atomic ranges with them, since the atomic facet is derived from the same field. That also removes the invisible four-press dead zone the adjacent atomic ranges created at `]` `(` url `)`, all of which painted at the same x. Click still opens, caret placement now edits: source-mode marks use `cm-md-link-src`, a single class token, so `tryOpenLinkAtPointer`'s `.cm-md-link` lookup misses an unwrapped link and the click just moves the caret. Read-only published notes never unwrap. Closes #86
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #86.
Problem
Rolled-up
[label](/n/id)links (#64) were read-only chips with a destructive edge.Enter split the link. With the caret at the visual end of a rolled-up label — exactly where it lands after arrowing through the chip — Enter spliced the markup in half:
A CommonMark inline link cannot survive a list-item break:
[and](url)end up in different items, so the link is gone and its chrome becomes literal text. The user saw a blue chip, pressed Enter, and silently broke the note.An invisible caret dead zone.
hiddenLinkMarksemits four separate adjacent atomic ranges (],(, url,)), and CodeMirror'sskipAtomicRangesonly skips a position strictly inside one range:So the boundaries between them were legal caret positions. Measured:
Offsets 16, 17, 18, 24 and 25 all paint at the same x (everything between is replaced to zero width). Walking off the end of a link took five indistinguishable presses, and four of the five resting spots put Enter/Backspace inside link markup.
No edit affordance. The decoration fields rebuilt only on
docChangedor a syntax-tree change — selection was not an input, so a link never unwrapped. Renaming a label meant deleting the chip and retyping it.Enter
stateBrokenOutOfLinkmoves the caret past the closing)when it sits strictly inside a link span, and the break is taken there. Every interior offset has the same destructive outcome, so there is no split worth honouring — Enter finishes the link and starts the next line after it.Outside a list,
continueMarkupdeclines anddefaultKeymap's Enter would still run at the original caret, back inside the link, so the command falls back toinsertNewlineAndIndenton the escaped state.rebaseOntore-issues the result against the real state, so the caret move and the break are a single transaction — one undo step underY.UndoManager, one CRDT update.Boundary offsets are not interior, so breaking immediately before or after a link is untouched. Multi-cursor is left alone.
Unwrap
A new
activeLinksstate field tracks which links hold a selection endpoint. While a link is active itshideranges are skipped — which drops the atomic ranges with them, since the atomic facet is derived from the same field — and it paints ascm-md-link-src/cm-md-link-src-labelinstead:The dead zone disappears as a consequence: the chrome is real visible text, so Left/Right step through it one character at a time, without loosening the atomic policy for rolled-up links.
Click opens, caret placement edits — never both. Source-mode marks use
cm-md-link-src, a single class token, sotryOpenLinkAtPointer'sel.closest(".cm-md-link")misses an unwrapped link and a plain click just moves the caret. (.cm-md-link--barekeeps opening: its spec carriescm-md-linkas a separate token.)mousedownfires before the selection updates, so clicking a rolled-up chip always opens.Deliberate choices, documented in code:
/p/…, so a reader dragging across a link should not see raw markup.Performance
linkSpanAtusesresolveInner+ a parent walk (O(log n)), not the full-documentdocumentTree().iterate()— it runs on every selection change.activeLinksreturns its previous array by identity when the span set is unchanged, so caret motion outside a link costs one reference compare inneedsRebuild, not a decoration rebuild.Not regressed
.cm-md-linkpainting, modifier-click fallthrough.documentTree()and the parse-progress rebuild are untouched.[Title](/n/{id}). This is decorations + one command; stored text is unchanged and old notes behave identically.<https://…>) ride the same path for free.Tests
lib/editor/links.test.ts—linkSpanAtboundaries; rolled-up vs unwrapped decorations; re-roll on leaving; atomic dropped while unwrapped (16 -> 17 -> 18 -> 19) and kept while rolled up (18 -> 24); sweep-select does not unwrap; read-only does not unwrap; click opens rolled up, places the caret unwrapped.lib/editor/list-continue.test.ts(new) — Enter at the visual end of the label and at every other interior offset; unchanged at both boundaries; plain-paragraph fallback; single-transaction assertion; multi-cursor untouched; existing non-tight-list tightening.379 tests pass,
tsc --noEmitclean,pnpm buildgreen, no new lint findings.QA after merge
Hard-refresh agentnote.dev, then in a note with
- [label](/n/{id}): arrow into the label (expands to source), keep arrowing (caret walks](/n/…)one char at a time, then re-rolls); Enter at the end of the label (new list item after the intact chip); edit the label and URL, arrow out, click it; type Korean into a label while unwrapped.🤖 Generated with Claude Code