Repository navigation
Keep a link's opaque id across edits that rewrite its name - #185
xVanTuring wants to merge 3 commits into
Conversation
A link or image embed lives in the buffer as its DISPLAY form (`[[Name]]`, `![[Name]]`); the `|id` suffix sits on the `.wikiLinkID` side-channel and `makeStorageState` re-attaches it on every writeback. Three ordinary edits took the attribute away with nothing left to read it from, and the id then left the document for good — for an image embed that means the picture is gone and its stored bytes are orphaned, with no way to tell which image the reference used to name. - `makeStorageState` probed `.wikiLinkID` only at `contentRange.location`. Typed text takes the base typing attributes, so a character inserted at the START of a name carried no id and the probe read nil — even though every surviving character of the name still had it. Scan the run instead. - Clicking a rendered embed selects its whole name, so the next keystroke replaces every character the attribute sat on and there is nothing left to scan. `shouldChangeTextIn` now captures the id when the replaced text was covered by one name run and the replacement carries no link grammar, and `textDidChange` re-applies it. `![[` / `]]` markers never carry the attribute, so replacing a whole embed — or typing a fresh one over it — cannot inherit a stale id. - `copy` serialized the buffer verbatim, so the clipboard held `![[Name]]` with no id: pasting it back created a dead reference, and pasting it over the original destroyed the only copy of the id (cut/paste goes through the same path). The private raw-markdown flavor now carries the storage form, rebuilt from the selection's own attributes; the plain-text, HTML and RTF flavors keep the display form, so no id leaks into another app.
|
Thanks — the three paths are real: on current One blocker, one open question, one nit: 1. Paste puts the id on screen. The private flavor now carries the storage form, but Storage is correct, the buffer isn't — 2. Retyping a whole node-link name now snaps back. With a resolver that knows both names: select 3. Nit: Also needs a merge from |
`copy` puts the storage form on the private pasteboard flavor, but `paste` inserted it as plain text, so the buffer showed `[[Note|abc123]]` until the next rebuild. The private-flavor branch now rewrites each storage-form link to display form and stamps its suffix on the name as `.wikiLinkID` before insertion, so the writeback in `textDidChange` reads it. It goes through `makeDisplayState` rather than `displayFragmentAndID`, since one paste can carry several links. Raw source mode still pastes verbatim. `wikiLinkIDToCarry` called `longestEffectiveRange` over the whole document on every selection replacement; on a nil value that walks every attribute run. Check the attribute at the location first and look up the run only when an id is there.
|
Thanks for the careful review. Pushed: 1. Paste — the private-flavor branch now rewrites storage-form links back to display form and stamps each suffix on its name as 3. Merged On 2: my take is that retyping a whole node-link name means "point somewhere else", while for an image embed the id is the content itself. One option: carry the id over a whole-name replacement only for |
A link or image embed lives in the buffer as its display form —
[[Name]],![[Name]]— and the|idsuffix rides along on the.wikiLinkIDattribute, whichmakeStorageStatere-attaches on every writeback. That works right up until an edit takes the attribute away and leaves nothing to rebuild it from. Three ordinary ones do, and in each the id leaves the document permanently.For a node link that costs a link. For an image embed it costs the picture: the bytes are still in the embedder's store, but the only thing that named them was the suffix, so nothing can find them again.
The three
1. A character typed at the start of a name.
makeStorageStateprobed.wikiLinkIDatcontentRange.locationonly. Typed text takes the base typing attributes, so that one fresh character carries no id — even though every surviving character of the name still has it — and the writeback emits a suffix-less![[Za.png]].2. A whole name replaced. Clicking a rendered embed selects its name (the auto-select in
textViewDidChangeSelection), so the very next keystroke replaces every character the attribute sat on. Nothing survives to scan.3. Copy, and therefore cut.
copyserialized the buffer verbatim:The clipboard gets
![[Name]]with no id. Pasting it back creates a dead reference; pasting it over the original destroys the only copy of the id.MarkdownPasteboardWriter's own doc comment already promised the private flavor round-trips[[Name|UUID]]byte-exact — it could not, because the caller handed it display text.The fix
makeStorageStatescans the name's attribute run (recoveredLinkID) instead of probing its first character. One lookup in the normal case, since the styler re-applies the attribute uniformly over the run.shouldChangeTextIncaptures the id when the replaced text was covered by a single.wikiLinkIDrun and the replacement carries no link grammar;textDidChangere-applies it to what landed there.![[/]]markers never carry the attribute, so replacing a whole embed — or typing a fresh one over it — cannot inherit a stale id. Both guards are tested.copyrebuilds the storage form from the selection's own attributes and passes it as the new optionalrawMarkdown, which reaches only the private flavor..string, HTML and RTF keep the display form, so no id leaks into another app — the property the 0.9.0 note ("side-channels no longer leak into copied text") was after.No public API changes.
MarkdownPasteboardWriteris internal, and its new parameter is defaulted.Tests
Tests/MarkdownEngineTests/WikiLinkIDSurvivalTests.swift— 10 tests driving a realNativeTextView+ coordinator throughrebuildTextStorageAndStyle, so the display form and its attributes come from the engine rather than a fixture. Covers each of the three paths, the edits that already worked (insert at the end, delete inside a name), the two negative cases above, and the node-link copy.swift buildandswift testare green (513 tests).