Compare commits

...
Author SHA1 Message Date
l0ng-ai 919de6ab48 fix(vim): only shadow leader keys, not operators a mapping starts with
Since #2355 every rc mapping unmapped the built-in bound to its first key,
so a mapping such as nmap dx or nnoremap yz disabled d / c / y entirely,
and the built-in stayed gone after the mapping was removed. Shadow only the
single-key motions a leader reuses (<Space>, <BS>, <Del>, comma, semicolon
and their variants) and always restore them on the next rc apply.
2026-11-09 20:50:11 +08:00
2 changed files with 23 additions and 6 deletions

No files matched your search

+15
View File
@@ -173,6 +173,21 @@ describe("Vim engine", () => {
d();
});
it.each([
["nmap dx <Action>(GotoFile)", ["d", "w"], "two\nthree"],
["nmap cs <Action>(GotoFile)", ["c", "w"], " two\nthree"],
["nnoremap yz yy", ["y", "y", "p"], "one two\none two\nthree"],
])("keeps built-in operators a mapping starts with (%s)", async (rc, keys, doc) => {
const view = makeView();
const cm = getCM(view)!;
applyRcCommands(parseVimrc(rc).commands);
for (const k of keys) Vim.handleKey(cm as never, k, "user");
await tick();
expect(view.state.doc.toString()).toBe(doc);
applyRcCommands([]);
view.destroy();
});
it("restores the built-in key the leader shadowed once the rc stops using it (#2337)", async () => {
const view = makeView();
const cm = getCM(view)!;
+8 -6
View File
@@ -327,10 +327,10 @@ const LEADER_BUILTIN_DEFAULTS: Record<string, Record<string, unknown>> = {
};
/** Built-ins unmapped to make room for a user mapping's first key, by key. */
const shadowedBuiltins = new Map<string, Record<string, unknown> | null>();
const shadowedBuiltins = new Map<string, Record<string, unknown>>();
function firstKey(lhs: string): string {
return /^<[CSMA]-*\w+>|^./i.exec(lhs)?.[0] ?? lhs;
return /^<[^<>]+>|^./.exec(lhs)?.[0] ?? lhs;
}
/** Applies rc commands (replacing earlier rc mappings). Returns problems to report. */
@@ -342,17 +342,19 @@ export function applyRcCommands(commands: readonly RcCommand[]): string[] {
// one's first keys collide with. Restoring first keeps mapclear's
// user/default boundary honest — it assumes the default region never shrank.
for (const [key, entry] of shadowedBuiltins) {
if (entry) Vim._mapCommand(entry as never);
Vim._mapCommand(entry as never);
shadowedBuiltins.delete(key);
}
for (const c of commands) {
if (c.kind !== "map" && c.kind !== "mapEx") continue;
const key = firstKey(c.lhs);
// Only the single-key motions a leader reuses: unmapping an operator or prefix
// (`d`, `c`, `y`, `<C-d>`…) would break every command built on it.
const builtin = LEADER_BUILTIN_DEFAULTS[key];
if (!builtin || shadowedBuiltins.has(key)) continue;
// The builtins this shadows are context-less; `undefined` matches them.
if (!shadowedBuiltins.has(key) && Vim.unmap(key, undefined as unknown as string)) {
shadowedBuiltins.set(key, LEADER_BUILTIN_DEFAULTS[key] ?? null);
}
if (Vim.unmap(key, undefined as unknown as string)) shadowedBuiltins.set(key, builtin);
}
for (const c of commands) {
try {