Skip to content

fix(editor): end links where their text ends - #40

Merged
christianhuening merged 1 commit into
mainfrom
fix/link-paste-cursor
Oct 13, 2026
Merged

christianhuening merged 1 commit into
mainfrom
fix/link-paste-cursor

Conversation

@christianhuening

Copy link
Copy Markdown
Contributor

Problem

Pasting a link left the cursor at the link's right edge, and everything typed afterwards became part of the link. The href stayed the original URL, so the page and the Markdown export ended up with [https://example.com and more](https://example.com).

Root cause

@tiptap/extension-link sets inclusive() { return this.options.autolink }, and knot turns autolink on. ProseMirror carries an inclusive mark onto text typed at its right edge. Paste made this the common case, because the core paste rule deliberately keeps the caret inside a link that ends the pasted text (tiptap#7692). The same thing happened in other places:

  • after inserting a doc link from the toolbar
  • after editing a date
  • after an HTML paste
  • after clicking at the end of any link
  • when typing at the start of a paragraph that opens with a link

Upstream's fix (tiptap#8335, 3.31.4) only stops a typed space from joining the link. Punctuation, or text typed without a space, still joined.

Fix

  • KnotLink is non-inclusive (extensions.ts). This matches prosemirror-schema-basic's link. inclusive is a local editing rule and is never serialised, so the Y.Doc, tools/schema.json and the server are unchanged, and no migration is needed. excludes is untouched, so y-tiptap still writes the link key that to_markdown reads.
  • Pasting a URL over selected text leaves the caret after the link. Before, the new link stayed selected, so the next keystroke replaced it. This uses Selection.near, so select-all and table-cell selections stay valid.
  • The toolbar's Link button shows the link that Apply and Remove act on (toolbarLink.ts). Before, with the caret at a link's edge, the popover opened empty and an empty Apply removed the link.
  • Toolbar Apply with nothing selected inserts the URL as linked text. Before, it set a stored mark, which only ever linked the next character once links stopped being inclusive. It still goes through setLink, so isAllowedUri still rejects javascript:.
  • The // date picker no longer opens after a URL scheme (https://, file:///). It took keyboard focus right after pasting https://host, so the next keystrokes went into the picker instead of the page.

Intended behaviour changes

  • Typing at either edge of a link gives plain text.
  • Retyping a link's last character, or typing over all of its text, gives plain text. This is the same as prosemirror-schema-basic. Use the toolbar to change a link.
  • Upstream's exitable ArrowRight handler is now inert. Before, at the end of a paragraph it wrote a stray space into the shared doc.
  • When two users type at the same link edge at the same moment, Yjs may interleave their first characters. The result still converges.
  • Links that already over-extended in stored documents are not repaired.

Tests

  • linkBoundary.test.ts covers paste and typing at link edges, the date picker after URL schemes (with a positive control), and select-all paste.
  • toolbarLink.test.ts covers the helpers.
  • EditorToolbar.link.test.tsx covers the toolbar wiring.
  • Shared input helpers live in src/test/editorInput.ts.
  • Each new test was checked to fail against the code it guards.
  • vitest: 405/405 pass. tsc and eslint are clean. e2e was not run: no spec types after a link, uses the toolbar link flow or touches the date picker.

Not in this PR

  • The toolbar's Document link mode still inserts a new link at the caret, even when the caret touches an existing link.
  • knot:// is not a registered link protocol, so setLink with a knot://doc/… href over a selection does nothing.

🤖 Generated with Claude Code

Pasting a URL left the caret at the link's right edge, and everything
typed from there joined the link while its href stayed the URL. Tiptap
makes the link mark inclusive whenever autolink is on, so the same
happened after a doc link, an edited date, an HTML paste, a click at a
link's end, and at the start of a paragraph opening with a link.

KnotLink is now non-inclusive (as prosemirror-schema-basic's link is).
Tiptap's own fix (tiptap#8335, 3.31.4) only covers a typed space.

Follow-ups the change needs or exposed:
- Pasting a URL over a selection collapses the caret to the link end,
  so the next keystroke no longer replaces the new link.
- The toolbar's Link button shows the link Apply/Remove act on, also at
  a link's edges; an empty Apply no longer silently unlinks it.
- Toolbar Apply with nothing selected inserts the URL as linked text
  instead of relying on a stored (now one-character) mark.
- The `//` date picker no longer opens after a URL scheme; it took
  keyboard focus right after pasting https://host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@christianhuening
christianhuening merged commit bd3a896 into main Oct 13, 2026
5 checks passed
@christianhuening
christianhuening deleted the fix/link-paste-cursor branch October 13, 2026 15:54
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