diff --git a/CHANGELOG.md b/CHANGELOG.md index 185dfab..938d03c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/static/index.html b/static/index.html index c4a9587..2df7e16 100644 --- a/static/index.html +++ b/static/index.html @@ -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));