fix(editor): correct caret behavior in autocomplete accept + disable auto-close
Four smoke-discovered fixes in acceptCompletion / CodeJar options: - Backward selection walk via Selection.modify replaces the buggy range.setStart(endContainer, endOffset - fragment.length). The old code assumed the fragment lived in endContainer; at end-of-line the caret often sits in a post-Prism-<span> text node, so the subtraction went negative → IndexSizeError → caught silently → popup dismissed with no insert. - Save/restore caret around updateCode because codejar.js:469-474 does editor.textContent = code; highlight(editor) with no caret preservation, which dropped the caret to the start of the editor. - Set the selection inside the inserted text node before save() so CodeJar's save() doesn't trip its anchorNode === editor special case at codejar.js:122-127, which collapses to end-of-all-text. - addClosing: false on both CodeJar constructors so closing quotes don't duplicate — CodeJar's addClosing: true default inserts a paired closing character without skipping past an existing one, producing e.g. "rcon_password"" when you finish a string literal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5bec91ab17
commit
9a773093a8
1 changed files with 42 additions and 18 deletions
|
|
@ -156,7 +156,7 @@
|
|||
|
||||
// CodeJar mounts on the contenteditable and re-runs the highlighter
|
||||
// on each input while preserving caret position.
|
||||
const jar = window.CodeJar(code, highlightFor(language), { tab: " " });
|
||||
const jar = window.CodeJar(code, highlightFor(language), { tab: " ", addClosing: false });
|
||||
|
||||
function attachOnUpdate(jarInstance) {
|
||||
jarInstance.onUpdate(function (value) {
|
||||
|
|
@ -205,28 +205,52 @@
|
|||
hidePopup();
|
||||
return;
|
||||
}
|
||||
// Replace the trailing word fragment with the chosen identifier.
|
||||
// Assumes the fragment lives in one text node — true for the current
|
||||
// srccfg and bash grammars (identifiers tokenize as a single \b…\b
|
||||
// chunk). If a future grammar splits identifiers across nodes,
|
||||
// setStart(endContainer, endOffset - fragment.length) will land
|
||||
// inside the wrong node and the deleteContents will corrupt text.
|
||||
// The try/catch below converts IndexSizeError or any other DOM
|
||||
// exception into a graceful popup-dismiss rather than a broken caret.
|
||||
// Walk the selection focus backwards `fragment.length` characters
|
||||
// and replace the result with the chosen identifier.
|
||||
//
|
||||
// We can't use `range.setStart(endContainer, endOffset - fragment.length)`
|
||||
// because at end-of-line the caret often sits in a text node AFTER
|
||||
// Prism's <span class="token …"> (the trailing newline / empty text
|
||||
// node), so `endOffset` is small and the subtraction would go
|
||||
// negative or land in the wrong node. `Selection.modify` walks
|
||||
// across node boundaries the way the browser's own caret-movement
|
||||
// logic does, so it works regardless of which DOM node the caret
|
||||
// ended up in after the last Prism rehighlight.
|
||||
//
|
||||
// (Selection.modify is well-supported in all evergreen browsers
|
||||
// even though it's not standardised — same family of API as
|
||||
// contenteditable itself.)
|
||||
try {
|
||||
const sel = window.getSelection();
|
||||
for (let i = 0; i < ctx.fragment.length; i++) {
|
||||
sel.modify("extend", "backward", "character");
|
||||
}
|
||||
const range = sel.getRangeAt(0);
|
||||
range.setStart(range.endContainer, range.endOffset - ctx.fragment.length);
|
||||
range.deleteContents();
|
||||
range.insertNode(document.createTextNode(entry.name));
|
||||
range.collapse(false);
|
||||
const insertedNode = document.createTextNode(entry.name);
|
||||
range.insertNode(insertedNode);
|
||||
// Move the caret to inside the inserted text node (at its end),
|
||||
// NOT just "after" it via range.collapse(false). The latter
|
||||
// leaves anchorNode/focusNode pointing at <code> with an offset
|
||||
// — and CodeJar's save() has a special case (codejar.js:122-127)
|
||||
// that collapses any such selection to end-of-all-text, sending
|
||||
// restore() to the wrong place. Pointing the selection inside
|
||||
// the inserted text node keeps save() on its normal walk path.
|
||||
const caretRange = document.createRange();
|
||||
caretRange.setStart(insertedNode, insertedNode.length);
|
||||
caretRange.setEnd(insertedNode, insertedNode.length);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
// Force CodeJar to re-highlight + emit onUpdate.
|
||||
// Use instance.jar (not bare `jar` closure) for consistency with
|
||||
// setLanguage's tear-down-and-remount — the jar reference can be
|
||||
// swapped at runtime.
|
||||
sel.addRange(caretRange);
|
||||
// Force CodeJar to re-highlight + emit onUpdate. updateCode does
|
||||
// `editor.textContent = code; highlight(editor)` — no caret
|
||||
// preservation — so we save the linear-text offset of the caret
|
||||
// (now positioned after the inserted identifier) before and
|
||||
// restore after. Use instance.jar (not bare `jar` closure) for
|
||||
// consistency with setLanguage's tear-down-and-remount: the jar
|
||||
// reference is swapped at runtime.
|
||||
const caretPos = instance.jar.save();
|
||||
instance.jar.updateCode(instance.jar.toString());
|
||||
instance.jar.restore(caretPos);
|
||||
} catch (err) {
|
||||
console.warn("[editor] acceptCompletion failed; dismissing popup", err);
|
||||
}
|
||||
|
|
@ -341,7 +365,7 @@
|
|||
instance.language = next;
|
||||
code.className = "editor-code language-" + next;
|
||||
code.textContent = currentText;
|
||||
instance.jar = window.CodeJar(code, highlightFor(next), { tab: " " });
|
||||
instance.jar = window.CodeJar(code, highlightFor(next), { tab: " ", addClosing: false });
|
||||
attachOnUpdate(instance.jar);
|
||||
},
|
||||
destroy: function () {
|
||||
|
|
|
|||
Loading…
Reference in a new issue