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.
@@ -0,0 +1,265 @@
1
+ // extraction runtime-first monkey-patch capture with strict-mode fallback
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ import { tmpdir } from "node:os";
7
+
8
+ const START_MARKER = "__NVIM_KEYMAP_MIGRATOR_JSON_START__";
9
+ const END_MARKER = "__NVIM_KEYMAP_MIGRATOR_JSON_END__";
10
+
11
+ const INJECT_LUA = `
12
+ local user_keymaps = {}
13
+ local warnings = {}
14
+ local original_set = vim.keymap.set
15
+ _G.__nkm_source_ok = nil
16
+
17
+ local function normalize_modes(mode)
18
+ if type(mode) == 'table' then
19
+ return mode
20
+ end
21
+ if type(mode) == 'string' then
22
+ return { mode }
23
+ end
24
+ return { 'n' }
25
+ end
26
+
27
+ local function add_warning(message)
28
+ table.insert(warnings, tostring(message))
29
+ end
30
+
31
+ vim.keymap.set = function(mode, lhs, rhs, opts)
32
+ opts = opts or {}
33
+ local modes = normalize_modes(mode)
34
+
35
+ for _, single_mode in ipairs(modes) do
36
+ local entry = {
37
+ mode = single_mode,
38
+ lhs = lhs,
39
+ rhs_type = type(rhs),
40
+ rhs = nil,
41
+ rhs_source = nil,
42
+ rhs_what = nil,
43
+ rhs_name = nil,
44
+ rhs_line = nil,
45
+ desc = opts.desc,
46
+ silent = opts.silent,
47
+ noremap = opts.noremap,
48
+ buffer = opts.buffer,
49
+ nowait = opts.nowait,
50
+ expr = opts.expr,
51
+ callback_source = nil,
52
+ }
53
+
54
+ if type(rhs) == 'function' then
55
+ entry.rhs = '<Lua function>'
56
+ local info = debug.getinfo(rhs, 'nS')
57
+ if info then
58
+ entry.rhs_source = info.source
59
+ entry.rhs_what = info.what
60
+ entry.rhs_name = info.name
61
+ entry.rhs_line = info.linedefined
62
+ entry.callback_source = info.source
63
+ end
64
+ elseif type(rhs) == 'string' then
65
+ entry.rhs = rhs
66
+ else
67
+ entry.rhs = tostring(rhs)
68
+ end
69
+
70
+ table.insert(user_keymaps, entry)
71
+ end
72
+
73
+ return original_set(mode, lhs, rhs, opts)
74
+ end
75
+
76
+ function _G.__nkm_source_user_config()
77
+ local config_path = vim.fn.stdpath('config')
78
+ local init_lua = config_path .. '/init.lua'
79
+ local init_vim = config_path .. '/init.vim'
80
+ local source_ok = false
81
+
82
+ if vim.fn.filereadable(init_lua) == 1 then
83
+ local ok, err = pcall(dofile, init_lua)
84
+ source_ok = ok
85
+ if not ok then
86
+ add_warning('failed_to_source_init_lua: ' .. tostring(err))
87
+ end
88
+ elseif vim.fn.filereadable(init_vim) == 1 then
89
+ local ok, err = pcall(vim.cmd, 'source ' .. vim.fn.fnameescape(init_vim))
90
+ source_ok = ok
91
+ if not ok then
92
+ add_warning('failed_to_source_init_vim: ' .. tostring(err))
93
+ end
94
+ else
95
+ add_warning('no_init_file_found')
96
+ end
97
+
98
+ _G.__nkm_source_ok = source_ok
99
+ end
100
+
101
+ function _G.__nkm_emit_and_quit(mode_label)
102
+ if vim.v.errmsg and vim.v.errmsg ~= '' then
103
+ add_warning('vim_errmsg: ' .. tostring(vim.v.errmsg))
104
+ end
105
+
106
+ local payload = {
107
+ keymaps = user_keymaps,
108
+ _meta = {
109
+ leader = vim.g.mapleader or vim.g.maplocalleader or '\\\\',
110
+ mapleader_set = (vim.g.mapleader ~= nil or vim.g.maplocalleader ~= nil),
111
+ config_path = vim.fn.stdpath('config'),
112
+ source_ok = _G.__nkm_source_ok,
113
+ extraction_mode = mode_label,
114
+ },
115
+ _warnings = warnings,
116
+ }
117
+
118
+ io.write('${START_MARKER}\\n')
119
+ io.write(vim.json.encode(payload))
120
+ io.write('\\n${END_MARKER}\\n')
121
+ vim.cmd('qa!')
122
+ end
123
+ `;
124
+
125
+ export async function extractKeymaps() {
126
+ const tempDir = await mkdtemp(join(tmpdir(), "nvim-keymap-migrator-"));
127
+ const scriptPath = join(tempDir, "inject.lua");
128
+
129
+ try {
130
+ await writeFile(scriptPath, INJECT_LUA, "utf8");
131
+ let runtimeError = null;
132
+ let payload = null;
133
+
134
+ try {
135
+ payload = await runHeadlessExtraction(scriptPath, tempDir, "runtime");
136
+ } catch (error) {
137
+ runtimeError = error;
138
+ }
139
+
140
+ if (!payload) {
141
+ payload = await runHeadlessExtraction(scriptPath, tempDir, "strict");
142
+ payload._meta = payload._meta ?? {};
143
+ payload._meta.fallback_from = "runtime";
144
+ payload._meta.fallback_reason = summarizeError(runtimeError);
145
+ payload._warnings = payload._warnings ?? [];
146
+ payload._warnings.unshift(
147
+ `runtime_extraction_failed: ${summarizeError(runtimeError)}`,
148
+ );
149
+ }
150
+
151
+ const keymaps = Array.isArray(payload?.keymaps) ? payload.keymaps : [];
152
+
153
+ keymaps._meta = payload?._meta ?? {};
154
+ keymaps._warnings = payload?._warnings ?? [];
155
+
156
+ return keymaps;
157
+ } finally {
158
+ await rm(tempDir, { recursive: true, force: true });
159
+ }
160
+ }
161
+
162
+ function runHeadlessExtraction(scriptPath, tempDir, mode) {
163
+ return new Promise((resolve, reject) => {
164
+ const logPath = join(tempDir, `nvim-${mode}.log`);
165
+ const args = ["--headless"];
166
+
167
+ if (mode === "strict") {
168
+ args.push("-u", "NONE");
169
+ }
170
+
171
+ args.push("--cmd", `lua dofile([[${scriptPath}]])`);
172
+
173
+ if (mode === "strict") {
174
+ args.push("-c", "lua _G.__nkm_source_user_config()");
175
+ }
176
+
177
+ args.push("-c", `lua _G.__nkm_emit_and_quit('${mode}')`);
178
+
179
+ const nvim = spawn("nvim", args, {
180
+ env: {
181
+ ...process.env,
182
+ NVIM_LOG_FILE: logPath,
183
+ },
184
+ });
185
+
186
+ let stdout = "";
187
+ let stderr = "";
188
+
189
+ nvim.on("error", (err) => {
190
+ reject(new Error(`Failed to start Neovim process: ${err.message}`));
191
+ });
192
+
193
+ nvim.stdout.on("data", (data) => {
194
+ stdout += data.toString();
195
+ });
196
+
197
+ nvim.stderr.on("data", (data) => {
198
+ stderr += data.toString();
199
+ });
200
+
201
+ nvim.on("close", (code) => {
202
+ const combined = `${stdout}\n${stderr}`;
203
+ let payload = null;
204
+ try {
205
+ payload = extractPayload(combined);
206
+ } catch (error) {
207
+ reject(
208
+ new Error(
209
+ `Failed to parse extraction payload (${mode}): ${error.message}`,
210
+ ),
211
+ );
212
+ return;
213
+ }
214
+
215
+ if (payload !== null) {
216
+ resolve(payload);
217
+ return;
218
+ }
219
+
220
+ if (code !== 0) {
221
+ reject(
222
+ new Error(
223
+ `Neovim exited with code ${code}. Output:\n${combined.trim()}`,
224
+ ),
225
+ );
226
+ return;
227
+ }
228
+
229
+ reject(
230
+ new Error(
231
+ `Extraction output not found in Neovim stdout/stderr (${mode}).`,
232
+ ),
233
+ );
234
+ });
235
+ });
236
+ }
237
+
238
+ function extractPayload(output) {
239
+ const start = output.indexOf(START_MARKER);
240
+ const end = output.indexOf(END_MARKER);
241
+
242
+ if (start === -1 || end === -1 || end <= start) {
243
+ return null;
244
+ }
245
+
246
+ const json = output.slice(start + START_MARKER.length, end).trim();
247
+ if (!json) {
248
+ return null;
249
+ }
250
+
251
+ return JSON.parse(json);
252
+ }
253
+
254
+ function summarizeError(error) {
255
+ if (!error) {
256
+ return "unknown_runtime_error";
257
+ }
258
+
259
+ const message =
260
+ typeof error.message === "string" && error.message.trim()
261
+ ? error.message.trim()
262
+ : String(error);
263
+
264
+ return message.split("\n")[0];
265
+ }
@@ -0,0 +1,178 @@
1
+ import { loadMappings, lookupIntent } from "../registry.js";
2
+ import { loadDefaults, MODE_TO_MAP, readString, truthy } from "../utils.js";
3
+
4
+ export function generateIdeaVimrc(keymaps = [], options = {}) {
5
+ const registry = options.registry ?? loadMappings();
6
+ const defaults = loadDefaults();
7
+ const defaultKeymaps = Array.isArray(defaults.keymaps)
8
+ ? defaults.keymaps
9
+ : [];
10
+
11
+ const userBindings = new Set();
12
+ for (const keymap of keymaps) {
13
+ const mode = normalizeMode(keymap.mode);
14
+ const lhs = readString(keymap.lhs);
15
+ if (!mode || !lhs) {
16
+ continue;
17
+ }
18
+ userBindings.add(`${mode}|${lhs}`);
19
+ }
20
+
21
+ const defaultLines = [];
22
+ let defaultsAdded = 0;
23
+ for (const def of defaultKeymaps) {
24
+ const mode = normalizeMode(def.mode);
25
+ const lhs = readString(def.lhs);
26
+ if (!mode || !lhs) continue;
27
+ if (userBindings.has(`${mode}|${lhs}`)) {
28
+ continue;
29
+ }
30
+
31
+ const intent = readString(def.intent);
32
+ const action = lookupIntent(intent, "intellij", registry);
33
+ if (!action) continue;
34
+
35
+ const mapCmd = pickMapCommand(mode);
36
+ const line = `${mapCmd} ${def.lhs} <Action>(${action})`;
37
+ defaultLines.push(line);
38
+ defaultsAdded += 1;
39
+ }
40
+
41
+ const mapped = [];
42
+ const pureVimLines = [];
43
+ const manual = [];
44
+ const seen = new Set();
45
+
46
+ for (const keymap of keymaps) {
47
+ const lhs = readString(keymap.lhs);
48
+ const mode = normalizeMode(keymap.mode);
49
+ const intent = readString(keymap.intent);
50
+ if (!lhs || !mode) {
51
+ continue;
52
+ }
53
+
54
+ // no intent - output as pure vim mapping
55
+ if (!intent) {
56
+ const opts = readOpts(keymap);
57
+ const rhs = readRhs(keymap);
58
+ if (!rhs || rhs === "<Lua function>") {
59
+ continue;
60
+ }
61
+
62
+ // use nnoremap for all pure vim mappings
63
+ const mapCmd = "nnoremap";
64
+ const flags = [];
65
+ if (truthy(opts.silent)) flags.push("<silent>");
66
+ if (truthy(opts.expr)) flags.push("<expr>");
67
+ if (truthy(opts.nowait)) flags.push("<nowait>");
68
+ if (truthy(opts.buffer)) flags.push("<buffer>");
69
+
70
+ const line = `${[mapCmd, ...flags].join(" ")} ${lhs} ${rhs}`;
71
+ const key = `${mode}|${lhs}|${mapCmd}|${flags.join(",")}`;
72
+ if (!seen.has(key)) {
73
+ seen.add(key);
74
+ pureVimLines.push(line);
75
+ }
76
+ continue;
77
+ }
78
+
79
+ const action = lookupIntent(intent, "intellij", registry);
80
+ if (!action) {
81
+ manual.push({
82
+ lhs,
83
+ intent,
84
+ });
85
+ continue;
86
+ }
87
+
88
+ const opts = readOpts(keymap);
89
+ const mapCmd = pickMapCommand(mode, opts.noremap);
90
+ const flags = [];
91
+ if (truthy(opts.silent)) flags.push("<silent>");
92
+ if (truthy(opts.expr)) flags.push("<expr>");
93
+ if (truthy(opts.nowait)) flags.push("<nowait>");
94
+ if (truthy(opts.buffer)) flags.push("<buffer>");
95
+
96
+ const line = `${[mapCmd, ...flags].join(" ")} ${lhs} <Action>(${action})`;
97
+ const key = `${mode}|${lhs}|${action}|${mapCmd}|${flags.join(",")}`;
98
+ if (seen.has(key)) {
99
+ continue;
100
+ }
101
+ seen.add(key);
102
+ mapped.push(line);
103
+ }
104
+
105
+ const lines = [
106
+ '" Generated by nvim-keymap-migrator',
107
+ '" IntelliJ IdeaVim action mappings',
108
+ "",
109
+ ];
110
+
111
+ if (defaultLines.length > 0) {
112
+ lines.push('" Default LSP keymaps (from Neovim defaults)');
113
+ lines.push(...defaultLines);
114
+ lines.push("");
115
+ }
116
+
117
+ if (pureVimLines.length > 0) {
118
+ lines.push('" Pure Vim mappings (native Vim motions)');
119
+ lines.push(...pureVimLines);
120
+ lines.push("");
121
+ }
122
+
123
+ if (mapped.length === 0 && pureVimLines.length === 0) {
124
+ lines.push('" No IDE-translatable mappings detected.');
125
+ } else {
126
+ lines.push(...mapped);
127
+ }
128
+
129
+ lines.push("");
130
+ lines.push(`" Generated actions: ${defaultLines.length + mapped.length}`);
131
+ lines.push(`" Pure Vim mappings: ${pureVimLines.length}`);
132
+ lines.push(`" Manual mappings: ${manual.length}`);
133
+
134
+ if (manual.length > 0) {
135
+ lines.push('" Manual review:');
136
+ for (const item of manual.slice(0, 20)) {
137
+ lines.push(`" ${item.lhs} -> ${item.intent}`);
138
+ }
139
+ }
140
+
141
+ const text = `${lines.join("\n")}\n`;
142
+ return { text, defaultsAdded };
143
+ }
144
+
145
+ function normalizeMode(mode) {
146
+ if (typeof mode !== "string" || !MODE_TO_MAP[mode]) {
147
+ return null;
148
+ }
149
+ return mode;
150
+ }
151
+
152
+ function pickMapCommand(mode, noremap = false) {
153
+ const table = MODE_TO_MAP[mode];
154
+ return noremap ? table.noremap : table.map;
155
+ }
156
+
157
+ function readRhs(keymap) {
158
+ const rawRhs = keymap?.raw_rhs;
159
+ if (typeof rawRhs === "string" && rawRhs.trim()) {
160
+ return rawRhs.trim();
161
+ }
162
+ const rhs = keymap?.rhs;
163
+ return typeof rhs === "string" ? rhs.trim() : "";
164
+ }
165
+
166
+ function readOpts(keymap) {
167
+ if (keymap && keymap.opts && typeof keymap.opts === "object") {
168
+ return keymap.opts;
169
+ }
170
+
171
+ return {
172
+ silent: keymap?.silent,
173
+ noremap: keymap?.noremap ?? false,
174
+ buffer: keymap?.buffer,
175
+ nowait: keymap?.nowait,
176
+ expr: keymap?.expr,
177
+ };
178
+ }
@@ -0,0 +1,118 @@
1
+ import { MODE_TO_MAP, truthy } from "../utils.js";
2
+
3
+ const EXCLUDED_INTENT_PREFIXES = [
4
+ "lsp.",
5
+ "git.",
6
+ "harpoon.",
7
+ "todo.",
8
+ "plugin.",
9
+ ];
10
+
11
+ export function generateVimrc(keymaps = []) {
12
+ const lines = [
13
+ '" Generated by nvim-keymap-migrator',
14
+ '" Shared pure-Vim mappings',
15
+ "",
16
+ ];
17
+
18
+ let generated = 0;
19
+ let skipped = 0;
20
+
21
+ for (const keymap of keymaps) {
22
+ const line = toVimMapLine(keymap);
23
+ if (!line) {
24
+ skipped += 1;
25
+ continue;
26
+ }
27
+ lines.push(line);
28
+ generated += 1;
29
+ }
30
+
31
+ if (generated === 0) {
32
+ lines.push('" No pure Vim-compatible mappings detected.');
33
+ }
34
+
35
+ lines.push("");
36
+ lines.push(`" Generated mappings: ${generated}`);
37
+ lines.push(`" Skipped mappings: ${skipped}`);
38
+
39
+ return `${lines.join("\n")}\n`;
40
+ }
41
+
42
+ function toVimMapLine(keymap = {}) {
43
+ const mode = normalizeMode(keymap.mode);
44
+ if (!mode) return null;
45
+
46
+ const lhs = readField(keymap, "lhs");
47
+ const rhs = readRhs(keymap);
48
+ if (!lhs || !rhs || rhs === "<Lua function>") {
49
+ return null;
50
+ }
51
+
52
+ if (lhs.startsWith("<Plug>") || lhs.startsWith("<plug>")) {
53
+ return null;
54
+ }
55
+
56
+ const intent = typeof keymap.intent === "string" ? keymap.intent : "";
57
+ if (shouldExcludeIntent(intent)) {
58
+ return null;
59
+ }
60
+
61
+ const opts = getOpts(keymap);
62
+ const mapCmd = pickMapCommand(mode, opts.noremap);
63
+ const flags = [];
64
+ if (truthy(opts.silent)) flags.push("<silent>");
65
+ if (truthy(opts.expr)) flags.push("<expr>");
66
+ if (truthy(opts.nowait)) flags.push("<nowait>");
67
+ if (truthy(opts.buffer)) flags.push("<buffer>");
68
+
69
+ const prefix = [mapCmd, ...flags].join(" ");
70
+ return `${prefix} ${lhs} ${rhs}`;
71
+ }
72
+
73
+ function pickMapCommand(mode, noremap) {
74
+ const table = MODE_TO_MAP[mode] ?? { noremap: "noremap", map: "map" };
75
+ return truthy(noremap) ? table.noremap : table.map;
76
+ }
77
+
78
+ function normalizeMode(mode) {
79
+ if (typeof mode !== "string") return null;
80
+ if (mode.length === 0) return null;
81
+ if (MODE_TO_MAP[mode]) return mode;
82
+ return null;
83
+ }
84
+
85
+ function readRhs(keymap) {
86
+ const rawRhs = readField(keymap, "raw_rhs");
87
+ if (rawRhs) return rawRhs;
88
+ return readField(keymap, "rhs");
89
+ }
90
+
91
+ function readField(obj, name) {
92
+ if (!obj || typeof obj !== "object") return "";
93
+ const value = obj[name];
94
+ return typeof value === "string" ? value.trim() : "";
95
+ }
96
+
97
+ function getOpts(keymap) {
98
+ if (keymap && keymap.opts && typeof keymap.opts === "object") {
99
+ return keymap.opts;
100
+ }
101
+
102
+ return {
103
+ silent: keymap?.silent,
104
+ noremap: keymap?.noremap ?? true,
105
+ buffer: keymap?.buffer,
106
+ nowait: keymap?.nowait,
107
+ expr: keymap?.expr,
108
+ };
109
+ }
110
+
111
+ function shouldExcludeIntent(intent) {
112
+ if (!intent) return false;
113
+ return EXCLUDED_INTENT_PREFIXES.some((prefix) => intent.startsWith(prefix));
114
+ }
115
+
116
+ export function isPureVimMapping(keymap) {
117
+ return Boolean(toVimMapLine(keymap));
118
+ }