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
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { loadMappings, lookupIntent } from "../registry.js";
|
|
2
|
+
import { loadDefaults, readString } from "../utils.js";
|
|
3
|
+
|
|
4
|
+
export const MANAGED_BY_MARKER = "nvim-keymap-migrator";
|
|
5
|
+
|
|
6
|
+
const MODE_TO_SECTION = {
|
|
7
|
+
n: "vim.normalModeKeyBindings",
|
|
8
|
+
v: "vim.visualModeKeyBindings",
|
|
9
|
+
x: "vim.visualModeKeyBindings",
|
|
10
|
+
s: "vim.visualModeKeyBindings",
|
|
11
|
+
i: "vim.insertModeKeyBindings",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const VIM_KEYBINDING_SECTIONS = [
|
|
15
|
+
"vim.normalModeKeyBindings",
|
|
16
|
+
"vim.visualModeKeyBindings",
|
|
17
|
+
"vim.insertModeKeyBindings",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export function generateVSCodeBindings(keymaps = [], options = {}) {
|
|
21
|
+
const registry = options.registry ?? loadMappings();
|
|
22
|
+
const defaults = loadDefaults();
|
|
23
|
+
const defaultKeymaps = Array.isArray(defaults.keymaps)
|
|
24
|
+
? defaults.keymaps
|
|
25
|
+
: [];
|
|
26
|
+
let defaultsAdded = 0;
|
|
27
|
+
const sections = {
|
|
28
|
+
"vim.normalModeKeyBindings": [],
|
|
29
|
+
"vim.visualModeKeyBindings": [],
|
|
30
|
+
"vim.insertModeKeyBindings": [],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const userBindings = new Set();
|
|
34
|
+
for (const keymap of keymaps) {
|
|
35
|
+
const intent = readString(keymap.intent);
|
|
36
|
+
if (intent) {
|
|
37
|
+
const mode = normalizeMode(keymap.mode);
|
|
38
|
+
const lhs = readString(keymap.lhs);
|
|
39
|
+
if (mode && lhs) {
|
|
40
|
+
userBindings.add(`${mode}|${lhs}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const def of defaultKeymaps) {
|
|
46
|
+
const mode = normalizeMode(def.mode);
|
|
47
|
+
const lhs = readString(def.lhs);
|
|
48
|
+
if (!mode || !lhs) continue;
|
|
49
|
+
if (userBindings.has(`${mode}|${lhs}`)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const intent = readString(def.intent);
|
|
54
|
+
const command = lookupIntent(intent, "vscode", registry);
|
|
55
|
+
if (!command) continue;
|
|
56
|
+
|
|
57
|
+
const before = toBeforeTokens(lhs);
|
|
58
|
+
if (before.length === 0) continue;
|
|
59
|
+
|
|
60
|
+
const section = MODE_TO_SECTION[mode];
|
|
61
|
+
sections[section].push({
|
|
62
|
+
before,
|
|
63
|
+
commands: [command],
|
|
64
|
+
_managedBy: MANAGED_BY_MARKER,
|
|
65
|
+
});
|
|
66
|
+
defaultsAdded += 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
const manual = [];
|
|
71
|
+
|
|
72
|
+
for (const keymap of keymaps) {
|
|
73
|
+
const intent = readString(keymap.intent);
|
|
74
|
+
const lhs = readString(keymap.lhs);
|
|
75
|
+
const mode = normalizeMode(keymap.mode);
|
|
76
|
+
if (!intent || !lhs || !mode) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const command = lookupIntent(intent, "vscode", registry);
|
|
81
|
+
if (!command) {
|
|
82
|
+
manual.push({ lhs, intent });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const before = toBeforeTokens(lhs);
|
|
87
|
+
if (before.length === 0) {
|
|
88
|
+
manual.push({ lhs, intent });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const section = MODE_TO_SECTION[mode];
|
|
93
|
+
const entryKey = `${section}|${before.join(",")}|${command}`;
|
|
94
|
+
if (seen.has(entryKey)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
seen.add(entryKey);
|
|
98
|
+
|
|
99
|
+
sections[section].push({
|
|
100
|
+
before,
|
|
101
|
+
commands: [command],
|
|
102
|
+
_managedBy: MANAGED_BY_MARKER,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
...sections,
|
|
108
|
+
_meta: {
|
|
109
|
+
generated: Object.values(sections).reduce(
|
|
110
|
+
(sum, list) => sum + list.length,
|
|
111
|
+
0,
|
|
112
|
+
),
|
|
113
|
+
defaultsAdded,
|
|
114
|
+
manual: manual.length,
|
|
115
|
+
manual_examples: manual.slice(0, 20),
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeMode(mode) {
|
|
121
|
+
if (typeof mode !== "string" || !MODE_TO_SECTION[mode]) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return mode;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function toBeforeTokens(lhs) {
|
|
128
|
+
const tokens = [];
|
|
129
|
+
let i = 0;
|
|
130
|
+
|
|
131
|
+
while (i < lhs.length) {
|
|
132
|
+
const ch = lhs[i];
|
|
133
|
+
if (ch === "<") {
|
|
134
|
+
const end = lhs.indexOf(">", i + 1);
|
|
135
|
+
if (end === -1) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
const token = lhs.slice(i + 1, end).trim();
|
|
139
|
+
const mapped = mapSpecialToken(token);
|
|
140
|
+
if (!mapped) {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
tokens.push(mapped);
|
|
144
|
+
i = end + 1;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
tokens.push(ch);
|
|
149
|
+
i += 1;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return tokens.filter(Boolean);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function mapSpecialToken(token) {
|
|
156
|
+
const raw = token.toLowerCase();
|
|
157
|
+
if (raw === "leader") return "<leader>";
|
|
158
|
+
if (raw === "cr" || raw === "enter" || raw === "return") return "enter";
|
|
159
|
+
if (raw === "esc" || raw === "escape") return "escape";
|
|
160
|
+
if (raw === "tab") return "tab";
|
|
161
|
+
if (raw === "space") return "<space>";
|
|
162
|
+
if (raw === "bs" || raw === "backspace") return "backspace";
|
|
163
|
+
if (raw === "lt") return "<";
|
|
164
|
+
|
|
165
|
+
const ctrl = raw.match(/^c-(.)$/);
|
|
166
|
+
if (ctrl) return `ctrl+${ctrl[1]}`;
|
|
167
|
+
|
|
168
|
+
const shift = raw.match(/^s-(.)$/);
|
|
169
|
+
if (shift) return `shift+${shift[1]}`;
|
|
170
|
+
|
|
171
|
+
const alt = raw.match(/^a-(.)$/);
|
|
172
|
+
if (alt) return `alt+${alt[1]}`;
|
|
173
|
+
|
|
174
|
+
return raw;
|
|
175
|
+
}
|
package/src/install.js
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { readFile, writeFile, access, mkdir, rename } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir, platform } from "node:os";
|
|
4
|
+
import {
|
|
5
|
+
ensureNamespaceDir,
|
|
6
|
+
readMetadata,
|
|
7
|
+
writeMetadata,
|
|
8
|
+
} from "./namespace.js";
|
|
9
|
+
import {
|
|
10
|
+
MANAGED_BY_MARKER,
|
|
11
|
+
VIM_KEYBINDING_SECTIONS,
|
|
12
|
+
} from "./generators/vscode.js";
|
|
13
|
+
|
|
14
|
+
const MARKER_START = '" <<< nvim-keymap-migrator start >>>';
|
|
15
|
+
const MARKER_END = '" >>> nvim-keymap-migrator end <<<';
|
|
16
|
+
|
|
17
|
+
function leaderToVSCodeFormat(leader) {
|
|
18
|
+
if (leader === " ") return "<space>";
|
|
19
|
+
if (leader === "\t") return "<tab>";
|
|
20
|
+
if (leader === "\\") return "\\";
|
|
21
|
+
if (leader === "\n") return "<cr>";
|
|
22
|
+
if (leader === "\r") return "<cr>";
|
|
23
|
+
return leader;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function leaderToIdeaVimFormat(leader) {
|
|
27
|
+
if (leader === " ") return "\\<space>";
|
|
28
|
+
if (leader === "\t") return "\\<tab>";
|
|
29
|
+
if (leader === "\\") return "\\\\";
|
|
30
|
+
if (leader === "\n") return "\\<cr>";
|
|
31
|
+
if (leader === "\r") return "\\<cr>";
|
|
32
|
+
if (leader.includes("<")) {
|
|
33
|
+
return leader.replace(/</g, "\\<");
|
|
34
|
+
}
|
|
35
|
+
return leader;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getIdeaVimrcPath() {
|
|
39
|
+
return join(homedir(), ".ideavimrc");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getVSCodeSettingsPath() {
|
|
43
|
+
const p = platform();
|
|
44
|
+
|
|
45
|
+
if (p === "darwin") {
|
|
46
|
+
return join(
|
|
47
|
+
homedir(),
|
|
48
|
+
"Library/Application Support/Code/User/settings.json",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (p === "win32") {
|
|
53
|
+
const appData = process.env.APPDATA;
|
|
54
|
+
if (appData) {
|
|
55
|
+
return join(appData, "Code/User/settings.json");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return join(homedir(), ".config/Code/User/settings.json");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function fileExists(path) {
|
|
63
|
+
try {
|
|
64
|
+
await access(path);
|
|
65
|
+
return true;
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function readTextFile(path, fallback = "") {
|
|
72
|
+
try {
|
|
73
|
+
return await readFile(path, "utf8");
|
|
74
|
+
} catch {
|
|
75
|
+
return fallback;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function readJsonFile(path, fallback = {}) {
|
|
80
|
+
try {
|
|
81
|
+
const content = await readFile(path, "utf8");
|
|
82
|
+
return JSON.parse(content);
|
|
83
|
+
} catch {
|
|
84
|
+
return fallback;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function ensureDirForFile(filePath) {
|
|
89
|
+
const dir = filePath.substring(0, filePath.lastIndexOf("/"));
|
|
90
|
+
if (dir) {
|
|
91
|
+
await mkdir(dir, { recursive: true });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function writeJsonAtomic(path, data) {
|
|
96
|
+
const dir = path.substring(0, path.lastIndexOf("/"));
|
|
97
|
+
const tempPath = join(dir, `.nkm-temp-${Date.now()}.json`);
|
|
98
|
+
await writeFile(tempPath, JSON.stringify(data, null, 2), "utf8");
|
|
99
|
+
await rename(tempPath, path);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function extractManagedBlock(content) {
|
|
103
|
+
const startIdx = content.indexOf(MARKER_START);
|
|
104
|
+
const endIdx = content.indexOf(MARKER_END);
|
|
105
|
+
|
|
106
|
+
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
|
|
107
|
+
return { before: content, block: null, after: "" };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const before = content.slice(0, startIdx);
|
|
111
|
+
const block = content.slice(startIdx, endIdx + MARKER_END.length);
|
|
112
|
+
const after = content.slice(endIdx + MARKER_END.length);
|
|
113
|
+
|
|
114
|
+
return { before, block, after };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function buildManagedBlock(mappings, leader) {
|
|
118
|
+
const leaderLine = leader
|
|
119
|
+
? `let mapleader = "${leaderToIdeaVimFormat(leader)}"`
|
|
120
|
+
: "";
|
|
121
|
+
const leaderSection = leaderLine ? `${leaderLine}\n\n` : "";
|
|
122
|
+
|
|
123
|
+
return `${MARKER_START}
|
|
124
|
+
" Managed by nvim-keymap-migrator. Run with --clean to remove.
|
|
125
|
+
${leaderSection}${mappings}
|
|
126
|
+
${MARKER_END}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function integrateIdeaVim(mappings, options = {}) {
|
|
130
|
+
const vimrcPath = getIdeaVimrcPath();
|
|
131
|
+
const content = await readTextFile(vimrcPath, "");
|
|
132
|
+
const { before, block, after } = extractManagedBlock(content);
|
|
133
|
+
|
|
134
|
+
const managedBlock = buildManagedBlock(mappings, options.leader);
|
|
135
|
+
|
|
136
|
+
if (block) {
|
|
137
|
+
const newContent = before + managedBlock + after;
|
|
138
|
+
await writeFile(vimrcPath, newContent, "utf8");
|
|
139
|
+
return { integrated: true, updated: true };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const newContent = content.trim()
|
|
143
|
+
? `${content.trim()}\n\n${managedBlock}\n`
|
|
144
|
+
: `${managedBlock}\n`;
|
|
145
|
+
|
|
146
|
+
await writeFile(vimrcPath, newContent, "utf8");
|
|
147
|
+
return { integrated: true, updated: false };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function cleanIdeaVim() {
|
|
151
|
+
const vimrcPath = getIdeaVimrcPath();
|
|
152
|
+
const exists = await fileExists(vimrcPath);
|
|
153
|
+
|
|
154
|
+
if (!exists) {
|
|
155
|
+
return { cleaned: true, reason: "no_file" };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const content = await readTextFile(vimrcPath, "");
|
|
159
|
+
const { before, block, after } = extractManagedBlock(content);
|
|
160
|
+
|
|
161
|
+
if (!block) {
|
|
162
|
+
return { cleaned: false, reason: "no_block" };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const cleaned = (before + after).trim();
|
|
166
|
+
if (cleaned) {
|
|
167
|
+
await writeFile(vimrcPath, cleaned + "\n", "utf8");
|
|
168
|
+
} else {
|
|
169
|
+
await writeFile(vimrcPath, "", "utf8");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { cleaned: true, reason: "removed_block" };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function integrateVSCode(bindings, options = {}) {
|
|
176
|
+
const settingsPath = getVSCodeSettingsPath();
|
|
177
|
+
await ensureDirForFile(settingsPath);
|
|
178
|
+
|
|
179
|
+
const existing = await readJsonFile(settingsPath, {});
|
|
180
|
+
const warnings = [];
|
|
181
|
+
const sectionsModified = [];
|
|
182
|
+
let setLeader = false;
|
|
183
|
+
|
|
184
|
+
const leader = options.leader ? leaderToVSCodeFormat(options.leader) : null;
|
|
185
|
+
const vimrcPath = "~/.config/nvim-keymap-migrator/.vimrc";
|
|
186
|
+
let setVimrcPath = false;
|
|
187
|
+
|
|
188
|
+
if (leader && existing["vim.leader"] === undefined) {
|
|
189
|
+
existing["vim.leader"] = leader;
|
|
190
|
+
setLeader = true;
|
|
191
|
+
} else if (leader && existing["vim.leader"] !== leader) {
|
|
192
|
+
warnings.push(
|
|
193
|
+
`vim.leader already set to "${existing["vim.leader"]}" (ours: "${leader}"), keeping existing`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (existing["vim.vimrc.path"] === undefined) {
|
|
198
|
+
existing["vim.vimrc.path"] = vimrcPath;
|
|
199
|
+
setVimrcPath = true;
|
|
200
|
+
} else if (existing["vim.vimrc.path"] !== vimrcPath) {
|
|
201
|
+
warnings.push(
|
|
202
|
+
`vim.vimrc.path already set to "${existing["vim.vimrc.path"]}" (ours: "${vimrcPath}"), keeping existing`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let setVimrcEnable = false;
|
|
207
|
+
if (existing["vim.vimrc.enable"] === undefined) {
|
|
208
|
+
existing["vim.vimrc.enable"] = true;
|
|
209
|
+
setVimrcEnable = true;
|
|
210
|
+
} else if (existing["vim.vimrc.enable"] !== true) {
|
|
211
|
+
warnings.push(
|
|
212
|
+
`vim.vimrc.enable is "${existing["vim.vimrc.enable"]}" (expected true), keeping existing`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const section of VIM_KEYBINDING_SECTIONS) {
|
|
217
|
+
const newBindings = bindings[section];
|
|
218
|
+
if (!Array.isArray(newBindings) || newBindings.length === 0) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const existingBindings = Array.isArray(existing[section])
|
|
223
|
+
? existing[section]
|
|
224
|
+
: [];
|
|
225
|
+
|
|
226
|
+
const existingManaged = existingBindings.filter(
|
|
227
|
+
(e) => e._managedBy === MANAGED_BY_MARKER,
|
|
228
|
+
);
|
|
229
|
+
const existingUnmanaged = existingBindings.filter(
|
|
230
|
+
(e) => e._managedBy !== MANAGED_BY_MARKER,
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
if (existingManaged.length > 0) {
|
|
234
|
+
warnings.push(
|
|
235
|
+
`${section}: replacing ${existingManaged.length} previously managed binding(s)`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
existing[section] = [...existingUnmanaged, ...newBindings];
|
|
240
|
+
sectionsModified.push(section);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
await writeJsonAtomic(settingsPath, existing);
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
integrated: true,
|
|
247
|
+
sections: sectionsModified,
|
|
248
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
249
|
+
setLeader,
|
|
250
|
+
setVimrcPath,
|
|
251
|
+
setVimrcEnable,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function cleanVSCode() {
|
|
256
|
+
const settingsPath = getVSCodeSettingsPath();
|
|
257
|
+
const exists = await fileExists(settingsPath);
|
|
258
|
+
|
|
259
|
+
if (!exists) {
|
|
260
|
+
return { cleaned: true, reason: "no_file" };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const existing = await readJsonFile(settingsPath, null);
|
|
264
|
+
|
|
265
|
+
if (!existing || typeof existing !== "object") {
|
|
266
|
+
return { cleaned: false, reason: "invalid_json" };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let totalRemoved = 0;
|
|
270
|
+
const sectionsCleaned = [];
|
|
271
|
+
let removedLeader = false;
|
|
272
|
+
let removedVimrcPath = false;
|
|
273
|
+
let removedVimrcEnable = false;
|
|
274
|
+
|
|
275
|
+
const metadata = await readMetadata();
|
|
276
|
+
if (metadata?.leader_set && existing["vim.leader"] !== undefined) {
|
|
277
|
+
delete existing["vim.leader"];
|
|
278
|
+
removedLeader = true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (metadata?.vimrc_path_set && existing["vim.vimrc.path"] !== undefined) {
|
|
282
|
+
delete existing["vim.vimrc.path"];
|
|
283
|
+
removedVimrcPath = true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (
|
|
287
|
+
metadata?.vimrc_enable_set &&
|
|
288
|
+
existing["vim.vimrc.enable"] !== undefined
|
|
289
|
+
) {
|
|
290
|
+
delete existing["vim.vimrc.enable"];
|
|
291
|
+
removedVimrcEnable = true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
for (const section of VIM_KEYBINDING_SECTIONS) {
|
|
295
|
+
if (!Array.isArray(existing[section])) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const before = existing[section].length;
|
|
300
|
+
existing[section] = existing[section].filter(
|
|
301
|
+
(entry) => entry._managedBy !== MANAGED_BY_MARKER,
|
|
302
|
+
);
|
|
303
|
+
const after = existing[section].length;
|
|
304
|
+
|
|
305
|
+
if (before !== after) {
|
|
306
|
+
totalRemoved += before - after;
|
|
307
|
+
sectionsCleaned.push(section);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (existing[section].length === 0) {
|
|
311
|
+
delete existing[section];
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (
|
|
316
|
+
totalRemoved === 0 &&
|
|
317
|
+
!removedLeader &&
|
|
318
|
+
!removedVimrcPath &&
|
|
319
|
+
!removedVimrcEnable
|
|
320
|
+
) {
|
|
321
|
+
return { cleaned: false, reason: "no_managed_bindings" };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
await writeJsonAtomic(settingsPath, existing);
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
cleaned: true,
|
|
328
|
+
reason: "removed_bindings",
|
|
329
|
+
removed: totalRemoved,
|
|
330
|
+
sections: sectionsCleaned.length > 0 ? sectionsCleaned : undefined,
|
|
331
|
+
removedLeader,
|
|
332
|
+
removedVimrcPath,
|
|
333
|
+
removedVimrcEnable,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function saveMetadata(config, counts, options = {}) {
|
|
338
|
+
await ensureNamespaceDir();
|
|
339
|
+
|
|
340
|
+
const existing = await readMetadata();
|
|
341
|
+
|
|
342
|
+
const metadata = existing
|
|
343
|
+
? {
|
|
344
|
+
version: 1,
|
|
345
|
+
created_at: existing.created_at,
|
|
346
|
+
updated_at: new Date().toISOString(),
|
|
347
|
+
leader: config.leader ?? "\\",
|
|
348
|
+
leader_set: options.leaderSet ?? existing.leader_set ?? false,
|
|
349
|
+
vimrc_path_set:
|
|
350
|
+
options.vimrcPathSet ?? existing.vimrc_path_set ?? false,
|
|
351
|
+
vimrc_enable_set:
|
|
352
|
+
options.vimrcEnableSet ?? existing.vimrc_enable_set ?? false,
|
|
353
|
+
config_path: config.config_path ?? null,
|
|
354
|
+
counts,
|
|
355
|
+
}
|
|
356
|
+
: {
|
|
357
|
+
version: 1,
|
|
358
|
+
created_at: new Date().toISOString(),
|
|
359
|
+
updated_at: new Date().toISOString(),
|
|
360
|
+
leader: config.leader ?? "\\",
|
|
361
|
+
leader_set: options.leaderSet ?? false,
|
|
362
|
+
vimrc_path_set: options.vimrcPathSet ?? false,
|
|
363
|
+
vimrc_enable_set: options.vimrcEnableSet ?? false,
|
|
364
|
+
config_path: config.config_path ?? null,
|
|
365
|
+
counts,
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
await writeMetadata(metadata);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export {
|
|
372
|
+
integrateIdeaVim,
|
|
373
|
+
cleanIdeaVim,
|
|
374
|
+
integrateVSCode,
|
|
375
|
+
cleanVSCode,
|
|
376
|
+
saveMetadata,
|
|
377
|
+
getIdeaVimrcPath,
|
|
378
|
+
getVSCodeSettingsPath,
|
|
379
|
+
};
|
package/src/namespace.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile, access, rm } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
const NAMESPACE_DIR = ".config/nvim-keymap-migrator";
|
|
6
|
+
const METADATA_FILE = "metadata.json";
|
|
7
|
+
|
|
8
|
+
function getNamespaceDir() {
|
|
9
|
+
return join(homedir(), NAMESPACE_DIR);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getMetadataPath() {
|
|
13
|
+
return join(getNamespaceDir(), METADATA_FILE);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getRcPath(editor) {
|
|
17
|
+
if (editor === "neovim") {
|
|
18
|
+
return join(getNamespaceDir(), ".vimrc");
|
|
19
|
+
}
|
|
20
|
+
return join(getNamespaceDir(), `${editor}.rc`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function ensureNamespaceDir() {
|
|
24
|
+
const dir = getNamespaceDir();
|
|
25
|
+
await mkdir(dir, { recursive: true });
|
|
26
|
+
return dir;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function readMetadata() {
|
|
30
|
+
const path = getMetadataPath();
|
|
31
|
+
try {
|
|
32
|
+
await access(path);
|
|
33
|
+
const content = await readFile(path, "utf8");
|
|
34
|
+
return JSON.parse(content);
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function writeMetadata(data) {
|
|
41
|
+
await ensureNamespaceDir();
|
|
42
|
+
const path = getMetadataPath();
|
|
43
|
+
await writeFile(path, JSON.stringify(data, null, 2), "utf8");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function namespaceExists() {
|
|
47
|
+
try {
|
|
48
|
+
await access(getNamespaceDir());
|
|
49
|
+
return true;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export {
|
|
56
|
+
getNamespaceDir,
|
|
57
|
+
getMetadataPath,
|
|
58
|
+
getRcPath,
|
|
59
|
+
ensureNamespaceDir,
|
|
60
|
+
readMetadata,
|
|
61
|
+
writeMetadata,
|
|
62
|
+
namespaceExists,
|
|
63
|
+
};
|
package/src/registry.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
6
|
+
const TEMPLATE_DIR = join(ROOT, "templates");
|
|
7
|
+
|
|
8
|
+
function readJson(name) {
|
|
9
|
+
try {
|
|
10
|
+
const raw = readFileSync(join(TEMPLATE_DIR, name), "utf8");
|
|
11
|
+
return JSON.parse(raw);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
throw new Error(`Failed to load template ${name}: ${error.message}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function loadMappings() {
|
|
18
|
+
const aliases = readJson("aliases.json");
|
|
19
|
+
const normalizedAliases = {};
|
|
20
|
+
for (const [key, value] of Object.entries(aliases)) {
|
|
21
|
+
normalizedAliases[normalizeIntentKey(key)] = value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
mappings: {
|
|
26
|
+
...readJson("lsp-mappings.json"),
|
|
27
|
+
...readJson("git-mappings.json"),
|
|
28
|
+
...readJson("navigation-mappings.json"),
|
|
29
|
+
...readJson("editing-mappings.json"),
|
|
30
|
+
},
|
|
31
|
+
aliases: normalizedAliases,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function lookupIntent(intent, editor, registry = loadMappings()) {
|
|
36
|
+
const key = normalizeIntentKey(intent);
|
|
37
|
+
const normalized = registry.aliases[key] ?? key;
|
|
38
|
+
return registry.mappings[normalized]?.[editor] ?? null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeIntentKey(value) {
|
|
42
|
+
if (typeof value !== "string") {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return value.trim().toLowerCase().replace(/\s+/g, " ");
|
|
47
|
+
}
|