fix: Shift-H/L also exit the editor in vim normal mode

The previous version only handled Shift-H/L when focus was outside
the editor; inside vim normal mode they were viewport top/bottom
and required Esc+Tab to escape. Now Vim.mapCommand overrides H/L
in normal-mode context to call the same moveFocus(±1) helper, so
the shortcut works uniformly regardless of focus position.
This commit is contained in:
João Pedro Battistella Nadas
2026-06-09 16:39:05 +02:00
parent 9564900909
commit 876014a7b7
2 changed files with 31 additions and 15 deletions

View File

@@ -224,7 +224,7 @@
import { EditorView, keymap, placeholder } from "https://esm.sh/@codemirror/view@6";
import { EditorState } from "https://esm.sh/@codemirror/state@6";
import { history, defaultKeymap, historyKeymap } from "https://esm.sh/@codemirror/commands@6";
import { vim } from "https://esm.sh/@replit/codemirror-vim@6";
import { vim, Vim } from "https://esm.sh/@replit/codemirror-vim@6";
const LANGUAGES = [
"English", "Portuguese (Brazil)", "Portuguese (Portugal)", "Spanish",
@@ -478,9 +478,11 @@
goBtn.addEventListener("click", translate);
// Shift-H / Shift-L navigate focus across controls (vim-style Shift-Tab /
// Tab). Skipped when focus is inside the CodeMirror editor — there H/L
// are vim's viewport motions and must reach the editor untouched.
// Shift-H / Shift-L cycle focus across controls (vim-style Shift-Tab /
// Tab). Two paths kept in sync:
// 1. Document keydown listener — fires when focus is on a select / button
// 2. Vim normal-mode mapping (below) — fires when focus is in the editor
// Together they make Shift-H/L work everywhere, including escaping the editor.
const focusables = [
{ el: sourceSel, focus: () => sourceSel.focus() },
{ el: targetSel, focus: () => targetSel.focus() },
@@ -488,23 +490,37 @@
{ el: editor.contentDOM, focus: () => editor.focus() },
{ el: goBtn, focus: () => goBtn.focus() },
];
function moveFocus(delta) {
const ae = document.activeElement;
let idx = focusables.findIndex(f => f.el === ae || f.el.contains?.(ae));
if (idx === -1) {
focusables[delta > 0 ? 0 : focusables.length - 1].focus();
return;
}
const next = focusables[(idx + delta + focusables.length) % focusables.length];
next.focus();
}
document.addEventListener("keydown", (e) => {
if (!e.shiftKey) return;
const k = e.key.toLowerCase();
if (k !== "h" && k !== "l") return;
if (editor.dom.contains(document.activeElement)) return; // vim owns H/L in the editor
// Inside the editor, vim's mapCommand handles H/L (see below). Letting
// the event bubble here would double-jump.
if (editor.dom.contains(document.activeElement)) return;
e.preventDefault();
const ae = document.activeElement;
let idx = focusables.findIndex(f => f.el === ae);
if (idx === -1) {
focusables[k === "l" ? 0 : focusables.length - 1].focus();
return;
}
const delta = k === "l" ? 1 : -1;
const next = focusables[(idx + delta + focusables.length) % focusables.length];
next.focus();
moveFocus(k === "l" ? 1 : -1);
});
// Override vim's H/L (normally "viewport top/bottom") so they exit the
// editor to the previous/next control, matching the same Shift-H/L
// contract the document handler uses elsewhere.
if (!isCoarsePointer) {
Vim.defineAction("finrodFocusPrev", () => moveFocus(-1));
Vim.defineAction("finrodFocusNext", () => moveFocus(1));
Vim.mapCommand("H", "action", "finrodFocusPrev", {}, { context: "normal" });
Vim.mapCommand("L", "action", "finrodFocusNext", {}, { context: "normal" });
}
fillLangs();
loadModels().then(() => refreshLoaded(true));
</script>