// Zed keymap — WASD-style vim bindings // Mirrors keymaps.lua for Neovim. // // Key remapping legend (nvim lhs → nvim rhs → Zed action): // Movement : w→k s→j a→h d→l // Word fwd : i→w I→W j→e J→E // Word back : (b/B unchanged) // Insert : e→i E→a A→I D→A // Delete op : r→d R→D // Substitute: f→s F→S // Replace : t→r T→R // Open line : W→O S→o // Visual : h→v H→V // Find fwd : l→f L→F // Till fwd : p→t P→T // Paste : v→p V→P // Nop'd : k K O (collisions cleared) // // Place this file at: // macOS : ~/.config/zed/keymap.json // Linux : ~/.config/zed/keymap.json // Windows: %APPDATA%\Zed\keymap.json [ // ───────────────────────────────────────────────────────────── // NORMAL MODE // ───────────────────────────────────────────────────────────── { "context": "vim_mode == normal && !menu", "bindings": { // ── Movement (WASD) ────────────────────────────────────── "w": "vim::Up", "s": "vim::Down", "a": "vim::Left", "d": "vim::Right", // ── Word motions ───────────────────────────────────────── // i → w (next word start), I → W (WORD) "i": "vim::NextWordStart", "I": ["vim::NextWordStart", { "ignore_punctuation": true }], // j → e (next word end), J → E (WORD end) "j": "vim::NextWordEnd", "J": ["vim::NextWordEnd", { "ignore_punctuation": true }], // b / B unchanged (previous word start) // ── Enter insert mode ──────────────────────────────────── "e": "vim::InsertBefore", // e → i "E": "vim::InsertAfter", // E → a "A": "vim::InsertFirstNonWhitespace", // A → I "D": "vim::InsertEndOfLine", // D → A // ── Open lines ─────────────────────────────────────────── "S": "vim::InsertLineBelow", // S → o "W": "vim::InsertLineAbove", // W → O // ── Delete operator ────────────────────────────────────── // r → d (push delete, waits for motion/object) "r": "vim::PushDelete", // R → D (delete to end of line) "R": "vim::DeleteToEndOfLine", // ── Change / substitute ────────────────────────────────── // c unchanged (push change operator) // f → s (substitute character under cursor, i.e. vim 's') "f": "vim::Substitute", // F → S (substitute whole line) "F": "vim::SubstituteLine", // ── Replace (single char) ──────────────────────────────── // t → r (push replace — waits for char) "t": "vim::PushReplace", // T → R (toggle replace mode) "T": "vim::ToggleReplace", // ── Visual mode entry ──────────────────────────────────── "h": "vim::ToggleVisual", // h → v "H": "vim::ToggleVisualLine", // H → V // ── Find / till ────────────────────────────────────────── // l → f (find forward) "l": [ "vim::PushFindForward", { "before": false, "multiline": false }, ], // L → F (find backward) "L": [ "vim::PushFindBackward", { "after": false, "multiline": false }, ], // p → t (till forward, stops before char) "p": [ "vim::PushFindForward", { "before": true, "multiline": false }, ], // P → T (till backward) "P": [ "vim::PushFindBackward", { "after": true, "multiline": false }, ], // ── Paste ──────────────────────────────────────────────── "v": "vim::Paste", // v → p "V": ["vim::Paste", { "before": true }], // V → P // ── Nop — clear dangerous collisions ──────────────────── "k": null, "K": null, "o": null, "O": null, // ── Repeat find (unchanged) ────────────────────────────── ";": "vim::RepeatFind", ",": "vim::RepeatFindReversed", }, }, // ───────────────────────────────────────────────────────────── // VISUAL MODE // ───────────────────────────────────────────────────────────── { "context": "vim_mode == visual && !menu", "bindings": { // ── Movement ───────────────────────────────────────────── "w": "vim::Up", "s": "vim::Down", "a": ["vim::PushObject", { "around": true }], // 'a' text-object prefix kept "d": "vim::Right", // ── Word motions ───────────────────────────────────────── "i": ["vim::PushObject", { "around": false }], // 'i' text-object prefix kept in visual "I": ["vim::NextWordStart", { "ignore_punctuation": true }], "j": "vim::NextWordEnd", "J": ["vim::NextWordEnd", { "ignore_punctuation": true }], // ── Operators ──────────────────────────────────────────── "r": "vim::VisualDelete", // r → d (delete selection) "R": "vim::VisualDeleteLine", // R → D "f": "vim::Substitute", // f → s (substitute selection) "F": "vim::SubstituteLine", // F → S // ── Replace char in selection ──────────────────────────── "t": "vim::PushReplace", // ── Paste ──────────────────────────────────────────────── "v": "vim::Paste", "V": ["vim::Paste", { "preserve_clipboard": true }], // ── Find / till ────────────────────────────────────────── "l": [ "vim::PushFindForward", { "before": false, "multiline": false }, ], "L": [ "vim::PushFindBackward", { "after": false, "multiline": false }, ], "p": [ "vim::PushFindForward", { "before": true, "multiline": false }, ], "P": [ "vim::PushFindBackward", { "after": true, "multiline": false }, ], // ── Nop ────────────────────────────────────────────────── "k": null, "K": null, "O": null, // ── Repeat find ────────────────────────────────────────── ";": "vim::RepeatFind", ",": "vim::RepeatFindReversed", }, }, // ───────────────────────────────────────────────────────────── // OPERATOR-PENDING MODE // Entered after pressing an operator (r=d, c, y, etc.). // Zed evaluates BOTH vim_mode==operator bindings AND // vim_operator==X bindings simultaneously, most-specific wins. // // CRITICAL: 'a' and 'i' must NOT be movement keys here — // they need to push the text-object context so that sequences // like "rai" (daw) and "cai" (caw) work correctly. // 'd' (right) is safe because it doesn't clash with any object prefix. // 's' (down) is safe for counts: r2s = delete 2 lines down. // ───────────────────────────────────────────────────────────── { "context": "vim_mode == operator && !menu", "bindings": { // ── Movement motions ───────────────────────────────────── "w": "vim::Up", "s": "vim::Down", // NOTE: 'a' is intentionally OMITTED here — left to the // default vim binding so it acts as PushObject {around:true}, // enabling "ra", "ca" chains. // NOTE: 'i' is intentionally OMITTED here — same reason, // so "ri", "ci" chains work. "d": "vim::Right", // ── Word motions (your i→w, j→e remaps) ───────────────── // These only fire when NOT immediately followed by an object key. // Zed resolves ambiguity by waiting for the next key: // "ri" → i pushes object context (PushObject inner) → wait for obj key // "rij" → i pushes object context → j=NextWordEnd (word end object? no — // j in object context = vim::NextWordEnd motion, acts as object) // So we put word motions on i/j only in contexts where they're unambiguous: // We handle this via the object-context layer below instead. // ── Nop ────────────────────────────────────────────────── "k": null, "K": null, // ── Find / till motions ────────────────────────────────── "l": [ "vim::PushFindForward", { "before": false, "multiline": false }, ], "L": [ "vim::PushFindBackward", { "after": false, "multiline": false }, ], "p": [ "vim::PushFindForward", { "before": true, "multiline": false }, ], "P": [ "vim::PushFindBackward", { "after": true, "multiline": false }, ], // ── Repeat find ────────────────────────────────────────── ";": "vim::RepeatFind", ",": "vim::RepeatFindReversed", }, }, // ───────────────────────────────────────────────────────────── // TEXT OBJECTS (vim_operator == a or vim_operator == i) // Entered after pressing 'a' (around) or 'i' (inner) in operator mode. // // Your nvim remaps (operator-pending): // i → w so "ci" = change inner word (vanilla: "cw" but really "ciw") // ai → aw so "cai" = change a word (vanilla: "caw") // ai → aw so "rai" = delete a word (vanilla: "daw") // // In Zed: after 'a' prefix → vim_operator == a → next key selects object // after 'i' prefix → vim_operator == i → next key selects object // // Key remaps inside object context: // i → Word (your word key, so "cii"=inner word, "cai"=a word) // j → NextWordEnd used as object? No — j maps to vim::Word for end-of-word obj // s → Sentence (s was your Down, but inside obj context it's unambiguous) // w → Word (vanilla fallback) // d → Right motion doesn't make sense as object, leave as Right? No — // inside obj context all keys are object names, not motions. // 'd' has no standard vim text object so leave unmapped / null. // ───────────────────────────────────────────────────────────── { "context": "vim_operator == a || vim_operator == i || vim_operator == cs || vim_operator == helix_next || vim_operator == helix_previous", "bindings": { // ── Your remapped word keys → word objects ──────────────── "i": "vim::Word", // cai / cii / rai / rii → word "I": ["vim::Word", { "ignore_punctuation": true }], // caI / ciI → WORD // ── Standard object keys (vanilla vim, kept as-is) ──────── "w": "vim::Word", "s": "vim::Sentence", // s=Sentence (not Down here) "p": "vim::Paragraph", "t": "vim::Tag", "e": "vim::EntireFile", "f": "vim::Method", "c": "vim::Class", "a": "vim::Argument", // 'a' = Argument object (e.g. "cia") "\"": "vim::DoubleQuotes", "'": "vim::Quotes", "`": "vim::BackQuotes", "q": "vim::MiniQuotes", "|": "vim::VerticalBars", "(": ["vim::Parentheses", { "opening": true }], ")": "vim::Parentheses", "b": "vim::Parentheses", "[": ["vim::SquareBrackets", { "opening": true }], "]": "vim::SquareBrackets", "r": "vim::SquareBrackets", // 'r' = [] object (inside obj ctx) "{": ["vim::CurlyBrackets", { "opening": true }], "}": "vim::CurlyBrackets", "B": "vim::CurlyBrackets", "<": ["vim::AngleBrackets", { "opening": true }], ">": "vim::AngleBrackets", }, }, // ───────────────────────────────────────────────────────────── // DELETE OPERATOR (after pressing r) // r→d, so vim_operator == d fires. We need "rr" → dd (current line). // ───────────────────────────────────────────────────────────── { "context": "vim_operator == d", "bindings": { "r": "vim::CurrentLine", // rr → dd (delete line) "s": "vim::PushDeleteSurrounds", // rs → ds (delete surrounds) }, }, // ───────────────────────────────────────────────────────────── // CHANGE OPERATOR (after pressing c, vim_operator == c) // c is unchanged so "cc" still works via the default binding. // ───────────────────────────────────────────────────────────── { "context": "vim_operator == c", "bindings": { "c": "vim::CurrentLine", // cc → change line (default, kept) "s": ["vim::PushChangeSurrounds", {}], // cs → change surrounds }, }, // ───────────────────────────────────────────────────────────── // YANK OPERATOR (after pressing y) // ───────────────────────────────────────────────────────────── { "context": "vim_operator == y", "bindings": { "y": "vim::CurrentLine", // yy → yank line (default, kept) }, }, // ───────────────────────────────────────────────────────────── // INSERT MODE — keep ctrl shortcuts working // ───────────────────────────────────────────────────────────── { "context": "vim_mode == insert", "bindings": { // Standard escape routes unchanged "ctrl-[": "vim::NormalBefore", "escape": "vim::NormalBefore", // All other insert-mode bindings from the default vim keymap // remain active (ctrl-w, ctrl-u, ctrl-r, ctrl-o, etc.) }, }, ]