mirror of
https://github.com/vxcontrol/pentagi.git
synced 2026-08-28 14:06:36 +00:00
fix(editor): map token end so a hard break inside {{.Var}}/<tag> spans fully
collectInlineMatches derived `to = from + match.length`, which undershoots
when a non-text inline node (a hard break from Shift+Enter) sits inside a
{{...}} or <tag> token: the highlight decoration and the cycle/select then
land one char short (off by the node's size). Read `to` from the per-character
position map instead. View-only — getMarkdown() was already byte-identical.
The module comment asserted the opposite (false) invariant; corrected. Test
inserts a hard break inside a token.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
69f86ae566
commit
bb695df721
@@ -2,16 +2,16 @@ import type { Node as PMNode } from '@tiptap/pm/model';
|
||||
|
||||
export interface InlineMatch {
|
||||
from: number;
|
||||
text: string;
|
||||
to: number;
|
||||
}
|
||||
|
||||
// Find `regex` matches in each textblock's inline text, mapped to document positions. Unlike a per-text-node
|
||||
// scan, this reunites a token split across text nodes by a mark (e.g. a user styles one brace of `{{.Var}}`,
|
||||
// so ProseMirror splits it) — the brace and the rest sit in one block string again. ProseMirror positions are
|
||||
// one per UTF-16 unit and mark-independent, so offset `i` in the block string maps to `blockStart + i`; a
|
||||
// token never spans a non-text inline node (image/hard-break), so `to = from + length` holds. `regex` MUST be
|
||||
// global (`/g`). Scanning per textblock — NOT over `doc.textContent` — keeps positions aligned across blocks.
|
||||
// one per UTF-16 unit and mark-independent; `from`/`to` are read from the per-character position map (NOT
|
||||
// `from + length`) so a token that also contains a non-text inline node — a hard break from Shift+Enter —
|
||||
// still spans the right range. `regex` MUST be global (`/g`). Scanning per textblock — NOT over
|
||||
// `doc.textContent` — keeps positions aligned across blocks.
|
||||
export const collectInlineMatches = (doc: PMNode, regex: RegExp): InlineMatch[] => {
|
||||
const matches: InlineMatch[] = [];
|
||||
|
||||
@@ -36,10 +36,12 @@ export const collectInlineMatches = (doc: PMNode, regex: RegExp): InlineMatch[]
|
||||
});
|
||||
|
||||
for (const match of text.matchAll(regex)) {
|
||||
const from = positions[match.index ?? 0];
|
||||
const start = match.index ?? 0;
|
||||
const from = positions[start];
|
||||
const last = positions[start + match[0].length - 1];
|
||||
|
||||
if (from !== undefined) {
|
||||
matches.push({ from, text: match[0], to: from + match[0].length });
|
||||
if (from !== undefined && last !== undefined) {
|
||||
matches.push({ from, to: last + 1 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -173,4 +173,19 @@ describe('findVariableOccurrences — doc spans for the Available-variables cycl
|
||||
expect(doc.textBetween(hit.from, hit.to)).toBe('{{.Foo}}');
|
||||
}
|
||||
});
|
||||
|
||||
it('spans the full token when a hard break sits inside it (non-text-node off-by-N)', () => {
|
||||
const doc = docOf('{{.Foo\\\n}}');
|
||||
|
||||
// sanity — the hard break really is inside the token (text, hardBreak, text)
|
||||
expect(doc.firstChild?.childCount ?? 0).toBeGreaterThan(1);
|
||||
|
||||
const foo = findVariableOccurrences(doc, 'Foo');
|
||||
|
||||
expect(foo).toHaveLength(1);
|
||||
|
||||
for (const hit of foo) {
|
||||
expect(doc.textBetween(hit.from, hit.to)).toBe('{{.Foo}}');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user