Skip to content

Instantly share code, notes, and snippets.

@ayghri
Last active August 12, 2026 20:23
Show Gist options
  • Select an option

  • Save ayghri/a4d70ecea3f4eaafb7985fe0c525f4b9 to your computer and use it in GitHub Desktop.

Select an option

Save ayghri/a4d70ecea3f4eaafb7985fe0c525f4b9 to your computer and use it in GitHub Desktop.
Overleaf Colemak Vim Bindings
// ==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