v0.2.0: vim bindings, Shift-H/L focus nav, hot-model default #3
@@ -9,7 +9,7 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
### Added
|
### 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.
|
- 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.
|
- 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
|
### Changed
|
||||||
|
|||||||
@@ -224,7 +224,7 @@
|
|||||||
import { EditorView, keymap, placeholder } from "https://esm.sh/@codemirror/view@6";
|
import { EditorView, keymap, placeholder } from "https://esm.sh/@codemirror/view@6";
|
||||||
import { EditorState } from "https://esm.sh/@codemirror/state@6";
|
import { EditorState } from "https://esm.sh/@codemirror/state@6";
|
||||||
import { history, defaultKeymap, historyKeymap } from "https://esm.sh/@codemirror/commands@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 = [
|
const LANGUAGES = [
|
||||||
"English", "Portuguese (Brazil)", "Portuguese (Portugal)", "Spanish",
|
"English", "Portuguese (Brazil)", "Portuguese (Portugal)", "Spanish",
|
||||||
@@ -478,9 +478,11 @@
|
|||||||
|
|
||||||
goBtn.addEventListener("click", translate);
|
goBtn.addEventListener("click", translate);
|
||||||
|
|
||||||
// Shift-H / Shift-L navigate focus across controls (vim-style Shift-Tab /
|
// Shift-H / Shift-L cycle focus across controls (vim-style Shift-Tab /
|
||||||
// Tab). Skipped when focus is inside the CodeMirror editor — there H/L
|
// Tab). Two paths kept in sync:
|
||||||
// are vim's viewport motions and must reach the editor untouched.
|
// 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 = [
|
const focusables = [
|
||||||
{ el: sourceSel, focus: () => sourceSel.focus() },
|
{ el: sourceSel, focus: () => sourceSel.focus() },
|
||||||
{ el: targetSel, focus: () => targetSel.focus() },
|
{ el: targetSel, focus: () => targetSel.focus() },
|
||||||
@@ -488,23 +490,37 @@
|
|||||||
{ el: editor.contentDOM, focus: () => editor.focus() },
|
{ el: editor.contentDOM, focus: () => editor.focus() },
|
||||||
{ el: goBtn, focus: () => goBtn.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) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (!e.shiftKey) return;
|
if (!e.shiftKey) return;
|
||||||
const k = e.key.toLowerCase();
|
const k = e.key.toLowerCase();
|
||||||
if (k !== "h" && k !== "l") return;
|
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();
|
e.preventDefault();
|
||||||
const ae = document.activeElement;
|
moveFocus(k === "l" ? 1 : -1);
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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();
|
fillLangs();
|
||||||
loadModels().then(() => refreshLoaded(true));
|
loadModels().then(() => refreshLoaded(true));
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user