@involvex/fresh-editor 0.1.76 → 0.1.78
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/bin/CHANGELOG.md +1017 -0
- package/bin/LICENSE +117 -0
- package/bin/README.md +248 -0
- package/bin/fresh.exe +0 -0
- package/bin/plugins/README.md +71 -0
- package/bin/plugins/audit_mode.i18n.json +821 -0
- package/bin/plugins/audit_mode.ts +1810 -0
- package/bin/plugins/buffer_modified.i18n.json +67 -0
- package/bin/plugins/buffer_modified.ts +281 -0
- package/bin/plugins/calculator.i18n.json +93 -0
- package/bin/plugins/calculator.ts +770 -0
- package/bin/plugins/clangd-lsp.ts +168 -0
- package/bin/plugins/clangd_support.i18n.json +223 -0
- package/bin/plugins/clangd_support.md +20 -0
- package/bin/plugins/clangd_support.ts +325 -0
- package/bin/plugins/color_highlighter.i18n.json +145 -0
- package/bin/plugins/color_highlighter.ts +304 -0
- package/bin/plugins/config-schema.json +768 -0
- package/bin/plugins/csharp-lsp.ts +147 -0
- package/bin/plugins/csharp_support.i18n.json +80 -0
- package/bin/plugins/csharp_support.ts +170 -0
- package/bin/plugins/css-lsp.ts +143 -0
- package/bin/plugins/diagnostics_panel.i18n.json +236 -0
- package/bin/plugins/diagnostics_panel.ts +642 -0
- package/bin/plugins/examples/README.md +85 -0
- package/bin/plugins/examples/async_demo.ts +165 -0
- package/bin/plugins/examples/bookmarks.ts +329 -0
- package/bin/plugins/examples/buffer_query_demo.ts +110 -0
- package/bin/plugins/examples/git_grep.ts +262 -0
- package/bin/plugins/examples/hello_world.ts +93 -0
- package/bin/plugins/examples/virtual_buffer_demo.ts +116 -0
- package/bin/plugins/find_references.i18n.json +275 -0
- package/bin/plugins/find_references.ts +359 -0
- package/bin/plugins/git_blame.i18n.json +496 -0
- package/bin/plugins/git_blame.ts +707 -0
- package/bin/plugins/git_find_file.i18n.json +314 -0
- package/bin/plugins/git_find_file.ts +300 -0
- package/bin/plugins/git_grep.i18n.json +171 -0
- package/bin/plugins/git_grep.ts +191 -0
- package/bin/plugins/git_gutter.i18n.json +93 -0
- package/bin/plugins/git_gutter.ts +477 -0
- package/bin/plugins/git_log.i18n.json +481 -0
- package/bin/plugins/git_log.ts +1285 -0
- package/bin/plugins/go-lsp.ts +143 -0
- package/bin/plugins/html-lsp.ts +145 -0
- package/bin/plugins/json-lsp.ts +145 -0
- package/bin/plugins/lib/fresh.d.ts +1321 -0
- package/bin/plugins/lib/index.ts +24 -0
- package/bin/plugins/lib/navigation-controller.ts +214 -0
- package/bin/plugins/lib/panel-manager.ts +220 -0
- package/bin/plugins/lib/types.ts +72 -0
- package/bin/plugins/lib/virtual-buffer-factory.ts +130 -0
- package/bin/plugins/live_grep.i18n.json +171 -0
- package/bin/plugins/live_grep.ts +422 -0
- package/bin/plugins/markdown_compose.i18n.json +223 -0
- package/bin/plugins/markdown_compose.ts +630 -0
- package/bin/plugins/merge_conflict.i18n.json +821 -0
- package/bin/plugins/merge_conflict.ts +1810 -0
- package/bin/plugins/path_complete.i18n.json +80 -0
- package/bin/plugins/path_complete.ts +165 -0
- package/bin/plugins/python-lsp.ts +162 -0
- package/bin/plugins/rust-lsp.ts +166 -0
- package/bin/plugins/search_replace.i18n.json +405 -0
- package/bin/plugins/search_replace.ts +484 -0
- package/bin/plugins/test_i18n.i18n.json +67 -0
- package/bin/plugins/test_i18n.ts +18 -0
- package/bin/plugins/theme_editor.i18n.json +3746 -0
- package/bin/plugins/theme_editor.ts +2063 -0
- package/bin/plugins/todo_highlighter.i18n.json +184 -0
- package/bin/plugins/todo_highlighter.ts +206 -0
- package/bin/plugins/typescript-lsp.ts +167 -0
- package/bin/plugins/vi_mode.i18n.json +1549 -0
- package/bin/plugins/vi_mode.ts +2747 -0
- package/bin/plugins/welcome.i18n.json +236 -0
- package/bin/plugins/welcome.ts +76 -0
- package/bin/themes/dark.json +102 -0
- package/bin/themes/dracula.json +62 -0
- package/bin/themes/high-contrast.json +102 -0
- package/bin/themes/light.json +102 -0
- package/bin/themes/nord.json +62 -0
- package/bin/themes/nostalgia.json +102 -0
- package/bin/themes/solarized-dark.json +62 -0
- package/binary-install.js +1 -1
- package/dist/bin/fresh.js +9 -0
- package/dist/binary-install.js +149 -0
- package/dist/binary.js +30 -0
- package/dist/fresh-6yhknp07.exe +0 -0
- package/dist/install.js +158 -0
- package/dist/run-fresh.js +43 -0
- package/package.json +7 -2
|
@@ -0,0 +1,2747 @@
|
|
|
1
|
+
/// <reference path="./lib/fresh.d.ts" />
|
|
2
|
+
const editor = getEditor();
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Vi Mode Plugin for Fresh Editor
|
|
7
|
+
*
|
|
8
|
+
* Implements vi-style modal editing with:
|
|
9
|
+
* - Normal mode: navigation and commands
|
|
10
|
+
* - Insert mode: text input
|
|
11
|
+
* - Operator-pending mode: composable operators with motions
|
|
12
|
+
*
|
|
13
|
+
* Uses the plugin API's executeAction() for true operator+motion composability:
|
|
14
|
+
* any operator works with any motion via O(operators + motions) code.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Vi mode state
|
|
18
|
+
type ViMode = "normal" | "insert" | "operator-pending" | "find-char" | "visual" | "visual-line" | "visual-block" | "text-object";
|
|
19
|
+
type FindCharType = "f" | "t" | "F" | "T" | null;
|
|
20
|
+
type TextObjectType = "inner" | "around" | null;
|
|
21
|
+
|
|
22
|
+
// Types for tracking repeatable changes
|
|
23
|
+
type ChangeType = "simple" | "operator-motion" | "operator-textobj" | "insert" | "line-op";
|
|
24
|
+
|
|
25
|
+
interface LastChange {
|
|
26
|
+
type: ChangeType;
|
|
27
|
+
action?: string; // For simple actions like "delete_forward", "delete_line"
|
|
28
|
+
operator?: string; // For operator+motion/textobj: "d", "c", "y"
|
|
29
|
+
motion?: string; // For operator+motion: the motion action
|
|
30
|
+
textObject?: { modifier: TextObjectType; object: string }; // For operator+textobj
|
|
31
|
+
count?: number; // Count used with the command
|
|
32
|
+
insertedText?: string; // Text inserted during insert mode
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ViState {
|
|
36
|
+
mode: ViMode;
|
|
37
|
+
pendingOperator: string | null;
|
|
38
|
+
pendingFindChar: FindCharType; // For f/t/F/T motions
|
|
39
|
+
pendingTextObject: TextObjectType; // For i/a text objects
|
|
40
|
+
lastFindChar: { type: FindCharType; char: string } | null; // For ; and , repeat
|
|
41
|
+
count: number | null;
|
|
42
|
+
lastChange: LastChange | null; // For '.' repeat
|
|
43
|
+
lastYankWasLinewise: boolean; // Track if last yank was line-wise for proper paste
|
|
44
|
+
visualAnchor: number | null; // Starting position for visual mode selection
|
|
45
|
+
insertStartPos: number | null; // Cursor position when entering insert mode
|
|
46
|
+
visualBlockAnchor: { line: number; col: number } | null; // For visual block mode
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const state: ViState = {
|
|
50
|
+
mode: "normal",
|
|
51
|
+
pendingOperator: null,
|
|
52
|
+
pendingFindChar: null,
|
|
53
|
+
pendingTextObject: null,
|
|
54
|
+
lastFindChar: null,
|
|
55
|
+
count: null,
|
|
56
|
+
lastChange: null,
|
|
57
|
+
lastYankWasLinewise: false,
|
|
58
|
+
visualAnchor: null,
|
|
59
|
+
insertStartPos: null,
|
|
60
|
+
visualBlockAnchor: null,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Mode indicator for status bar
|
|
64
|
+
function getModeIndicator(mode: ViMode): string {
|
|
65
|
+
const countPrefix = state.count !== null ? `${state.count} ` : "";
|
|
66
|
+
switch (mode) {
|
|
67
|
+
case "normal":
|
|
68
|
+
return `-- ${editor.t("mode.normal")} --${countPrefix ? ` (${state.count})` : ""}`;
|
|
69
|
+
case "insert":
|
|
70
|
+
return `-- ${editor.t("mode.insert")} --`;
|
|
71
|
+
case "operator-pending":
|
|
72
|
+
return `-- ${editor.t("mode.operator")} (${state.pendingOperator}) --${countPrefix ? ` (${state.count})` : ""}`;
|
|
73
|
+
case "find-char":
|
|
74
|
+
return `-- ${editor.t("mode.find")} (${state.pendingFindChar}) --`;
|
|
75
|
+
case "visual":
|
|
76
|
+
return `-- ${editor.t("mode.visual")} --${countPrefix ? ` (${state.count})` : ""}`;
|
|
77
|
+
case "visual-line":
|
|
78
|
+
return `-- ${editor.t("mode.visual_line")} --${countPrefix ? ` (${state.count})` : ""}`;
|
|
79
|
+
case "visual-block":
|
|
80
|
+
return `-- ${editor.t("mode.visual_block")} --${countPrefix ? ` (${state.count})` : ""}`;
|
|
81
|
+
case "text-object":
|
|
82
|
+
return `-- ${state.pendingOperator}${state.pendingTextObject === "inner" ? "i" : "a"}? --`;
|
|
83
|
+
default:
|
|
84
|
+
return "";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Switch between modes
|
|
89
|
+
function switchMode(newMode: ViMode): void {
|
|
90
|
+
const oldMode = state.mode;
|
|
91
|
+
state.mode = newMode;
|
|
92
|
+
|
|
93
|
+
// Only clear pendingOperator when leaving operator-pending and text-object modes
|
|
94
|
+
if (newMode !== "operator-pending" && newMode !== "text-object") {
|
|
95
|
+
state.pendingOperator = null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Clear text object type when leaving text-object mode
|
|
99
|
+
if (newMode !== "text-object") {
|
|
100
|
+
state.pendingTextObject = null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Preserve count when entering operator-pending or text-object mode (for 3dw = delete 3 words)
|
|
104
|
+
// Also preserve count in visual modes
|
|
105
|
+
if (newMode !== "operator-pending" && newMode !== "text-object" &&
|
|
106
|
+
newMode !== "visual" && newMode !== "visual-line" && newMode !== "visual-block") {
|
|
107
|
+
state.count = null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Clear visual anchor when leaving visual modes
|
|
111
|
+
if (newMode !== "visual" && newMode !== "visual-line" && newMode !== "visual-block") {
|
|
112
|
+
state.visualAnchor = null;
|
|
113
|
+
state.visualBlockAnchor = null;
|
|
114
|
+
// Clear any selection when leaving visual mode by moving cursor
|
|
115
|
+
// (any non-select movement clears selection in Fresh)
|
|
116
|
+
if (oldMode === "visual" || oldMode === "visual-line" || oldMode === "visual-block") {
|
|
117
|
+
editor.executeAction("move_left");
|
|
118
|
+
editor.executeAction("move_right");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Track insert mode start position for '.' repeat
|
|
123
|
+
if (newMode === "insert" && oldMode !== "insert") {
|
|
124
|
+
state.insertStartPos = editor.getCursorPosition();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Capture inserted text when leaving insert mode (for '.' repeat)
|
|
128
|
+
if (oldMode === "insert" && newMode !== "insert" && state.insertStartPos !== null) {
|
|
129
|
+
captureInsertedText();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// All modes use vi-{mode} naming, including insert mode
|
|
133
|
+
// vi-insert has read_only=false so normal typing works, but Escape is bound
|
|
134
|
+
editor.setEditorMode(`vi-${newMode}`);
|
|
135
|
+
editor.setStatus(getModeIndicator(newMode));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Capture text inserted during insert mode for '.' repeat
|
|
139
|
+
async function captureInsertedText(): Promise<void> {
|
|
140
|
+
if (state.insertStartPos === null) return;
|
|
141
|
+
|
|
142
|
+
const endPos = editor.getCursorPosition();
|
|
143
|
+
if (endPos === null || endPos <= state.insertStartPos) {
|
|
144
|
+
state.insertStartPos = null;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const bufferId = editor.getActiveBufferId();
|
|
149
|
+
const text = await editor.getBufferText(bufferId, state.insertStartPos, endPos);
|
|
150
|
+
|
|
151
|
+
if (text && text.length > 0) {
|
|
152
|
+
// Only record if we have a pending insert change or if there was actual text inserted
|
|
153
|
+
if (state.lastChange?.type === "insert" || !state.lastChange) {
|
|
154
|
+
state.lastChange = {
|
|
155
|
+
type: "insert",
|
|
156
|
+
insertedText: text,
|
|
157
|
+
};
|
|
158
|
+
} else if (state.lastChange.type === "simple" || state.lastChange.type === "operator-motion" ||
|
|
159
|
+
state.lastChange.type === "operator-textobj" || state.lastChange.type === "line-op") {
|
|
160
|
+
// A change command (c, s, etc.) was used - append the inserted text
|
|
161
|
+
state.lastChange.insertedText = text;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
state.insertStartPos = null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Get the current count (defaults to 1 if no count specified)
|
|
169
|
+
// Does NOT clear the count - that's done in switchMode or explicitly
|
|
170
|
+
function getCount(): number {
|
|
171
|
+
return state.count ?? 1;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Consume the current count and clear it
|
|
175
|
+
// Returns the count (defaults to 1)
|
|
176
|
+
function consumeCount(): number {
|
|
177
|
+
const count = state.count ?? 1;
|
|
178
|
+
state.count = null;
|
|
179
|
+
return count;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Accumulate a digit into the count
|
|
183
|
+
function accumulateCount(digit: number): void {
|
|
184
|
+
if (state.count === null) {
|
|
185
|
+
state.count = digit;
|
|
186
|
+
} else {
|
|
187
|
+
state.count = state.count * 10 + digit;
|
|
188
|
+
}
|
|
189
|
+
// Update status to show accumulated count
|
|
190
|
+
editor.setStatus(getModeIndicator(state.mode));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Execute a single action with count (uses new executeActions API for efficiency)
|
|
194
|
+
function executeWithCount(action: string, count?: number): void {
|
|
195
|
+
const n = count ?? consumeCount();
|
|
196
|
+
if (n === 1) {
|
|
197
|
+
editor.executeAction(action);
|
|
198
|
+
} else {
|
|
199
|
+
editor.executeActions([{ action, count: n }]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Map motion actions to their selection equivalents
|
|
204
|
+
const motionToSelection: Record<string, string> = {
|
|
205
|
+
move_left: "select_left",
|
|
206
|
+
move_right: "select_right",
|
|
207
|
+
move_up: "select_up",
|
|
208
|
+
move_down: "select_down",
|
|
209
|
+
move_word_left: "select_word_left",
|
|
210
|
+
move_word_right: "select_word_right",
|
|
211
|
+
move_line_start: "select_line_start",
|
|
212
|
+
move_line_end: "select_line_end",
|
|
213
|
+
move_document_start: "select_document_start",
|
|
214
|
+
move_document_end: "select_document_end",
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Map (operator, motion) pairs to atomic Rust actions
|
|
218
|
+
// These are single actions that combine the operator and motion atomically
|
|
219
|
+
// This avoids async issues with selection-based approach
|
|
220
|
+
type OperatorMotionMap = Record<string, Record<string, string>>;
|
|
221
|
+
const atomicOperatorActions: OperatorMotionMap = {
|
|
222
|
+
d: {
|
|
223
|
+
// Delete operators
|
|
224
|
+
move_word_right: "delete_word_forward",
|
|
225
|
+
move_word_left: "delete_word_backward",
|
|
226
|
+
move_line_end: "delete_to_line_end",
|
|
227
|
+
move_line_start: "delete_to_line_start",
|
|
228
|
+
},
|
|
229
|
+
y: {
|
|
230
|
+
// Yank operators
|
|
231
|
+
move_word_right: "yank_word_forward",
|
|
232
|
+
move_word_left: "yank_word_backward",
|
|
233
|
+
move_line_end: "yank_to_line_end",
|
|
234
|
+
move_line_start: "yank_to_line_start",
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Apply an operator using atomic actions if available, otherwise selection-based approach
|
|
239
|
+
// The count parameter specifies how many times to apply the motion (e.g., d3w = delete 3 words)
|
|
240
|
+
function applyOperatorWithMotion(operator: string, motionAction: string, count: number = 1): void {
|
|
241
|
+
// Record last change for '.' repeat (only for delete and change, not yank)
|
|
242
|
+
if (operator === "d" || operator === "c") {
|
|
243
|
+
state.lastChange = { type: "operator-motion", operator, motion: motionAction, count };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// For "change" operator, use delete action and then enter insert mode
|
|
247
|
+
const lookupOperator = operator === "c" ? "d" : operator;
|
|
248
|
+
|
|
249
|
+
// Check if we have an atomic action for this operator+motion combination
|
|
250
|
+
const operatorActions = atomicOperatorActions[lookupOperator];
|
|
251
|
+
const atomicAction = operatorActions?.[motionAction];
|
|
252
|
+
|
|
253
|
+
if (atomicAction) {
|
|
254
|
+
// Use the atomic action - single command, no async issues
|
|
255
|
+
// Apply count times for 3dw, etc.
|
|
256
|
+
if (count === 1) {
|
|
257
|
+
editor.executeAction(atomicAction);
|
|
258
|
+
} else {
|
|
259
|
+
editor.executeActions([{ action: atomicAction, count }]);
|
|
260
|
+
}
|
|
261
|
+
if (operator === "y") {
|
|
262
|
+
state.lastYankWasLinewise = false;
|
|
263
|
+
}
|
|
264
|
+
if (operator === "c") {
|
|
265
|
+
switchMode("insert");
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
switchMode("normal");
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Fall back to selection-based approach for motions without atomic actions
|
|
273
|
+
const selectAction = motionToSelection[motionAction];
|
|
274
|
+
if (!selectAction) {
|
|
275
|
+
editor.debug(`No selection equivalent for motion: ${motionAction}`);
|
|
276
|
+
switchMode("normal");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Execute the selection action count times (synchronous - extends selection to target)
|
|
281
|
+
if (count === 1) {
|
|
282
|
+
editor.executeAction(selectAction);
|
|
283
|
+
} else {
|
|
284
|
+
editor.executeActions([{ action: selectAction, count }]);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
switch (operator) {
|
|
288
|
+
case "d": // delete
|
|
289
|
+
editor.executeAction("cut"); // Cut removes selection
|
|
290
|
+
break;
|
|
291
|
+
case "c": // change (delete and enter insert mode)
|
|
292
|
+
editor.executeAction("cut");
|
|
293
|
+
switchMode("insert");
|
|
294
|
+
return; // Don't switch back to normal mode
|
|
295
|
+
case "y": // yank
|
|
296
|
+
state.lastYankWasLinewise = false; // Motion-based yank is character-wise
|
|
297
|
+
editor.executeAction("copy");
|
|
298
|
+
// Move cursor back to start of selection (left side)
|
|
299
|
+
editor.executeAction("move_left");
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
switchMode("normal");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Handle motion in operator-pending mode
|
|
307
|
+
// Consumes any pending count and applies it to the motion
|
|
308
|
+
function handleMotionWithOperator(motionAction: string): void {
|
|
309
|
+
if (!state.pendingOperator) {
|
|
310
|
+
switchMode("normal");
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const count = consumeCount();
|
|
315
|
+
applyOperatorWithMotion(state.pendingOperator, motionAction, count);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ============================================================================
|
|
319
|
+
// Normal Mode Commands
|
|
320
|
+
// ============================================================================
|
|
321
|
+
|
|
322
|
+
// Navigation (all support count prefix, e.g., 5j moves down 5 lines)
|
|
323
|
+
globalThis.vi_left = function (): void {
|
|
324
|
+
executeWithCount("move_left");
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
globalThis.vi_down = function (): void {
|
|
328
|
+
executeWithCount("move_down");
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
globalThis.vi_up = function (): void {
|
|
332
|
+
executeWithCount("move_up");
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
globalThis.vi_right = function (): void {
|
|
336
|
+
executeWithCount("move_right");
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
globalThis.vi_word = function (): void {
|
|
340
|
+
executeWithCount("move_word_right");
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
globalThis.vi_word_back = function (): void {
|
|
344
|
+
executeWithCount("move_word_left");
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
globalThis.vi_word_end = function (): void {
|
|
348
|
+
// Move to end of word - for count, repeat the whole operation
|
|
349
|
+
const count = consumeCount();
|
|
350
|
+
for (let i = 0; i < count; i++) {
|
|
351
|
+
editor.executeAction("move_word_right");
|
|
352
|
+
editor.executeAction("move_left");
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
globalThis.vi_line_start = function (): void {
|
|
357
|
+
consumeCount(); // Count doesn't apply to line start
|
|
358
|
+
editor.executeAction("move_line_start");
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
globalThis.vi_line_end = function (): void {
|
|
362
|
+
consumeCount(); // Count doesn't apply to line end
|
|
363
|
+
editor.executeAction("move_line_end");
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
globalThis.vi_first_non_blank = function (): void {
|
|
367
|
+
consumeCount(); // Count doesn't apply
|
|
368
|
+
editor.executeAction("move_line_start");
|
|
369
|
+
// TODO: skip whitespace
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
globalThis.vi_doc_start = function (): void {
|
|
373
|
+
consumeCount(); // Count doesn't apply
|
|
374
|
+
editor.executeAction("move_document_start");
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
globalThis.vi_doc_end = function (): void {
|
|
378
|
+
consumeCount(); // Count doesn't apply
|
|
379
|
+
editor.executeAction("move_document_end");
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
globalThis.vi_page_down = function (): void {
|
|
383
|
+
executeWithCount("page_down");
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
globalThis.vi_page_up = function (): void {
|
|
387
|
+
executeWithCount("page_up");
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
globalThis.vi_matching_bracket = function (): void {
|
|
391
|
+
editor.executeAction("go_to_matching_bracket");
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// Mode switching
|
|
395
|
+
globalThis.vi_insert_before = function (): void {
|
|
396
|
+
switchMode("insert");
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
globalThis.vi_insert_after = function (): void {
|
|
400
|
+
editor.executeAction("move_right");
|
|
401
|
+
switchMode("insert");
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
globalThis.vi_insert_line_start = function (): void {
|
|
405
|
+
editor.executeAction("move_line_start");
|
|
406
|
+
switchMode("insert");
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
globalThis.vi_insert_line_end = function (): void {
|
|
410
|
+
editor.executeAction("move_line_end");
|
|
411
|
+
switchMode("insert");
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
globalThis.vi_open_below = function (): void {
|
|
415
|
+
editor.executeAction("move_line_end");
|
|
416
|
+
editor.executeAction("insert_newline");
|
|
417
|
+
switchMode("insert");
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
globalThis.vi_open_above = function (): void {
|
|
421
|
+
editor.executeAction("move_line_start");
|
|
422
|
+
editor.executeAction("insert_newline");
|
|
423
|
+
editor.executeAction("move_up");
|
|
424
|
+
switchMode("insert");
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
globalThis.vi_escape = function (): void {
|
|
428
|
+
switchMode("normal");
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// Operators
|
|
432
|
+
globalThis.vi_delete_operator = function (): void {
|
|
433
|
+
state.pendingOperator = "d";
|
|
434
|
+
switchMode("operator-pending");
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
globalThis.vi_change_operator = function (): void {
|
|
438
|
+
state.pendingOperator = "c";
|
|
439
|
+
switchMode("operator-pending");
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
globalThis.vi_yank_operator = function (): void {
|
|
443
|
+
state.pendingOperator = "y";
|
|
444
|
+
switchMode("operator-pending");
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
// Line operations (dd, cc, yy) - support count prefix (3dd = delete 3 lines)
|
|
448
|
+
globalThis.vi_delete_line = function (): void {
|
|
449
|
+
const count = consumeCount();
|
|
450
|
+
state.lastChange = { type: "line-op", action: "delete_line", count };
|
|
451
|
+
if (count === 1) {
|
|
452
|
+
editor.executeAction("delete_line");
|
|
453
|
+
} else {
|
|
454
|
+
editor.executeActions([{ action: "delete_line", count }]);
|
|
455
|
+
}
|
|
456
|
+
switchMode("normal");
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
globalThis.vi_change_line = function (): void {
|
|
460
|
+
const count = consumeCount();
|
|
461
|
+
state.lastChange = { type: "line-op", action: "change_line", count };
|
|
462
|
+
editor.executeAction("move_line_start");
|
|
463
|
+
const start = editor.getCursorPosition();
|
|
464
|
+
editor.executeAction("move_line_end");
|
|
465
|
+
const end = editor.getCursorPosition();
|
|
466
|
+
if (start !== null && end !== null) {
|
|
467
|
+
editor.deleteRange(editor.getActiveBufferId(), start, end);
|
|
468
|
+
}
|
|
469
|
+
switchMode("insert");
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
globalThis.vi_yank_line = function (): void {
|
|
473
|
+
const count = consumeCount();
|
|
474
|
+
// select_line selects current line and moves cursor to next line
|
|
475
|
+
if (count === 1) {
|
|
476
|
+
editor.executeAction("select_line");
|
|
477
|
+
} else {
|
|
478
|
+
editor.executeActions([{ action: "select_line", count }]);
|
|
479
|
+
}
|
|
480
|
+
editor.executeAction("copy");
|
|
481
|
+
// Move back to original line using synchronous actions
|
|
482
|
+
// (setBufferCursor is async and doesn't take effect in time)
|
|
483
|
+
editor.executeAction("move_up");
|
|
484
|
+
editor.executeAction("move_line_start");
|
|
485
|
+
state.lastYankWasLinewise = true;
|
|
486
|
+
editor.setStatus(editor.t("status.yanked_lines", { count: String(count) }));
|
|
487
|
+
switchMode("normal");
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
// Single character operations - support count prefix (3x = delete 3 chars)
|
|
491
|
+
globalThis.vi_delete_char = function (): void {
|
|
492
|
+
const count = consumeCount();
|
|
493
|
+
state.lastChange = { type: "simple", action: "delete_forward", count };
|
|
494
|
+
executeWithCount("delete_forward", count);
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
globalThis.vi_delete_char_before = function (): void {
|
|
498
|
+
const count = consumeCount();
|
|
499
|
+
state.lastChange = { type: "simple", action: "delete_backward", count };
|
|
500
|
+
executeWithCount("delete_backward", count);
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
globalThis.vi_replace_char = function (): void {
|
|
504
|
+
// TODO: implement character replacement (need to read next char)
|
|
505
|
+
editor.setStatus(editor.t("status.replace_not_implemented"));
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
// Substitute (delete char and enter insert mode)
|
|
509
|
+
globalThis.vi_substitute = function (): void {
|
|
510
|
+
const count = consumeCount();
|
|
511
|
+
state.lastChange = { type: "simple", action: "substitute", count };
|
|
512
|
+
if (count > 1) {
|
|
513
|
+
editor.executeActions([{ action: "delete_forward", count }]);
|
|
514
|
+
} else {
|
|
515
|
+
editor.executeAction("delete_forward");
|
|
516
|
+
}
|
|
517
|
+
switchMode("insert");
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// Delete to end of line
|
|
521
|
+
globalThis.vi_delete_to_end = function (): void {
|
|
522
|
+
state.lastChange = { type: "operator-motion", operator: "d", motion: "move_line_end" };
|
|
523
|
+
const start = editor.getCursorPosition();
|
|
524
|
+
editor.executeAction("move_line_end");
|
|
525
|
+
const end = editor.getCursorPosition();
|
|
526
|
+
if (start !== null && end !== null && end > start) {
|
|
527
|
+
editor.deleteRange(editor.getActiveBufferId(), start, end);
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
// Change to end of line
|
|
532
|
+
globalThis.vi_change_to_end = function (): void {
|
|
533
|
+
state.lastChange = { type: "operator-motion", operator: "c", motion: "move_line_end" };
|
|
534
|
+
const start = editor.getCursorPosition();
|
|
535
|
+
editor.executeAction("move_line_end");
|
|
536
|
+
const end = editor.getCursorPosition();
|
|
537
|
+
if (start !== null && end !== null && end > start) {
|
|
538
|
+
editor.deleteRange(editor.getActiveBufferId(), start, end);
|
|
539
|
+
}
|
|
540
|
+
switchMode("insert");
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
// Clipboard
|
|
544
|
+
globalThis.vi_paste_after = function (): void {
|
|
545
|
+
if (state.lastYankWasLinewise) {
|
|
546
|
+
// Line-wise paste: go to next line start and paste there
|
|
547
|
+
// The yanked text includes trailing \n which pushes subsequent lines down
|
|
548
|
+
editor.executeAction("move_down");
|
|
549
|
+
editor.executeAction("move_line_start");
|
|
550
|
+
editor.executeAction("paste");
|
|
551
|
+
editor.executeAction("move_up"); // Stay on the pasted line
|
|
552
|
+
editor.executeAction("move_line_start");
|
|
553
|
+
} else {
|
|
554
|
+
// Character-wise paste: insert after cursor
|
|
555
|
+
editor.executeAction("move_right");
|
|
556
|
+
editor.executeAction("paste");
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
globalThis.vi_paste_before = function (): void {
|
|
561
|
+
if (state.lastYankWasLinewise) {
|
|
562
|
+
// Line-wise paste: paste at current line start
|
|
563
|
+
// The yanked text includes trailing \n which pushes current line down
|
|
564
|
+
editor.executeAction("move_line_start");
|
|
565
|
+
editor.executeAction("paste");
|
|
566
|
+
editor.executeAction("move_up"); // Stay on the pasted line
|
|
567
|
+
editor.executeAction("move_line_start");
|
|
568
|
+
} else {
|
|
569
|
+
// Character-wise paste: insert at cursor
|
|
570
|
+
editor.executeAction("paste");
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
// Undo/Redo
|
|
575
|
+
globalThis.vi_undo = function (): void {
|
|
576
|
+
editor.executeAction("undo");
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
globalThis.vi_redo = function (): void {
|
|
580
|
+
editor.executeAction("redo");
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
// Repeat last change (. command)
|
|
584
|
+
globalThis.vi_repeat = async function (): Promise<void> {
|
|
585
|
+
if (!state.lastChange) {
|
|
586
|
+
editor.setStatus(editor.t("status.no_change_to_repeat"));
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const change = state.lastChange;
|
|
591
|
+
const count = consumeCount() || change.count || 1;
|
|
592
|
+
|
|
593
|
+
switch (change.type) {
|
|
594
|
+
case "simple": {
|
|
595
|
+
// Simple actions like x, X, s
|
|
596
|
+
if (change.action === "substitute") {
|
|
597
|
+
// Substitute: delete chars and insert text
|
|
598
|
+
if (count > 1) {
|
|
599
|
+
editor.executeActions([{ action: "delete_forward", count }]);
|
|
600
|
+
} else {
|
|
601
|
+
editor.executeAction("delete_forward");
|
|
602
|
+
}
|
|
603
|
+
if (change.insertedText) {
|
|
604
|
+
editor.insertText(change.insertedText);
|
|
605
|
+
}
|
|
606
|
+
} else if (change.action) {
|
|
607
|
+
// Simple action like delete_forward, delete_backward
|
|
608
|
+
if (count > 1) {
|
|
609
|
+
editor.executeActions([{ action: change.action, count }]);
|
|
610
|
+
} else {
|
|
611
|
+
editor.executeAction(change.action);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
case "line-op": {
|
|
618
|
+
// Line operations like dd, cc
|
|
619
|
+
if (change.action === "delete_line") {
|
|
620
|
+
if (count > 1) {
|
|
621
|
+
editor.executeActions([{ action: "delete_line", count }]);
|
|
622
|
+
} else {
|
|
623
|
+
editor.executeAction("delete_line");
|
|
624
|
+
}
|
|
625
|
+
} else if (change.action === "change_line") {
|
|
626
|
+
// Change line: delete line content and insert text
|
|
627
|
+
editor.executeAction("move_line_start");
|
|
628
|
+
const start = editor.getCursorPosition();
|
|
629
|
+
editor.executeAction("move_line_end");
|
|
630
|
+
const end = editor.getCursorPosition();
|
|
631
|
+
if (start !== null && end !== null) {
|
|
632
|
+
editor.deleteRange(editor.getActiveBufferId(), start, end);
|
|
633
|
+
}
|
|
634
|
+
if (change.insertedText) {
|
|
635
|
+
editor.insertText(change.insertedText);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
case "operator-motion": {
|
|
642
|
+
// Operator + motion like dw, cw, d$
|
|
643
|
+
if (change.operator && change.motion) {
|
|
644
|
+
if (change.operator === "c") {
|
|
645
|
+
// For change: do the delete part, then insert the text
|
|
646
|
+
applyOperatorWithMotion("d", change.motion, count);
|
|
647
|
+
if (change.insertedText) {
|
|
648
|
+
editor.insertText(change.insertedText);
|
|
649
|
+
}
|
|
650
|
+
} else {
|
|
651
|
+
applyOperatorWithMotion(change.operator, change.motion, count);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
case "operator-textobj": {
|
|
658
|
+
// Operator + text object like diw, ci"
|
|
659
|
+
if (change.operator && change.textObject) {
|
|
660
|
+
// Set up the pending state and call applyTextObject
|
|
661
|
+
state.pendingOperator = change.operator === "c" ? "d" : change.operator;
|
|
662
|
+
state.pendingTextObject = change.textObject.modifier;
|
|
663
|
+
await applyTextObject(change.textObject.object);
|
|
664
|
+
if (change.operator === "c" && change.insertedText) {
|
|
665
|
+
editor.insertText(change.insertedText);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
case "insert": {
|
|
672
|
+
// Pure insert (i, a, o, O)
|
|
673
|
+
if (change.insertedText) {
|
|
674
|
+
editor.insertText(change.insertedText);
|
|
675
|
+
}
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
// Join lines
|
|
682
|
+
globalThis.vi_join = function (): void {
|
|
683
|
+
editor.executeAction("move_line_end");
|
|
684
|
+
editor.executeAction("delete_forward");
|
|
685
|
+
editor.executeAction("insert_text_at_cursor");
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
// Search
|
|
689
|
+
globalThis.vi_search_forward = function (): void {
|
|
690
|
+
editor.executeAction("search");
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
globalThis.vi_search_backward = function (): void {
|
|
694
|
+
// Use same search dialog, user can search backward manually
|
|
695
|
+
editor.executeAction("search");
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
globalThis.vi_find_next = function (): void {
|
|
699
|
+
editor.executeAction("find_next");
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
globalThis.vi_find_prev = function (): void {
|
|
703
|
+
editor.executeAction("find_previous");
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
// Center view
|
|
707
|
+
globalThis.vi_center_cursor = function (): void {
|
|
708
|
+
editor.executeAction("center_cursor");
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
// Half page movements
|
|
712
|
+
globalThis.vi_half_page_down = function (): void {
|
|
713
|
+
// Approximate half page with multiple down movements
|
|
714
|
+
const count = consumeCount();
|
|
715
|
+
editor.executeActions([{ action: "move_down", count: 10 * count }]);
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
globalThis.vi_half_page_up = function (): void {
|
|
719
|
+
const count = consumeCount();
|
|
720
|
+
editor.executeActions([{ action: "move_up", count: 10 * count }]);
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
// ============================================================================
|
|
724
|
+
// Count Prefix (digit keys 1-9, and 0 after initial digit)
|
|
725
|
+
// ============================================================================
|
|
726
|
+
|
|
727
|
+
// Digit handlers for count prefix
|
|
728
|
+
globalThis.vi_digit_1 = function (): void { accumulateCount(1); };
|
|
729
|
+
globalThis.vi_digit_2 = function (): void { accumulateCount(2); };
|
|
730
|
+
globalThis.vi_digit_3 = function (): void { accumulateCount(3); };
|
|
731
|
+
globalThis.vi_digit_4 = function (): void { accumulateCount(4); };
|
|
732
|
+
globalThis.vi_digit_5 = function (): void { accumulateCount(5); };
|
|
733
|
+
globalThis.vi_digit_6 = function (): void { accumulateCount(6); };
|
|
734
|
+
globalThis.vi_digit_7 = function (): void { accumulateCount(7); };
|
|
735
|
+
globalThis.vi_digit_8 = function (): void { accumulateCount(8); };
|
|
736
|
+
globalThis.vi_digit_9 = function (): void { accumulateCount(9); };
|
|
737
|
+
|
|
738
|
+
// 0 is special: if count is already started, it appends; otherwise it's "go to line start"
|
|
739
|
+
globalThis.vi_digit_0_or_line_start = function (): void {
|
|
740
|
+
if (state.count !== null) {
|
|
741
|
+
accumulateCount(0);
|
|
742
|
+
} else {
|
|
743
|
+
editor.executeAction("move_line_start");
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
// 0 in operator-pending mode: if count is started, append; otherwise apply operator to line start
|
|
748
|
+
globalThis.vi_op_digit_0_or_line_start = function (): void {
|
|
749
|
+
if (state.count !== null) {
|
|
750
|
+
accumulateCount(0);
|
|
751
|
+
} else {
|
|
752
|
+
handleMotionWithOperator("move_line_start");
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
// ============================================================================
|
|
757
|
+
// Visual Mode
|
|
758
|
+
// ============================================================================
|
|
759
|
+
|
|
760
|
+
// Enter character-wise visual mode
|
|
761
|
+
globalThis.vi_visual_char = function (): void {
|
|
762
|
+
state.visualAnchor = editor.getCursorPosition();
|
|
763
|
+
// Select current character to start visual selection
|
|
764
|
+
editor.executeAction("select_right");
|
|
765
|
+
switchMode("visual");
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
// Enter line-wise visual mode
|
|
769
|
+
globalThis.vi_visual_line = function (): void {
|
|
770
|
+
state.visualAnchor = editor.getCursorPosition();
|
|
771
|
+
// Select current line
|
|
772
|
+
editor.executeAction("move_line_start");
|
|
773
|
+
editor.executeAction("select_line");
|
|
774
|
+
switchMode("visual-line");
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
// Toggle between visual and visual-line modes
|
|
778
|
+
globalThis.vi_visual_toggle_line = function (): void {
|
|
779
|
+
if (state.mode === "visual") {
|
|
780
|
+
// Switch to line mode - extend selection to full lines
|
|
781
|
+
editor.executeAction("select_line");
|
|
782
|
+
state.mode = "visual-line";
|
|
783
|
+
editor.setEditorMode("vi-visual-line");
|
|
784
|
+
editor.setStatus(getModeIndicator("visual-line"));
|
|
785
|
+
} else if (state.mode === "visual-line") {
|
|
786
|
+
// Switch to char mode (keep selection but change mode)
|
|
787
|
+
state.mode = "visual";
|
|
788
|
+
editor.setEditorMode("vi-visual");
|
|
789
|
+
editor.setStatus(getModeIndicator("visual"));
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
// Enter visual block mode (Ctrl-v)
|
|
794
|
+
globalThis.vi_visual_block = function (): void {
|
|
795
|
+
// Store anchor position for block selection
|
|
796
|
+
state.visualAnchor = editor.getCursorPosition();
|
|
797
|
+
|
|
798
|
+
// Calculate line and column for block anchor
|
|
799
|
+
const cursorPos = editor.getCursorPosition();
|
|
800
|
+
if (cursorPos !== null) {
|
|
801
|
+
const line = editor.getCursorLine() ?? 1;
|
|
802
|
+
const lineStart = editor.getLineStartPosition(line);
|
|
803
|
+
const col = lineStart !== null ? cursorPos - lineStart : 0;
|
|
804
|
+
state.visualBlockAnchor = { line, col };
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Select current character to start
|
|
808
|
+
editor.executeAction("select_right");
|
|
809
|
+
switchMode("visual-block");
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
// Visual block mode motions - these extend the rectangular selection
|
|
813
|
+
globalThis.vi_vblock_left = function (): void {
|
|
814
|
+
executeWithCount("select_left");
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
globalThis.vi_vblock_down = function (): void {
|
|
818
|
+
executeWithCount("select_down");
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
globalThis.vi_vblock_up = function (): void {
|
|
822
|
+
executeWithCount("select_up");
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
globalThis.vi_vblock_right = function (): void {
|
|
826
|
+
executeWithCount("select_right");
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
globalThis.vi_vblock_line_start = function (): void {
|
|
830
|
+
consumeCount();
|
|
831
|
+
editor.executeAction("select_line_start");
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
globalThis.vi_vblock_line_end = function (): void {
|
|
835
|
+
consumeCount();
|
|
836
|
+
editor.executeAction("select_line_end");
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
// Visual block delete - delete the selected block
|
|
840
|
+
globalThis.vi_vblock_delete = function (): void {
|
|
841
|
+
editor.executeAction("cut");
|
|
842
|
+
state.lastYankWasLinewise = false;
|
|
843
|
+
switchMode("normal");
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
// Visual block change - delete and enter insert mode
|
|
847
|
+
globalThis.vi_vblock_change = function (): void {
|
|
848
|
+
editor.executeAction("cut");
|
|
849
|
+
switchMode("insert");
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
// Visual block yank
|
|
853
|
+
globalThis.vi_vblock_yank = function (): void {
|
|
854
|
+
editor.executeAction("copy");
|
|
855
|
+
state.lastYankWasLinewise = false;
|
|
856
|
+
// Move cursor to start of selection
|
|
857
|
+
editor.executeAction("move_left");
|
|
858
|
+
switchMode("normal");
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
// Exit visual block mode
|
|
862
|
+
globalThis.vi_vblock_escape = function (): void {
|
|
863
|
+
switchMode("normal");
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
// Toggle from visual block to other visual modes
|
|
867
|
+
globalThis.vi_vblock_toggle_char = function (): void {
|
|
868
|
+
// Switch to character visual mode
|
|
869
|
+
state.mode = "visual";
|
|
870
|
+
editor.setEditorMode("vi-visual");
|
|
871
|
+
editor.setStatus(getModeIndicator("visual"));
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
globalThis.vi_vblock_toggle_line = function (): void {
|
|
875
|
+
// Switch to line visual mode
|
|
876
|
+
editor.executeAction("select_line");
|
|
877
|
+
state.mode = "visual-line";
|
|
878
|
+
editor.setEditorMode("vi-visual-line");
|
|
879
|
+
editor.setStatus(getModeIndicator("visual-line"));
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
// Visual mode motions - these extend the selection
|
|
883
|
+
globalThis.vi_vis_left = function (): void {
|
|
884
|
+
executeWithCount("select_left");
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
globalThis.vi_vis_down = function (): void {
|
|
888
|
+
executeWithCount("select_down");
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
globalThis.vi_vis_up = function (): void {
|
|
892
|
+
executeWithCount("select_up");
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
globalThis.vi_vis_right = function (): void {
|
|
896
|
+
executeWithCount("select_right");
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
globalThis.vi_vis_word = function (): void {
|
|
900
|
+
executeWithCount("select_word_right");
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
globalThis.vi_vis_word_back = function (): void {
|
|
904
|
+
executeWithCount("select_word_left");
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
globalThis.vi_vis_word_end = function (): void {
|
|
908
|
+
const count = consumeCount();
|
|
909
|
+
for (let i = 0; i < count; i++) {
|
|
910
|
+
editor.executeAction("select_word_right");
|
|
911
|
+
editor.executeAction("select_left");
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
globalThis.vi_vis_line_start = function (): void {
|
|
916
|
+
consumeCount();
|
|
917
|
+
editor.executeAction("select_line_start");
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
globalThis.vi_vis_line_end = function (): void {
|
|
921
|
+
consumeCount();
|
|
922
|
+
editor.executeAction("select_line_end");
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
globalThis.vi_vis_doc_start = function (): void {
|
|
926
|
+
consumeCount();
|
|
927
|
+
editor.executeAction("select_document_start");
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
globalThis.vi_vis_doc_end = function (): void {
|
|
931
|
+
consumeCount();
|
|
932
|
+
editor.executeAction("select_document_end");
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
// Visual line mode motions - extend selection by whole lines
|
|
936
|
+
globalThis.vi_vline_down = function (): void {
|
|
937
|
+
executeWithCount("select_down");
|
|
938
|
+
// Ensure full line selection
|
|
939
|
+
editor.executeAction("select_line_end");
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
globalThis.vi_vline_up = function (): void {
|
|
943
|
+
executeWithCount("select_up");
|
|
944
|
+
// Ensure full line selection
|
|
945
|
+
editor.executeAction("select_line_start");
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
// Visual mode operators - act on selection
|
|
949
|
+
globalThis.vi_vis_delete = function (): void {
|
|
950
|
+
const wasLinewise = state.mode === "visual-line";
|
|
951
|
+
editor.executeAction("cut");
|
|
952
|
+
state.lastYankWasLinewise = wasLinewise;
|
|
953
|
+
switchMode("normal");
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
globalThis.vi_vis_change = function (): void {
|
|
957
|
+
editor.executeAction("cut");
|
|
958
|
+
switchMode("insert");
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
globalThis.vi_vis_yank = function (): void {
|
|
962
|
+
const wasLinewise = state.mode === "visual-line";
|
|
963
|
+
editor.executeAction("copy");
|
|
964
|
+
state.lastYankWasLinewise = wasLinewise;
|
|
965
|
+
// Move cursor to start of selection (vim behavior)
|
|
966
|
+
editor.executeAction("move_left");
|
|
967
|
+
switchMode("normal");
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
// Exit visual mode without doing anything
|
|
971
|
+
globalThis.vi_vis_escape = function (): void {
|
|
972
|
+
switchMode("normal");
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
// ============================================================================
|
|
976
|
+
// Text Objects (iw, aw, i", a", etc.)
|
|
977
|
+
// ============================================================================
|
|
978
|
+
|
|
979
|
+
// Enter text-object mode with "inner" modifier
|
|
980
|
+
globalThis.vi_text_object_inner = function (): void {
|
|
981
|
+
state.pendingTextObject = "inner";
|
|
982
|
+
state.mode = "text-object";
|
|
983
|
+
editor.setEditorMode("vi-text-object");
|
|
984
|
+
editor.setStatus(getModeIndicator("text-object"));
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
// Enter text-object mode with "around" modifier
|
|
988
|
+
globalThis.vi_text_object_around = function (): void {
|
|
989
|
+
state.pendingTextObject = "around";
|
|
990
|
+
state.mode = "text-object";
|
|
991
|
+
editor.setEditorMode("vi-text-object");
|
|
992
|
+
editor.setStatus(getModeIndicator("text-object"));
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
// Apply text object selection and then the pending operator
|
|
996
|
+
async function applyTextObject(objectType: string): Promise<void> {
|
|
997
|
+
const operator = state.pendingOperator;
|
|
998
|
+
const isInner = state.pendingTextObject === "inner";
|
|
999
|
+
const modifier = state.pendingTextObject;
|
|
1000
|
+
|
|
1001
|
+
if (!operator) {
|
|
1002
|
+
switchMode("normal");
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Record last change for '.' repeat (only for delete and change, not yank)
|
|
1007
|
+
if ((operator === "d" || operator === "c") && modifier) {
|
|
1008
|
+
state.lastChange = { type: "operator-textobj", operator, textObject: { modifier, object: objectType } };
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const bufferId = editor.getActiveBufferId();
|
|
1012
|
+
const cursorPos = editor.getCursorPosition();
|
|
1013
|
+
if (cursorPos === null) {
|
|
1014
|
+
switchMode("normal");
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Get text around cursor to find the text object boundaries
|
|
1019
|
+
const windowSize = 1000;
|
|
1020
|
+
const startOffset = Math.max(0, cursorPos - windowSize);
|
|
1021
|
+
const bufLen = editor.getBufferLength(bufferId);
|
|
1022
|
+
const endOffset = Math.min(bufLen, cursorPos + windowSize);
|
|
1023
|
+
const text = await editor.getBufferText(bufferId, startOffset, endOffset);
|
|
1024
|
+
if (!text) {
|
|
1025
|
+
switchMode("normal");
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const posInChunk = cursorPos - startOffset;
|
|
1030
|
+
let selectStart = -1;
|
|
1031
|
+
let selectEnd = -1;
|
|
1032
|
+
|
|
1033
|
+
switch (objectType) {
|
|
1034
|
+
case "word": {
|
|
1035
|
+
// Find word boundaries
|
|
1036
|
+
const wordChars = /[a-zA-Z0-9_]/;
|
|
1037
|
+
let start = posInChunk;
|
|
1038
|
+
let end = posInChunk;
|
|
1039
|
+
|
|
1040
|
+
// Expand to find word start
|
|
1041
|
+
while (start > 0 && wordChars.test(text[start - 1])) start--;
|
|
1042
|
+
// Expand to find word end
|
|
1043
|
+
while (end < text.length && wordChars.test(text[end])) end++;
|
|
1044
|
+
|
|
1045
|
+
if (!isInner) {
|
|
1046
|
+
// "a word" includes trailing whitespace
|
|
1047
|
+
while (end < text.length && /\s/.test(text[end]) && text[end] !== '\n') end++;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
selectStart = startOffset + start;
|
|
1051
|
+
selectEnd = startOffset + end;
|
|
1052
|
+
break;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
case "WORD": {
|
|
1056
|
+
// WORD is whitespace-delimited
|
|
1057
|
+
let start = posInChunk;
|
|
1058
|
+
let end = posInChunk;
|
|
1059
|
+
|
|
1060
|
+
while (start > 0 && !/\s/.test(text[start - 1])) start--;
|
|
1061
|
+
while (end < text.length && !/\s/.test(text[end])) end++;
|
|
1062
|
+
|
|
1063
|
+
if (!isInner) {
|
|
1064
|
+
while (end < text.length && /\s/.test(text[end]) && text[end] !== '\n') end++;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
selectStart = startOffset + start;
|
|
1068
|
+
selectEnd = startOffset + end;
|
|
1069
|
+
break;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
case "\"":
|
|
1073
|
+
case "'":
|
|
1074
|
+
case "`": {
|
|
1075
|
+
// Find matching quotes on current line
|
|
1076
|
+
// First find line boundaries
|
|
1077
|
+
let lineStart = posInChunk;
|
|
1078
|
+
let lineEnd = posInChunk;
|
|
1079
|
+
while (lineStart > 0 && text[lineStart - 1] !== '\n') lineStart--;
|
|
1080
|
+
while (lineEnd < text.length && text[lineEnd] !== '\n') lineEnd++;
|
|
1081
|
+
|
|
1082
|
+
const line = text.substring(lineStart, lineEnd);
|
|
1083
|
+
const colInLine = posInChunk - lineStart;
|
|
1084
|
+
|
|
1085
|
+
// Find quote pair containing cursor
|
|
1086
|
+
let quoteStart = -1;
|
|
1087
|
+
let quoteEnd = -1;
|
|
1088
|
+
let inQuote = false;
|
|
1089
|
+
|
|
1090
|
+
for (let i = 0; i < line.length; i++) {
|
|
1091
|
+
if (line[i] === objectType) {
|
|
1092
|
+
if (!inQuote) {
|
|
1093
|
+
quoteStart = i;
|
|
1094
|
+
inQuote = true;
|
|
1095
|
+
} else {
|
|
1096
|
+
quoteEnd = i;
|
|
1097
|
+
if (colInLine >= quoteStart && colInLine <= quoteEnd) {
|
|
1098
|
+
break; // Found the pair containing cursor
|
|
1099
|
+
}
|
|
1100
|
+
inQuote = false;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
if (quoteStart !== -1 && quoteEnd !== -1 && colInLine >= quoteStart && colInLine <= quoteEnd) {
|
|
1106
|
+
if (isInner) {
|
|
1107
|
+
selectStart = startOffset + lineStart + quoteStart + 1;
|
|
1108
|
+
selectEnd = startOffset + lineStart + quoteEnd;
|
|
1109
|
+
} else {
|
|
1110
|
+
selectStart = startOffset + lineStart + quoteStart;
|
|
1111
|
+
selectEnd = startOffset + lineStart + quoteEnd + 1;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
case "(":
|
|
1118
|
+
case ")":
|
|
1119
|
+
case "b": {
|
|
1120
|
+
// Find matching parentheses
|
|
1121
|
+
const result = findMatchingPair(text, posInChunk, '(', ')');
|
|
1122
|
+
if (result) {
|
|
1123
|
+
if (isInner) {
|
|
1124
|
+
selectStart = startOffset + result.start + 1;
|
|
1125
|
+
selectEnd = startOffset + result.end;
|
|
1126
|
+
} else {
|
|
1127
|
+
selectStart = startOffset + result.start;
|
|
1128
|
+
selectEnd = startOffset + result.end + 1;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
break;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
case "{":
|
|
1135
|
+
case "}":
|
|
1136
|
+
case "B": {
|
|
1137
|
+
const result = findMatchingPair(text, posInChunk, '{', '}');
|
|
1138
|
+
if (result) {
|
|
1139
|
+
if (isInner) {
|
|
1140
|
+
selectStart = startOffset + result.start + 1;
|
|
1141
|
+
selectEnd = startOffset + result.end;
|
|
1142
|
+
} else {
|
|
1143
|
+
selectStart = startOffset + result.start;
|
|
1144
|
+
selectEnd = startOffset + result.end + 1;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
break;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
case "[":
|
|
1151
|
+
case "]": {
|
|
1152
|
+
const result = findMatchingPair(text, posInChunk, '[', ']');
|
|
1153
|
+
if (result) {
|
|
1154
|
+
if (isInner) {
|
|
1155
|
+
selectStart = startOffset + result.start + 1;
|
|
1156
|
+
selectEnd = startOffset + result.end;
|
|
1157
|
+
} else {
|
|
1158
|
+
selectStart = startOffset + result.start;
|
|
1159
|
+
selectEnd = startOffset + result.end + 1;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
break;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
case "<":
|
|
1166
|
+
case ">": {
|
|
1167
|
+
const result = findMatchingPair(text, posInChunk, '<', '>');
|
|
1168
|
+
if (result) {
|
|
1169
|
+
if (isInner) {
|
|
1170
|
+
selectStart = startOffset + result.start + 1;
|
|
1171
|
+
selectEnd = startOffset + result.end;
|
|
1172
|
+
} else {
|
|
1173
|
+
selectStart = startOffset + result.start;
|
|
1174
|
+
selectEnd = startOffset + result.end + 1;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
break;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
if (selectStart === -1 || selectEnd === -1 || selectStart >= selectEnd) {
|
|
1182
|
+
switchMode("normal");
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// Apply the operator directly using deleteRange/copyRange
|
|
1187
|
+
switch (operator) {
|
|
1188
|
+
case "d": {
|
|
1189
|
+
// Delete the range directly
|
|
1190
|
+
editor.deleteRange(bufferId, selectStart, selectEnd);
|
|
1191
|
+
state.lastYankWasLinewise = false;
|
|
1192
|
+
break;
|
|
1193
|
+
}
|
|
1194
|
+
case "c": {
|
|
1195
|
+
// Delete and enter insert mode
|
|
1196
|
+
editor.deleteRange(bufferId, selectStart, selectEnd);
|
|
1197
|
+
switchMode("insert");
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
case "y": {
|
|
1201
|
+
// For yank, we need to select the range and copy
|
|
1202
|
+
// First move cursor to start
|
|
1203
|
+
editor.setBufferCursor(bufferId, selectStart);
|
|
1204
|
+
// Select the range
|
|
1205
|
+
for (let i = 0; i < selectEnd - selectStart; i++) {
|
|
1206
|
+
editor.executeAction("select_right");
|
|
1207
|
+
}
|
|
1208
|
+
editor.executeAction("copy");
|
|
1209
|
+
state.lastYankWasLinewise = false;
|
|
1210
|
+
// Move back to start
|
|
1211
|
+
editor.setBufferCursor(bufferId, selectStart);
|
|
1212
|
+
break;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
switchMode("normal");
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Helper to find matching bracket pair containing the cursor
|
|
1220
|
+
function findMatchingPair(text: string, pos: number, openChar: string, closeChar: string): { start: number; end: number } | null {
|
|
1221
|
+
let depth = 0;
|
|
1222
|
+
let start = -1;
|
|
1223
|
+
|
|
1224
|
+
// Search backward for opening bracket
|
|
1225
|
+
for (let i = pos; i >= 0; i--) {
|
|
1226
|
+
if (text[i] === closeChar) depth++;
|
|
1227
|
+
if (text[i] === openChar) {
|
|
1228
|
+
if (depth === 0) {
|
|
1229
|
+
start = i;
|
|
1230
|
+
break;
|
|
1231
|
+
}
|
|
1232
|
+
depth--;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
if (start === -1) return null;
|
|
1237
|
+
|
|
1238
|
+
// Search forward for closing bracket
|
|
1239
|
+
depth = 0;
|
|
1240
|
+
for (let i = start; i < text.length; i++) {
|
|
1241
|
+
if (text[i] === openChar) depth++;
|
|
1242
|
+
if (text[i] === closeChar) {
|
|
1243
|
+
depth--;
|
|
1244
|
+
if (depth === 0) {
|
|
1245
|
+
return { start, end: i };
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
return null;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Text object handlers
|
|
1254
|
+
globalThis.vi_to_word = async function (): Promise<void> { await applyTextObject("word"); };
|
|
1255
|
+
globalThis.vi_to_WORD = async function (): Promise<void> { await applyTextObject("WORD"); };
|
|
1256
|
+
globalThis.vi_to_dquote = async function (): Promise<void> { await applyTextObject("\""); };
|
|
1257
|
+
globalThis.vi_to_squote = async function (): Promise<void> { await applyTextObject("'"); };
|
|
1258
|
+
globalThis.vi_to_backtick = async function (): Promise<void> { await applyTextObject("`"); };
|
|
1259
|
+
globalThis.vi_to_paren = async function (): Promise<void> { await applyTextObject("("); };
|
|
1260
|
+
globalThis.vi_to_brace = async function (): Promise<void> { await applyTextObject("{"); };
|
|
1261
|
+
globalThis.vi_to_bracket = async function (): Promise<void> { await applyTextObject("["); };
|
|
1262
|
+
globalThis.vi_to_angle = async function (): Promise<void> { await applyTextObject("<"); };
|
|
1263
|
+
|
|
1264
|
+
// Cancel text object mode
|
|
1265
|
+
globalThis.vi_to_cancel = function (): void {
|
|
1266
|
+
switchMode("normal");
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
// ============================================================================
|
|
1270
|
+
// Find Character Motions (f/t/F/T)
|
|
1271
|
+
// ============================================================================
|
|
1272
|
+
|
|
1273
|
+
// Enter find-char mode waiting for the target character
|
|
1274
|
+
function enterFindCharMode(findType: FindCharType): void {
|
|
1275
|
+
state.pendingFindChar = findType;
|
|
1276
|
+
state.mode = "find-char";
|
|
1277
|
+
editor.setEditorMode("vi-find-char");
|
|
1278
|
+
editor.setStatus(getModeIndicator("find-char"));
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Execute find char motion (async because getBufferText is async)
|
|
1282
|
+
async function executeFindChar(findType: FindCharType, char: string): Promise<void> {
|
|
1283
|
+
if (!findType) return;
|
|
1284
|
+
|
|
1285
|
+
const bufferId = editor.getActiveBufferId();
|
|
1286
|
+
const cursorPos = editor.getCursorPosition();
|
|
1287
|
+
if (cursorPos === null || (cursorPos === 0 && (findType === "F" || findType === "T"))) {
|
|
1288
|
+
// Can't search backward from position 0
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// Get text around cursor to find line boundaries
|
|
1293
|
+
// Read up to 10KB before and after cursor for context
|
|
1294
|
+
const windowSize = 10000;
|
|
1295
|
+
const startOffset = Math.max(0, cursorPos - windowSize);
|
|
1296
|
+
const bufLen = editor.getBufferLength(bufferId);
|
|
1297
|
+
const endOffset = Math.min(bufLen, cursorPos + windowSize);
|
|
1298
|
+
|
|
1299
|
+
// Get buffer text around cursor
|
|
1300
|
+
const text = await editor.getBufferText(bufferId, startOffset, endOffset);
|
|
1301
|
+
if (!text) return;
|
|
1302
|
+
|
|
1303
|
+
// Calculate position within this text chunk
|
|
1304
|
+
const posInChunk = cursorPos - startOffset;
|
|
1305
|
+
|
|
1306
|
+
// Find line start (last newline before cursor, or start of chunk)
|
|
1307
|
+
let lineStart = 0;
|
|
1308
|
+
for (let i = posInChunk - 1; i >= 0; i--) {
|
|
1309
|
+
if (text[i] === '\n') {
|
|
1310
|
+
lineStart = i + 1;
|
|
1311
|
+
break;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// Find line end (next newline after cursor, or end of chunk)
|
|
1316
|
+
let lineEnd = text.length;
|
|
1317
|
+
for (let i = posInChunk; i < text.length; i++) {
|
|
1318
|
+
if (text[i] === '\n') {
|
|
1319
|
+
lineEnd = i;
|
|
1320
|
+
break;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// Extract line text and calculate column
|
|
1325
|
+
const lineText = text.substring(lineStart, lineEnd);
|
|
1326
|
+
const col = posInChunk - lineStart;
|
|
1327
|
+
|
|
1328
|
+
let targetCol = -1;
|
|
1329
|
+
|
|
1330
|
+
if (findType === "f" || findType === "t") {
|
|
1331
|
+
// Search forward on the line
|
|
1332
|
+
for (let i = col + 1; i < lineText.length; i++) {
|
|
1333
|
+
if (lineText[i] === char) {
|
|
1334
|
+
targetCol = findType === "f" ? i : i - 1;
|
|
1335
|
+
break;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
} else {
|
|
1339
|
+
// Search backward (F/T)
|
|
1340
|
+
for (let i = col - 1; i >= 0; i--) {
|
|
1341
|
+
if (lineText[i] === char) {
|
|
1342
|
+
targetCol = findType === "F" ? i : i + 1;
|
|
1343
|
+
break;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (targetCol >= 0 && targetCol !== col) {
|
|
1349
|
+
// Move to target column
|
|
1350
|
+
const diff = targetCol - col;
|
|
1351
|
+
const moveAction = diff > 0 ? "move_right" : "move_left";
|
|
1352
|
+
const steps = Math.abs(diff);
|
|
1353
|
+
for (let i = 0; i < steps; i++) {
|
|
1354
|
+
editor.executeAction(moveAction);
|
|
1355
|
+
}
|
|
1356
|
+
// Save for ; and , repeat
|
|
1357
|
+
state.lastFindChar = { type: findType, char };
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// Handler for when a character is typed in find-char mode (async)
|
|
1362
|
+
globalThis.vi_find_char_handler = async function (char: string): Promise<void> {
|
|
1363
|
+
if (state.pendingFindChar) {
|
|
1364
|
+
await executeFindChar(state.pendingFindChar, char);
|
|
1365
|
+
}
|
|
1366
|
+
// Return to normal mode
|
|
1367
|
+
state.pendingFindChar = null;
|
|
1368
|
+
switchMode("normal");
|
|
1369
|
+
};
|
|
1370
|
+
|
|
1371
|
+
// Commands to enter find-char mode
|
|
1372
|
+
globalThis.vi_find_char_f = function (): void {
|
|
1373
|
+
enterFindCharMode("f");
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1376
|
+
globalThis.vi_find_char_t = function (): void {
|
|
1377
|
+
enterFindCharMode("t");
|
|
1378
|
+
};
|
|
1379
|
+
|
|
1380
|
+
globalThis.vi_find_char_F = function (): void {
|
|
1381
|
+
enterFindCharMode("F");
|
|
1382
|
+
};
|
|
1383
|
+
|
|
1384
|
+
globalThis.vi_find_char_T = function (): void {
|
|
1385
|
+
enterFindCharMode("T");
|
|
1386
|
+
};
|
|
1387
|
+
|
|
1388
|
+
// Repeat last find char (async)
|
|
1389
|
+
globalThis.vi_find_char_repeat = async function (): Promise<void> {
|
|
1390
|
+
if (state.lastFindChar) {
|
|
1391
|
+
await executeFindChar(state.lastFindChar.type, state.lastFindChar.char);
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
// Repeat last find char in opposite direction (async)
|
|
1396
|
+
globalThis.vi_find_char_repeat_reverse = async function (): Promise<void> {
|
|
1397
|
+
if (state.lastFindChar) {
|
|
1398
|
+
const reversedType: FindCharType =
|
|
1399
|
+
state.lastFindChar.type === "f" ? "F" :
|
|
1400
|
+
state.lastFindChar.type === "F" ? "f" :
|
|
1401
|
+
state.lastFindChar.type === "t" ? "T" : "t";
|
|
1402
|
+
await executeFindChar(reversedType, state.lastFindChar.char);
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
|
|
1406
|
+
// Cancel find-char mode
|
|
1407
|
+
globalThis.vi_find_char_cancel = function (): void {
|
|
1408
|
+
state.pendingFindChar = null;
|
|
1409
|
+
switchMode("normal");
|
|
1410
|
+
};
|
|
1411
|
+
|
|
1412
|
+
// ============================================================================
|
|
1413
|
+
// Operator-Pending Mode Commands
|
|
1414
|
+
// ============================================================================
|
|
1415
|
+
|
|
1416
|
+
globalThis.vi_op_left = function (): void {
|
|
1417
|
+
handleMotionWithOperator("move_left");
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
globalThis.vi_op_down = function (): void {
|
|
1421
|
+
handleMotionWithOperator("move_down");
|
|
1422
|
+
};
|
|
1423
|
+
|
|
1424
|
+
globalThis.vi_op_up = function (): void {
|
|
1425
|
+
handleMotionWithOperator("move_up");
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1428
|
+
globalThis.vi_op_right = function (): void {
|
|
1429
|
+
handleMotionWithOperator("move_right");
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
globalThis.vi_op_word = function (): void {
|
|
1433
|
+
handleMotionWithOperator("move_word_right");
|
|
1434
|
+
};
|
|
1435
|
+
|
|
1436
|
+
globalThis.vi_op_word_back = function (): void {
|
|
1437
|
+
handleMotionWithOperator("move_word_left");
|
|
1438
|
+
};
|
|
1439
|
+
|
|
1440
|
+
globalThis.vi_op_line_start = function (): void {
|
|
1441
|
+
handleMotionWithOperator("move_line_start");
|
|
1442
|
+
};
|
|
1443
|
+
|
|
1444
|
+
globalThis.vi_op_line_end = function (): void {
|
|
1445
|
+
handleMotionWithOperator("move_line_end");
|
|
1446
|
+
};
|
|
1447
|
+
|
|
1448
|
+
globalThis.vi_op_doc_start = function (): void {
|
|
1449
|
+
handleMotionWithOperator("move_document_start");
|
|
1450
|
+
};
|
|
1451
|
+
|
|
1452
|
+
globalThis.vi_op_doc_end = function (): void {
|
|
1453
|
+
handleMotionWithOperator("move_document_end");
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
globalThis.vi_op_matching_bracket = function (): void {
|
|
1457
|
+
handleMotionWithOperator("go_to_matching_bracket");
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1460
|
+
globalThis.vi_cancel = function (): void {
|
|
1461
|
+
switchMode("normal");
|
|
1462
|
+
};
|
|
1463
|
+
|
|
1464
|
+
// ============================================================================
|
|
1465
|
+
// Mode Definitions
|
|
1466
|
+
// ============================================================================
|
|
1467
|
+
|
|
1468
|
+
// Define vi-normal mode
|
|
1469
|
+
editor.defineMode("vi-normal", null, [
|
|
1470
|
+
// Count prefix (digits 1-9 start count, 0 is special)
|
|
1471
|
+
["1", "vi_digit_1"],
|
|
1472
|
+
["2", "vi_digit_2"],
|
|
1473
|
+
["3", "vi_digit_3"],
|
|
1474
|
+
["4", "vi_digit_4"],
|
|
1475
|
+
["5", "vi_digit_5"],
|
|
1476
|
+
["6", "vi_digit_6"],
|
|
1477
|
+
["7", "vi_digit_7"],
|
|
1478
|
+
["8", "vi_digit_8"],
|
|
1479
|
+
["9", "vi_digit_9"],
|
|
1480
|
+
["0", "vi_digit_0_or_line_start"], // 0 appends to count, or moves to line start
|
|
1481
|
+
|
|
1482
|
+
// Navigation
|
|
1483
|
+
["h", "vi_left"],
|
|
1484
|
+
["j", "vi_down"],
|
|
1485
|
+
["k", "vi_up"],
|
|
1486
|
+
["l", "vi_right"],
|
|
1487
|
+
["w", "vi_word"],
|
|
1488
|
+
["b", "vi_word_back"],
|
|
1489
|
+
["e", "vi_word_end"],
|
|
1490
|
+
["$", "vi_line_end"],
|
|
1491
|
+
["^", "vi_first_non_blank"],
|
|
1492
|
+
["g g", "vi_doc_start"],
|
|
1493
|
+
["G", "vi_doc_end"],
|
|
1494
|
+
["C-f", "vi_page_down"],
|
|
1495
|
+
["C-b", "vi_page_up"],
|
|
1496
|
+
["C-d", "vi_half_page_down"],
|
|
1497
|
+
["C-u", "vi_half_page_up"],
|
|
1498
|
+
["%", "vi_matching_bracket"],
|
|
1499
|
+
["z z", "vi_center_cursor"],
|
|
1500
|
+
|
|
1501
|
+
// Search
|
|
1502
|
+
["/", "vi_search_forward"],
|
|
1503
|
+
["?", "vi_search_backward"],
|
|
1504
|
+
["n", "vi_find_next"],
|
|
1505
|
+
["N", "vi_find_prev"],
|
|
1506
|
+
|
|
1507
|
+
// Find character on line
|
|
1508
|
+
["f", "vi_find_char_f"],
|
|
1509
|
+
["t", "vi_find_char_t"],
|
|
1510
|
+
["F", "vi_find_char_F"],
|
|
1511
|
+
["T", "vi_find_char_T"],
|
|
1512
|
+
[";", "vi_find_char_repeat"],
|
|
1513
|
+
[",", "vi_find_char_repeat_reverse"],
|
|
1514
|
+
|
|
1515
|
+
// Mode switching
|
|
1516
|
+
["i", "vi_insert_before"],
|
|
1517
|
+
["a", "vi_insert_after"],
|
|
1518
|
+
["I", "vi_insert_line_start"],
|
|
1519
|
+
["A", "vi_insert_line_end"],
|
|
1520
|
+
["o", "vi_open_below"],
|
|
1521
|
+
["O", "vi_open_above"],
|
|
1522
|
+
["Escape", "vi_escape"],
|
|
1523
|
+
|
|
1524
|
+
// Operators (single key - switches to operator-pending mode)
|
|
1525
|
+
// The second d/c/y is handled in operator-pending mode
|
|
1526
|
+
["d", "vi_delete_operator"],
|
|
1527
|
+
["c", "vi_change_operator"],
|
|
1528
|
+
["y", "vi_yank_operator"],
|
|
1529
|
+
|
|
1530
|
+
// Single char operations
|
|
1531
|
+
["x", "vi_delete_char"],
|
|
1532
|
+
["X", "vi_delete_char_before"],
|
|
1533
|
+
["r", "vi_replace_char"],
|
|
1534
|
+
["s", "vi_substitute"],
|
|
1535
|
+
["S", "vi_change_line"],
|
|
1536
|
+
["D", "vi_delete_to_end"],
|
|
1537
|
+
["C", "vi_change_to_end"],
|
|
1538
|
+
|
|
1539
|
+
// Clipboard
|
|
1540
|
+
["p", "vi_paste_after"],
|
|
1541
|
+
["P", "vi_paste_before"],
|
|
1542
|
+
|
|
1543
|
+
// Undo/Redo
|
|
1544
|
+
["u", "vi_undo"],
|
|
1545
|
+
["C-r", "vi_redo"],
|
|
1546
|
+
|
|
1547
|
+
// Repeat last change
|
|
1548
|
+
[".", "vi_repeat"],
|
|
1549
|
+
|
|
1550
|
+
// Visual mode
|
|
1551
|
+
["v", "vi_visual_char"],
|
|
1552
|
+
["V", "vi_visual_line"],
|
|
1553
|
+
["C-v", "vi_visual_block"],
|
|
1554
|
+
|
|
1555
|
+
// Other
|
|
1556
|
+
["J", "vi_join"],
|
|
1557
|
+
|
|
1558
|
+
// Command mode
|
|
1559
|
+
[":", "vi_command_mode"],
|
|
1560
|
+
], true); // read_only = true to prevent character insertion
|
|
1561
|
+
|
|
1562
|
+
// Define vi-insert mode - only Escape is special, other keys insert text
|
|
1563
|
+
editor.defineMode("vi-insert", null, [
|
|
1564
|
+
["Escape", "vi_escape"],
|
|
1565
|
+
], false); // read_only = false to allow normal typing
|
|
1566
|
+
|
|
1567
|
+
// Define vi-find-char mode - binds all printable chars to the handler
|
|
1568
|
+
// This mode waits for a single character input for f/t/F/T motions
|
|
1569
|
+
|
|
1570
|
+
// Explicitly define handlers for each character to ensure they're accessible
|
|
1571
|
+
// These return Promises so the runtime can await them
|
|
1572
|
+
globalThis.vi_fc_a = async function(): Promise<void> { return globalThis.vi_find_char_handler("a"); };
|
|
1573
|
+
globalThis.vi_fc_b = async function(): Promise<void> { return globalThis.vi_find_char_handler("b"); };
|
|
1574
|
+
globalThis.vi_fc_c = async function(): Promise<void> { return globalThis.vi_find_char_handler("c"); };
|
|
1575
|
+
globalThis.vi_fc_d = async function(): Promise<void> { return globalThis.vi_find_char_handler("d"); };
|
|
1576
|
+
globalThis.vi_fc_e = async function(): Promise<void> { return globalThis.vi_find_char_handler("e"); };
|
|
1577
|
+
globalThis.vi_fc_f = async function(): Promise<void> { return globalThis.vi_find_char_handler("f"); };
|
|
1578
|
+
globalThis.vi_fc_g = async function(): Promise<void> { return globalThis.vi_find_char_handler("g"); };
|
|
1579
|
+
globalThis.vi_fc_h = async function(): Promise<void> { return globalThis.vi_find_char_handler("h"); };
|
|
1580
|
+
globalThis.vi_fc_i = async function(): Promise<void> { return globalThis.vi_find_char_handler("i"); };
|
|
1581
|
+
globalThis.vi_fc_j = async function(): Promise<void> { return globalThis.vi_find_char_handler("j"); };
|
|
1582
|
+
globalThis.vi_fc_k = async function(): Promise<void> { return globalThis.vi_find_char_handler("k"); };
|
|
1583
|
+
globalThis.vi_fc_l = async function(): Promise<void> { return globalThis.vi_find_char_handler("l"); };
|
|
1584
|
+
globalThis.vi_fc_m = async function(): Promise<void> { return globalThis.vi_find_char_handler("m"); };
|
|
1585
|
+
globalThis.vi_fc_n = async function(): Promise<void> { return globalThis.vi_find_char_handler("n"); };
|
|
1586
|
+
globalThis.vi_fc_o = async function(): Promise<void> { return globalThis.vi_find_char_handler("o"); };
|
|
1587
|
+
globalThis.vi_fc_p = async function(): Promise<void> { return globalThis.vi_find_char_handler("p"); };
|
|
1588
|
+
globalThis.vi_fc_q = async function(): Promise<void> { return globalThis.vi_find_char_handler("q"); };
|
|
1589
|
+
globalThis.vi_fc_r = async function(): Promise<void> { return globalThis.vi_find_char_handler("r"); };
|
|
1590
|
+
globalThis.vi_fc_s = async function(): Promise<void> { return globalThis.vi_find_char_handler("s"); };
|
|
1591
|
+
globalThis.vi_fc_t = async function(): Promise<void> { return globalThis.vi_find_char_handler("t"); };
|
|
1592
|
+
globalThis.vi_fc_u = async function(): Promise<void> { return globalThis.vi_find_char_handler("u"); };
|
|
1593
|
+
globalThis.vi_fc_v = async function(): Promise<void> { return globalThis.vi_find_char_handler("v"); };
|
|
1594
|
+
globalThis.vi_fc_w = async function(): Promise<void> { return globalThis.vi_find_char_handler("w"); };
|
|
1595
|
+
globalThis.vi_fc_x = async function(): Promise<void> { return globalThis.vi_find_char_handler("x"); };
|
|
1596
|
+
globalThis.vi_fc_y = async function(): Promise<void> { return globalThis.vi_find_char_handler("y"); };
|
|
1597
|
+
globalThis.vi_fc_z = async function(): Promise<void> { return globalThis.vi_find_char_handler("z"); };
|
|
1598
|
+
globalThis.vi_fc_A = async function(): Promise<void> { return globalThis.vi_find_char_handler("A"); };
|
|
1599
|
+
globalThis.vi_fc_B = async function(): Promise<void> { return globalThis.vi_find_char_handler("B"); };
|
|
1600
|
+
globalThis.vi_fc_C = async function(): Promise<void> { return globalThis.vi_find_char_handler("C"); };
|
|
1601
|
+
globalThis.vi_fc_D = async function(): Promise<void> { return globalThis.vi_find_char_handler("D"); };
|
|
1602
|
+
globalThis.vi_fc_E = async function(): Promise<void> { return globalThis.vi_find_char_handler("E"); };
|
|
1603
|
+
globalThis.vi_fc_F = async function(): Promise<void> { return globalThis.vi_find_char_handler("F"); };
|
|
1604
|
+
globalThis.vi_fc_G = async function(): Promise<void> { return globalThis.vi_find_char_handler("G"); };
|
|
1605
|
+
globalThis.vi_fc_H = async function(): Promise<void> { return globalThis.vi_find_char_handler("H"); };
|
|
1606
|
+
globalThis.vi_fc_I = async function(): Promise<void> { return globalThis.vi_find_char_handler("I"); };
|
|
1607
|
+
globalThis.vi_fc_J = async function(): Promise<void> { return globalThis.vi_find_char_handler("J"); };
|
|
1608
|
+
globalThis.vi_fc_K = async function(): Promise<void> { return globalThis.vi_find_char_handler("K"); };
|
|
1609
|
+
globalThis.vi_fc_L = async function(): Promise<void> { return globalThis.vi_find_char_handler("L"); };
|
|
1610
|
+
globalThis.vi_fc_M = async function(): Promise<void> { return globalThis.vi_find_char_handler("M"); };
|
|
1611
|
+
globalThis.vi_fc_N = async function(): Promise<void> { return globalThis.vi_find_char_handler("N"); };
|
|
1612
|
+
globalThis.vi_fc_O = async function(): Promise<void> { return globalThis.vi_find_char_handler("O"); };
|
|
1613
|
+
globalThis.vi_fc_P = async function(): Promise<void> { return globalThis.vi_find_char_handler("P"); };
|
|
1614
|
+
globalThis.vi_fc_Q = async function(): Promise<void> { return globalThis.vi_find_char_handler("Q"); };
|
|
1615
|
+
globalThis.vi_fc_R = async function(): Promise<void> { return globalThis.vi_find_char_handler("R"); };
|
|
1616
|
+
globalThis.vi_fc_S = async function(): Promise<void> { return globalThis.vi_find_char_handler("S"); };
|
|
1617
|
+
globalThis.vi_fc_T = async function(): Promise<void> { return globalThis.vi_find_char_handler("T"); };
|
|
1618
|
+
globalThis.vi_fc_U = async function(): Promise<void> { return globalThis.vi_find_char_handler("U"); };
|
|
1619
|
+
globalThis.vi_fc_V = async function(): Promise<void> { return globalThis.vi_find_char_handler("V"); };
|
|
1620
|
+
globalThis.vi_fc_W = async function(): Promise<void> { return globalThis.vi_find_char_handler("W"); };
|
|
1621
|
+
globalThis.vi_fc_X = async function(): Promise<void> { return globalThis.vi_find_char_handler("X"); };
|
|
1622
|
+
globalThis.vi_fc_Y = async function(): Promise<void> { return globalThis.vi_find_char_handler("Y"); };
|
|
1623
|
+
globalThis.vi_fc_Z = async function(): Promise<void> { return globalThis.vi_find_char_handler("Z"); };
|
|
1624
|
+
globalThis.vi_fc_0 = async function(): Promise<void> { return globalThis.vi_find_char_handler("0"); };
|
|
1625
|
+
globalThis.vi_fc_1 = async function(): Promise<void> { return globalThis.vi_find_char_handler("1"); };
|
|
1626
|
+
globalThis.vi_fc_2 = async function(): Promise<void> { return globalThis.vi_find_char_handler("2"); };
|
|
1627
|
+
globalThis.vi_fc_3 = async function(): Promise<void> { return globalThis.vi_find_char_handler("3"); };
|
|
1628
|
+
globalThis.vi_fc_4 = async function(): Promise<void> { return globalThis.vi_find_char_handler("4"); };
|
|
1629
|
+
globalThis.vi_fc_5 = async function(): Promise<void> { return globalThis.vi_find_char_handler("5"); };
|
|
1630
|
+
globalThis.vi_fc_6 = async function(): Promise<void> { return globalThis.vi_find_char_handler("6"); };
|
|
1631
|
+
globalThis.vi_fc_7 = async function(): Promise<void> { return globalThis.vi_find_char_handler("7"); };
|
|
1632
|
+
globalThis.vi_fc_8 = async function(): Promise<void> { return globalThis.vi_find_char_handler("8"); };
|
|
1633
|
+
globalThis.vi_fc_9 = async function(): Promise<void> { return globalThis.vi_find_char_handler("9"); };
|
|
1634
|
+
globalThis.vi_fc_space = async function(): Promise<void> { return globalThis.vi_find_char_handler(" "); };
|
|
1635
|
+
|
|
1636
|
+
// Define vi-find-char mode with all the character bindings
|
|
1637
|
+
editor.defineMode("vi-find-char", null, [
|
|
1638
|
+
["Escape", "vi_find_char_cancel"],
|
|
1639
|
+
// Letters
|
|
1640
|
+
["a", "vi_fc_a"], ["b", "vi_fc_b"], ["c", "vi_fc_c"], ["d", "vi_fc_d"],
|
|
1641
|
+
["e", "vi_fc_e"], ["f", "vi_fc_f"], ["g", "vi_fc_g"], ["h", "vi_fc_h"],
|
|
1642
|
+
["i", "vi_fc_i"], ["j", "vi_fc_j"], ["k", "vi_fc_k"], ["l", "vi_fc_l"],
|
|
1643
|
+
["m", "vi_fc_m"], ["n", "vi_fc_n"], ["o", "vi_fc_o"], ["p", "vi_fc_p"],
|
|
1644
|
+
["q", "vi_fc_q"], ["r", "vi_fc_r"], ["s", "vi_fc_s"], ["t", "vi_fc_t"],
|
|
1645
|
+
["u", "vi_fc_u"], ["v", "vi_fc_v"], ["w", "vi_fc_w"], ["x", "vi_fc_x"],
|
|
1646
|
+
["y", "vi_fc_y"], ["z", "vi_fc_z"],
|
|
1647
|
+
["A", "vi_fc_A"], ["B", "vi_fc_B"], ["C", "vi_fc_C"], ["D", "vi_fc_D"],
|
|
1648
|
+
["E", "vi_fc_E"], ["F", "vi_fc_F"], ["G", "vi_fc_G"], ["H", "vi_fc_H"],
|
|
1649
|
+
["I", "vi_fc_I"], ["J", "vi_fc_J"], ["K", "vi_fc_K"], ["L", "vi_fc_L"],
|
|
1650
|
+
["M", "vi_fc_M"], ["N", "vi_fc_N"], ["O", "vi_fc_O"], ["P", "vi_fc_P"],
|
|
1651
|
+
["Q", "vi_fc_Q"], ["R", "vi_fc_R"], ["S", "vi_fc_S"], ["T", "vi_fc_T"],
|
|
1652
|
+
["U", "vi_fc_U"], ["V", "vi_fc_V"], ["W", "vi_fc_W"], ["X", "vi_fc_X"],
|
|
1653
|
+
["Y", "vi_fc_Y"], ["Z", "vi_fc_Z"],
|
|
1654
|
+
// Digits
|
|
1655
|
+
["0", "vi_fc_0"], ["1", "vi_fc_1"], ["2", "vi_fc_2"], ["3", "vi_fc_3"],
|
|
1656
|
+
["4", "vi_fc_4"], ["5", "vi_fc_5"], ["6", "vi_fc_6"], ["7", "vi_fc_7"],
|
|
1657
|
+
["8", "vi_fc_8"], ["9", "vi_fc_9"],
|
|
1658
|
+
// Common punctuation
|
|
1659
|
+
["Space", "vi_fc_space"],
|
|
1660
|
+
], true);
|
|
1661
|
+
|
|
1662
|
+
// Define vi-operator-pending mode
|
|
1663
|
+
editor.defineMode("vi-operator-pending", null, [
|
|
1664
|
+
// Count prefix in operator-pending mode (for d3w = delete 3 words)
|
|
1665
|
+
["1", "vi_digit_1"],
|
|
1666
|
+
["2", "vi_digit_2"],
|
|
1667
|
+
["3", "vi_digit_3"],
|
|
1668
|
+
["4", "vi_digit_4"],
|
|
1669
|
+
["5", "vi_digit_5"],
|
|
1670
|
+
["6", "vi_digit_6"],
|
|
1671
|
+
["7", "vi_digit_7"],
|
|
1672
|
+
["8", "vi_digit_8"],
|
|
1673
|
+
["9", "vi_digit_9"],
|
|
1674
|
+
["0", "vi_op_digit_0_or_line_start"], // 0 appends to count, or is motion to line start
|
|
1675
|
+
|
|
1676
|
+
// Motions for operators
|
|
1677
|
+
["h", "vi_op_left"],
|
|
1678
|
+
["j", "vi_op_down"],
|
|
1679
|
+
["k", "vi_op_up"],
|
|
1680
|
+
["l", "vi_op_right"],
|
|
1681
|
+
["w", "vi_op_word"],
|
|
1682
|
+
["b", "vi_op_word_back"],
|
|
1683
|
+
["$", "vi_op_line_end"],
|
|
1684
|
+
["g g", "vi_op_doc_start"],
|
|
1685
|
+
["G", "vi_op_doc_end"],
|
|
1686
|
+
["%", "vi_op_matching_bracket"],
|
|
1687
|
+
|
|
1688
|
+
// Text objects
|
|
1689
|
+
["i", "vi_text_object_inner"],
|
|
1690
|
+
["a", "vi_text_object_around"],
|
|
1691
|
+
|
|
1692
|
+
// Double operator = line operation
|
|
1693
|
+
["d", "vi_delete_line"],
|
|
1694
|
+
["c", "vi_change_line"],
|
|
1695
|
+
["y", "vi_yank_line"],
|
|
1696
|
+
|
|
1697
|
+
// Cancel
|
|
1698
|
+
["Escape", "vi_cancel"],
|
|
1699
|
+
], true);
|
|
1700
|
+
|
|
1701
|
+
// Define vi-text-object mode (waiting for object type: w, ", (, etc.)
|
|
1702
|
+
editor.defineMode("vi-text-object", null, [
|
|
1703
|
+
// Word objects
|
|
1704
|
+
["w", "vi_to_word"],
|
|
1705
|
+
["W", "vi_to_WORD"],
|
|
1706
|
+
|
|
1707
|
+
// Quote objects
|
|
1708
|
+
["\"", "vi_to_dquote"],
|
|
1709
|
+
["'", "vi_to_squote"],
|
|
1710
|
+
["`", "vi_to_backtick"],
|
|
1711
|
+
|
|
1712
|
+
// Bracket objects
|
|
1713
|
+
["(", "vi_to_paren"],
|
|
1714
|
+
[")", "vi_to_paren"],
|
|
1715
|
+
["b", "vi_to_paren"],
|
|
1716
|
+
["{", "vi_to_brace"],
|
|
1717
|
+
["}", "vi_to_brace"],
|
|
1718
|
+
["B", "vi_to_brace"],
|
|
1719
|
+
["[", "vi_to_bracket"],
|
|
1720
|
+
["]", "vi_to_bracket"],
|
|
1721
|
+
["<", "vi_to_angle"],
|
|
1722
|
+
[">", "vi_to_angle"],
|
|
1723
|
+
|
|
1724
|
+
// Cancel
|
|
1725
|
+
["Escape", "vi_to_cancel"],
|
|
1726
|
+
], true);
|
|
1727
|
+
|
|
1728
|
+
// Define vi-visual mode (character-wise)
|
|
1729
|
+
editor.defineMode("vi-visual", null, [
|
|
1730
|
+
// Count prefix
|
|
1731
|
+
["1", "vi_digit_1"],
|
|
1732
|
+
["2", "vi_digit_2"],
|
|
1733
|
+
["3", "vi_digit_3"],
|
|
1734
|
+
["4", "vi_digit_4"],
|
|
1735
|
+
["5", "vi_digit_5"],
|
|
1736
|
+
["6", "vi_digit_6"],
|
|
1737
|
+
["7", "vi_digit_7"],
|
|
1738
|
+
["8", "vi_digit_8"],
|
|
1739
|
+
["9", "vi_digit_9"],
|
|
1740
|
+
["0", "vi_vis_line_start"], // 0 moves to line start in visual mode
|
|
1741
|
+
|
|
1742
|
+
// Motions (extend selection)
|
|
1743
|
+
["h", "vi_vis_left"],
|
|
1744
|
+
["j", "vi_vis_down"],
|
|
1745
|
+
["k", "vi_vis_up"],
|
|
1746
|
+
["l", "vi_vis_right"],
|
|
1747
|
+
["w", "vi_vis_word"],
|
|
1748
|
+
["b", "vi_vis_word_back"],
|
|
1749
|
+
["e", "vi_vis_word_end"],
|
|
1750
|
+
["$", "vi_vis_line_end"],
|
|
1751
|
+
["^", "vi_vis_line_start"],
|
|
1752
|
+
["g g", "vi_vis_doc_start"],
|
|
1753
|
+
["G", "vi_vis_doc_end"],
|
|
1754
|
+
|
|
1755
|
+
// Switch to line mode
|
|
1756
|
+
["V", "vi_visual_toggle_line"],
|
|
1757
|
+
|
|
1758
|
+
// Operators
|
|
1759
|
+
["d", "vi_vis_delete"],
|
|
1760
|
+
["x", "vi_vis_delete"],
|
|
1761
|
+
["c", "vi_vis_change"],
|
|
1762
|
+
["s", "vi_vis_change"],
|
|
1763
|
+
["y", "vi_vis_yank"],
|
|
1764
|
+
|
|
1765
|
+
// Exit
|
|
1766
|
+
["Escape", "vi_vis_escape"],
|
|
1767
|
+
["v", "vi_vis_escape"], // v again exits visual mode
|
|
1768
|
+
], true);
|
|
1769
|
+
|
|
1770
|
+
// Define vi-visual-line mode (line-wise)
|
|
1771
|
+
editor.defineMode("vi-visual-line", null, [
|
|
1772
|
+
// Count prefix
|
|
1773
|
+
["1", "vi_digit_1"],
|
|
1774
|
+
["2", "vi_digit_2"],
|
|
1775
|
+
["3", "vi_digit_3"],
|
|
1776
|
+
["4", "vi_digit_4"],
|
|
1777
|
+
["5", "vi_digit_5"],
|
|
1778
|
+
["6", "vi_digit_6"],
|
|
1779
|
+
["7", "vi_digit_7"],
|
|
1780
|
+
["8", "vi_digit_8"],
|
|
1781
|
+
["9", "vi_digit_9"],
|
|
1782
|
+
|
|
1783
|
+
// Line motions (extend selection by lines)
|
|
1784
|
+
["j", "vi_vline_down"],
|
|
1785
|
+
["k", "vi_vline_up"],
|
|
1786
|
+
["g g", "vi_vis_doc_start"],
|
|
1787
|
+
["G", "vi_vis_doc_end"],
|
|
1788
|
+
|
|
1789
|
+
// Switch to char mode
|
|
1790
|
+
["v", "vi_visual_toggle_line"],
|
|
1791
|
+
|
|
1792
|
+
// Operators
|
|
1793
|
+
["d", "vi_vis_delete"],
|
|
1794
|
+
["x", "vi_vis_delete"],
|
|
1795
|
+
["c", "vi_vis_change"],
|
|
1796
|
+
["s", "vi_vis_change"],
|
|
1797
|
+
["y", "vi_vis_yank"],
|
|
1798
|
+
|
|
1799
|
+
// Exit
|
|
1800
|
+
["Escape", "vi_vis_escape"],
|
|
1801
|
+
["V", "vi_vis_escape"], // V again exits visual-line mode
|
|
1802
|
+
], true);
|
|
1803
|
+
|
|
1804
|
+
// Define vi-visual-block mode (column/block selection)
|
|
1805
|
+
editor.defineMode("vi-visual-block", null, [
|
|
1806
|
+
// Count prefix
|
|
1807
|
+
["1", "vi_digit_1"],
|
|
1808
|
+
["2", "vi_digit_2"],
|
|
1809
|
+
["3", "vi_digit_3"],
|
|
1810
|
+
["4", "vi_digit_4"],
|
|
1811
|
+
["5", "vi_digit_5"],
|
|
1812
|
+
["6", "vi_digit_6"],
|
|
1813
|
+
["7", "vi_digit_7"],
|
|
1814
|
+
["8", "vi_digit_8"],
|
|
1815
|
+
["9", "vi_digit_9"],
|
|
1816
|
+
["0", "vi_vblock_line_start"],
|
|
1817
|
+
|
|
1818
|
+
// Motions (extend block selection)
|
|
1819
|
+
["h", "vi_vblock_left"],
|
|
1820
|
+
["j", "vi_vblock_down"],
|
|
1821
|
+
["k", "vi_vblock_up"],
|
|
1822
|
+
["l", "vi_vblock_right"],
|
|
1823
|
+
["$", "vi_vblock_line_end"],
|
|
1824
|
+
["^", "vi_vblock_line_start"],
|
|
1825
|
+
|
|
1826
|
+
// Switch to other visual modes
|
|
1827
|
+
["v", "vi_vblock_toggle_char"],
|
|
1828
|
+
["V", "vi_vblock_toggle_line"],
|
|
1829
|
+
|
|
1830
|
+
// Operators
|
|
1831
|
+
["d", "vi_vblock_delete"],
|
|
1832
|
+
["x", "vi_vblock_delete"],
|
|
1833
|
+
["c", "vi_vblock_change"],
|
|
1834
|
+
["s", "vi_vblock_change"],
|
|
1835
|
+
["y", "vi_vblock_yank"],
|
|
1836
|
+
|
|
1837
|
+
// Exit
|
|
1838
|
+
["Escape", "vi_vblock_escape"],
|
|
1839
|
+
["C-v", "vi_vblock_escape"], // Ctrl-v again exits visual-block mode
|
|
1840
|
+
], true);
|
|
1841
|
+
|
|
1842
|
+
// ============================================================================
|
|
1843
|
+
// Register Commands
|
|
1844
|
+
// ============================================================================
|
|
1845
|
+
|
|
1846
|
+
// Navigation commands
|
|
1847
|
+
const navCommands = [
|
|
1848
|
+
["vi_left", "move_left"],
|
|
1849
|
+
["vi_down", "move_down"],
|
|
1850
|
+
["vi_up", "move_up"],
|
|
1851
|
+
["vi_right", "move_right"],
|
|
1852
|
+
["vi_word", "move_word"],
|
|
1853
|
+
["vi_word_back", "move_word_back"],
|
|
1854
|
+
["vi_word_end", "move_word_end"],
|
|
1855
|
+
["vi_line_start", "move_line_start"],
|
|
1856
|
+
["vi_line_end", "move_line_end"],
|
|
1857
|
+
["vi_doc_start", "move_doc_start"],
|
|
1858
|
+
["vi_doc_end", "move_doc_end"],
|
|
1859
|
+
["vi_page_down", "page_down"],
|
|
1860
|
+
["vi_page_up", "page_up"],
|
|
1861
|
+
["vi_half_page_down", "half_page_down"],
|
|
1862
|
+
["vi_half_page_up", "half_page_up"],
|
|
1863
|
+
["vi_center_cursor", "center_cursor"],
|
|
1864
|
+
["vi_search_forward", "search_forward"],
|
|
1865
|
+
["vi_search_backward", "search_backward"],
|
|
1866
|
+
["vi_find_next", "find_next"],
|
|
1867
|
+
["vi_find_prev", "find_prev"],
|
|
1868
|
+
["vi_find_char_f", "find_char_f"],
|
|
1869
|
+
["vi_find_char_t", "find_char_t"],
|
|
1870
|
+
["vi_find_char_F", "find_char_F"],
|
|
1871
|
+
["vi_find_char_T", "find_char_T"],
|
|
1872
|
+
["vi_find_char_repeat", "find_char_repeat"],
|
|
1873
|
+
["vi_find_char_repeat_reverse", "find_char_repeat_reverse"],
|
|
1874
|
+
];
|
|
1875
|
+
|
|
1876
|
+
for (const [name, key] of navCommands) {
|
|
1877
|
+
editor.registerCommand(`%cmd.${key}`, `%cmd.${key}`, name, "vi-normal");
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
// Mode commands
|
|
1881
|
+
const modeCommands = [
|
|
1882
|
+
["vi_insert_before", "insert_before"],
|
|
1883
|
+
["vi_insert_after", "insert_after"],
|
|
1884
|
+
["vi_insert_line_start", "insert_line_start"],
|
|
1885
|
+
["vi_insert_line_end", "insert_line_end"],
|
|
1886
|
+
["vi_open_below", "open_below"],
|
|
1887
|
+
["vi_open_above", "open_above"],
|
|
1888
|
+
["vi_escape", "return_to_normal"],
|
|
1889
|
+
];
|
|
1890
|
+
|
|
1891
|
+
for (const [name, key] of modeCommands) {
|
|
1892
|
+
editor.registerCommand(`%cmd.${key}`, `%cmd.${key}`, name, "vi-normal");
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
// Operator commands
|
|
1896
|
+
const opCommands = [
|
|
1897
|
+
["vi_delete_operator", "delete_operator"],
|
|
1898
|
+
["vi_change_operator", "change_operator"],
|
|
1899
|
+
["vi_yank_operator", "yank_operator"],
|
|
1900
|
+
["vi_delete_line", "delete_line"],
|
|
1901
|
+
["vi_change_line", "change_line"],
|
|
1902
|
+
["vi_yank_line", "yank_line"],
|
|
1903
|
+
["vi_delete_char", "delete_char"],
|
|
1904
|
+
["vi_delete_char_before", "delete_char_before"],
|
|
1905
|
+
["vi_substitute", "substitute"],
|
|
1906
|
+
["vi_delete_to_end", "delete_to_end"],
|
|
1907
|
+
["vi_change_to_end", "change_to_end"],
|
|
1908
|
+
["vi_paste_after", "paste_after"],
|
|
1909
|
+
["vi_paste_before", "paste_before"],
|
|
1910
|
+
["vi_undo", "undo"],
|
|
1911
|
+
["vi_redo", "redo"],
|
|
1912
|
+
["vi_join", "join_lines"],
|
|
1913
|
+
];
|
|
1914
|
+
|
|
1915
|
+
for (const [name, key] of opCommands) {
|
|
1916
|
+
editor.registerCommand(`%cmd.${key}`, `%cmd.${key}`, name, "vi-normal");
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// ============================================================================
|
|
1920
|
+
// Colon Command Mode (:w, :q, :wq, :q!, :e, etc.)
|
|
1921
|
+
// ============================================================================
|
|
1922
|
+
|
|
1923
|
+
// Start command mode - shows ":" prompt at the bottom
|
|
1924
|
+
globalThis.vi_command_mode = function (): void {
|
|
1925
|
+
editor.startPrompt(":", "vi-command");
|
|
1926
|
+
};
|
|
1927
|
+
|
|
1928
|
+
// Handle command execution when user presses Enter
|
|
1929
|
+
globalThis.vi_command_handler = async function (args: { prompt_type: string; input: string }): Promise<boolean> {
|
|
1930
|
+
if (args.prompt_type !== "vi-command") {
|
|
1931
|
+
return false; // Not our prompt, let other handlers process it
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
const input = args.input.trim();
|
|
1935
|
+
if (!input) {
|
|
1936
|
+
return true; // Empty command, just dismiss
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// Parse the command
|
|
1940
|
+
const result = await executeViCommand(input);
|
|
1941
|
+
|
|
1942
|
+
if (result.error) {
|
|
1943
|
+
editor.setStatus(`E: ${result.error}`);
|
|
1944
|
+
} else if (result.message) {
|
|
1945
|
+
editor.setStatus(result.message);
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
return true; // We handled it
|
|
1949
|
+
};
|
|
1950
|
+
|
|
1951
|
+
interface CommandResult {
|
|
1952
|
+
error?: string;
|
|
1953
|
+
message?: string;
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
// Command definition for the command table
|
|
1957
|
+
interface CommandDef {
|
|
1958
|
+
name: string; // Full command name
|
|
1959
|
+
minAbbrev: number; // Minimum abbreviation length (e.g., 1 for "w" -> "write")
|
|
1960
|
+
allowBang: boolean; // Whether command accepts ! suffix
|
|
1961
|
+
hasArgs: boolean; // Whether command accepts arguments
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
// Command table - defines all supported commands with their abbreviations
|
|
1965
|
+
// Vim allows any unambiguous prefix of a command name
|
|
1966
|
+
const commandTable: CommandDef[] = [
|
|
1967
|
+
// File operations
|
|
1968
|
+
{ name: "write", minAbbrev: 1, allowBang: true, hasArgs: true }, // :w, :wri, :write
|
|
1969
|
+
{ name: "quit", minAbbrev: 1, allowBang: true, hasArgs: false }, // :q, :qu, :quit
|
|
1970
|
+
{ name: "wq", minAbbrev: 2, allowBang: true, hasArgs: false }, // :wq
|
|
1971
|
+
{ name: "wall", minAbbrev: 2, allowBang: false, hasArgs: false }, // :wa, :wall
|
|
1972
|
+
{ name: "qall", minAbbrev: 2, allowBang: true, hasArgs: false }, // :qa, :qall
|
|
1973
|
+
{ name: "wqall", minAbbrev: 3, allowBang: false, hasArgs: false }, // :wqa, :wqall
|
|
1974
|
+
{ name: "xit", minAbbrev: 1, allowBang: false, hasArgs: false }, // :x, :xit (same as :wq)
|
|
1975
|
+
{ name: "exit", minAbbrev: 3, allowBang: false, hasArgs: false }, // :exi, :exit
|
|
1976
|
+
{ name: "edit", minAbbrev: 1, allowBang: true, hasArgs: true }, // :e, :ed, :edit
|
|
1977
|
+
{ name: "enew", minAbbrev: 3, allowBang: true, hasArgs: false }, // :ene, :enew
|
|
1978
|
+
{ name: "saveas", minAbbrev: 3, allowBang: false, hasArgs: true }, // :sav, :saveas
|
|
1979
|
+
|
|
1980
|
+
// Buffer navigation
|
|
1981
|
+
{ name: "next", minAbbrev: 1, allowBang: true, hasArgs: false }, // :n, :next
|
|
1982
|
+
{ name: "previous", minAbbrev: 4, allowBang: true, hasArgs: false }, // :prev, :previous
|
|
1983
|
+
{ name: "bnext", minAbbrev: 2, allowBang: false, hasArgs: false }, // :bn, :bnext
|
|
1984
|
+
{ name: "bprevious", minAbbrev: 2, allowBang: false, hasArgs: false },// :bp, :bprev, :bprevious
|
|
1985
|
+
{ name: "bdelete", minAbbrev: 2, allowBang: true, hasArgs: false }, // :bd, :bdelete
|
|
1986
|
+
{ name: "buffer", minAbbrev: 1, allowBang: false, hasArgs: true }, // :b, :buffer
|
|
1987
|
+
{ name: "buffers", minAbbrev: 2, allowBang: false, hasArgs: false }, // :bu, :buffers (same as :ls)
|
|
1988
|
+
{ name: "ls", minAbbrev: 2, allowBang: false, hasArgs: false }, // :ls
|
|
1989
|
+
{ name: "files", minAbbrev: 3, allowBang: false, hasArgs: false }, // :fil, :files
|
|
1990
|
+
|
|
1991
|
+
// Splits
|
|
1992
|
+
{ name: "split", minAbbrev: 2, allowBang: false, hasArgs: true }, // :sp, :split
|
|
1993
|
+
{ name: "vsplit", minAbbrev: 2, allowBang: false, hasArgs: true }, // :vs, :vsplit
|
|
1994
|
+
{ name: "new", minAbbrev: 3, allowBang: false, hasArgs: true }, // :new
|
|
1995
|
+
{ name: "vnew", minAbbrev: 3, allowBang: false, hasArgs: true }, // :vne, :vnew
|
|
1996
|
+
{ name: "only", minAbbrev: 2, allowBang: true, hasArgs: false }, // :on, :only
|
|
1997
|
+
{ name: "close", minAbbrev: 3, allowBang: true, hasArgs: false }, // :clo, :close
|
|
1998
|
+
|
|
1999
|
+
// Tabs (mapped to buffers in Fresh)
|
|
2000
|
+
{ name: "tabnew", minAbbrev: 4, allowBang: false, hasArgs: true }, // :tabn, :tabnew
|
|
2001
|
+
{ name: "tabedit", minAbbrev: 4, allowBang: false, hasArgs: true }, // :tabe, :tabedit
|
|
2002
|
+
{ name: "tabclose", minAbbrev: 4, allowBang: true, hasArgs: false }, // :tabc, :tabclose
|
|
2003
|
+
{ name: "tabnext", minAbbrev: 5, allowBang: false, hasArgs: false }, // :tabne, :tabnext (note: different from :tabn)
|
|
2004
|
+
{ name: "tabprevious", minAbbrev: 4, allowBang: false, hasArgs: false }, // :tabp, :tabprevious
|
|
2005
|
+
|
|
2006
|
+
// Quickfix (mapped to diagnostics in Fresh)
|
|
2007
|
+
{ name: "copen", minAbbrev: 3, allowBang: false, hasArgs: false }, // :cop, :copen
|
|
2008
|
+
{ name: "cclose", minAbbrev: 3, allowBang: false, hasArgs: false }, // :ccl, :cclose
|
|
2009
|
+
{ name: "cnext", minAbbrev: 2, allowBang: true, hasArgs: false }, // :cn, :cnext
|
|
2010
|
+
{ name: "cprevious", minAbbrev: 2, allowBang: true, hasArgs: false },// :cp, :cprev, :cprevious
|
|
2011
|
+
{ name: "cfirst", minAbbrev: 3, allowBang: true, hasArgs: false }, // :cfir, :cfirst
|
|
2012
|
+
{ name: "clast", minAbbrev: 3, allowBang: true, hasArgs: false }, // :cla, :clast
|
|
2013
|
+
|
|
2014
|
+
// Search and replace
|
|
2015
|
+
{ name: "nohlsearch", minAbbrev: 3, allowBang: false, hasArgs: false }, // :noh, :nohlsearch
|
|
2016
|
+
{ name: "substitute", minAbbrev: 1, allowBang: false, hasArgs: true }, // :s, :substitute
|
|
2017
|
+
{ name: "global", minAbbrev: 1, allowBang: false, hasArgs: true }, // :g, :global
|
|
2018
|
+
{ name: "vglobal", minAbbrev: 2, allowBang: false, hasArgs: true }, // :vg, :vglobal
|
|
2019
|
+
|
|
2020
|
+
// Undo/redo
|
|
2021
|
+
{ name: "undo", minAbbrev: 1, allowBang: true, hasArgs: false }, // :u, :undo
|
|
2022
|
+
{ name: "redo", minAbbrev: 3, allowBang: false, hasArgs: false }, // :red, :redo
|
|
2023
|
+
|
|
2024
|
+
// Settings
|
|
2025
|
+
{ name: "set", minAbbrev: 2, allowBang: false, hasArgs: true }, // :se, :set
|
|
2026
|
+
|
|
2027
|
+
// Info commands
|
|
2028
|
+
{ name: "pwd", minAbbrev: 2, allowBang: false, hasArgs: false }, // :pw, :pwd
|
|
2029
|
+
{ name: "cd", minAbbrev: 2, allowBang: false, hasArgs: true }, // :cd
|
|
2030
|
+
{ name: "file", minAbbrev: 1, allowBang: false, hasArgs: true }, // :f, :file
|
|
2031
|
+
{ name: "help", minAbbrev: 1, allowBang: false, hasArgs: true }, // :h, :help
|
|
2032
|
+
{ name: "version", minAbbrev: 3, allowBang: false, hasArgs: false }, // :ver, :version
|
|
2033
|
+
|
|
2034
|
+
// Other
|
|
2035
|
+
{ name: "marks", minAbbrev: 4, allowBang: false, hasArgs: false }, // :mark, :marks
|
|
2036
|
+
{ name: "registers", minAbbrev: 3, allowBang: false, hasArgs: false },// :reg, :registers
|
|
2037
|
+
{ name: "jumps", minAbbrev: 2, allowBang: false, hasArgs: false }, // :ju, :jumps
|
|
2038
|
+
{ name: "syntax", minAbbrev: 2, allowBang: false, hasArgs: true }, // :sy, :syntax
|
|
2039
|
+
{ name: "read", minAbbrev: 1, allowBang: false, hasArgs: true }, // :r, :read
|
|
2040
|
+
{ name: "grep", minAbbrev: 2, allowBang: false, hasArgs: true }, // :gr, :grep
|
|
2041
|
+
{ name: "vimgrep", minAbbrev: 3, allowBang: false, hasArgs: true }, // :vim, :vimgrep
|
|
2042
|
+
{ name: "make", minAbbrev: 3, allowBang: true, hasArgs: true }, // :mak, :make
|
|
2043
|
+
{ name: "ascii", minAbbrev: 2, allowBang: false, hasArgs: false }, // :as, :ascii
|
|
2044
|
+
{ name: "revert", minAbbrev: 3, allowBang: false, hasArgs: false }, // :rev, :revert (Fresh-specific)
|
|
2045
|
+
];
|
|
2046
|
+
|
|
2047
|
+
// Find a command by name or abbreviation
|
|
2048
|
+
function findCommand(input: string): CommandDef | null {
|
|
2049
|
+
// Exact match first
|
|
2050
|
+
for (const cmd of commandTable) {
|
|
2051
|
+
if (cmd.name === input) {
|
|
2052
|
+
return cmd;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// Then try abbreviation matching
|
|
2057
|
+
const matches: CommandDef[] = [];
|
|
2058
|
+
for (const cmd of commandTable) {
|
|
2059
|
+
// Input must be at least minAbbrev chars and be a prefix of the command name
|
|
2060
|
+
if (input.length >= cmd.minAbbrev && cmd.name.startsWith(input)) {
|
|
2061
|
+
matches.push(cmd);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// Return only if unambiguous
|
|
2066
|
+
if (matches.length === 1) {
|
|
2067
|
+
return matches[0];
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// Handle special short aliases that vim supports even if ambiguous
|
|
2071
|
+
// These are the classic vim abbreviations that always work
|
|
2072
|
+
const shortAliases: Record<string, string> = {
|
|
2073
|
+
"w": "write",
|
|
2074
|
+
"q": "quit",
|
|
2075
|
+
"e": "edit",
|
|
2076
|
+
"n": "next",
|
|
2077
|
+
"N": "previous",
|
|
2078
|
+
"b": "buffer",
|
|
2079
|
+
"f": "file",
|
|
2080
|
+
"h": "help",
|
|
2081
|
+
"u": "undo",
|
|
2082
|
+
"r": "read",
|
|
2083
|
+
"s": "substitute",
|
|
2084
|
+
"g": "global",
|
|
2085
|
+
"x": "xit",
|
|
2086
|
+
};
|
|
2087
|
+
|
|
2088
|
+
if (shortAliases[input]) {
|
|
2089
|
+
return commandTable.find(c => c.name === shortAliases[input]) || null;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
return null;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
// Execute a vi command and return result
|
|
2096
|
+
async function executeViCommand(cmd: string): Promise<CommandResult> {
|
|
2097
|
+
// Handle pure line numbers first (e.g., :42)
|
|
2098
|
+
const lineNumMatch = cmd.match(/^(\d+)$/);
|
|
2099
|
+
if (lineNumMatch) {
|
|
2100
|
+
const lineNum = parseInt(lineNumMatch[1], 10);
|
|
2101
|
+
return gotoLine(lineNum);
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
// Handle range prefix with command (e.g., :1,10d or :%d)
|
|
2105
|
+
// Supported range formats: %, ., $, 'a, line numbers, and combinations with ,
|
|
2106
|
+
let processedCmd = cmd;
|
|
2107
|
+
let range: string | null = null;
|
|
2108
|
+
|
|
2109
|
+
const rangePattern = /^([%.$]|\d+|'[a-z])?(?:,([%.$]|\d+|'[a-z]))?\s*(.*)$/;
|
|
2110
|
+
const rangeMatch = cmd.match(rangePattern);
|
|
2111
|
+
if (rangeMatch && rangeMatch[3]) {
|
|
2112
|
+
// There's a command after the range
|
|
2113
|
+
range = (rangeMatch[1] || "") + (rangeMatch[2] ? "," + rangeMatch[2] : "");
|
|
2114
|
+
processedCmd = rangeMatch[3];
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// Handle special commands that start with symbols
|
|
2118
|
+
if (processedCmd.startsWith("!")) {
|
|
2119
|
+
// Shell command - not implemented
|
|
2120
|
+
return { error: editor.t("error.shell_not_supported") };
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
// Handle +cmd syntax for :e +10 file (open file at line 10)
|
|
2124
|
+
let plusCmd: string | null = null;
|
|
2125
|
+
if (processedCmd.startsWith("+")) {
|
|
2126
|
+
const plusMatch = processedCmd.match(/^\+(\S*)\s*(.*)/);
|
|
2127
|
+
if (plusMatch) {
|
|
2128
|
+
plusCmd = plusMatch[1] || "$"; // + alone means go to end
|
|
2129
|
+
processedCmd = plusMatch[2];
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
// Split command into command name and arguments
|
|
2134
|
+
// Supports: cmd, cmd!, cmd args, cmd! args
|
|
2135
|
+
const match = processedCmd.match(/^([a-zA-Z]\w*)(!)?(?:\s+(.*))?$/);
|
|
2136
|
+
if (!match) {
|
|
2137
|
+
// Maybe it's just a command name without arguments
|
|
2138
|
+
if (processedCmd.match(/^[a-zA-Z]+$/)) {
|
|
2139
|
+
const cmdDef = findCommand(processedCmd);
|
|
2140
|
+
if (cmdDef) {
|
|
2141
|
+
return executeCommand(cmdDef.name, false, null, range);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
return { error: editor.t("error.not_valid_command", { cmd: processedCmd }) };
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
const [, commandInput, bang, args] = match;
|
|
2148
|
+
const force = bang === "!";
|
|
2149
|
+
|
|
2150
|
+
// Look up the command
|
|
2151
|
+
const cmdDef = findCommand(commandInput);
|
|
2152
|
+
if (!cmdDef) {
|
|
2153
|
+
return { error: editor.t("error.unknown_command", { cmd: commandInput }) };
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
// Validate bang usage
|
|
2157
|
+
if (force && !cmdDef.allowBang) {
|
|
2158
|
+
return { error: editor.t("error.command_no_bang", { cmd: cmdDef.name }) };
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
// Execute the command
|
|
2162
|
+
return executeCommand(cmdDef.name, force, args || null, range);
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
// Execute a resolved command
|
|
2166
|
+
async function executeCommand(
|
|
2167
|
+
command: string,
|
|
2168
|
+
force: boolean,
|
|
2169
|
+
args: string | null,
|
|
2170
|
+
_range: string | null // Range support is limited for now
|
|
2171
|
+
): Promise<CommandResult> {
|
|
2172
|
+
|
|
2173
|
+
switch (command) {
|
|
2174
|
+
case "write": {
|
|
2175
|
+
// :w - save current file
|
|
2176
|
+
// :w filename - save as filename (not implemented yet)
|
|
2177
|
+
if (args) {
|
|
2178
|
+
return { error: editor.t("error.save_as_not_implemented") };
|
|
2179
|
+
}
|
|
2180
|
+
editor.executeAction("save");
|
|
2181
|
+
return { message: editor.t("status.file_saved") };
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
case "quit": {
|
|
2185
|
+
// :q - quit (close buffer)
|
|
2186
|
+
// :q! - force quit (discard changes)
|
|
2187
|
+
const bufferId = editor.getActiveBufferId();
|
|
2188
|
+
if (!force && editor.isBufferModified(bufferId)) {
|
|
2189
|
+
return { error: editor.t("error.no_write_since_change", { cmd: ":q!" }) };
|
|
2190
|
+
}
|
|
2191
|
+
editor.executeAction("close_buffer");
|
|
2192
|
+
return {};
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
case "wq":
|
|
2196
|
+
case "xit":
|
|
2197
|
+
case "exit": {
|
|
2198
|
+
// :wq or :x - save and quit
|
|
2199
|
+
editor.executeAction("save");
|
|
2200
|
+
editor.executeAction("close_buffer");
|
|
2201
|
+
return {};
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
case "wall": {
|
|
2205
|
+
// :wa - save all buffers
|
|
2206
|
+
editor.executeAction("save_all");
|
|
2207
|
+
return { message: editor.t("status.all_files_saved") };
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
case "qall": {
|
|
2211
|
+
// :qa - quit all
|
|
2212
|
+
// :qa! - force quit all
|
|
2213
|
+
if (force) {
|
|
2214
|
+
editor.executeAction("quit_all");
|
|
2215
|
+
} else {
|
|
2216
|
+
// Check if any buffer is modified
|
|
2217
|
+
const buffers = editor.listBuffers();
|
|
2218
|
+
for (const buf of buffers) {
|
|
2219
|
+
if (buf.modified) {
|
|
2220
|
+
return { error: editor.t("error.no_write_since_change", { cmd: ":qa!" }) };
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
editor.executeAction("quit_all");
|
|
2224
|
+
}
|
|
2225
|
+
return {};
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
case "wqall": {
|
|
2229
|
+
// :wqa or :xa - save all and quit
|
|
2230
|
+
editor.executeAction("save_all");
|
|
2231
|
+
editor.executeAction("quit_all");
|
|
2232
|
+
return {};
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
case "edit": {
|
|
2236
|
+
// :e - reload current file
|
|
2237
|
+
// :e filename - open file
|
|
2238
|
+
// :e! - force reload (discard changes)
|
|
2239
|
+
if (!args) {
|
|
2240
|
+
if (force) {
|
|
2241
|
+
editor.executeAction("revert");
|
|
2242
|
+
return { message: editor.t("status.file_reverted_discarded") };
|
|
2243
|
+
}
|
|
2244
|
+
const bufferId = editor.getActiveBufferId();
|
|
2245
|
+
if (editor.isBufferModified(bufferId)) {
|
|
2246
|
+
return { error: editor.t("error.no_write_since_change", { cmd: ":e!" }) };
|
|
2247
|
+
}
|
|
2248
|
+
editor.executeAction("revert");
|
|
2249
|
+
return { message: editor.t("status.file_reverted") };
|
|
2250
|
+
}
|
|
2251
|
+
// Open the specified file
|
|
2252
|
+
const path = args.trim();
|
|
2253
|
+
editor.openFile(path, 0, 0);
|
|
2254
|
+
return {};
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
case "enew": {
|
|
2258
|
+
// :enew - create new buffer in current split
|
|
2259
|
+
if (!force) {
|
|
2260
|
+
const bufferId = editor.getActiveBufferId();
|
|
2261
|
+
if (editor.isBufferModified(bufferId)) {
|
|
2262
|
+
return { error: editor.t("error.no_write_since_change", { cmd: ":enew!" }) };
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
editor.executeAction("new_buffer");
|
|
2266
|
+
return {};
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
case "revert": {
|
|
2270
|
+
// :revert - Fresh-specific command to reload file
|
|
2271
|
+
editor.executeAction("revert");
|
|
2272
|
+
return { message: editor.t("status.file_reverted") };
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
case "next": {
|
|
2276
|
+
// :n - next buffer
|
|
2277
|
+
editor.executeAction("next_buffer");
|
|
2278
|
+
return {};
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
case "previous": {
|
|
2282
|
+
// :prev - previous buffer
|
|
2283
|
+
editor.executeAction("prev_buffer");
|
|
2284
|
+
return {};
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
case "bnext": {
|
|
2288
|
+
// :bn - next buffer
|
|
2289
|
+
editor.executeAction("next_buffer");
|
|
2290
|
+
return {};
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
case "bprevious": {
|
|
2294
|
+
// :bp - previous buffer
|
|
2295
|
+
editor.executeAction("prev_buffer");
|
|
2296
|
+
return {};
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
case "bdelete": {
|
|
2300
|
+
// :bd - delete buffer (close)
|
|
2301
|
+
// :bd! - force close even if modified
|
|
2302
|
+
const bufferId = editor.getActiveBufferId();
|
|
2303
|
+
if (!force && editor.isBufferModified(bufferId)) {
|
|
2304
|
+
return { error: editor.t("error.no_write_since_change", { cmd: ":bd!" }) };
|
|
2305
|
+
}
|
|
2306
|
+
editor.executeAction("close_buffer");
|
|
2307
|
+
return {};
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
case "buffer": {
|
|
2311
|
+
// :b [N] - go to buffer N
|
|
2312
|
+
// :b name - go to buffer matching name
|
|
2313
|
+
if (!args) {
|
|
2314
|
+
// Show current buffer info
|
|
2315
|
+
const bufferId = editor.getActiveBufferId();
|
|
2316
|
+
const info = editor.getBufferInfo(bufferId);
|
|
2317
|
+
if (info) {
|
|
2318
|
+
const name = info.path ? editor.pathBasename(info.path) : editor.t("info.no_name");
|
|
2319
|
+
return { message: editor.t("info.buffer", { id: String(info.id), name }) };
|
|
2320
|
+
}
|
|
2321
|
+
return {};
|
|
2322
|
+
}
|
|
2323
|
+
// Try to parse as buffer number
|
|
2324
|
+
const bufNum = parseInt(args.trim(), 10);
|
|
2325
|
+
if (!isNaN(bufNum)) {
|
|
2326
|
+
const buffers = editor.listBuffers();
|
|
2327
|
+
const target = buffers.find(b => b.id === bufNum);
|
|
2328
|
+
if (target) {
|
|
2329
|
+
editor.showBuffer(target.id);
|
|
2330
|
+
return {};
|
|
2331
|
+
}
|
|
2332
|
+
return { error: editor.t("error.buffer_not_found", { id: String(bufNum) }) };
|
|
2333
|
+
}
|
|
2334
|
+
// Try to match buffer by name
|
|
2335
|
+
const buffers = editor.listBuffers();
|
|
2336
|
+
const pattern = args.trim().toLowerCase();
|
|
2337
|
+
const matches = buffers.filter(b => {
|
|
2338
|
+
const name = b.path ? editor.pathBasename(b.path).toLowerCase() : "";
|
|
2339
|
+
return name.includes(pattern);
|
|
2340
|
+
});
|
|
2341
|
+
if (matches.length === 1) {
|
|
2342
|
+
editor.showBuffer(matches[0].id);
|
|
2343
|
+
return {};
|
|
2344
|
+
} else if (matches.length > 1) {
|
|
2345
|
+
return { error: editor.t("error.multiple_buffers_match", { pattern: args }) };
|
|
2346
|
+
}
|
|
2347
|
+
return { error: editor.t("error.no_buffer_matching", { pattern: args }) };
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
case "buffers":
|
|
2351
|
+
case "ls":
|
|
2352
|
+
case "files": {
|
|
2353
|
+
// :ls - list buffers
|
|
2354
|
+
const buffers = editor.listBuffers();
|
|
2355
|
+
const lines = buffers.map(buf => {
|
|
2356
|
+
const modified = buf.modified ? " [+]" : "";
|
|
2357
|
+
const current = buf.id === editor.getActiveBufferId() ? "%" : " ";
|
|
2358
|
+
const name = buf.path ? editor.pathBasename(buf.path) : editor.t("info.no_name");
|
|
2359
|
+
return `${current}${buf.id}: ${name}${modified}`;
|
|
2360
|
+
});
|
|
2361
|
+
return { message: lines.join(" | ") || editor.t("info.no_buffers") };
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
case "split": {
|
|
2365
|
+
// :sp - horizontal split
|
|
2366
|
+
editor.executeAction("split_horizontal");
|
|
2367
|
+
if (args) {
|
|
2368
|
+
// Open file in new split
|
|
2369
|
+
const path = args.trim();
|
|
2370
|
+
editor.openFile(path, 0, 0);
|
|
2371
|
+
}
|
|
2372
|
+
return {};
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
case "vsplit": {
|
|
2376
|
+
// :vs - vertical split
|
|
2377
|
+
editor.executeAction("split_vertical");
|
|
2378
|
+
if (args) {
|
|
2379
|
+
// Open file in new split
|
|
2380
|
+
const path = args.trim();
|
|
2381
|
+
editor.openFile(path, 0, 0);
|
|
2382
|
+
}
|
|
2383
|
+
return {};
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
case "new": {
|
|
2387
|
+
// :new - create new buffer in horizontal split
|
|
2388
|
+
editor.executeAction("split_horizontal");
|
|
2389
|
+
editor.executeAction("new_buffer");
|
|
2390
|
+
if (args) {
|
|
2391
|
+
const path = args.trim();
|
|
2392
|
+
editor.openFile(path, 0, 0);
|
|
2393
|
+
}
|
|
2394
|
+
return {};
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
case "vnew": {
|
|
2398
|
+
// :vnew - create new buffer in vertical split
|
|
2399
|
+
editor.executeAction("split_vertical");
|
|
2400
|
+
editor.executeAction("new_buffer");
|
|
2401
|
+
if (args) {
|
|
2402
|
+
const path = args.trim();
|
|
2403
|
+
editor.openFile(path, 0, 0);
|
|
2404
|
+
}
|
|
2405
|
+
return {};
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
case "only": {
|
|
2409
|
+
// :only - close all other splits
|
|
2410
|
+
editor.executeAction("close_other_splits");
|
|
2411
|
+
return {};
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
case "close": {
|
|
2415
|
+
// :close - close current split (same as :q for Fresh)
|
|
2416
|
+
const bufferId = editor.getActiveBufferId();
|
|
2417
|
+
if (!force && editor.isBufferModified(bufferId)) {
|
|
2418
|
+
return { error: editor.t("error.no_write_since_change", { cmd: ":close!" }) };
|
|
2419
|
+
}
|
|
2420
|
+
editor.executeAction("close_buffer");
|
|
2421
|
+
return {};
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
case "tabnew":
|
|
2425
|
+
case "tabedit": {
|
|
2426
|
+
// :tabnew - new tab (creates new buffer in Fresh)
|
|
2427
|
+
editor.executeAction("new_buffer");
|
|
2428
|
+
if (args) {
|
|
2429
|
+
const path = args.trim();
|
|
2430
|
+
editor.openFile(path, 0, 0);
|
|
2431
|
+
}
|
|
2432
|
+
return {};
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
case "tabclose": {
|
|
2436
|
+
// :tabclose - close current tab/buffer
|
|
2437
|
+
const bufferId = editor.getActiveBufferId();
|
|
2438
|
+
if (!force && editor.isBufferModified(bufferId)) {
|
|
2439
|
+
return { error: editor.t("error.no_write_since_change", { cmd: ":tabclose!" }) };
|
|
2440
|
+
}
|
|
2441
|
+
editor.executeAction("close_buffer");
|
|
2442
|
+
return {};
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
case "tabnext": {
|
|
2446
|
+
// :tabnext - next tab/buffer
|
|
2447
|
+
editor.executeAction("next_buffer");
|
|
2448
|
+
return {};
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
case "tabprevious": {
|
|
2452
|
+
// :tabprev - previous tab/buffer
|
|
2453
|
+
editor.executeAction("prev_buffer");
|
|
2454
|
+
return {};
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
case "copen": {
|
|
2458
|
+
// :copen - open diagnostics panel (Fresh equivalent)
|
|
2459
|
+
editor.executeAction("show_diagnostics");
|
|
2460
|
+
return {};
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
case "cclose": {
|
|
2464
|
+
// :cclose - close diagnostics panel
|
|
2465
|
+
return { message: editor.t("info.close_diagnostics") };
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
case "cnext": {
|
|
2469
|
+
// :cnext - next diagnostic
|
|
2470
|
+
editor.executeAction("goto_next_diagnostic");
|
|
2471
|
+
return {};
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
case "cprevious": {
|
|
2475
|
+
// :cprev - previous diagnostic
|
|
2476
|
+
editor.executeAction("goto_prev_diagnostic");
|
|
2477
|
+
return {};
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
case "cfirst": {
|
|
2481
|
+
// :cfirst - first diagnostic
|
|
2482
|
+
editor.executeAction("goto_first_diagnostic");
|
|
2483
|
+
return {};
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
case "clast": {
|
|
2487
|
+
// :clast - last diagnostic
|
|
2488
|
+
editor.executeAction("goto_last_diagnostic");
|
|
2489
|
+
return {};
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
case "nohlsearch": {
|
|
2493
|
+
// :noh - clear search highlighting
|
|
2494
|
+
editor.executeAction("clear_search");
|
|
2495
|
+
return {};
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
case "substitute": {
|
|
2499
|
+
// :s - substitute (not implemented)
|
|
2500
|
+
// This would require parsing /pattern/replacement/flags
|
|
2501
|
+
return { error: editor.t("error.substitute_not_implemented") };
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
case "global":
|
|
2505
|
+
case "vglobal": {
|
|
2506
|
+
// :g - global command (not implemented)
|
|
2507
|
+
return { error: editor.t("error.global_not_implemented") };
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
case "undo": {
|
|
2511
|
+
// :undo - undo
|
|
2512
|
+
editor.executeAction("undo");
|
|
2513
|
+
return {};
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
case "redo": {
|
|
2517
|
+
// :redo - redo
|
|
2518
|
+
editor.executeAction("redo");
|
|
2519
|
+
return {};
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
case "set": {
|
|
2523
|
+
// :set - set options (limited implementation)
|
|
2524
|
+
if (!args) {
|
|
2525
|
+
return { error: editor.t("error.set_usage") };
|
|
2526
|
+
}
|
|
2527
|
+
return handleSetCommand(args);
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
case "pwd": {
|
|
2531
|
+
// :pwd - print working directory
|
|
2532
|
+
const cwd = editor.getCwd();
|
|
2533
|
+
return { message: cwd };
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
case "cd": {
|
|
2537
|
+
// :cd - change directory (info only, can't actually change)
|
|
2538
|
+
if (!args) {
|
|
2539
|
+
return { message: editor.getCwd() };
|
|
2540
|
+
}
|
|
2541
|
+
return { error: editor.t("error.cannot_change_directory") };
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
case "file": {
|
|
2545
|
+
// :f - show current file info
|
|
2546
|
+
// :f name - rename current buffer (not implemented)
|
|
2547
|
+
if (args) {
|
|
2548
|
+
return { error: editor.t("error.rename_not_implemented") };
|
|
2549
|
+
}
|
|
2550
|
+
const bufferId = editor.getActiveBufferId();
|
|
2551
|
+
const info = editor.getBufferInfo(bufferId);
|
|
2552
|
+
if (info) {
|
|
2553
|
+
const modified = info.modified ? editor.t("info.modified") : "";
|
|
2554
|
+
const path = info.path || editor.t("info.no_name");
|
|
2555
|
+
const line = editor.getCursorLine();
|
|
2556
|
+
return { message: editor.t("info.file", { path, modified, line: String(line), bytes: String(info.length) }) };
|
|
2557
|
+
}
|
|
2558
|
+
return { error: editor.t("error.no_buffer") };
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
case "help": {
|
|
2562
|
+
// :help - show help
|
|
2563
|
+
if (args) {
|
|
2564
|
+
return { message: editor.t("info.help_not_available", { topic: args }) };
|
|
2565
|
+
}
|
|
2566
|
+
return {
|
|
2567
|
+
message: editor.t("info.help_commands")
|
|
2568
|
+
};
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
case "version": {
|
|
2572
|
+
// :version - show version
|
|
2573
|
+
return { message: editor.t("info.version") };
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
case "marks": {
|
|
2577
|
+
// :marks - show marks (not implemented)
|
|
2578
|
+
return { error: editor.t("error.marks_not_implemented") };
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
case "registers": {
|
|
2582
|
+
// :registers - show registers (not implemented)
|
|
2583
|
+
return { error: editor.t("error.registers_not_implemented") };
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
case "jumps": {
|
|
2587
|
+
// :jumps - show jump list (not implemented)
|
|
2588
|
+
return { error: editor.t("error.jump_list_not_implemented") };
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
case "syntax": {
|
|
2592
|
+
// :syntax - syntax info
|
|
2593
|
+
if (args === "off") {
|
|
2594
|
+
return { error: editor.t("error.syntax_cannot_disable") };
|
|
2595
|
+
}
|
|
2596
|
+
return { message: editor.t("status.syntax_always_on") };
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
case "read": {
|
|
2600
|
+
// :r - read file into buffer (not implemented)
|
|
2601
|
+
return { error: editor.t("error.read_not_implemented") };
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
case "saveas": {
|
|
2605
|
+
// :saveas - save as (not implemented)
|
|
2606
|
+
return { error: editor.t("error.saveas_not_implemented") };
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
case "grep":
|
|
2610
|
+
case "vimgrep": {
|
|
2611
|
+
// :grep - search (use Fresh's grep)
|
|
2612
|
+
if (args) {
|
|
2613
|
+
// Could potentially pass args to search, but for now just open search
|
|
2614
|
+
editor.executeAction("search");
|
|
2615
|
+
return { message: editor.t("info.use_search_dialog", { pattern: args }) };
|
|
2616
|
+
}
|
|
2617
|
+
editor.executeAction("search");
|
|
2618
|
+
return {};
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
case "make": {
|
|
2622
|
+
// :make - run build command (not implemented)
|
|
2623
|
+
return { error: editor.t("error.use_terminal") };
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
case "ascii": {
|
|
2627
|
+
// :ascii - show ASCII value of char under cursor
|
|
2628
|
+
return { message: editor.t("info.status_bar_char") };
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
default: {
|
|
2632
|
+
return { error: editor.t("error.unknown_command", { cmd: command }) };
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
// Go to a specific line number
|
|
2638
|
+
async function gotoLine(lineNum: number): Promise<CommandResult> {
|
|
2639
|
+
if (lineNum < 1) {
|
|
2640
|
+
return { error: editor.t("error.line_must_be_positive") };
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
const bufferId = editor.getActiveBufferId();
|
|
2644
|
+
const bufferLength = editor.getBufferLength(bufferId);
|
|
2645
|
+
|
|
2646
|
+
// Get the text to find the line offset
|
|
2647
|
+
const text = await editor.getBufferText(bufferId, 0, bufferLength);
|
|
2648
|
+
if (!text) {
|
|
2649
|
+
return { error: editor.t("error.cannot_read_buffer") };
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
let lineStart = 0;
|
|
2653
|
+
let currentLine = 1;
|
|
2654
|
+
|
|
2655
|
+
for (let i = 0; i < text.length && currentLine < lineNum; i++) {
|
|
2656
|
+
if (text[i] === '\n') {
|
|
2657
|
+
currentLine++;
|
|
2658
|
+
lineStart = i + 1;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
if (currentLine >= lineNum || lineStart < text.length) {
|
|
2663
|
+
editor.setBufferCursor(bufferId, lineStart);
|
|
2664
|
+
return {};
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
// If requested line is beyond file, go to last line
|
|
2668
|
+
editor.executeAction("move_document_end");
|
|
2669
|
+
return { message: editor.t("status.line_beyond_end", { line: String(lineNum) }) };
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
// Handle :set command options
|
|
2673
|
+
function handleSetCommand(args: string): CommandResult {
|
|
2674
|
+
const parts = args.split("=");
|
|
2675
|
+
const option = parts[0].trim();
|
|
2676
|
+
const value = parts.length > 1 ? parts[1].trim() : null;
|
|
2677
|
+
|
|
2678
|
+
switch (option) {
|
|
2679
|
+
case "number":
|
|
2680
|
+
case "nu": {
|
|
2681
|
+
// :set number - show line numbers
|
|
2682
|
+
const bufferId = editor.getActiveBufferId();
|
|
2683
|
+
editor.setLineNumbers(bufferId, true);
|
|
2684
|
+
return { message: editor.t("status.line_numbers_on") };
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
case "nonumber":
|
|
2688
|
+
case "nonu": {
|
|
2689
|
+
// :set nonumber - hide line numbers
|
|
2690
|
+
const bufferId = editor.getActiveBufferId();
|
|
2691
|
+
editor.setLineNumbers(bufferId, false);
|
|
2692
|
+
return { message: editor.t("status.line_numbers_off") };
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
case "wrap": {
|
|
2696
|
+
// :set wrap - enable line wrap
|
|
2697
|
+
editor.executeAction("toggle_wrap");
|
|
2698
|
+
return { message: editor.t("status.line_wrap_toggled") };
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
case "nowrap": {
|
|
2702
|
+
// :set nowrap - disable line wrap
|
|
2703
|
+
editor.executeAction("toggle_wrap");
|
|
2704
|
+
return { message: editor.t("status.line_wrap_toggled") };
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
default: {
|
|
2708
|
+
return { error: editor.t("error.unknown_option", { option }) };
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
// Register event handler for prompt confirmation
|
|
2714
|
+
editor.on("prompt_confirmed", "vi_command_handler");
|
|
2715
|
+
|
|
2716
|
+
// ============================================================================
|
|
2717
|
+
// Toggle Command
|
|
2718
|
+
// ============================================================================
|
|
2719
|
+
|
|
2720
|
+
let viModeEnabled = false;
|
|
2721
|
+
|
|
2722
|
+
globalThis.vi_mode_toggle = function (): void {
|
|
2723
|
+
viModeEnabled = !viModeEnabled;
|
|
2724
|
+
|
|
2725
|
+
if (viModeEnabled) {
|
|
2726
|
+
switchMode("normal");
|
|
2727
|
+
editor.setStatus(editor.t("status.enabled"));
|
|
2728
|
+
} else {
|
|
2729
|
+
editor.setEditorMode(null);
|
|
2730
|
+
state.mode = "normal";
|
|
2731
|
+
state.pendingOperator = null;
|
|
2732
|
+
editor.setStatus(editor.t("status.disabled"));
|
|
2733
|
+
}
|
|
2734
|
+
};
|
|
2735
|
+
|
|
2736
|
+
editor.registerCommand(
|
|
2737
|
+
"%cmd.toggle_vi_mode",
|
|
2738
|
+
"%cmd.toggle_vi_mode_desc",
|
|
2739
|
+
"vi_mode_toggle",
|
|
2740
|
+
"normal",
|
|
2741
|
+
);
|
|
2742
|
+
|
|
2743
|
+
// ============================================================================
|
|
2744
|
+
// Initialization
|
|
2745
|
+
// ============================================================================
|
|
2746
|
+
|
|
2747
|
+
editor.setStatus(editor.t("status.loaded"));
|