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
|
// CodeJar mounts on the contenteditable and re-runs the highlighter
|
||||||
// on each input while preserving caret position.
|
// 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) {
|
function attachOnUpdate(jarInstance) {
|
||||||
jarInstance.onUpdate(function (value) {
|
jarInstance.onUpdate(function (value) {
|
||||||
|
|
@ -205,28 +205,52 @@
|
||||||
hidePopup();
|
hidePopup();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Replace the trailing word fragment with the chosen identifier.
|
// Walk the selection focus backwards `fragment.length` characters
|
||||||
// Assumes the fragment lives in one text node — true for the current
|
// and replace the result with the chosen identifier.
|
||||||
// srccfg and bash grammars (identifiers tokenize as a single \b…\b
|
//
|
||||||
// chunk). If a future grammar splits identifiers across nodes,
|
// We can't use `range.setStart(endContainer, endOffset - fragment.length)`
|
||||||
// setStart(endContainer, endOffset - fragment.length) will land
|
// because at end-of-line the caret often sits in a text node AFTER
|
||||||
// inside the wrong node and the deleteContents will corrupt text.
|
// Prism's <span class="token …"> (the trailing newline / empty text
|
||||||
// The try/catch below converts IndexSizeError or any other DOM
|
// node), so `endOffset` is small and the subtraction would go
|
||||||
// exception into a graceful popup-dismiss rather than a broken caret.
|
// 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 {
|
try {
|
||||||
const sel = window.getSelection();
|
const sel = window.getSelection();
|
||||||
|
for (let i = 0; i < ctx.fragment.length; i++) {
|
||||||
|
sel.modify("extend", "backward", "character");
|
||||||
|
}
|
||||||
const range = sel.getRangeAt(0);
|
const range = sel.getRangeAt(0);
|
||||||
range.setStart(range.endContainer, range.endOffset - ctx.fragment.length);
|
|
||||||
range.deleteContents();
|
range.deleteContents();
|
||||||
range.insertNode(document.createTextNode(entry.name));
|
const insertedNode = document.createTextNode(entry.name);
|
||||||
range.collapse(false);
|
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.removeAllRanges();
|
||||||
sel.addRange(range);
|
sel.addRange(caretRange);
|
||||||
// Force CodeJar to re-highlight + emit onUpdate.
|
// Force CodeJar to re-highlight + emit onUpdate. updateCode does
|
||||||
// Use instance.jar (not bare `jar` closure) for consistency with
|
// `editor.textContent = code; highlight(editor)` — no caret
|
||||||
// setLanguage's tear-down-and-remount — the jar reference can be
|
// preservation — so we save the linear-text offset of the caret
|
||||||
// swapped at runtime.
|
// (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.updateCode(instance.jar.toString());
|
||||||
|
instance.jar.restore(caretPos);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[editor] acceptCompletion failed; dismissing popup", err);
|
console.warn("[editor] acceptCompletion failed; dismissing popup", err);
|
||||||
}
|
}
|
||||||
|
|
@ -341,7 +365,7 @@
|
||||||
instance.language = next;
|
instance.language = next;
|
||||||
code.className = "editor-code language-" + next;
|
code.className = "editor-code language-" + next;
|
||||||
code.textContent = currentText;
|
code.textContent = currentText;
|
||||||
instance.jar = window.CodeJar(code, highlightFor(next), { tab: " " });
|
instance.jar = window.CodeJar(code, highlightFor(next), { tab: " ", addClosing: false });
|
||||||
attachOnUpdate(instance.jar);
|
attachOnUpdate(instance.jar);
|
||||||
},
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue