nvim-keymap-migrator 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # nvim-keymap-migrator
2
+
3
+ A CLI tool that extracts user-defined keymaps from your Neovim configuration and integrates them with vim emulator plugins (IdeaVim, VSCodeVim, etc.).
4
+
5
+ ## Warning
6
+
7
+ **Back up your files before running this tool!**. This tool was made for me, so it may have unintended consequences on your setup. Always review the changes it proposes before applying them.
8
+
9
+ This tool modifies the following files:
10
+
11
+ - `~/.ideavimrc` (IntelliJ/IdeaVim config)
12
+ - VS Code `settings.json` (location varies by platform - see below)
13
+
14
+ While `--clean` attempts to cleanly remove all changes, **always back up these files** before running:
15
+
16
+ | Platform | VS Code settings.json |
17
+ | -------- | ------------------------------------------------------- |
18
+ | Linux | `~/.config/Code/User/settings.json` |
19
+ | macOS | `~/Library/Application Support/Code/User/settings.json` |
20
+ | Windows | `%APPDATA%/Code/User/settings.json` |
21
+
22
+ ## Why?
23
+
24
+ When moving from Neovim to another editor (VS Code, IntelliJ, etc.), you'll likely use a vim emulator plugin like IdeaVim or VSCodeVim. This tool extracts your custom keymaps from your Neovim config so you don't have to manually recreate them.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ npm install -g nvim-keymap-migrator
30
+ ```
31
+
32
+ ## Requirements
33
+
34
+ - Node.js 18+
35
+ - Neovim 0.8+ (for `vim.keymap.set` and `vim.api.nvim_get_keymap`)
36
+ - Editor with a Vim emulator plugin (e.g., IdeaVim for IntelliJ, VSCodeVim for VS Code)
37
+ - Your Neovim config must be loadable via `nvim --headless`
38
+
39
+ ## Usage
40
+
41
+ ```bash
42
+ nvim-keymap-migrator <editor> [options]
43
+ ```
44
+
45
+ ### Editors
46
+
47
+ - `vscode` - Generate and integrate keybindings for VS Code
48
+ - `intellij` - Generate and integrate keybindings for IntelliJ
49
+
50
+ ### Options
51
+
52
+ - `--dry-run` - Print report without writing files
53
+ - `--clean` - Remove managed keybindings from editor config
54
+ - `--help, -h` - Show help
55
+ - `--version, -v` - Show version
56
+
57
+ ### Examples
58
+
59
+ ```bash
60
+ # Integrate with VS Code
61
+ nvim-keymap-migrator vscode
62
+
63
+ # Integrate with IntelliJ
64
+ nvim-keymap-migrator intellij
65
+
66
+ # Preview without making changes
67
+ nvim-keymap-migrator vscode --dry-run
68
+
69
+ # Remove VS Code keybindings
70
+ nvim-keymap-migrator vscode --clean
71
+
72
+ # Remove IntelliJ mappings
73
+ nvim-keymap-migrator intellij --clean
74
+ ```
75
+
76
+ ## How It Works
77
+
78
+ The tool extracts keymaps from your Neovim config and categorizes them:
79
+
80
+ 1. **IDE actions** - Keymaps that can be translated to IDE-specific actions (e.g., LSP, file explorer)
81
+ 2. **Pure Vim mappings** - Native Vim keymaps that work in any Vim emulator
82
+ 3. **Plugin mappings** - Keymaps from plugins (require manual configuration)
83
+ 4. **Unsupported** - Keymaps that couldn't be categorized
84
+
85
+ ### IntelliJ
86
+
87
+ Mappings are appended to `~/.ideavimrc` wrapped in markers:
88
+
89
+ ```vim
90
+ " <<< nvim-keymap-migrator start >>>
91
+ " Managed by nvim-keymap-migrator. Run with --clean to remove.
92
+
93
+ " Pure Vim mappings (native Vim motions)
94
+ nnoremap K mzK`z
95
+ vnoremap K :m '<-2<CR>gv=gv
96
+
97
+ " IDE action mappings
98
+ nnoremap <leader>ff <Action>(GotoFile)
99
+ " >>> nvim-keymap-migrator end <<<
100
+ ```
101
+
102
+ Re-running replaces the content between markers. Use `--clean` to remove.
103
+
104
+ ### VS Code
105
+
106
+ VS Code uses a two-pronged approach:
107
+
108
+ 1. **IDE actions** - Merged directly into `settings.json`:
109
+
110
+ ```json
111
+ "vim.normalModeKeyBindings": [
112
+ { "before": ["<leader>", "f"], "commands": ["workbench.action.quickOpen"], "_managedBy": "nvim-keymap-migrator" }
113
+ ]
114
+ ```
115
+
116
+ 2. **Pure Vim mappings** - Written to a shared `.vimrc` file, configured via `vim.vimrc.path` and `vim.vimrc.enable`:
117
+
118
+ ```json
119
+ "vim.vimrc.path": "~/.config/nvim-keymap-migrator/.vimrc",
120
+ "vim.vimrc.enable": true
121
+ ```
122
+
123
+ The tool manages both settings:
124
+
125
+ - `vim.vimrc.path` - Set to point to the shared .vimrc
126
+ - `vim.vimrc.enable` - Set to `true` only if unset (respects existing user values)
127
+
128
+ Re-running replaces managed keybindings. Use `--clean` to remove them.
129
+
130
+ ## Namespace
131
+
132
+ A small namespace directory stores shared files:
133
+
134
+ ```
135
+ ~/.config/nvim-keymap-migrator/
136
+ .vimrc Shared pure-Vim mappings (read by VS Code and IntelliJ)
137
+ metadata.json Extraction metadata
138
+ ```
139
+
140
+ ## Supported Intents
141
+
142
+ The tool detects and translates these keymap intents:
143
+
144
+ ### Navigation
145
+
146
+ - `navigation.file_explorer` - File explorer (`:Ex`, `<leader>pv`)
147
+ - `navigation.find_files` - Quick open files
148
+ - `navigation.live_grep` - Search in files
149
+ - `navigation.buffers` - Switch buffers
150
+ - `navigation.recent_files` - Recent files
151
+ - `navigation.grep_string` - Search word under cursor
152
+
153
+ ### LSP
154
+
155
+ - `lsp.definition` - Go to definition (`gd`)
156
+ - `lsp.declaration` - Go to declaration (`gD`)
157
+ - `lsp.references` - Find references (`gr`)
158
+ - `lsp.implementation` - Go to implementation (`gi`)
159
+ - `lsp.hover` - Show hover info
160
+ - `lsp.signature_help` - Signature help
161
+ - `lsp.rename` - Rename symbol
162
+ - `lsp.code_action` - Code actions
163
+ - `lsp.format` - Format document
164
+
165
+ ### Git
166
+
167
+ - `git.fugitive` - Git commands
168
+ - `git.push` / `git.pull` - Push/pull
169
+ - `git.commit` - Git commit
170
+
171
+ ### Pure Vim Mappings
172
+
173
+ Native Vim motions and commands are detected and output as pure Vim mappings, so any mapping from one Vim command to another is included here.
174
+
175
+ These work out of the box in any Vim emulator.
176
+
177
+ ## What Gets Extracted
178
+
179
+ Only **user-defined** keymaps are extracted (not plugin defaults or built-in mappings). The tool identifies these by checking:
180
+
181
+ - If the keymap's callback source is in your config directory
182
+ - If the keymap has a `desc` field
183
+ - If the keymap's script path starts with your config path
184
+
185
+ ## Limitations
186
+
187
+ - `<Lua function>` keymaps without a command string are included as comments (vim emulators can't execute arbitrary Lua)
188
+ - Buffer-local keymaps are marked with `<buffer>` - they'll only work in the current buffer
189
+ - Some plugin-specific keymaps may require manual configuration
190
+
191
+ ## License
192
+
193
+ GPL-3.0-only
package/index.js ADDED
@@ -0,0 +1,423 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { mkdir, writeFile } from "node:fs/promises";
4
+ import { readFileSync } from "node:fs";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { spawn } from "node:child_process";
8
+ import { extractKeymaps } from "./src/extractor.js";
9
+ import { detectConfig } from "./src/config.js";
10
+ import { detectIntents } from "./src/detector.js";
11
+ import { loadMappings, lookupIntent } from "./src/registry.js";
12
+ import { generateVimrc, isPureVimMapping } from "./src/generators/vimrc.js";
13
+ import { generateIdeaVimrc } from "./src/generators/intellij.js";
14
+ import { generateVSCodeBindings } from "./src/generators/vscode.js";
15
+ import { generateReport } from "./src/report.js";
16
+ import { ensureNamespaceDir, getRcPath } from "./src/namespace.js";
17
+ import {
18
+ integrateIdeaVim,
19
+ cleanIdeaVim,
20
+ integrateVSCode,
21
+ cleanVSCode,
22
+ saveMetadata,
23
+ } from "./src/install.js";
24
+
25
+ const __filename = fileURLToPath(import.meta.url);
26
+ const __dirname = dirname(__filename);
27
+
28
+ const pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf8"));
29
+ const args = process.argv.slice(2);
30
+
31
+ await main(args);
32
+
33
+ function checkNeovimAvailable() {
34
+ return new Promise((resolve) => {
35
+ const proc = spawn("nvim", ["--version"], { stdio: "ignore" });
36
+ proc.on("error", () => resolve(false));
37
+ proc.on("close", (code) => resolve(code === 0));
38
+ });
39
+ }
40
+
41
+ async function main(argv) {
42
+ const parsed = parseArgs(argv);
43
+
44
+ if (parsed.error) {
45
+ printHelp(parsed.error);
46
+ process.exitCode = 1;
47
+ return;
48
+ }
49
+
50
+ if (parsed.help) {
51
+ printHelp();
52
+ return;
53
+ }
54
+
55
+ if (parsed.version) {
56
+ console.log(pkg.version);
57
+ return;
58
+ }
59
+
60
+ if (!parsed.editor) {
61
+ printHelp('Missing required <editor>. Use "vscode" or "intellij".');
62
+ process.exitCode = 1;
63
+ return;
64
+ }
65
+
66
+ if (!["vscode", "intellij"].includes(parsed.editor)) {
67
+ printHelp(`Unsupported editor: ${parsed.editor}`);
68
+ process.exitCode = 1;
69
+ return;
70
+ }
71
+
72
+ if (parsed.clean) {
73
+ await handleClean(parsed.editor);
74
+ return;
75
+ }
76
+
77
+ await handleGenerate(parsed);
78
+ }
79
+
80
+ async function handleGenerate(parsed) {
81
+ try {
82
+ const nvimAvailable = await checkNeovimAvailable();
83
+ if (!nvimAvailable) {
84
+ console.error("Error: Neovim not found. Please install Neovim 0.8+");
85
+ process.exitCode = 1;
86
+ return;
87
+ }
88
+
89
+ const config = await detectConfig();
90
+ const extracted = await extractKeymaps();
91
+ const intents = detectIntents(extracted);
92
+ const registry = loadMappings();
93
+
94
+ if (intents.length === 0) {
95
+ console.log(`No user-defined keymaps found in your Neovim config.
96
+ Nothing to write.`);
97
+ return;
98
+ }
99
+
100
+ const translated = [];
101
+ const manual = [];
102
+ const unsupported = [];
103
+
104
+ for (const item of intents) {
105
+ if (!item.intent) {
106
+ unsupported.push(item);
107
+ continue;
108
+ }
109
+
110
+ const command = lookupIntent(item.intent, parsed.editor, registry);
111
+ if (!command) {
112
+ manual.push(item);
113
+ continue;
114
+ }
115
+
116
+ translated.push({ ...item, command });
117
+ }
118
+
119
+ const vimrcText = generateVimrc(intents);
120
+ const pureVim = intents.filter(isPureVimMapping);
121
+ const counts = {
122
+ total: intents.length,
123
+ translated: translated.length,
124
+ pureVim: pureVim.length,
125
+ manual: manual.length,
126
+ unsupported: unsupported.length,
127
+ };
128
+
129
+ const leaderLabel = formatKeyDisplay(config.leader);
130
+
131
+ if (parsed.dryRun) {
132
+ const report = generateReport({
133
+ target: parsed.editor,
134
+ configPath: config.config_path,
135
+ leader: leaderLabel,
136
+ total: intents.length,
137
+ translated,
138
+ manual,
139
+ manualPlugin: manual.filter((item) => item.category === "plugin"),
140
+ manualOther: manual.filter((item) => item.category !== "plugin"),
141
+ pureVim,
142
+ unsupported: unsupported.filter((item) => !isPureVimMapping(item)),
143
+ outputs: [],
144
+ });
145
+ console.log(report.trimEnd());
146
+ printDetectionWarnings(config, extracted);
147
+ return;
148
+ }
149
+
150
+ await ensureNamespaceDir();
151
+ const vimrcPath = getRcPath("neovim");
152
+ await writeFile(vimrcPath, vimrcText, "utf8");
153
+
154
+ console.log(`=== nvim-keymap-migrator ===
155
+ Editor: ${parsed.editor}
156
+ Config: ${config.config_path}
157
+ Leader: ${leaderLabel}
158
+
159
+ `);
160
+
161
+ if (parsed.editor === "intellij") {
162
+ const ideaVimrcResult = generateIdeaVimrc(intents, { registry });
163
+ const ideaVimrcText = ideaVimrcResult.text;
164
+ const ideaDefaults = ideaVimrcResult.defaultsAdded ?? 0;
165
+ const result = await integrateIdeaVim(ideaVimrcText, {
166
+ leader: config.leader,
167
+ });
168
+
169
+ await saveMetadata(config, counts);
170
+
171
+ console.log(`Shared .vimrc: ${vimrcPath}
172
+
173
+ IntelliJ:
174
+ Mappings appended to ~/.ideavimrc${result.updated ? "\n (replaced previous mappings)" : ""}
175
+ Defaults added: ${ideaDefaults}`);
176
+ } else if (parsed.editor === "vscode") {
177
+ const vscodeBindings = generateVSCodeBindings(intents, { registry });
178
+ const result = await integrateVSCode(vscodeBindings, {
179
+ leader: config.leader,
180
+ });
181
+
182
+ await saveMetadata(config, counts, {
183
+ leaderSet: result.setLeader,
184
+ vimrcPathSet: result.setVimrcPath,
185
+ vimrcEnableSet: result.setVimrcEnable,
186
+ });
187
+
188
+ let vscodeOutput = `Shared .vimrc: ${vimrcPath}
189
+
190
+ VS Code:
191
+ Keybindings merged into settings.json`;
192
+
193
+ if (result.setVimrcPath) {
194
+ vscodeOutput +=
195
+ "\n vim.vimrc.path set in settings.json (reads shared .vimrc)";
196
+ }
197
+ if (result.setVimrcEnable) {
198
+ vscodeOutput +=
199
+ "\n vim.vimrc.enable set in settings.json (loads shared .vimrc)";
200
+ }
201
+ if (result.setLeader) {
202
+ vscodeOutput += "\n vim.leader set in settings.json";
203
+ }
204
+ if (result.sections) {
205
+ vscodeOutput += `\n Sections: ${result.sections.join(", ")}`;
206
+ }
207
+ if (result.warnings) {
208
+ for (const w of result.warnings) {
209
+ vscodeOutput += `\n ${w}`;
210
+ }
211
+ }
212
+ const vsDefaults = vscodeBindings._meta?.defaultsAdded ?? 0;
213
+ vscodeOutput += `\n Defaults added: ${vsDefaults}`;
214
+
215
+ console.log(vscodeOutput);
216
+ }
217
+
218
+ console.log(`
219
+ Total keymaps: ${counts.total}
220
+ Translated: ${counts.translated}
221
+ Pure Vim: ${counts.pureVim}
222
+ Manual review: ${counts.manual}
223
+ Unsupported: ${counts.unsupported}`);
224
+
225
+ printDetectionWarnings(config, extracted);
226
+ } catch (error) {
227
+ console.error(`Error: ${error.message}`);
228
+ process.exitCode = 1;
229
+ }
230
+ }
231
+
232
+ async function handleClean(editor) {
233
+ try {
234
+ console.log(`=== nvim-keymap-migrator --clean ===
235
+ Editor: ${editor}
236
+
237
+ `);
238
+
239
+ if (editor === "intellij") {
240
+ const result = await cleanIdeaVim();
241
+ if (result.cleaned) {
242
+ console.log("Removed managed mappings from ~/.ideavimrc");
243
+ } else {
244
+ console.log("No managed mappings found in ~/.ideavimrc");
245
+ }
246
+ } else if (editor === "vscode") {
247
+ const result = await cleanVSCode();
248
+ if (result.cleaned) {
249
+ let output = `Removed ${result.removed} keybinding(s) from settings.json`;
250
+ if (result.sections) {
251
+ output += `\nSections cleaned: ${result.sections.join(", ")}`;
252
+ }
253
+ if (result.removedVimrcPath) {
254
+ output += "\nRemoved vim.vimrc.path from settings.json";
255
+ }
256
+ if (result.removedVimrcEnable) {
257
+ output += "\nRemoved vim.vimrc.enable from settings.json";
258
+ }
259
+ if (result.removedLeader) {
260
+ output += "\nRemoved vim.leader from settings.json";
261
+ }
262
+ console.log(output);
263
+ } else {
264
+ console.log("No managed keybindings found in settings.json");
265
+ }
266
+ }
267
+ } catch (error) {
268
+ console.error(`Error: ${error.message}`);
269
+ process.exitCode = 1;
270
+ }
271
+ }
272
+
273
+ function parseArgs(argv) {
274
+ const flags = {
275
+ help: false,
276
+ version: false,
277
+ dryRun: false,
278
+ clean: false,
279
+ editor: null,
280
+ error: null,
281
+ };
282
+
283
+ const tokens = [...argv];
284
+
285
+ for (let i = 0; i < tokens.length; i += 1) {
286
+ const arg = tokens[i];
287
+
288
+ if (arg === "--help" || arg === "-h" || arg === "help") {
289
+ flags.help = true;
290
+ continue;
291
+ }
292
+
293
+ if (arg === "--version" || arg === "-v" || arg === "version") {
294
+ flags.version = true;
295
+ continue;
296
+ }
297
+
298
+ if (arg === "--dry-run") {
299
+ flags.dryRun = true;
300
+ continue;
301
+ }
302
+
303
+ if (arg === "--clean") {
304
+ flags.clean = true;
305
+ continue;
306
+ }
307
+
308
+ if (arg === "run") {
309
+ continue;
310
+ }
311
+
312
+ if (!flags.editor && ["vscode", "intellij"].includes(arg)) {
313
+ flags.editor = arg;
314
+ continue;
315
+ }
316
+
317
+ if (arg.startsWith("-")) {
318
+ flags.error = `Unknown option: ${arg}`;
319
+ return flags;
320
+ }
321
+
322
+ flags.error = `Unknown argument: ${arg}`;
323
+ return flags;
324
+ }
325
+
326
+ return flags;
327
+ }
328
+
329
+ function printHelp(error) {
330
+ if (error) {
331
+ console.error(error + "\n");
332
+ }
333
+
334
+ const help = `Usage: ${pkg.bin["nvim-keymap-migrator"]} <editor> [options]
335
+
336
+ Editors:
337
+ vscode Generate and integrate keybindings for VS Code
338
+ intellij Generate and integrate keybindings for IntelliJ
339
+
340
+ Options:
341
+ --dry-run Print report without writing files
342
+ --clean Remove managed keybindings from editor config
343
+ --help, -h Show help
344
+ --version, -v Show version
345
+
346
+ Examples:
347
+ nvim-keymap-migrator vscode # Integrate with VS Code
348
+ nvim-keymap-migrator intellij # Integrate with IntelliJ
349
+ nvim-keymap-migrator vscode --dry-run # Preview without changes
350
+ nvim-keymap-migrator vscode --clean # Remove VS Code keybindings
351
+
352
+ Files modified:
353
+ IntelliJ: ~/.ideavimrc
354
+ VS Code: settings.json (platform-specific path)`;
355
+
356
+ console.log(help);
357
+ }
358
+
359
+ function formatKeyDisplay(value) {
360
+ if (value == null) {
361
+ return "<none>";
362
+ }
363
+
364
+ if (value.length === 0) {
365
+ return "<empty>";
366
+ }
367
+
368
+ const special = {
369
+ " ": "<space>",
370
+ "\t": "<Tab>",
371
+ "\n": "<NL>",
372
+ "\r": "<CR>",
373
+ "\u001B": "<Esc>",
374
+ };
375
+
376
+ return [...value]
377
+ .map((char) => special[char] ?? printableChar(char))
378
+ .join("");
379
+ }
380
+
381
+ function printableChar(char) {
382
+ const code = char.charCodeAt(0);
383
+ if (code < 32) {
384
+ return `<0x${code.toString(16).padStart(2, "0")}>`;
385
+ }
386
+ if (char === "\\") {
387
+ return "\\\\";
388
+ }
389
+ return char;
390
+ }
391
+
392
+ function printDetectionWarnings(config, extracted) {
393
+ const extractionMeta = extracted?._meta ?? {};
394
+ const extractionWarnings = Array.isArray(extracted?._warnings)
395
+ ? extracted._warnings
396
+ : [];
397
+
398
+ if (config.fallback_from) {
399
+ console.warn(
400
+ `Warning: config detection fell back from ${config.fallback_from} to ${config.mode} (${config.fallback_reason})`,
401
+ );
402
+ }
403
+
404
+ if (Array.isArray(config.warnings) && config.warnings.length > 0) {
405
+ console.warn(`Config warnings (${config.warnings.length}):`);
406
+ for (const warning of config.warnings.slice(0, 5)) {
407
+ console.warn(`- ${warning}`);
408
+ }
409
+ }
410
+
411
+ if (extractionMeta.fallback_from) {
412
+ console.warn(
413
+ `Warning: extraction fell back from ${extractionMeta.fallback_from} to ${extractionMeta.extraction_mode} (${extractionMeta.fallback_reason})`,
414
+ );
415
+ }
416
+
417
+ if (extractionWarnings.length > 0) {
418
+ console.warn(`Extraction warnings (${extractionWarnings.length}):`);
419
+ for (const warning of extractionWarnings.slice(0, 5)) {
420
+ console.warn(`- ${warning}`);
421
+ }
422
+ }
423
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "nvim-keymap-migrator",
3
+ "version": "1.0.0",
4
+ "description": "Creates a keymap file from your Neovim config to migrate your keybinds to other editors.",
5
+ "files": [
6
+ "index.js",
7
+ "src",
8
+ "templates",
9
+ "README.md",
10
+ "LICENSE"
11
+ ],
12
+ "bin": {
13
+ "nvim-keymap-migrator": "index.js"
14
+ },
15
+ "keywords": [
16
+ "tools",
17
+ "neovim",
18
+ "nvim",
19
+ "editor",
20
+ "ide"
21
+ ],
22
+ "homepage": "https://github.com/DerekCorniello/nvim-keymap-migrator#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/DerekCorniello/nvim-keymap-migrator/issues"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/DerekCorniello/nvim-keymap-migrator.git"
29
+ },
30
+ "license": "GPL-3.0-only",
31
+ "author": "Derek Corniello",
32
+ "type": "module",
33
+ "main": "index.js",
34
+ "scripts": {
35
+ "test": "echo \"Error: no test specified\" && exit 1",
36
+ "lint": "prettier --check ."
37
+ },
38
+ "devDependencies": {
39
+ "prettier": "^3.0.0"
40
+ }
41
+ }