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:
@@ -9,7 +9,7 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
### Added
|
||||
- Vim bindings in the input via CodeMirror 6 + `@replit/codemirror-vim` (loaded from esm.sh, no build step). Supports `i`/`Esc`, `hjkl`, word/line motions, `dd`/`yy`/`p`, undo/redo, `/` search, `:` command line. Auto-disabled on touch devices (detected via `(pointer: coarse)`) where modal editing fights the virtual keyboard.
|
||||
- `Shift-H` / `Shift-L` cycle focus across the page controls (source → target → model → editor → button) like `Shift-Tab` / `Tab`. Suppressed inside the editor where vim already owns those motions.
|
||||
- `Shift-H` / `Shift-L` cycle focus across the page controls (source → target → model → editor → button) like `Shift-Tab` / `Tab`. Also overridden in vim normal mode (replacing vim's viewport-top/bottom default) so the same shortcut works to leave the editor.
|
||||
- On page load, the model dropdown defaults to whichever model Ollama already has hot — skips the 5-15 s disk-reload pain on the first translation. After load it follows the user's manual selection.
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user