Apply custom vim mappings to LeetCode's built-in Monaco Vim mode
// ==UserScript==
// @name LeetCode Custom Mapping
// @namespace http://tampermonkey.net/
// @version 0.2
// @description Apply custom vim mappings to LeetCode's built-in Monaco Vim mode
// @author thraxis
// @match https://leetcode.com/problems/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @license MIT
// ==/UserScript==
(function () {
"use strict";
const defaultMappings = [
{ lhs: "jk", rhs: "<Esc>", mode: "insert" },
{ lhs: "jj", rhs: "<Esc>", mode: "insert" },
{ lhs: "H", rhs: "^", mode: "normal" },
{ lhs: "L", rhs: "$", mode: "normal" },
{ lhs: "J", rhs: "5j", mode: "normal" },
{ lhs: "K", rhs: "5k", mode: "normal" },
];
function normalizeEntry(entry) {
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
const { lhs, rhs, mode } = entry;
if (typeof lhs === "string" && typeof rhs === "string" && typeof mode === "string") {
return { lhs, rhs, mode };
}
return null;
}
if (typeof entry === "string") {
const parts = entry.trim().split(/\s+/);
if (parts[0] === "map" && parts.length >= 4) {
return { lhs: parts[1], rhs: parts[2], mode: parts[3] };
}
return null;
}
return null;
}
function getMappings() {
const stored = GM_getValue("mappings", defaultMappings);
if (!Array.isArray(stored)) {
GM_setValue("mappings", defaultMappings);
return defaultMappings;
}
return stored.map(normalizeEntry).filter(Boolean);
}
function applyMappings() {
if (!unsafeWindow.monacoVim?.VimMode?.Vim) return false;
const V = unsafeWindow.monacoVim.VimMode.Vim;
getMappings().forEach(({ lhs, rhs, mode }) => {
try {
V.map(lhs, rhs, mode);
} catch (e) {
console.error("Map failed:", lhs, rhs, mode, e);
}
});
return true;
}
function startPolling() {
let tries = 0;
const maxTries = 60;
const interval = setInterval(() => {
tries++;
if (applyMappings() || tries >= maxTries) {
clearInterval(interval);
}
}, 1000);
}
startPolling();
let lastUrl = location.href;
new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
startPolling();
}
}).observe(document.body, { childList: true, subtree: true });
function mappingsToText(mappings) {
return mappings.map((m) => `map ${m.lhs} ${m.rhs} ${m.mode}`).join("\n");
}
function textToMappings(text) {
return text.split("\n").map(normalizeEntry).filter(Boolean);
}
GM_registerMenuCommand("Edit Mappings", () => {
const current = mappingsToText(getMappings());
const newText = prompt('Enter mappings, one per line (e.g. "map jk <Esc> insert"):', current);
if (newText !== null) {
GM_setValue("mappings", textToMappings(newText));
alert("Saved. Reload the page for changes to take effect.");
}
});
GM_registerMenuCommand("Reset Mappings to Defaults", () => {
GM_setValue("mappings", defaultMappings);
alert("Reset to defaults. Reload the page.");
});
})();