From 73aa54d7bddb08a481d0e0fc90a3bc27a1771e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Battistella=20Nadas?= Date: Tue, 9 Jun 2026 16:53:11 +0200 Subject: [PATCH] fix: Firefox clipboard, flip dropdown default to normal mode - Replace Vim.map("...", "\"+...", ...) with custom Vim.defineAction handlers calling navigator.clipboard directly. The library's "+" register implementation doesn't reliably write on Firefox. Limitation: only yy/Y/visual-y reach system clipboard; bare y still uses vim's internal register. - Flip dropdown modal: default NORMAL (j/k arrows), `/` or `i` enters search. Listener moved to document capture phase so closed dropdowns receive keydown on Firefox. --- CHANGELOG.md | 13 ++--- static/index.html | 145 +++++++++++++++++++++++++++++----------------- 2 files changed, 99 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0477617..99862f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,13 +10,12 @@ 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`. Also overridden in vim normal mode (replacing vim's viewport-top/bottom default) so the same shortcut works to leave the editor. -- `y` / `Y` / `yy` and visual-mode `y` in the editor now copy to the system clipboard via vim's `+` register (synchronous `document.execCommand("copy")` under the hood — no permission prompt). -- `p` / `P` in vim normal mode paste from the system clipboard at / before the cursor via the same `+` register. The first paste may show a one-time browser permission chip; granting clipboard-read on the origin makes future calls silent. -- Vim-style modal navigation on the From / To / Model dropdowns: - - Default is native typeahead search (type letters to filter) - - `Esc` enters normal mode; an accent-coloured border indicates this - - In normal mode, `j` / `k` move selectedIndex down / up - - `i` or `/` returns to search mode; blur also resets so the next focus starts in search +- `yy` / `Y` and visual-mode `y` in the editor copy to the system clipboard via custom `navigator.clipboard.writeText()` actions (the library's `+` register is broken on Firefox). Bare `y` (e.g. `yw`, `y$`) still uses vim's internal register — that path would need a deeper hook into the library's register system. +- `p` / `P` in vim normal mode paste from the system clipboard at / before the cursor via `navigator.clipboard.readText()`. Browsers may show a one-time permission prompt or chip; granting clipboard-read for the origin makes future calls silent. On Firefox, ensure `dom.events.asyncClipboard.readText` is `true` (default in recent versions) or the read will be blocked. +- Vim-style modal navigation on the From / To / Model dropdowns (default NORMAL): + - `j` / `k` move selectedIndex down / up (works whether the dropdown is open or collapsed; uses document-level capture-phase listener so Firefox plays along) + - `/` or `i` enters search mode — letter keys then do native typeahead; accent-coloured border indicates it + - `Esc` returns to normal mode; blur also resets so the next focus starts in normal - 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 4b21c74..2259bdc 100644 --- a/static/index.html +++ b/static/index.html @@ -179,8 +179,9 @@ font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } - /* Vim normal-mode indicator on the language/model dropdowns */ - select.vim-normal { + /* Vim search-mode indicator on the language/model dropdowns + (default is normal mode — j/k navigate; / or i enters search). */ + select.vim-search { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); } @@ -517,71 +518,111 @@ moveFocus(k === "l" ? 1 : -1); }); - // Vim customisations: focus navigation + system-clipboard register routing. + // Vim customisations: focus navigation + system-clipboard yank/paste. // Override H/L (normally "viewport top/bottom") to exit the editor across - // controls, matching the same Shift-H/L contract the document handler - // uses elsewhere. Route y/Y/yy and p/P through vim's "+" register so they - // sync with the system clipboard — the @replit/codemirror-vim "+" handler - // uses document.execCommand("copy") for yank (no permission prompt) and - // navigator.clipboard.readText() for paste (browser may show a one-time - // permission chip; "Allow" makes it permanent). + // controls, matching the Shift-H/L contract the document handler uses. + // Custom yank/paste via navigator.clipboard — the library's "+" register + // path is unreliable on Firefox, so we own the clipboard I/O ourselves. + // Limitation: bare `y` (yw, y$, etc.) still goes to vim's internal + // register; only yy / Y / visual `y` reach the system clipboard. 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" }); - // Yank → system clipboard (synchronous, no prompt). - Vim.map("y", '"+y', "normal"); - Vim.map("Y", '"+Y', "normal"); - Vim.map("yy", '"+yy', "normal"); - Vim.map("y", '"+y', "visual"); + Vim.defineAction("finrodYankLine", () => { + const line = editor.state.doc.lineAt(editor.state.selection.main.head); + const text = editor.state.doc.sliceString(line.from, line.to); + navigator.clipboard.writeText(text) + .catch(e => console.warn("Clipboard write failed:", e)); + }); + Vim.defineAction("finrodYankSelection", () => { + const { from, to } = editor.state.selection.main; + if (from === to) return; + const text = editor.state.sliceDoc(from, to); + navigator.clipboard.writeText(text) + .catch(e => console.warn("Clipboard write failed:", e)); + }); + async function pasteAt(after) { + let text; + try { + text = await navigator.clipboard.readText(); + } catch (e) { + console.warn("Clipboard read failed:", e); + return; + } + if (!text) return; + const sel = editor.state.selection.main; + const pos = after ? sel.to : sel.from; + editor.dispatch({ + changes: { from: pos, insert: text }, + selection: { anchor: pos + text.length }, + }); + } + Vim.defineAction("finrodPasteAfter", () => pasteAt(true)); + Vim.defineAction("finrodPasteBefore", () => pasteAt(false)); - // Paste ← system clipboard. The chip nag is browser-side; granting - // clipboard-read permission for the origin makes it silent. - Vim.map("p", '"+p', "normal"); - Vim.map("P", '"+P', "normal"); + Vim.mapCommand("yy", "action", "finrodYankLine", {}, { context: "normal" }); + Vim.mapCommand("Y", "action", "finrodYankLine", {}, { context: "normal" }); + Vim.mapCommand("y", "action", "finrodYankSelection", {}, { context: "visual" }); + Vim.mapCommand("p", "action", "finrodPasteAfter", {}, { context: "normal" }); + Vim.mapCommand("P", "action", "finrodPasteBefore", {}, { context: "normal" }); } // Modal vim-style behavior on the language/model dropdowns. - // default: typeahead search (browser native) - // Esc: switch to "normal" mode — j/k move selectedIndex - // i, /: back to search mode - // blur: reset to search mode on next focus - // Disabled on touch (no Esc key, no value). + // default: NORMAL mode — j/k move selectedIndex (vim everywhere) + // `/` or `i`: enter search mode — letter keys do native typeahead + // Esc: back to normal mode + // blur: reset to normal mode on next focus + // Uses a document-level CAPTURE listener so events fire whether the + // dropdown is open or collapsed (Firefox doesn't bubble keydown + // reliably from a collapsed