@fresh-editor/fresh-editor 0.1.63 → 0.1.67
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/CHANGELOG.md +74 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/plugins/audit_mode.ts +1045 -0
- package/plugins/clangd-lsp.ts +166 -0
- package/plugins/config-schema.json +134 -15
- package/plugins/csharp-lsp.ts +145 -0
- package/plugins/css-lsp.ts +141 -0
- package/plugins/go-lsp.ts +141 -0
- package/plugins/html-lsp.ts +143 -0
- package/plugins/json-lsp.ts +143 -0
- package/plugins/lib/fresh.d.ts +98 -2
- package/plugins/python-lsp.ts +160 -0
- package/plugins/rust-lsp.ts +164 -0
- package/plugins/typescript-lsp.ts +165 -0
- package/plugins/vi_mode.ts +1630 -0
|
@@ -0,0 +1,1630 @@
|
|
|
1
|
+
/// <reference path="./lib/fresh.d.ts" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Vi Mode Plugin for Fresh Editor
|
|
5
|
+
*
|
|
6
|
+
* Implements vi-style modal editing with:
|
|
7
|
+
* - Normal mode: navigation and commands
|
|
8
|
+
* - Insert mode: text input
|
|
9
|
+
* - Operator-pending mode: composable operators with motions
|
|
10
|
+
*
|
|
11
|
+
* Uses the plugin API's executeAction() for true operator+motion composability:
|
|
12
|
+
* any operator works with any motion via O(operators + motions) code.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Vi mode state
|
|
16
|
+
type ViMode = "normal" | "insert" | "operator-pending" | "find-char" | "visual" | "visual-line" | "text-object";
|
|
17
|
+
type FindCharType = "f" | "t" | "F" | "T" | null;
|
|
18
|
+
type TextObjectType = "inner" | "around" | null;
|
|
19
|
+
|
|
20
|
+
interface ViState {
|
|
21
|
+
mode: ViMode;
|
|
22
|
+
pendingOperator: string | null;
|
|
23
|
+
pendingFindChar: FindCharType; // For f/t/F/T motions
|
|
24
|
+
pendingTextObject: TextObjectType; // For i/a text objects
|
|
25
|
+
lastFindChar: { type: FindCharType; char: string } | null; // For ; and , repeat
|
|
26
|
+
count: number | null;
|
|
27
|
+
lastCommand: (() => void) | null; // For '.' repeat
|
|
28
|
+
lastYankWasLinewise: boolean; // Track if last yank was line-wise for proper paste
|
|
29
|
+
visualAnchor: number | null; // Starting position for visual mode selection
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const state: ViState = {
|
|
33
|
+
mode: "normal",
|
|
34
|
+
pendingOperator: null,
|
|
35
|
+
pendingFindChar: null,
|
|
36
|
+
pendingTextObject: null,
|
|
37
|
+
lastFindChar: null,
|
|
38
|
+
count: null,
|
|
39
|
+
lastCommand: null,
|
|
40
|
+
lastYankWasLinewise: false,
|
|
41
|
+
visualAnchor: null,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Mode indicator for status bar
|
|
45
|
+
function getModeIndicator(mode: ViMode): string {
|
|
46
|
+
const countPrefix = state.count !== null ? `${state.count} ` : "";
|
|
47
|
+
switch (mode) {
|
|
48
|
+
case "normal":
|
|
49
|
+
return `-- NORMAL --${countPrefix ? ` (${state.count})` : ""}`;
|
|
50
|
+
case "insert":
|
|
51
|
+
return "-- INSERT --";
|
|
52
|
+
case "operator-pending":
|
|
53
|
+
return `-- OPERATOR (${state.pendingOperator}) --${countPrefix ? ` (${state.count})` : ""}`;
|
|
54
|
+
case "find-char":
|
|
55
|
+
return `-- FIND (${state.pendingFindChar}) --`;
|
|
56
|
+
case "visual":
|
|
57
|
+
return `-- VISUAL --${countPrefix ? ` (${state.count})` : ""}`;
|
|
58
|
+
case "visual-line":
|
|
59
|
+
return `-- VISUAL LINE --${countPrefix ? ` (${state.count})` : ""}`;
|
|
60
|
+
case "text-object":
|
|
61
|
+
return `-- ${state.pendingOperator}${state.pendingTextObject === "inner" ? "i" : "a"}? --`;
|
|
62
|
+
default:
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Switch between modes
|
|
68
|
+
function switchMode(newMode: ViMode): void {
|
|
69
|
+
const oldMode = state.mode;
|
|
70
|
+
state.mode = newMode;
|
|
71
|
+
|
|
72
|
+
// Only clear pendingOperator when leaving operator-pending and text-object modes
|
|
73
|
+
if (newMode !== "operator-pending" && newMode !== "text-object") {
|
|
74
|
+
state.pendingOperator = null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Clear text object type when leaving text-object mode
|
|
78
|
+
if (newMode !== "text-object") {
|
|
79
|
+
state.pendingTextObject = null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Preserve count when entering operator-pending or text-object mode (for 3dw = delete 3 words)
|
|
83
|
+
// Also preserve count in visual modes
|
|
84
|
+
if (newMode !== "operator-pending" && newMode !== "text-object" && newMode !== "visual" && newMode !== "visual-line") {
|
|
85
|
+
state.count = null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Clear visual anchor when leaving visual modes
|
|
89
|
+
if (newMode !== "visual" && newMode !== "visual-line") {
|
|
90
|
+
state.visualAnchor = null;
|
|
91
|
+
// Clear any selection when leaving visual mode by moving cursor
|
|
92
|
+
// (any non-select movement clears selection in Fresh)
|
|
93
|
+
if (oldMode === "visual" || oldMode === "visual-line") {
|
|
94
|
+
editor.executeAction("move_left");
|
|
95
|
+
editor.executeAction("move_right");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// All modes use vi-{mode} naming, including insert mode
|
|
100
|
+
// vi-insert has read_only=false so normal typing works, but Escape is bound
|
|
101
|
+
editor.setEditorMode(`vi-${newMode}`);
|
|
102
|
+
editor.setStatus(getModeIndicator(newMode));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Get the current count (defaults to 1 if no count specified)
|
|
106
|
+
// Does NOT clear the count - that's done in switchMode or explicitly
|
|
107
|
+
function getCount(): number {
|
|
108
|
+
return state.count ?? 1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Consume the current count and clear it
|
|
112
|
+
// Returns the count (defaults to 1)
|
|
113
|
+
function consumeCount(): number {
|
|
114
|
+
const count = state.count ?? 1;
|
|
115
|
+
state.count = null;
|
|
116
|
+
return count;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Accumulate a digit into the count
|
|
120
|
+
function accumulateCount(digit: number): void {
|
|
121
|
+
if (state.count === null) {
|
|
122
|
+
state.count = digit;
|
|
123
|
+
} else {
|
|
124
|
+
state.count = state.count * 10 + digit;
|
|
125
|
+
}
|
|
126
|
+
// Update status to show accumulated count
|
|
127
|
+
editor.setStatus(getModeIndicator(state.mode));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Execute a single action with count (uses new executeActions API for efficiency)
|
|
131
|
+
function executeWithCount(action: string, count?: number): void {
|
|
132
|
+
const n = count ?? consumeCount();
|
|
133
|
+
if (n === 1) {
|
|
134
|
+
editor.executeAction(action);
|
|
135
|
+
} else {
|
|
136
|
+
editor.executeActions([{ action, count: n }]);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Map motion actions to their selection equivalents
|
|
141
|
+
const motionToSelection: Record<string, string> = {
|
|
142
|
+
move_left: "select_left",
|
|
143
|
+
move_right: "select_right",
|
|
144
|
+
move_up: "select_up",
|
|
145
|
+
move_down: "select_down",
|
|
146
|
+
move_word_left: "select_word_left",
|
|
147
|
+
move_word_right: "select_word_right",
|
|
148
|
+
move_line_start: "select_line_start",
|
|
149
|
+
move_line_end: "select_line_end",
|
|
150
|
+
move_document_start: "select_document_start",
|
|
151
|
+
move_document_end: "select_document_end",
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Map (operator, motion) pairs to atomic Rust actions
|
|
155
|
+
// These are single actions that combine the operator and motion atomically
|
|
156
|
+
// This avoids async issues with selection-based approach
|
|
157
|
+
type OperatorMotionMap = Record<string, Record<string, string>>;
|
|
158
|
+
const atomicOperatorActions: OperatorMotionMap = {
|
|
159
|
+
d: {
|
|
160
|
+
// Delete operators
|
|
161
|
+
move_word_right: "delete_word_forward",
|
|
162
|
+
move_word_left: "delete_word_backward",
|
|
163
|
+
move_line_end: "delete_to_line_end",
|
|
164
|
+
move_line_start: "delete_to_line_start",
|
|
165
|
+
},
|
|
166
|
+
y: {
|
|
167
|
+
// Yank operators
|
|
168
|
+
move_word_right: "yank_word_forward",
|
|
169
|
+
move_word_left: "yank_word_backward",
|
|
170
|
+
move_line_end: "yank_to_line_end",
|
|
171
|
+
move_line_start: "yank_to_line_start",
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Apply an operator using atomic actions if available, otherwise selection-based approach
|
|
176
|
+
// The count parameter specifies how many times to apply the motion (e.g., d3w = delete 3 words)
|
|
177
|
+
function applyOperatorWithMotion(operator: string, motionAction: string, count: number = 1): void {
|
|
178
|
+
// For "change" operator, use delete action and then enter insert mode
|
|
179
|
+
const lookupOperator = operator === "c" ? "d" : operator;
|
|
180
|
+
|
|
181
|
+
// Check if we have an atomic action for this operator+motion combination
|
|
182
|
+
const operatorActions = atomicOperatorActions[lookupOperator];
|
|
183
|
+
const atomicAction = operatorActions?.[motionAction];
|
|
184
|
+
|
|
185
|
+
if (atomicAction) {
|
|
186
|
+
// Use the atomic action - single command, no async issues
|
|
187
|
+
// Apply count times for 3dw, etc.
|
|
188
|
+
if (count === 1) {
|
|
189
|
+
editor.executeAction(atomicAction);
|
|
190
|
+
} else {
|
|
191
|
+
editor.executeActions([{ action: atomicAction, count }]);
|
|
192
|
+
}
|
|
193
|
+
if (operator === "y") {
|
|
194
|
+
state.lastYankWasLinewise = false;
|
|
195
|
+
}
|
|
196
|
+
if (operator === "c") {
|
|
197
|
+
switchMode("insert");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
switchMode("normal");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Fall back to selection-based approach for motions without atomic actions
|
|
205
|
+
const selectAction = motionToSelection[motionAction];
|
|
206
|
+
if (!selectAction) {
|
|
207
|
+
editor.debug(`No selection equivalent for motion: ${motionAction}`);
|
|
208
|
+
switchMode("normal");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Execute the selection action count times (synchronous - extends selection to target)
|
|
213
|
+
if (count === 1) {
|
|
214
|
+
editor.executeAction(selectAction);
|
|
215
|
+
} else {
|
|
216
|
+
editor.executeActions([{ action: selectAction, count }]);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
switch (operator) {
|
|
220
|
+
case "d": // delete
|
|
221
|
+
editor.executeAction("cut"); // Cut removes selection
|
|
222
|
+
break;
|
|
223
|
+
case "c": // change (delete and enter insert mode)
|
|
224
|
+
editor.executeAction("cut");
|
|
225
|
+
switchMode("insert");
|
|
226
|
+
return; // Don't switch back to normal mode
|
|
227
|
+
case "y": // yank
|
|
228
|
+
state.lastYankWasLinewise = false; // Motion-based yank is character-wise
|
|
229
|
+
editor.executeAction("copy");
|
|
230
|
+
// Move cursor back to start of selection (left side)
|
|
231
|
+
editor.executeAction("move_left");
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
switchMode("normal");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Handle motion in operator-pending mode
|
|
239
|
+
// Consumes any pending count and applies it to the motion
|
|
240
|
+
function handleMotionWithOperator(motionAction: string): void {
|
|
241
|
+
if (!state.pendingOperator) {
|
|
242
|
+
switchMode("normal");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const count = consumeCount();
|
|
247
|
+
applyOperatorWithMotion(state.pendingOperator, motionAction, count);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ============================================================================
|
|
251
|
+
// Normal Mode Commands
|
|
252
|
+
// ============================================================================
|
|
253
|
+
|
|
254
|
+
// Navigation (all support count prefix, e.g., 5j moves down 5 lines)
|
|
255
|
+
globalThis.vi_left = function (): void {
|
|
256
|
+
executeWithCount("move_left");
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
globalThis.vi_down = function (): void {
|
|
260
|
+
executeWithCount("move_down");
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
globalThis.vi_up = function (): void {
|
|
264
|
+
executeWithCount("move_up");
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
globalThis.vi_right = function (): void {
|
|
268
|
+
executeWithCount("move_right");
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
globalThis.vi_word = function (): void {
|
|
272
|
+
executeWithCount("move_word_right");
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
globalThis.vi_word_back = function (): void {
|
|
276
|
+
executeWithCount("move_word_left");
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
globalThis.vi_word_end = function (): void {
|
|
280
|
+
// Move to end of word - for count, repeat the whole operation
|
|
281
|
+
const count = consumeCount();
|
|
282
|
+
for (let i = 0; i < count; i++) {
|
|
283
|
+
editor.executeAction("move_word_right");
|
|
284
|
+
editor.executeAction("move_left");
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
globalThis.vi_line_start = function (): void {
|
|
289
|
+
consumeCount(); // Count doesn't apply to line start
|
|
290
|
+
editor.executeAction("move_line_start");
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
globalThis.vi_line_end = function (): void {
|
|
294
|
+
consumeCount(); // Count doesn't apply to line end
|
|
295
|
+
editor.executeAction("move_line_end");
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
globalThis.vi_first_non_blank = function (): void {
|
|
299
|
+
consumeCount(); // Count doesn't apply
|
|
300
|
+
editor.executeAction("move_line_start");
|
|
301
|
+
// TODO: skip whitespace
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
globalThis.vi_doc_start = function (): void {
|
|
305
|
+
consumeCount(); // Count doesn't apply
|
|
306
|
+
editor.executeAction("move_document_start");
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
globalThis.vi_doc_end = function (): void {
|
|
310
|
+
consumeCount(); // Count doesn't apply
|
|
311
|
+
editor.executeAction("move_document_end");
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
globalThis.vi_page_down = function (): void {
|
|
315
|
+
executeWithCount("page_down");
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
globalThis.vi_page_up = function (): void {
|
|
319
|
+
executeWithCount("page_up");
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
globalThis.vi_matching_bracket = function (): void {
|
|
323
|
+
editor.executeAction("go_to_matching_bracket");
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// Mode switching
|
|
327
|
+
globalThis.vi_insert_before = function (): void {
|
|
328
|
+
switchMode("insert");
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
globalThis.vi_insert_after = function (): void {
|
|
332
|
+
editor.executeAction("move_right");
|
|
333
|
+
switchMode("insert");
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
globalThis.vi_insert_line_start = function (): void {
|
|
337
|
+
editor.executeAction("move_line_start");
|
|
338
|
+
switchMode("insert");
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
globalThis.vi_insert_line_end = function (): void {
|
|
342
|
+
editor.executeAction("move_line_end");
|
|
343
|
+
switchMode("insert");
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
globalThis.vi_open_below = function (): void {
|
|
347
|
+
editor.executeAction("move_line_end");
|
|
348
|
+
editor.executeAction("insert_newline");
|
|
349
|
+
switchMode("insert");
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
globalThis.vi_open_above = function (): void {
|
|
353
|
+
editor.executeAction("move_line_start");
|
|
354
|
+
editor.executeAction("insert_newline");
|
|
355
|
+
editor.executeAction("move_up");
|
|
356
|
+
switchMode("insert");
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
globalThis.vi_escape = function (): void {
|
|
360
|
+
switchMode("normal");
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// Operators
|
|
364
|
+
globalThis.vi_delete_operator = function (): void {
|
|
365
|
+
state.pendingOperator = "d";
|
|
366
|
+
switchMode("operator-pending");
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
globalThis.vi_change_operator = function (): void {
|
|
370
|
+
state.pendingOperator = "c";
|
|
371
|
+
switchMode("operator-pending");
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
globalThis.vi_yank_operator = function (): void {
|
|
375
|
+
state.pendingOperator = "y";
|
|
376
|
+
switchMode("operator-pending");
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
// Line operations (dd, cc, yy) - support count prefix (3dd = delete 3 lines)
|
|
380
|
+
globalThis.vi_delete_line = function (): void {
|
|
381
|
+
const count = consumeCount();
|
|
382
|
+
if (count === 1) {
|
|
383
|
+
editor.executeAction("delete_line");
|
|
384
|
+
} else {
|
|
385
|
+
editor.executeActions([{ action: "delete_line", count }]);
|
|
386
|
+
}
|
|
387
|
+
switchMode("normal");
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
globalThis.vi_change_line = function (): void {
|
|
391
|
+
consumeCount(); // TODO: support count for change line
|
|
392
|
+
editor.executeAction("move_line_start");
|
|
393
|
+
const start = editor.getCursorPosition();
|
|
394
|
+
editor.executeAction("move_line_end");
|
|
395
|
+
const end = editor.getCursorPosition();
|
|
396
|
+
if (start !== null && end !== null) {
|
|
397
|
+
editor.deleteRange(editor.getActiveBufferId(), start, end);
|
|
398
|
+
}
|
|
399
|
+
switchMode("insert");
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
globalThis.vi_yank_line = function (): void {
|
|
403
|
+
const count = consumeCount();
|
|
404
|
+
// select_line selects current line and moves cursor to next line
|
|
405
|
+
if (count === 1) {
|
|
406
|
+
editor.executeAction("select_line");
|
|
407
|
+
} else {
|
|
408
|
+
editor.executeActions([{ action: "select_line", count }]);
|
|
409
|
+
}
|
|
410
|
+
editor.executeAction("copy");
|
|
411
|
+
// Move back to original line using synchronous actions
|
|
412
|
+
// (setBufferCursor is async and doesn't take effect in time)
|
|
413
|
+
editor.executeAction("move_up");
|
|
414
|
+
editor.executeAction("move_line_start");
|
|
415
|
+
state.lastYankWasLinewise = true;
|
|
416
|
+
editor.setStatus(`Yanked ${count} line${count > 1 ? "s" : ""}`);
|
|
417
|
+
switchMode("normal");
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// Single character operations - support count prefix (3x = delete 3 chars)
|
|
421
|
+
globalThis.vi_delete_char = function (): void {
|
|
422
|
+
executeWithCount("delete_forward");
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
globalThis.vi_delete_char_before = function (): void {
|
|
426
|
+
executeWithCount("delete_backward");
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
globalThis.vi_replace_char = function (): void {
|
|
430
|
+
// TODO: implement character replacement (need to read next char)
|
|
431
|
+
editor.setStatus("Replace char not yet implemented");
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
// Substitute (delete char and enter insert mode)
|
|
435
|
+
globalThis.vi_substitute = function (): void {
|
|
436
|
+
editor.executeAction("delete_forward");
|
|
437
|
+
switchMode("insert");
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// Delete to end of line
|
|
441
|
+
globalThis.vi_delete_to_end = function (): void {
|
|
442
|
+
const start = editor.getCursorPosition();
|
|
443
|
+
editor.executeAction("move_line_end");
|
|
444
|
+
const end = editor.getCursorPosition();
|
|
445
|
+
if (start !== null && end !== null && end > start) {
|
|
446
|
+
editor.deleteRange(editor.getActiveBufferId(), start, end);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// Change to end of line
|
|
451
|
+
globalThis.vi_change_to_end = function (): void {
|
|
452
|
+
const start = editor.getCursorPosition();
|
|
453
|
+
editor.executeAction("move_line_end");
|
|
454
|
+
const end = editor.getCursorPosition();
|
|
455
|
+
if (start !== null && end !== null && end > start) {
|
|
456
|
+
editor.deleteRange(editor.getActiveBufferId(), start, end);
|
|
457
|
+
}
|
|
458
|
+
switchMode("insert");
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// Clipboard
|
|
462
|
+
globalThis.vi_paste_after = function (): void {
|
|
463
|
+
if (state.lastYankWasLinewise) {
|
|
464
|
+
// Line-wise paste: go to next line start and paste there
|
|
465
|
+
// The yanked text includes trailing \n which pushes subsequent lines down
|
|
466
|
+
editor.executeAction("move_down");
|
|
467
|
+
editor.executeAction("move_line_start");
|
|
468
|
+
editor.executeAction("paste");
|
|
469
|
+
editor.executeAction("move_up"); // Stay on the pasted line
|
|
470
|
+
editor.executeAction("move_line_start");
|
|
471
|
+
} else {
|
|
472
|
+
// Character-wise paste: insert after cursor
|
|
473
|
+
editor.executeAction("move_right");
|
|
474
|
+
editor.executeAction("paste");
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
globalThis.vi_paste_before = function (): void {
|
|
479
|
+
if (state.lastYankWasLinewise) {
|
|
480
|
+
// Line-wise paste: paste at current line start
|
|
481
|
+
// The yanked text includes trailing \n which pushes current line down
|
|
482
|
+
editor.executeAction("move_line_start");
|
|
483
|
+
editor.executeAction("paste");
|
|
484
|
+
editor.executeAction("move_up"); // Stay on the pasted line
|
|
485
|
+
editor.executeAction("move_line_start");
|
|
486
|
+
} else {
|
|
487
|
+
// Character-wise paste: insert at cursor
|
|
488
|
+
editor.executeAction("paste");
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// Undo/Redo
|
|
493
|
+
globalThis.vi_undo = function (): void {
|
|
494
|
+
editor.executeAction("undo");
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
globalThis.vi_redo = function (): void {
|
|
498
|
+
editor.executeAction("redo");
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
// Join lines
|
|
502
|
+
globalThis.vi_join = function (): void {
|
|
503
|
+
editor.executeAction("move_line_end");
|
|
504
|
+
editor.executeAction("delete_forward");
|
|
505
|
+
editor.executeAction("insert_text_at_cursor");
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
// Search
|
|
509
|
+
globalThis.vi_search_forward = function (): void {
|
|
510
|
+
editor.executeAction("search");
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
globalThis.vi_search_backward = function (): void {
|
|
514
|
+
// Use same search dialog, user can search backward manually
|
|
515
|
+
editor.executeAction("search");
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
globalThis.vi_find_next = function (): void {
|
|
519
|
+
editor.executeAction("find_next");
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
globalThis.vi_find_prev = function (): void {
|
|
523
|
+
editor.executeAction("find_previous");
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
// Center view
|
|
527
|
+
globalThis.vi_center_cursor = function (): void {
|
|
528
|
+
editor.executeAction("center_cursor");
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
// Half page movements
|
|
532
|
+
globalThis.vi_half_page_down = function (): void {
|
|
533
|
+
// Approximate half page with multiple down movements
|
|
534
|
+
const count = consumeCount();
|
|
535
|
+
editor.executeActions([{ action: "move_down", count: 10 * count }]);
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
globalThis.vi_half_page_up = function (): void {
|
|
539
|
+
const count = consumeCount();
|
|
540
|
+
editor.executeActions([{ action: "move_up", count: 10 * count }]);
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
// ============================================================================
|
|
544
|
+
// Count Prefix (digit keys 1-9, and 0 after initial digit)
|
|
545
|
+
// ============================================================================
|
|
546
|
+
|
|
547
|
+
// Digit handlers for count prefix
|
|
548
|
+
globalThis.vi_digit_1 = function (): void { accumulateCount(1); };
|
|
549
|
+
globalThis.vi_digit_2 = function (): void { accumulateCount(2); };
|
|
550
|
+
globalThis.vi_digit_3 = function (): void { accumulateCount(3); };
|
|
551
|
+
globalThis.vi_digit_4 = function (): void { accumulateCount(4); };
|
|
552
|
+
globalThis.vi_digit_5 = function (): void { accumulateCount(5); };
|
|
553
|
+
globalThis.vi_digit_6 = function (): void { accumulateCount(6); };
|
|
554
|
+
globalThis.vi_digit_7 = function (): void { accumulateCount(7); };
|
|
555
|
+
globalThis.vi_digit_8 = function (): void { accumulateCount(8); };
|
|
556
|
+
globalThis.vi_digit_9 = function (): void { accumulateCount(9); };
|
|
557
|
+
|
|
558
|
+
// 0 is special: if count is already started, it appends; otherwise it's "go to line start"
|
|
559
|
+
globalThis.vi_digit_0_or_line_start = function (): void {
|
|
560
|
+
if (state.count !== null) {
|
|
561
|
+
accumulateCount(0);
|
|
562
|
+
} else {
|
|
563
|
+
editor.executeAction("move_line_start");
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
// 0 in operator-pending mode: if count is started, append; otherwise apply operator to line start
|
|
568
|
+
globalThis.vi_op_digit_0_or_line_start = function (): void {
|
|
569
|
+
if (state.count !== null) {
|
|
570
|
+
accumulateCount(0);
|
|
571
|
+
} else {
|
|
572
|
+
handleMotionWithOperator("move_line_start");
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
// ============================================================================
|
|
577
|
+
// Visual Mode
|
|
578
|
+
// ============================================================================
|
|
579
|
+
|
|
580
|
+
// Enter character-wise visual mode
|
|
581
|
+
globalThis.vi_visual_char = function (): void {
|
|
582
|
+
state.visualAnchor = editor.getCursorPosition();
|
|
583
|
+
// Select current character to start visual selection
|
|
584
|
+
editor.executeAction("select_right");
|
|
585
|
+
switchMode("visual");
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
// Enter line-wise visual mode
|
|
589
|
+
globalThis.vi_visual_line = function (): void {
|
|
590
|
+
state.visualAnchor = editor.getCursorPosition();
|
|
591
|
+
// Select current line
|
|
592
|
+
editor.executeAction("move_line_start");
|
|
593
|
+
editor.executeAction("select_line");
|
|
594
|
+
switchMode("visual-line");
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
// Toggle between visual and visual-line modes
|
|
598
|
+
globalThis.vi_visual_toggle_line = function (): void {
|
|
599
|
+
if (state.mode === "visual") {
|
|
600
|
+
// Switch to line mode - extend selection to full lines
|
|
601
|
+
editor.executeAction("select_line");
|
|
602
|
+
state.mode = "visual-line";
|
|
603
|
+
editor.setEditorMode("vi-visual-line");
|
|
604
|
+
editor.setStatus(getModeIndicator("visual-line"));
|
|
605
|
+
} else if (state.mode === "visual-line") {
|
|
606
|
+
// Switch to char mode (keep selection but change mode)
|
|
607
|
+
state.mode = "visual";
|
|
608
|
+
editor.setEditorMode("vi-visual");
|
|
609
|
+
editor.setStatus(getModeIndicator("visual"));
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// Visual mode motions - these extend the selection
|
|
614
|
+
globalThis.vi_vis_left = function (): void {
|
|
615
|
+
executeWithCount("select_left");
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
globalThis.vi_vis_down = function (): void {
|
|
619
|
+
executeWithCount("select_down");
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
globalThis.vi_vis_up = function (): void {
|
|
623
|
+
executeWithCount("select_up");
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
globalThis.vi_vis_right = function (): void {
|
|
627
|
+
executeWithCount("select_right");
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
globalThis.vi_vis_word = function (): void {
|
|
631
|
+
executeWithCount("select_word_right");
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
globalThis.vi_vis_word_back = function (): void {
|
|
635
|
+
executeWithCount("select_word_left");
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
globalThis.vi_vis_word_end = function (): void {
|
|
639
|
+
const count = consumeCount();
|
|
640
|
+
for (let i = 0; i < count; i++) {
|
|
641
|
+
editor.executeAction("select_word_right");
|
|
642
|
+
editor.executeAction("select_left");
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
globalThis.vi_vis_line_start = function (): void {
|
|
647
|
+
consumeCount();
|
|
648
|
+
editor.executeAction("select_line_start");
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
globalThis.vi_vis_line_end = function (): void {
|
|
652
|
+
consumeCount();
|
|
653
|
+
editor.executeAction("select_line_end");
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
globalThis.vi_vis_doc_start = function (): void {
|
|
657
|
+
consumeCount();
|
|
658
|
+
editor.executeAction("select_document_start");
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
globalThis.vi_vis_doc_end = function (): void {
|
|
662
|
+
consumeCount();
|
|
663
|
+
editor.executeAction("select_document_end");
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
// Visual line mode motions - extend selection by whole lines
|
|
667
|
+
globalThis.vi_vline_down = function (): void {
|
|
668
|
+
executeWithCount("select_down");
|
|
669
|
+
// Ensure full line selection
|
|
670
|
+
editor.executeAction("select_line_end");
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
globalThis.vi_vline_up = function (): void {
|
|
674
|
+
executeWithCount("select_up");
|
|
675
|
+
// Ensure full line selection
|
|
676
|
+
editor.executeAction("select_line_start");
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
// Visual mode operators - act on selection
|
|
680
|
+
globalThis.vi_vis_delete = function (): void {
|
|
681
|
+
const wasLinewise = state.mode === "visual-line";
|
|
682
|
+
editor.executeAction("cut");
|
|
683
|
+
state.lastYankWasLinewise = wasLinewise;
|
|
684
|
+
switchMode("normal");
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
globalThis.vi_vis_change = function (): void {
|
|
688
|
+
editor.executeAction("cut");
|
|
689
|
+
switchMode("insert");
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
globalThis.vi_vis_yank = function (): void {
|
|
693
|
+
const wasLinewise = state.mode === "visual-line";
|
|
694
|
+
editor.executeAction("copy");
|
|
695
|
+
state.lastYankWasLinewise = wasLinewise;
|
|
696
|
+
// Move cursor to start of selection (vim behavior)
|
|
697
|
+
editor.executeAction("move_left");
|
|
698
|
+
switchMode("normal");
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
// Exit visual mode without doing anything
|
|
702
|
+
globalThis.vi_vis_escape = function (): void {
|
|
703
|
+
switchMode("normal");
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
// ============================================================================
|
|
707
|
+
// Text Objects (iw, aw, i", a", etc.)
|
|
708
|
+
// ============================================================================
|
|
709
|
+
|
|
710
|
+
// Enter text-object mode with "inner" modifier
|
|
711
|
+
globalThis.vi_text_object_inner = function (): void {
|
|
712
|
+
state.pendingTextObject = "inner";
|
|
713
|
+
state.mode = "text-object";
|
|
714
|
+
editor.setEditorMode("vi-text-object");
|
|
715
|
+
editor.setStatus(getModeIndicator("text-object"));
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
// Enter text-object mode with "around" modifier
|
|
719
|
+
globalThis.vi_text_object_around = function (): void {
|
|
720
|
+
state.pendingTextObject = "around";
|
|
721
|
+
state.mode = "text-object";
|
|
722
|
+
editor.setEditorMode("vi-text-object");
|
|
723
|
+
editor.setStatus(getModeIndicator("text-object"));
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
// Apply text object selection and then the pending operator
|
|
727
|
+
async function applyTextObject(objectType: string): Promise<void> {
|
|
728
|
+
const operator = state.pendingOperator;
|
|
729
|
+
const isInner = state.pendingTextObject === "inner";
|
|
730
|
+
|
|
731
|
+
if (!operator) {
|
|
732
|
+
switchMode("normal");
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const bufferId = editor.getActiveBufferId();
|
|
737
|
+
const cursorPos = editor.getCursorPosition();
|
|
738
|
+
if (cursorPos === null) {
|
|
739
|
+
switchMode("normal");
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Get text around cursor to find the text object boundaries
|
|
744
|
+
const windowSize = 1000;
|
|
745
|
+
const startOffset = Math.max(0, cursorPos - windowSize);
|
|
746
|
+
const bufLen = editor.getBufferLength(bufferId);
|
|
747
|
+
const endOffset = Math.min(bufLen, cursorPos + windowSize);
|
|
748
|
+
const text = await editor.getBufferText(bufferId, startOffset, endOffset);
|
|
749
|
+
if (!text) {
|
|
750
|
+
switchMode("normal");
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const posInChunk = cursorPos - startOffset;
|
|
755
|
+
let selectStart = -1;
|
|
756
|
+
let selectEnd = -1;
|
|
757
|
+
|
|
758
|
+
switch (objectType) {
|
|
759
|
+
case "word": {
|
|
760
|
+
// Find word boundaries
|
|
761
|
+
const wordChars = /[a-zA-Z0-9_]/;
|
|
762
|
+
let start = posInChunk;
|
|
763
|
+
let end = posInChunk;
|
|
764
|
+
|
|
765
|
+
// Expand to find word start
|
|
766
|
+
while (start > 0 && wordChars.test(text[start - 1])) start--;
|
|
767
|
+
// Expand to find word end
|
|
768
|
+
while (end < text.length && wordChars.test(text[end])) end++;
|
|
769
|
+
|
|
770
|
+
if (!isInner) {
|
|
771
|
+
// "a word" includes trailing whitespace
|
|
772
|
+
while (end < text.length && /\s/.test(text[end]) && text[end] !== '\n') end++;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
selectStart = startOffset + start;
|
|
776
|
+
selectEnd = startOffset + end;
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
case "WORD": {
|
|
781
|
+
// WORD is whitespace-delimited
|
|
782
|
+
let start = posInChunk;
|
|
783
|
+
let end = posInChunk;
|
|
784
|
+
|
|
785
|
+
while (start > 0 && !/\s/.test(text[start - 1])) start--;
|
|
786
|
+
while (end < text.length && !/\s/.test(text[end])) end++;
|
|
787
|
+
|
|
788
|
+
if (!isInner) {
|
|
789
|
+
while (end < text.length && /\s/.test(text[end]) && text[end] !== '\n') end++;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
selectStart = startOffset + start;
|
|
793
|
+
selectEnd = startOffset + end;
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
case "\"":
|
|
798
|
+
case "'":
|
|
799
|
+
case "`": {
|
|
800
|
+
// Find matching quotes on current line
|
|
801
|
+
// First find line boundaries
|
|
802
|
+
let lineStart = posInChunk;
|
|
803
|
+
let lineEnd = posInChunk;
|
|
804
|
+
while (lineStart > 0 && text[lineStart - 1] !== '\n') lineStart--;
|
|
805
|
+
while (lineEnd < text.length && text[lineEnd] !== '\n') lineEnd++;
|
|
806
|
+
|
|
807
|
+
const line = text.substring(lineStart, lineEnd);
|
|
808
|
+
const colInLine = posInChunk - lineStart;
|
|
809
|
+
|
|
810
|
+
// Find quote pair containing cursor
|
|
811
|
+
let quoteStart = -1;
|
|
812
|
+
let quoteEnd = -1;
|
|
813
|
+
let inQuote = false;
|
|
814
|
+
|
|
815
|
+
for (let i = 0; i < line.length; i++) {
|
|
816
|
+
if (line[i] === objectType) {
|
|
817
|
+
if (!inQuote) {
|
|
818
|
+
quoteStart = i;
|
|
819
|
+
inQuote = true;
|
|
820
|
+
} else {
|
|
821
|
+
quoteEnd = i;
|
|
822
|
+
if (colInLine >= quoteStart && colInLine <= quoteEnd) {
|
|
823
|
+
break; // Found the pair containing cursor
|
|
824
|
+
}
|
|
825
|
+
inQuote = false;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (quoteStart !== -1 && quoteEnd !== -1 && colInLine >= quoteStart && colInLine <= quoteEnd) {
|
|
831
|
+
if (isInner) {
|
|
832
|
+
selectStart = startOffset + lineStart + quoteStart + 1;
|
|
833
|
+
selectEnd = startOffset + lineStart + quoteEnd;
|
|
834
|
+
} else {
|
|
835
|
+
selectStart = startOffset + lineStart + quoteStart;
|
|
836
|
+
selectEnd = startOffset + lineStart + quoteEnd + 1;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
case "(":
|
|
843
|
+
case ")":
|
|
844
|
+
case "b": {
|
|
845
|
+
// Find matching parentheses
|
|
846
|
+
const result = findMatchingPair(text, posInChunk, '(', ')');
|
|
847
|
+
if (result) {
|
|
848
|
+
if (isInner) {
|
|
849
|
+
selectStart = startOffset + result.start + 1;
|
|
850
|
+
selectEnd = startOffset + result.end;
|
|
851
|
+
} else {
|
|
852
|
+
selectStart = startOffset + result.start;
|
|
853
|
+
selectEnd = startOffset + result.end + 1;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
case "{":
|
|
860
|
+
case "}":
|
|
861
|
+
case "B": {
|
|
862
|
+
const result = findMatchingPair(text, posInChunk, '{', '}');
|
|
863
|
+
if (result) {
|
|
864
|
+
if (isInner) {
|
|
865
|
+
selectStart = startOffset + result.start + 1;
|
|
866
|
+
selectEnd = startOffset + result.end;
|
|
867
|
+
} else {
|
|
868
|
+
selectStart = startOffset + result.start;
|
|
869
|
+
selectEnd = startOffset + result.end + 1;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
case "[":
|
|
876
|
+
case "]": {
|
|
877
|
+
const result = findMatchingPair(text, posInChunk, '[', ']');
|
|
878
|
+
if (result) {
|
|
879
|
+
if (isInner) {
|
|
880
|
+
selectStart = startOffset + result.start + 1;
|
|
881
|
+
selectEnd = startOffset + result.end;
|
|
882
|
+
} else {
|
|
883
|
+
selectStart = startOffset + result.start;
|
|
884
|
+
selectEnd = startOffset + result.end + 1;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
break;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
case "<":
|
|
891
|
+
case ">": {
|
|
892
|
+
const result = findMatchingPair(text, posInChunk, '<', '>');
|
|
893
|
+
if (result) {
|
|
894
|
+
if (isInner) {
|
|
895
|
+
selectStart = startOffset + result.start + 1;
|
|
896
|
+
selectEnd = startOffset + result.end;
|
|
897
|
+
} else {
|
|
898
|
+
selectStart = startOffset + result.start;
|
|
899
|
+
selectEnd = startOffset + result.end + 1;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (selectStart === -1 || selectEnd === -1 || selectStart >= selectEnd) {
|
|
907
|
+
switchMode("normal");
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Apply the operator directly using deleteRange/copyRange
|
|
912
|
+
switch (operator) {
|
|
913
|
+
case "d": {
|
|
914
|
+
// Delete the range directly
|
|
915
|
+
editor.deleteRange(bufferId, selectStart, selectEnd);
|
|
916
|
+
state.lastYankWasLinewise = false;
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
919
|
+
case "c": {
|
|
920
|
+
// Delete and enter insert mode
|
|
921
|
+
editor.deleteRange(bufferId, selectStart, selectEnd);
|
|
922
|
+
switchMode("insert");
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
case "y": {
|
|
926
|
+
// For yank, we need to select the range and copy
|
|
927
|
+
// First move cursor to start
|
|
928
|
+
editor.setBufferCursor(bufferId, selectStart);
|
|
929
|
+
// Select the range
|
|
930
|
+
for (let i = 0; i < selectEnd - selectStart; i++) {
|
|
931
|
+
editor.executeAction("select_right");
|
|
932
|
+
}
|
|
933
|
+
editor.executeAction("copy");
|
|
934
|
+
state.lastYankWasLinewise = false;
|
|
935
|
+
// Move back to start
|
|
936
|
+
editor.setBufferCursor(bufferId, selectStart);
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
switchMode("normal");
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Helper to find matching bracket pair containing the cursor
|
|
945
|
+
function findMatchingPair(text: string, pos: number, openChar: string, closeChar: string): { start: number; end: number } | null {
|
|
946
|
+
let depth = 0;
|
|
947
|
+
let start = -1;
|
|
948
|
+
|
|
949
|
+
// Search backward for opening bracket
|
|
950
|
+
for (let i = pos; i >= 0; i--) {
|
|
951
|
+
if (text[i] === closeChar) depth++;
|
|
952
|
+
if (text[i] === openChar) {
|
|
953
|
+
if (depth === 0) {
|
|
954
|
+
start = i;
|
|
955
|
+
break;
|
|
956
|
+
}
|
|
957
|
+
depth--;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (start === -1) return null;
|
|
962
|
+
|
|
963
|
+
// Search forward for closing bracket
|
|
964
|
+
depth = 0;
|
|
965
|
+
for (let i = start; i < text.length; i++) {
|
|
966
|
+
if (text[i] === openChar) depth++;
|
|
967
|
+
if (text[i] === closeChar) {
|
|
968
|
+
depth--;
|
|
969
|
+
if (depth === 0) {
|
|
970
|
+
return { start, end: i };
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Text object handlers
|
|
979
|
+
globalThis.vi_to_word = async function (): Promise<void> { await applyTextObject("word"); };
|
|
980
|
+
globalThis.vi_to_WORD = async function (): Promise<void> { await applyTextObject("WORD"); };
|
|
981
|
+
globalThis.vi_to_dquote = async function (): Promise<void> { await applyTextObject("\""); };
|
|
982
|
+
globalThis.vi_to_squote = async function (): Promise<void> { await applyTextObject("'"); };
|
|
983
|
+
globalThis.vi_to_backtick = async function (): Promise<void> { await applyTextObject("`"); };
|
|
984
|
+
globalThis.vi_to_paren = async function (): Promise<void> { await applyTextObject("("); };
|
|
985
|
+
globalThis.vi_to_brace = async function (): Promise<void> { await applyTextObject("{"); };
|
|
986
|
+
globalThis.vi_to_bracket = async function (): Promise<void> { await applyTextObject("["); };
|
|
987
|
+
globalThis.vi_to_angle = async function (): Promise<void> { await applyTextObject("<"); };
|
|
988
|
+
|
|
989
|
+
// Cancel text object mode
|
|
990
|
+
globalThis.vi_to_cancel = function (): void {
|
|
991
|
+
switchMode("normal");
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
// ============================================================================
|
|
995
|
+
// Find Character Motions (f/t/F/T)
|
|
996
|
+
// ============================================================================
|
|
997
|
+
|
|
998
|
+
// Enter find-char mode waiting for the target character
|
|
999
|
+
function enterFindCharMode(findType: FindCharType): void {
|
|
1000
|
+
state.pendingFindChar = findType;
|
|
1001
|
+
state.mode = "find-char";
|
|
1002
|
+
editor.setEditorMode("vi-find-char");
|
|
1003
|
+
editor.setStatus(getModeIndicator("find-char"));
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Execute find char motion (async because getBufferText is async)
|
|
1007
|
+
async function executeFindChar(findType: FindCharType, char: string): Promise<void> {
|
|
1008
|
+
if (!findType) return;
|
|
1009
|
+
|
|
1010
|
+
const bufferId = editor.getActiveBufferId();
|
|
1011
|
+
const cursorPos = editor.getCursorPosition();
|
|
1012
|
+
if (cursorPos === null || (cursorPos === 0 && (findType === "F" || findType === "T"))) {
|
|
1013
|
+
// Can't search backward from position 0
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Get text around cursor to find line boundaries
|
|
1018
|
+
// Read up to 10KB before and after cursor for context
|
|
1019
|
+
const windowSize = 10000;
|
|
1020
|
+
const startOffset = Math.max(0, cursorPos - windowSize);
|
|
1021
|
+
const bufLen = editor.getBufferLength(bufferId);
|
|
1022
|
+
const endOffset = Math.min(bufLen, cursorPos + windowSize);
|
|
1023
|
+
|
|
1024
|
+
// Get buffer text around cursor
|
|
1025
|
+
const text = await editor.getBufferText(bufferId, startOffset, endOffset);
|
|
1026
|
+
if (!text) return;
|
|
1027
|
+
|
|
1028
|
+
// Calculate position within this text chunk
|
|
1029
|
+
const posInChunk = cursorPos - startOffset;
|
|
1030
|
+
|
|
1031
|
+
// Find line start (last newline before cursor, or start of chunk)
|
|
1032
|
+
let lineStart = 0;
|
|
1033
|
+
for (let i = posInChunk - 1; i >= 0; i--) {
|
|
1034
|
+
if (text[i] === '\n') {
|
|
1035
|
+
lineStart = i + 1;
|
|
1036
|
+
break;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Find line end (next newline after cursor, or end of chunk)
|
|
1041
|
+
let lineEnd = text.length;
|
|
1042
|
+
for (let i = posInChunk; i < text.length; i++) {
|
|
1043
|
+
if (text[i] === '\n') {
|
|
1044
|
+
lineEnd = i;
|
|
1045
|
+
break;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// Extract line text and calculate column
|
|
1050
|
+
const lineText = text.substring(lineStart, lineEnd);
|
|
1051
|
+
const col = posInChunk - lineStart;
|
|
1052
|
+
|
|
1053
|
+
let targetCol = -1;
|
|
1054
|
+
|
|
1055
|
+
if (findType === "f" || findType === "t") {
|
|
1056
|
+
// Search forward on the line
|
|
1057
|
+
for (let i = col + 1; i < lineText.length; i++) {
|
|
1058
|
+
if (lineText[i] === char) {
|
|
1059
|
+
targetCol = findType === "f" ? i : i - 1;
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
} else {
|
|
1064
|
+
// Search backward (F/T)
|
|
1065
|
+
for (let i = col - 1; i >= 0; i--) {
|
|
1066
|
+
if (lineText[i] === char) {
|
|
1067
|
+
targetCol = findType === "F" ? i : i + 1;
|
|
1068
|
+
break;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
if (targetCol >= 0 && targetCol !== col) {
|
|
1074
|
+
// Move to target column
|
|
1075
|
+
const diff = targetCol - col;
|
|
1076
|
+
const moveAction = diff > 0 ? "move_right" : "move_left";
|
|
1077
|
+
const steps = Math.abs(diff);
|
|
1078
|
+
for (let i = 0; i < steps; i++) {
|
|
1079
|
+
editor.executeAction(moveAction);
|
|
1080
|
+
}
|
|
1081
|
+
// Save for ; and , repeat
|
|
1082
|
+
state.lastFindChar = { type: findType, char };
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Handler for when a character is typed in find-char mode (async)
|
|
1087
|
+
globalThis.vi_find_char_handler = async function (char: string): Promise<void> {
|
|
1088
|
+
if (state.pendingFindChar) {
|
|
1089
|
+
await executeFindChar(state.pendingFindChar, char);
|
|
1090
|
+
}
|
|
1091
|
+
// Return to normal mode
|
|
1092
|
+
state.pendingFindChar = null;
|
|
1093
|
+
switchMode("normal");
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
// Commands to enter find-char mode
|
|
1097
|
+
globalThis.vi_find_char_f = function (): void {
|
|
1098
|
+
enterFindCharMode("f");
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
globalThis.vi_find_char_t = function (): void {
|
|
1102
|
+
enterFindCharMode("t");
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
globalThis.vi_find_char_F = function (): void {
|
|
1106
|
+
enterFindCharMode("F");
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
globalThis.vi_find_char_T = function (): void {
|
|
1110
|
+
enterFindCharMode("T");
|
|
1111
|
+
};
|
|
1112
|
+
|
|
1113
|
+
// Repeat last find char (async)
|
|
1114
|
+
globalThis.vi_find_char_repeat = async function (): Promise<void> {
|
|
1115
|
+
if (state.lastFindChar) {
|
|
1116
|
+
await executeFindChar(state.lastFindChar.type, state.lastFindChar.char);
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
// Repeat last find char in opposite direction (async)
|
|
1121
|
+
globalThis.vi_find_char_repeat_reverse = async function (): Promise<void> {
|
|
1122
|
+
if (state.lastFindChar) {
|
|
1123
|
+
const reversedType: FindCharType =
|
|
1124
|
+
state.lastFindChar.type === "f" ? "F" :
|
|
1125
|
+
state.lastFindChar.type === "F" ? "f" :
|
|
1126
|
+
state.lastFindChar.type === "t" ? "T" : "t";
|
|
1127
|
+
await executeFindChar(reversedType, state.lastFindChar.char);
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
// Cancel find-char mode
|
|
1132
|
+
globalThis.vi_find_char_cancel = function (): void {
|
|
1133
|
+
state.pendingFindChar = null;
|
|
1134
|
+
switchMode("normal");
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
// ============================================================================
|
|
1138
|
+
// Operator-Pending Mode Commands
|
|
1139
|
+
// ============================================================================
|
|
1140
|
+
|
|
1141
|
+
globalThis.vi_op_left = function (): void {
|
|
1142
|
+
handleMotionWithOperator("move_left");
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
globalThis.vi_op_down = function (): void {
|
|
1146
|
+
handleMotionWithOperator("move_down");
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
globalThis.vi_op_up = function (): void {
|
|
1150
|
+
handleMotionWithOperator("move_up");
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
globalThis.vi_op_right = function (): void {
|
|
1154
|
+
handleMotionWithOperator("move_right");
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
globalThis.vi_op_word = function (): void {
|
|
1158
|
+
handleMotionWithOperator("move_word_right");
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
globalThis.vi_op_word_back = function (): void {
|
|
1162
|
+
handleMotionWithOperator("move_word_left");
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
globalThis.vi_op_line_start = function (): void {
|
|
1166
|
+
handleMotionWithOperator("move_line_start");
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
globalThis.vi_op_line_end = function (): void {
|
|
1170
|
+
handleMotionWithOperator("move_line_end");
|
|
1171
|
+
};
|
|
1172
|
+
|
|
1173
|
+
globalThis.vi_op_doc_start = function (): void {
|
|
1174
|
+
handleMotionWithOperator("move_document_start");
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
globalThis.vi_op_doc_end = function (): void {
|
|
1178
|
+
handleMotionWithOperator("move_document_end");
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
globalThis.vi_op_matching_bracket = function (): void {
|
|
1182
|
+
handleMotionWithOperator("go_to_matching_bracket");
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
globalThis.vi_cancel = function (): void {
|
|
1186
|
+
switchMode("normal");
|
|
1187
|
+
};
|
|
1188
|
+
|
|
1189
|
+
// ============================================================================
|
|
1190
|
+
// Mode Definitions
|
|
1191
|
+
// ============================================================================
|
|
1192
|
+
|
|
1193
|
+
// Define vi-normal mode
|
|
1194
|
+
editor.defineMode("vi-normal", null, [
|
|
1195
|
+
// Count prefix (digits 1-9 start count, 0 is special)
|
|
1196
|
+
["1", "vi_digit_1"],
|
|
1197
|
+
["2", "vi_digit_2"],
|
|
1198
|
+
["3", "vi_digit_3"],
|
|
1199
|
+
["4", "vi_digit_4"],
|
|
1200
|
+
["5", "vi_digit_5"],
|
|
1201
|
+
["6", "vi_digit_6"],
|
|
1202
|
+
["7", "vi_digit_7"],
|
|
1203
|
+
["8", "vi_digit_8"],
|
|
1204
|
+
["9", "vi_digit_9"],
|
|
1205
|
+
["0", "vi_digit_0_or_line_start"], // 0 appends to count, or moves to line start
|
|
1206
|
+
|
|
1207
|
+
// Navigation
|
|
1208
|
+
["h", "vi_left"],
|
|
1209
|
+
["j", "vi_down"],
|
|
1210
|
+
["k", "vi_up"],
|
|
1211
|
+
["l", "vi_right"],
|
|
1212
|
+
["w", "vi_word"],
|
|
1213
|
+
["b", "vi_word_back"],
|
|
1214
|
+
["e", "vi_word_end"],
|
|
1215
|
+
["$", "vi_line_end"],
|
|
1216
|
+
["^", "vi_first_non_blank"],
|
|
1217
|
+
["g g", "vi_doc_start"],
|
|
1218
|
+
["G", "vi_doc_end"],
|
|
1219
|
+
["C-f", "vi_page_down"],
|
|
1220
|
+
["C-b", "vi_page_up"],
|
|
1221
|
+
["C-d", "vi_half_page_down"],
|
|
1222
|
+
["C-u", "vi_half_page_up"],
|
|
1223
|
+
["%", "vi_matching_bracket"],
|
|
1224
|
+
["z z", "vi_center_cursor"],
|
|
1225
|
+
|
|
1226
|
+
// Search
|
|
1227
|
+
["/", "vi_search_forward"],
|
|
1228
|
+
["?", "vi_search_backward"],
|
|
1229
|
+
["n", "vi_find_next"],
|
|
1230
|
+
["N", "vi_find_prev"],
|
|
1231
|
+
|
|
1232
|
+
// Find character on line
|
|
1233
|
+
["f", "vi_find_char_f"],
|
|
1234
|
+
["t", "vi_find_char_t"],
|
|
1235
|
+
["F", "vi_find_char_F"],
|
|
1236
|
+
["T", "vi_find_char_T"],
|
|
1237
|
+
[";", "vi_find_char_repeat"],
|
|
1238
|
+
[",", "vi_find_char_repeat_reverse"],
|
|
1239
|
+
|
|
1240
|
+
// Mode switching
|
|
1241
|
+
["i", "vi_insert_before"],
|
|
1242
|
+
["a", "vi_insert_after"],
|
|
1243
|
+
["I", "vi_insert_line_start"],
|
|
1244
|
+
["A", "vi_insert_line_end"],
|
|
1245
|
+
["o", "vi_open_below"],
|
|
1246
|
+
["O", "vi_open_above"],
|
|
1247
|
+
["Escape", "vi_escape"],
|
|
1248
|
+
|
|
1249
|
+
// Operators (single key - switches to operator-pending mode)
|
|
1250
|
+
// The second d/c/y is handled in operator-pending mode
|
|
1251
|
+
["d", "vi_delete_operator"],
|
|
1252
|
+
["c", "vi_change_operator"],
|
|
1253
|
+
["y", "vi_yank_operator"],
|
|
1254
|
+
|
|
1255
|
+
// Single char operations
|
|
1256
|
+
["x", "vi_delete_char"],
|
|
1257
|
+
["X", "vi_delete_char_before"],
|
|
1258
|
+
["r", "vi_replace_char"],
|
|
1259
|
+
["s", "vi_substitute"],
|
|
1260
|
+
["S", "vi_change_line"],
|
|
1261
|
+
["D", "vi_delete_to_end"],
|
|
1262
|
+
["C", "vi_change_to_end"],
|
|
1263
|
+
|
|
1264
|
+
// Clipboard
|
|
1265
|
+
["p", "vi_paste_after"],
|
|
1266
|
+
["P", "vi_paste_before"],
|
|
1267
|
+
|
|
1268
|
+
// Undo/Redo
|
|
1269
|
+
["u", "vi_undo"],
|
|
1270
|
+
["C-r", "vi_redo"],
|
|
1271
|
+
|
|
1272
|
+
// Visual mode
|
|
1273
|
+
["v", "vi_visual_char"],
|
|
1274
|
+
["V", "vi_visual_line"],
|
|
1275
|
+
|
|
1276
|
+
// Other
|
|
1277
|
+
["J", "vi_join"],
|
|
1278
|
+
], true); // read_only = true to prevent character insertion
|
|
1279
|
+
|
|
1280
|
+
// Define vi-insert mode - only Escape is special, other keys insert text
|
|
1281
|
+
editor.defineMode("vi-insert", null, [
|
|
1282
|
+
["Escape", "vi_escape"],
|
|
1283
|
+
], false); // read_only = false to allow normal typing
|
|
1284
|
+
|
|
1285
|
+
// Define vi-find-char mode - binds all printable chars to the handler
|
|
1286
|
+
// This mode waits for a single character input for f/t/F/T motions
|
|
1287
|
+
|
|
1288
|
+
// Explicitly define handlers for each character to ensure they're accessible
|
|
1289
|
+
// These return Promises so the runtime can await them
|
|
1290
|
+
globalThis.vi_fc_a = async function(): Promise<void> { return globalThis.vi_find_char_handler("a"); };
|
|
1291
|
+
globalThis.vi_fc_b = async function(): Promise<void> { return globalThis.vi_find_char_handler("b"); };
|
|
1292
|
+
globalThis.vi_fc_c = async function(): Promise<void> { return globalThis.vi_find_char_handler("c"); };
|
|
1293
|
+
globalThis.vi_fc_d = async function(): Promise<void> { return globalThis.vi_find_char_handler("d"); };
|
|
1294
|
+
globalThis.vi_fc_e = async function(): Promise<void> { return globalThis.vi_find_char_handler("e"); };
|
|
1295
|
+
globalThis.vi_fc_f = async function(): Promise<void> { return globalThis.vi_find_char_handler("f"); };
|
|
1296
|
+
globalThis.vi_fc_g = async function(): Promise<void> { return globalThis.vi_find_char_handler("g"); };
|
|
1297
|
+
globalThis.vi_fc_h = async function(): Promise<void> { return globalThis.vi_find_char_handler("h"); };
|
|
1298
|
+
globalThis.vi_fc_i = async function(): Promise<void> { return globalThis.vi_find_char_handler("i"); };
|
|
1299
|
+
globalThis.vi_fc_j = async function(): Promise<void> { return globalThis.vi_find_char_handler("j"); };
|
|
1300
|
+
globalThis.vi_fc_k = async function(): Promise<void> { return globalThis.vi_find_char_handler("k"); };
|
|
1301
|
+
globalThis.vi_fc_l = async function(): Promise<void> { return globalThis.vi_find_char_handler("l"); };
|
|
1302
|
+
globalThis.vi_fc_m = async function(): Promise<void> { return globalThis.vi_find_char_handler("m"); };
|
|
1303
|
+
globalThis.vi_fc_n = async function(): Promise<void> { return globalThis.vi_find_char_handler("n"); };
|
|
1304
|
+
globalThis.vi_fc_o = async function(): Promise<void> { return globalThis.vi_find_char_handler("o"); };
|
|
1305
|
+
globalThis.vi_fc_p = async function(): Promise<void> { return globalThis.vi_find_char_handler("p"); };
|
|
1306
|
+
globalThis.vi_fc_q = async function(): Promise<void> { return globalThis.vi_find_char_handler("q"); };
|
|
1307
|
+
globalThis.vi_fc_r = async function(): Promise<void> { return globalThis.vi_find_char_handler("r"); };
|
|
1308
|
+
globalThis.vi_fc_s = async function(): Promise<void> { return globalThis.vi_find_char_handler("s"); };
|
|
1309
|
+
globalThis.vi_fc_t = async function(): Promise<void> { return globalThis.vi_find_char_handler("t"); };
|
|
1310
|
+
globalThis.vi_fc_u = async function(): Promise<void> { return globalThis.vi_find_char_handler("u"); };
|
|
1311
|
+
globalThis.vi_fc_v = async function(): Promise<void> { return globalThis.vi_find_char_handler("v"); };
|
|
1312
|
+
globalThis.vi_fc_w = async function(): Promise<void> { return globalThis.vi_find_char_handler("w"); };
|
|
1313
|
+
globalThis.vi_fc_x = async function(): Promise<void> { return globalThis.vi_find_char_handler("x"); };
|
|
1314
|
+
globalThis.vi_fc_y = async function(): Promise<void> { return globalThis.vi_find_char_handler("y"); };
|
|
1315
|
+
globalThis.vi_fc_z = async function(): Promise<void> { return globalThis.vi_find_char_handler("z"); };
|
|
1316
|
+
globalThis.vi_fc_A = async function(): Promise<void> { return globalThis.vi_find_char_handler("A"); };
|
|
1317
|
+
globalThis.vi_fc_B = async function(): Promise<void> { return globalThis.vi_find_char_handler("B"); };
|
|
1318
|
+
globalThis.vi_fc_C = async function(): Promise<void> { return globalThis.vi_find_char_handler("C"); };
|
|
1319
|
+
globalThis.vi_fc_D = async function(): Promise<void> { return globalThis.vi_find_char_handler("D"); };
|
|
1320
|
+
globalThis.vi_fc_E = async function(): Promise<void> { return globalThis.vi_find_char_handler("E"); };
|
|
1321
|
+
globalThis.vi_fc_F = async function(): Promise<void> { return globalThis.vi_find_char_handler("F"); };
|
|
1322
|
+
globalThis.vi_fc_G = async function(): Promise<void> { return globalThis.vi_find_char_handler("G"); };
|
|
1323
|
+
globalThis.vi_fc_H = async function(): Promise<void> { return globalThis.vi_find_char_handler("H"); };
|
|
1324
|
+
globalThis.vi_fc_I = async function(): Promise<void> { return globalThis.vi_find_char_handler("I"); };
|
|
1325
|
+
globalThis.vi_fc_J = async function(): Promise<void> { return globalThis.vi_find_char_handler("J"); };
|
|
1326
|
+
globalThis.vi_fc_K = async function(): Promise<void> { return globalThis.vi_find_char_handler("K"); };
|
|
1327
|
+
globalThis.vi_fc_L = async function(): Promise<void> { return globalThis.vi_find_char_handler("L"); };
|
|
1328
|
+
globalThis.vi_fc_M = async function(): Promise<void> { return globalThis.vi_find_char_handler("M"); };
|
|
1329
|
+
globalThis.vi_fc_N = async function(): Promise<void> { return globalThis.vi_find_char_handler("N"); };
|
|
1330
|
+
globalThis.vi_fc_O = async function(): Promise<void> { return globalThis.vi_find_char_handler("O"); };
|
|
1331
|
+
globalThis.vi_fc_P = async function(): Promise<void> { return globalThis.vi_find_char_handler("P"); };
|
|
1332
|
+
globalThis.vi_fc_Q = async function(): Promise<void> { return globalThis.vi_find_char_handler("Q"); };
|
|
1333
|
+
globalThis.vi_fc_R = async function(): Promise<void> { return globalThis.vi_find_char_handler("R"); };
|
|
1334
|
+
globalThis.vi_fc_S = async function(): Promise<void> { return globalThis.vi_find_char_handler("S"); };
|
|
1335
|
+
globalThis.vi_fc_T = async function(): Promise<void> { return globalThis.vi_find_char_handler("T"); };
|
|
1336
|
+
globalThis.vi_fc_U = async function(): Promise<void> { return globalThis.vi_find_char_handler("U"); };
|
|
1337
|
+
globalThis.vi_fc_V = async function(): Promise<void> { return globalThis.vi_find_char_handler("V"); };
|
|
1338
|
+
globalThis.vi_fc_W = async function(): Promise<void> { return globalThis.vi_find_char_handler("W"); };
|
|
1339
|
+
globalThis.vi_fc_X = async function(): Promise<void> { return globalThis.vi_find_char_handler("X"); };
|
|
1340
|
+
globalThis.vi_fc_Y = async function(): Promise<void> { return globalThis.vi_find_char_handler("Y"); };
|
|
1341
|
+
globalThis.vi_fc_Z = async function(): Promise<void> { return globalThis.vi_find_char_handler("Z"); };
|
|
1342
|
+
globalThis.vi_fc_0 = async function(): Promise<void> { return globalThis.vi_find_char_handler("0"); };
|
|
1343
|
+
globalThis.vi_fc_1 = async function(): Promise<void> { return globalThis.vi_find_char_handler("1"); };
|
|
1344
|
+
globalThis.vi_fc_2 = async function(): Promise<void> { return globalThis.vi_find_char_handler("2"); };
|
|
1345
|
+
globalThis.vi_fc_3 = async function(): Promise<void> { return globalThis.vi_find_char_handler("3"); };
|
|
1346
|
+
globalThis.vi_fc_4 = async function(): Promise<void> { return globalThis.vi_find_char_handler("4"); };
|
|
1347
|
+
globalThis.vi_fc_5 = async function(): Promise<void> { return globalThis.vi_find_char_handler("5"); };
|
|
1348
|
+
globalThis.vi_fc_6 = async function(): Promise<void> { return globalThis.vi_find_char_handler("6"); };
|
|
1349
|
+
globalThis.vi_fc_7 = async function(): Promise<void> { return globalThis.vi_find_char_handler("7"); };
|
|
1350
|
+
globalThis.vi_fc_8 = async function(): Promise<void> { return globalThis.vi_find_char_handler("8"); };
|
|
1351
|
+
globalThis.vi_fc_9 = async function(): Promise<void> { return globalThis.vi_find_char_handler("9"); };
|
|
1352
|
+
globalThis.vi_fc_space = async function(): Promise<void> { return globalThis.vi_find_char_handler(" "); };
|
|
1353
|
+
|
|
1354
|
+
// Define vi-find-char mode with all the character bindings
|
|
1355
|
+
editor.defineMode("vi-find-char", null, [
|
|
1356
|
+
["Escape", "vi_find_char_cancel"],
|
|
1357
|
+
// Letters
|
|
1358
|
+
["a", "vi_fc_a"], ["b", "vi_fc_b"], ["c", "vi_fc_c"], ["d", "vi_fc_d"],
|
|
1359
|
+
["e", "vi_fc_e"], ["f", "vi_fc_f"], ["g", "vi_fc_g"], ["h", "vi_fc_h"],
|
|
1360
|
+
["i", "vi_fc_i"], ["j", "vi_fc_j"], ["k", "vi_fc_k"], ["l", "vi_fc_l"],
|
|
1361
|
+
["m", "vi_fc_m"], ["n", "vi_fc_n"], ["o", "vi_fc_o"], ["p", "vi_fc_p"],
|
|
1362
|
+
["q", "vi_fc_q"], ["r", "vi_fc_r"], ["s", "vi_fc_s"], ["t", "vi_fc_t"],
|
|
1363
|
+
["u", "vi_fc_u"], ["v", "vi_fc_v"], ["w", "vi_fc_w"], ["x", "vi_fc_x"],
|
|
1364
|
+
["y", "vi_fc_y"], ["z", "vi_fc_z"],
|
|
1365
|
+
["A", "vi_fc_A"], ["B", "vi_fc_B"], ["C", "vi_fc_C"], ["D", "vi_fc_D"],
|
|
1366
|
+
["E", "vi_fc_E"], ["F", "vi_fc_F"], ["G", "vi_fc_G"], ["H", "vi_fc_H"],
|
|
1367
|
+
["I", "vi_fc_I"], ["J", "vi_fc_J"], ["K", "vi_fc_K"], ["L", "vi_fc_L"],
|
|
1368
|
+
["M", "vi_fc_M"], ["N", "vi_fc_N"], ["O", "vi_fc_O"], ["P", "vi_fc_P"],
|
|
1369
|
+
["Q", "vi_fc_Q"], ["R", "vi_fc_R"], ["S", "vi_fc_S"], ["T", "vi_fc_T"],
|
|
1370
|
+
["U", "vi_fc_U"], ["V", "vi_fc_V"], ["W", "vi_fc_W"], ["X", "vi_fc_X"],
|
|
1371
|
+
["Y", "vi_fc_Y"], ["Z", "vi_fc_Z"],
|
|
1372
|
+
// Digits
|
|
1373
|
+
["0", "vi_fc_0"], ["1", "vi_fc_1"], ["2", "vi_fc_2"], ["3", "vi_fc_3"],
|
|
1374
|
+
["4", "vi_fc_4"], ["5", "vi_fc_5"], ["6", "vi_fc_6"], ["7", "vi_fc_7"],
|
|
1375
|
+
["8", "vi_fc_8"], ["9", "vi_fc_9"],
|
|
1376
|
+
// Common punctuation
|
|
1377
|
+
["Space", "vi_fc_space"],
|
|
1378
|
+
], true);
|
|
1379
|
+
|
|
1380
|
+
// Define vi-operator-pending mode
|
|
1381
|
+
editor.defineMode("vi-operator-pending", null, [
|
|
1382
|
+
// Count prefix in operator-pending mode (for d3w = delete 3 words)
|
|
1383
|
+
["1", "vi_digit_1"],
|
|
1384
|
+
["2", "vi_digit_2"],
|
|
1385
|
+
["3", "vi_digit_3"],
|
|
1386
|
+
["4", "vi_digit_4"],
|
|
1387
|
+
["5", "vi_digit_5"],
|
|
1388
|
+
["6", "vi_digit_6"],
|
|
1389
|
+
["7", "vi_digit_7"],
|
|
1390
|
+
["8", "vi_digit_8"],
|
|
1391
|
+
["9", "vi_digit_9"],
|
|
1392
|
+
["0", "vi_op_digit_0_or_line_start"], // 0 appends to count, or is motion to line start
|
|
1393
|
+
|
|
1394
|
+
// Motions for operators
|
|
1395
|
+
["h", "vi_op_left"],
|
|
1396
|
+
["j", "vi_op_down"],
|
|
1397
|
+
["k", "vi_op_up"],
|
|
1398
|
+
["l", "vi_op_right"],
|
|
1399
|
+
["w", "vi_op_word"],
|
|
1400
|
+
["b", "vi_op_word_back"],
|
|
1401
|
+
["$", "vi_op_line_end"],
|
|
1402
|
+
["g g", "vi_op_doc_start"],
|
|
1403
|
+
["G", "vi_op_doc_end"],
|
|
1404
|
+
["%", "vi_op_matching_bracket"],
|
|
1405
|
+
|
|
1406
|
+
// Text objects
|
|
1407
|
+
["i", "vi_text_object_inner"],
|
|
1408
|
+
["a", "vi_text_object_around"],
|
|
1409
|
+
|
|
1410
|
+
// Double operator = line operation
|
|
1411
|
+
["d", "vi_delete_line"],
|
|
1412
|
+
["c", "vi_change_line"],
|
|
1413
|
+
["y", "vi_yank_line"],
|
|
1414
|
+
|
|
1415
|
+
// Cancel
|
|
1416
|
+
["Escape", "vi_cancel"],
|
|
1417
|
+
], true);
|
|
1418
|
+
|
|
1419
|
+
// Define vi-text-object mode (waiting for object type: w, ", (, etc.)
|
|
1420
|
+
editor.defineMode("vi-text-object", null, [
|
|
1421
|
+
// Word objects
|
|
1422
|
+
["w", "vi_to_word"],
|
|
1423
|
+
["W", "vi_to_WORD"],
|
|
1424
|
+
|
|
1425
|
+
// Quote objects
|
|
1426
|
+
["\"", "vi_to_dquote"],
|
|
1427
|
+
["'", "vi_to_squote"],
|
|
1428
|
+
["`", "vi_to_backtick"],
|
|
1429
|
+
|
|
1430
|
+
// Bracket objects
|
|
1431
|
+
["(", "vi_to_paren"],
|
|
1432
|
+
[")", "vi_to_paren"],
|
|
1433
|
+
["b", "vi_to_paren"],
|
|
1434
|
+
["{", "vi_to_brace"],
|
|
1435
|
+
["}", "vi_to_brace"],
|
|
1436
|
+
["B", "vi_to_brace"],
|
|
1437
|
+
["[", "vi_to_bracket"],
|
|
1438
|
+
["]", "vi_to_bracket"],
|
|
1439
|
+
["<", "vi_to_angle"],
|
|
1440
|
+
[">", "vi_to_angle"],
|
|
1441
|
+
|
|
1442
|
+
// Cancel
|
|
1443
|
+
["Escape", "vi_to_cancel"],
|
|
1444
|
+
], true);
|
|
1445
|
+
|
|
1446
|
+
// Define vi-visual mode (character-wise)
|
|
1447
|
+
editor.defineMode("vi-visual", null, [
|
|
1448
|
+
// Count prefix
|
|
1449
|
+
["1", "vi_digit_1"],
|
|
1450
|
+
["2", "vi_digit_2"],
|
|
1451
|
+
["3", "vi_digit_3"],
|
|
1452
|
+
["4", "vi_digit_4"],
|
|
1453
|
+
["5", "vi_digit_5"],
|
|
1454
|
+
["6", "vi_digit_6"],
|
|
1455
|
+
["7", "vi_digit_7"],
|
|
1456
|
+
["8", "vi_digit_8"],
|
|
1457
|
+
["9", "vi_digit_9"],
|
|
1458
|
+
["0", "vi_vis_line_start"], // 0 moves to line start in visual mode
|
|
1459
|
+
|
|
1460
|
+
// Motions (extend selection)
|
|
1461
|
+
["h", "vi_vis_left"],
|
|
1462
|
+
["j", "vi_vis_down"],
|
|
1463
|
+
["k", "vi_vis_up"],
|
|
1464
|
+
["l", "vi_vis_right"],
|
|
1465
|
+
["w", "vi_vis_word"],
|
|
1466
|
+
["b", "vi_vis_word_back"],
|
|
1467
|
+
["e", "vi_vis_word_end"],
|
|
1468
|
+
["$", "vi_vis_line_end"],
|
|
1469
|
+
["^", "vi_vis_line_start"],
|
|
1470
|
+
["g g", "vi_vis_doc_start"],
|
|
1471
|
+
["G", "vi_vis_doc_end"],
|
|
1472
|
+
|
|
1473
|
+
// Switch to line mode
|
|
1474
|
+
["V", "vi_visual_toggle_line"],
|
|
1475
|
+
|
|
1476
|
+
// Operators
|
|
1477
|
+
["d", "vi_vis_delete"],
|
|
1478
|
+
["x", "vi_vis_delete"],
|
|
1479
|
+
["c", "vi_vis_change"],
|
|
1480
|
+
["s", "vi_vis_change"],
|
|
1481
|
+
["y", "vi_vis_yank"],
|
|
1482
|
+
|
|
1483
|
+
// Exit
|
|
1484
|
+
["Escape", "vi_vis_escape"],
|
|
1485
|
+
["v", "vi_vis_escape"], // v again exits visual mode
|
|
1486
|
+
], true);
|
|
1487
|
+
|
|
1488
|
+
// Define vi-visual-line mode (line-wise)
|
|
1489
|
+
editor.defineMode("vi-visual-line", null, [
|
|
1490
|
+
// Count prefix
|
|
1491
|
+
["1", "vi_digit_1"],
|
|
1492
|
+
["2", "vi_digit_2"],
|
|
1493
|
+
["3", "vi_digit_3"],
|
|
1494
|
+
["4", "vi_digit_4"],
|
|
1495
|
+
["5", "vi_digit_5"],
|
|
1496
|
+
["6", "vi_digit_6"],
|
|
1497
|
+
["7", "vi_digit_7"],
|
|
1498
|
+
["8", "vi_digit_8"],
|
|
1499
|
+
["9", "vi_digit_9"],
|
|
1500
|
+
|
|
1501
|
+
// Line motions (extend selection by lines)
|
|
1502
|
+
["j", "vi_vline_down"],
|
|
1503
|
+
["k", "vi_vline_up"],
|
|
1504
|
+
["g g", "vi_vis_doc_start"],
|
|
1505
|
+
["G", "vi_vis_doc_end"],
|
|
1506
|
+
|
|
1507
|
+
// Switch to char mode
|
|
1508
|
+
["v", "vi_visual_toggle_line"],
|
|
1509
|
+
|
|
1510
|
+
// Operators
|
|
1511
|
+
["d", "vi_vis_delete"],
|
|
1512
|
+
["x", "vi_vis_delete"],
|
|
1513
|
+
["c", "vi_vis_change"],
|
|
1514
|
+
["s", "vi_vis_change"],
|
|
1515
|
+
["y", "vi_vis_yank"],
|
|
1516
|
+
|
|
1517
|
+
// Exit
|
|
1518
|
+
["Escape", "vi_vis_escape"],
|
|
1519
|
+
["V", "vi_vis_escape"], // V again exits visual-line mode
|
|
1520
|
+
], true);
|
|
1521
|
+
|
|
1522
|
+
// ============================================================================
|
|
1523
|
+
// Register Commands
|
|
1524
|
+
// ============================================================================
|
|
1525
|
+
|
|
1526
|
+
// Navigation commands
|
|
1527
|
+
const navCommands = [
|
|
1528
|
+
["vi_left", "Move left"],
|
|
1529
|
+
["vi_down", "Move down"],
|
|
1530
|
+
["vi_up", "Move up"],
|
|
1531
|
+
["vi_right", "Move right"],
|
|
1532
|
+
["vi_word", "Move to next word"],
|
|
1533
|
+
["vi_word_back", "Move to previous word"],
|
|
1534
|
+
["vi_word_end", "Move to end of word"],
|
|
1535
|
+
["vi_line_start", "Move to line start"],
|
|
1536
|
+
["vi_line_end", "Move to line end"],
|
|
1537
|
+
["vi_doc_start", "Move to document start"],
|
|
1538
|
+
["vi_doc_end", "Move to document end"],
|
|
1539
|
+
["vi_page_down", "Page down"],
|
|
1540
|
+
["vi_page_up", "Page up"],
|
|
1541
|
+
["vi_half_page_down", "Half page down"],
|
|
1542
|
+
["vi_half_page_up", "Half page up"],
|
|
1543
|
+
["vi_center_cursor", "Center cursor on screen"],
|
|
1544
|
+
["vi_search_forward", "Search forward"],
|
|
1545
|
+
["vi_search_backward", "Search backward"],
|
|
1546
|
+
["vi_find_next", "Find next match"],
|
|
1547
|
+
["vi_find_prev", "Find previous match"],
|
|
1548
|
+
["vi_find_char_f", "Find char forward"],
|
|
1549
|
+
["vi_find_char_t", "Find till char forward"],
|
|
1550
|
+
["vi_find_char_F", "Find char backward"],
|
|
1551
|
+
["vi_find_char_T", "Find till char backward"],
|
|
1552
|
+
["vi_find_char_repeat", "Repeat last find char"],
|
|
1553
|
+
["vi_find_char_repeat_reverse", "Repeat last find char (reverse)"],
|
|
1554
|
+
];
|
|
1555
|
+
|
|
1556
|
+
for (const [name, desc] of navCommands) {
|
|
1557
|
+
editor.registerCommand(name, `Vi: ${desc}`, name, "vi-normal");
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// Mode commands
|
|
1561
|
+
const modeCommands = [
|
|
1562
|
+
["vi_insert_before", "Insert before cursor"],
|
|
1563
|
+
["vi_insert_after", "Insert after cursor"],
|
|
1564
|
+
["vi_insert_line_start", "Insert at line start"],
|
|
1565
|
+
["vi_insert_line_end", "Insert at line end"],
|
|
1566
|
+
["vi_open_below", "Open line below"],
|
|
1567
|
+
["vi_open_above", "Open line above"],
|
|
1568
|
+
["vi_escape", "Return to normal mode"],
|
|
1569
|
+
];
|
|
1570
|
+
|
|
1571
|
+
for (const [name, desc] of modeCommands) {
|
|
1572
|
+
editor.registerCommand(name, `Vi: ${desc}`, name, "vi-normal");
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// Operator commands
|
|
1576
|
+
const opCommands = [
|
|
1577
|
+
["vi_delete_operator", "Delete operator"],
|
|
1578
|
+
["vi_change_operator", "Change operator"],
|
|
1579
|
+
["vi_yank_operator", "Yank operator"],
|
|
1580
|
+
["vi_delete_line", "Delete line"],
|
|
1581
|
+
["vi_change_line", "Change line"],
|
|
1582
|
+
["vi_yank_line", "Yank line"],
|
|
1583
|
+
["vi_delete_char", "Delete character"],
|
|
1584
|
+
["vi_delete_char_before", "Delete char before cursor"],
|
|
1585
|
+
["vi_substitute", "Substitute character"],
|
|
1586
|
+
["vi_delete_to_end", "Delete to end of line"],
|
|
1587
|
+
["vi_change_to_end", "Change to end of line"],
|
|
1588
|
+
["vi_paste_after", "Paste after"],
|
|
1589
|
+
["vi_paste_before", "Paste before"],
|
|
1590
|
+
["vi_undo", "Undo"],
|
|
1591
|
+
["vi_redo", "Redo"],
|
|
1592
|
+
["vi_join", "Join lines"],
|
|
1593
|
+
];
|
|
1594
|
+
|
|
1595
|
+
for (const [name, desc] of opCommands) {
|
|
1596
|
+
editor.registerCommand(name, `Vi: ${desc}`, name, "vi-normal");
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// ============================================================================
|
|
1600
|
+
// Toggle Command
|
|
1601
|
+
// ============================================================================
|
|
1602
|
+
|
|
1603
|
+
let viModeEnabled = false;
|
|
1604
|
+
|
|
1605
|
+
globalThis.vi_mode_toggle = function (): void {
|
|
1606
|
+
viModeEnabled = !viModeEnabled;
|
|
1607
|
+
|
|
1608
|
+
if (viModeEnabled) {
|
|
1609
|
+
switchMode("normal");
|
|
1610
|
+
editor.setStatus("Vi mode enabled - NORMAL");
|
|
1611
|
+
} else {
|
|
1612
|
+
editor.setEditorMode(null);
|
|
1613
|
+
state.mode = "normal";
|
|
1614
|
+
state.pendingOperator = null;
|
|
1615
|
+
editor.setStatus("Vi mode disabled");
|
|
1616
|
+
}
|
|
1617
|
+
};
|
|
1618
|
+
|
|
1619
|
+
editor.registerCommand(
|
|
1620
|
+
"Toggle Vi mode",
|
|
1621
|
+
"Enable or disable vi-style modal editing",
|
|
1622
|
+
"vi_mode_toggle",
|
|
1623
|
+
"normal",
|
|
1624
|
+
);
|
|
1625
|
+
|
|
1626
|
+
// ============================================================================
|
|
1627
|
+
// Initialization
|
|
1628
|
+
// ============================================================================
|
|
1629
|
+
|
|
1630
|
+
editor.setStatus("Vi mode plugin loaded. Use 'Toggle Vi mode' command to enable.");
|