Last active
August 12, 2026 20:23
-
-
Save ayghri/a4d70ecea3f4eaafb7985fe0c525f4b9 to your computer and use it in GitHub Desktop.
Overleaf Colemak Vim Bindings
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Overleaf Colemak Vim Bindings | |
| // @namespace http://tampermonkey.net/ | |
| // @version 0.2 | |
| // @description Colemak remaps for Overleaf's CodeMirror 6 Vim mode | |
| // @match https://www.overleaf.com/project/* | |
| // @grant none | |
| // ==/UserScript== | |
| (function() { | |
| "use strict"; | |
| window.addEventListener("UNSTABLE_editor:extensions", (event) => { | |
| const { CodeMirror, CodeMirrorVim, extensions } = event.detail; | |
| const Vim = CodeMirrorVim.Vim; | |
| // ── Colemak core movement ──────────────────────────────────── | |
| // QWERTY h j k l → Colemak m n e i | |
| // | |
| // We use noremap to avoid circular chains (n↔j, e↔k, i↔l). | |
| // "h" stays as-is (same physical key on Colemak-DH). | |
| // | |
| // IMPORTANT: CodeMirror vim's "normal" context does NOT affect | |
| // operator-pending, so `ci`, `di`, `yi` (inner text objects) | |
| // should still work. If you find they don't, see the fallback | |
| // note at the bottom of this file. | |
| // n → j (down) | |
| Vim.noremap("n", "j", "normal"); | |
| Vim.noremap("n", "j", "visual"); | |
| // e → k (up) | |
| Vim.noremap("e", "k", "normal"); | |
| Vim.noremap("e", "k", "visual"); | |
| // i → l (right) — this replaces "i" (insert) in normal mode | |
| Vim.noremap("i", "l", "normal"); | |
| Vim.noremap("i", "l", "visual"); | |
| // ── Inverse / displaced keys ───────────────────────────────── | |
| // t → i (enter insert mode, since "i" is now "right") | |
| Vim.noremap("t", "i", "normal"); | |
| // T → I (insert at beginning of line) | |
| Vim.noremap("T", "I", "normal"); | |
| Vim.noremap("T", "I", "visual"); | |
| // j → n (next search match, since "n" is now "down") | |
| Vim.noremap("j", "n", "normal"); | |
| Vim.noremap("j", "n", "visual"); | |
| // I → L (jump to bottom of visible screen) | |
| Vim.noremap("I", "L", "normal"); | |
| Vim.noremap("I", "L", "visual"); | |
| // ── Scrolling ──────────────────────────────────────────────── | |
| // Ctrl-N → Ctrl-D (half-page down) | |
| Vim.noremap("<C-n>", "<C-d>", "normal"); | |
| // Ctrl-E → Ctrl-U (half-page up) | |
| Vim.noremap("<C-e>", "<C-u>", "normal"); | |
| // Ctrl-H → ^ (first non-blank character) | |
| Vim.noremap("<C-h>", "^", "normal"); | |
| // ── Quality-of-life ────────────────────────────────────────── | |
| // L → mzJ`z (join lines without moving cursor) | |
| // Using map (recursive) here is fine — no circular dependency. | |
| Vim.map("L", "mzJ`z", "normal"); | |
| // Esc clears search highlights (like :noh) | |
| Vim.defineEx("noh", "noh", (cm) => { | |
| cm.removeOverlay("searchHighlight"); | |
| // CodeMirror vim's clearSearchHighlight if available | |
| if (Vim.getVimGlobalState_) { | |
| const state = Vim.getVimGlobalState_(); | |
| if (state) state.searchHighlight = false; | |
| } | |
| }); | |
| // NOTE: Overriding <Esc> in normal mode is risky in CodeMirror. | |
| // If you want it, uncomment the next line, but it may interfere | |
| // with exiting visual mode / command-line mode: | |
| // Vim.map("<Esc>", ":noh<CR>", "normal"); | |
| // ── Leader-based mappings ──────────────────────────────────── | |
| // CodeMirror vim supports <Space> as leader via literal " " | |
| // in the lhs. Not all leader combos work reliably; YMMV. | |
| // <Space>sn → :w (save — triggers Ctrl-S in Overleaf) | |
| // Overleaf auto-saves, so this just forces a recompile: | |
| Vim.defineEx("recompile", "rec", () => { | |
| document.querySelector('[aria-label="Recompile"]')?.click?.(); | |
| }); | |
| // Vim.map("<Space>sn", ":recompile<CR>", "normal"); | |
| // <Space>wq → close current pane isn't meaningful in Overleaf, | |
| // but we can map it to toggle the PDF or go to projects: | |
| Vim.defineEx("projects", "proj", () => { | |
| // Navigate back to Overleaf project list | |
| window.location.href = "/project"; | |
| }); | |
| // ── Overleaf-specific ex-commands ──────────────────────────── | |
| // These use querySelector on Overleaf's UI elements. | |
| // Selectors may break if Overleaf updates their markup. | |
| Vim.defineEx("pdf", "pdf", () => { | |
| // Toggle PDF preview panel | |
| const btn = | |
| document.querySelector('[aria-label="Toggle output panel"]') || | |
| document.querySelector('button[class*="toggle-pdf"]'); | |
| btn?.click?.(); | |
| }); | |
| Vim.defineEx("filetree", "ft", () => { | |
| const btn = document.querySelector( | |
| 'button[aria-label="Toggle file tree"]', | |
| ); | |
| btn?.click?.(); | |
| }); | |
| Vim.defineEx("logs", "log", () => { | |
| const btn = document.querySelector( | |
| 'button[aria-label="Toggle logs panel"]', | |
| ); | |
| btn?.click?.(); | |
| }); | |
| // ── Search/replace ex-commands ─────────────────────────────── | |
| // The Neovim :%s/ workflow works natively in CodeMirror vim: | |
| // :s/foo/bar/g (current line) | |
| // :%s/foo/bar/g (whole document) | |
| // :%s/foo/bar/gI (case-sensitive) | |
| // These just work out of the box — no extra config needed. | |
| console.log("✓ Colemak Vim keybindings applied to Overleaf"); | |
| }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment