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/LICENSE +674 -0
- package/README.md +193 -0
- package/index.js +423 -0
- package/package.json +41 -0
- package/src/config.js +207 -0
- package/src/detector.js +401 -0
- package/src/extractor.js +265 -0
- package/src/generators/intellij.js +178 -0
- package/src/generators/vimrc.js +118 -0
- package/src/generators/vscode.js +175 -0
- package/src/install.js +379 -0
- package/src/namespace.js +63 -0
- package/src/registry.js +47 -0
- package/src/report.js +81 -0
- package/src/utils.js +36 -0
- package/templates/aliases.json +31 -0
- package/templates/defaults.json +11 -0
- package/templates/editing-mappings.json +34 -0
- package/templates/git-mappings.json +38 -0
- package/templates/lsp-mappings.json +62 -0
- package/templates/navigation-mappings.json +50 -0
package/src/extractor.js
ADDED
|
@@ -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
|
+
}
|