@fresh-editor/fresh-editor 0.1.67 → 0.1.70
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 +97 -0
- package/README.md +2 -0
- package/package.json +1 -1
- package/plugins/audit_mode.i18n.json +380 -0
- package/plugins/audit_mode.ts +836 -68
- package/plugins/buffer_modified.i18n.json +32 -0
- package/plugins/buffer_modified.ts +5 -3
- package/plugins/calculator.i18n.json +44 -0
- package/plugins/calculator.ts +6 -4
- package/plugins/clangd-lsp.ts +2 -0
- package/plugins/clangd_support.i18n.json +104 -0
- package/plugins/clangd_support.ts +18 -16
- package/plugins/color_highlighter.i18n.json +68 -0
- package/plugins/color_highlighter.ts +12 -10
- package/plugins/config-schema.json +28 -145
- package/plugins/csharp-lsp.ts +2 -0
- package/plugins/csharp_support.i18n.json +38 -0
- package/plugins/csharp_support.ts +6 -4
- package/plugins/css-lsp.ts +2 -0
- package/plugins/diagnostics_panel.i18n.json +110 -0
- package/plugins/diagnostics_panel.ts +19 -17
- package/plugins/find_references.i18n.json +128 -0
- package/plugins/find_references.ts +22 -20
- package/plugins/git_blame.i18n.json +230 -0
- package/plugins/git_blame.ts +39 -37
- package/plugins/git_find_file.i18n.json +146 -0
- package/plugins/git_find_file.ts +24 -22
- package/plugins/git_grep.i18n.json +80 -0
- package/plugins/git_grep.ts +15 -13
- package/plugins/git_gutter.i18n.json +44 -0
- package/plugins/git_gutter.ts +7 -5
- package/plugins/git_log.i18n.json +224 -0
- package/plugins/git_log.ts +41 -39
- package/plugins/go-lsp.ts +2 -0
- package/plugins/html-lsp.ts +2 -0
- package/plugins/json-lsp.ts +2 -0
- package/plugins/lib/fresh.d.ts +53 -13
- package/plugins/lib/index.ts +1 -1
- package/plugins/lib/navigation-controller.ts +3 -3
- package/plugins/lib/panel-manager.ts +15 -13
- package/plugins/lib/virtual-buffer-factory.ts +84 -112
- package/plugins/live_grep.i18n.json +80 -0
- package/plugins/live_grep.ts +15 -13
- package/plugins/markdown_compose.i18n.json +104 -0
- package/plugins/markdown_compose.ts +17 -15
- package/plugins/merge_conflict.i18n.json +380 -0
- package/plugins/merge_conflict.ts +72 -73
- package/plugins/path_complete.i18n.json +38 -0
- package/plugins/path_complete.ts +6 -4
- package/plugins/python-lsp.ts +2 -0
- package/plugins/rust-lsp.ts +2 -0
- package/plugins/search_replace.i18n.json +188 -0
- package/plugins/search_replace.ts +31 -29
- package/plugins/test_i18n.i18n.json +12 -0
- package/plugins/test_i18n.ts +18 -0
- package/plugins/theme_editor.i18n.json +1417 -0
- package/plugins/theme_editor.ts +73 -69
- package/plugins/todo_highlighter.i18n.json +86 -0
- package/plugins/todo_highlighter.ts +15 -13
- package/plugins/typescript-lsp.ts +2 -0
- package/plugins/vi_mode.i18n.json +716 -0
- package/plugins/vi_mode.ts +1195 -78
- package/plugins/welcome.i18n.json +110 -0
- package/plugins/welcome.ts +18 -16
package/plugins/vi_mode.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/// <reference path="./lib/fresh.d.ts" />
|
|
2
|
+
const editor = getEditor();
|
|
3
|
+
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Vi Mode Plugin for Fresh Editor
|
|
@@ -13,10 +15,23 @@
|
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
// Vi mode state
|
|
16
|
-
type ViMode = "normal" | "insert" | "operator-pending" | "find-char" | "visual" | "visual-line" | "text-object";
|
|
18
|
+
type ViMode = "normal" | "insert" | "operator-pending" | "find-char" | "visual" | "visual-line" | "visual-block" | "text-object";
|
|
17
19
|
type FindCharType = "f" | "t" | "F" | "T" | null;
|
|
18
20
|
type TextObjectType = "inner" | "around" | null;
|
|
19
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
|
+
|
|
20
35
|
interface ViState {
|
|
21
36
|
mode: ViMode;
|
|
22
37
|
pendingOperator: string | null;
|
|
@@ -24,9 +39,11 @@ interface ViState {
|
|
|
24
39
|
pendingTextObject: TextObjectType; // For i/a text objects
|
|
25
40
|
lastFindChar: { type: FindCharType; char: string } | null; // For ; and , repeat
|
|
26
41
|
count: number | null;
|
|
27
|
-
|
|
42
|
+
lastChange: LastChange | null; // For '.' repeat
|
|
28
43
|
lastYankWasLinewise: boolean; // Track if last yank was line-wise for proper paste
|
|
29
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
|
|
30
47
|
}
|
|
31
48
|
|
|
32
49
|
const state: ViState = {
|
|
@@ -36,9 +53,11 @@ const state: ViState = {
|
|
|
36
53
|
pendingTextObject: null,
|
|
37
54
|
lastFindChar: null,
|
|
38
55
|
count: null,
|
|
39
|
-
|
|
56
|
+
lastChange: null,
|
|
40
57
|
lastYankWasLinewise: false,
|
|
41
58
|
visualAnchor: null,
|
|
59
|
+
insertStartPos: null,
|
|
60
|
+
visualBlockAnchor: null,
|
|
42
61
|
};
|
|
43
62
|
|
|
44
63
|
// Mode indicator for status bar
|
|
@@ -46,17 +65,19 @@ function getModeIndicator(mode: ViMode): string {
|
|
|
46
65
|
const countPrefix = state.count !== null ? `${state.count} ` : "";
|
|
47
66
|
switch (mode) {
|
|
48
67
|
case "normal":
|
|
49
|
-
return `--
|
|
68
|
+
return `-- ${editor.t("mode.normal")} --${countPrefix ? ` (${state.count})` : ""}`;
|
|
50
69
|
case "insert":
|
|
51
|
-
return "
|
|
70
|
+
return `-- ${editor.t("mode.insert")} --`;
|
|
52
71
|
case "operator-pending":
|
|
53
|
-
return `--
|
|
72
|
+
return `-- ${editor.t("mode.operator")} (${state.pendingOperator}) --${countPrefix ? ` (${state.count})` : ""}`;
|
|
54
73
|
case "find-char":
|
|
55
|
-
return `--
|
|
74
|
+
return `-- ${editor.t("mode.find")} (${state.pendingFindChar}) --`;
|
|
56
75
|
case "visual":
|
|
57
|
-
return `--
|
|
76
|
+
return `-- ${editor.t("mode.visual")} --${countPrefix ? ` (${state.count})` : ""}`;
|
|
58
77
|
case "visual-line":
|
|
59
|
-
return `--
|
|
78
|
+
return `-- ${editor.t("mode.visual_line")} --${countPrefix ? ` (${state.count})` : ""}`;
|
|
79
|
+
case "visual-block":
|
|
80
|
+
return `-- ${editor.t("mode.visual_block")} --${countPrefix ? ` (${state.count})` : ""}`;
|
|
60
81
|
case "text-object":
|
|
61
82
|
return `-- ${state.pendingOperator}${state.pendingTextObject === "inner" ? "i" : "a"}? --`;
|
|
62
83
|
default:
|
|
@@ -81,27 +102,69 @@ function switchMode(newMode: ViMode): void {
|
|
|
81
102
|
|
|
82
103
|
// Preserve count when entering operator-pending or text-object mode (for 3dw = delete 3 words)
|
|
83
104
|
// Also preserve count in visual modes
|
|
84
|
-
if (newMode !== "operator-pending" && newMode !== "text-object" &&
|
|
105
|
+
if (newMode !== "operator-pending" && newMode !== "text-object" &&
|
|
106
|
+
newMode !== "visual" && newMode !== "visual-line" && newMode !== "visual-block") {
|
|
85
107
|
state.count = null;
|
|
86
108
|
}
|
|
87
109
|
|
|
88
110
|
// Clear visual anchor when leaving visual modes
|
|
89
|
-
if (newMode !== "visual" && newMode !== "visual-line") {
|
|
111
|
+
if (newMode !== "visual" && newMode !== "visual-line" && newMode !== "visual-block") {
|
|
90
112
|
state.visualAnchor = null;
|
|
113
|
+
state.visualBlockAnchor = null;
|
|
91
114
|
// Clear any selection when leaving visual mode by moving cursor
|
|
92
115
|
// (any non-select movement clears selection in Fresh)
|
|
93
|
-
if (oldMode === "visual" || oldMode === "visual-line") {
|
|
116
|
+
if (oldMode === "visual" || oldMode === "visual-line" || oldMode === "visual-block") {
|
|
94
117
|
editor.executeAction("move_left");
|
|
95
118
|
editor.executeAction("move_right");
|
|
96
119
|
}
|
|
97
120
|
}
|
|
98
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
|
+
|
|
99
132
|
// All modes use vi-{mode} naming, including insert mode
|
|
100
133
|
// vi-insert has read_only=false so normal typing works, but Escape is bound
|
|
101
134
|
editor.setEditorMode(`vi-${newMode}`);
|
|
102
135
|
editor.setStatus(getModeIndicator(newMode));
|
|
103
136
|
}
|
|
104
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
|
+
|
|
105
168
|
// Get the current count (defaults to 1 if no count specified)
|
|
106
169
|
// Does NOT clear the count - that's done in switchMode or explicitly
|
|
107
170
|
function getCount(): number {
|
|
@@ -175,6 +238,11 @@ const atomicOperatorActions: OperatorMotionMap = {
|
|
|
175
238
|
// Apply an operator using atomic actions if available, otherwise selection-based approach
|
|
176
239
|
// The count parameter specifies how many times to apply the motion (e.g., d3w = delete 3 words)
|
|
177
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
|
+
|
|
178
246
|
// For "change" operator, use delete action and then enter insert mode
|
|
179
247
|
const lookupOperator = operator === "c" ? "d" : operator;
|
|
180
248
|
|
|
@@ -379,6 +447,7 @@ globalThis.vi_yank_operator = function (): void {
|
|
|
379
447
|
// Line operations (dd, cc, yy) - support count prefix (3dd = delete 3 lines)
|
|
380
448
|
globalThis.vi_delete_line = function (): void {
|
|
381
449
|
const count = consumeCount();
|
|
450
|
+
state.lastChange = { type: "line-op", action: "delete_line", count };
|
|
382
451
|
if (count === 1) {
|
|
383
452
|
editor.executeAction("delete_line");
|
|
384
453
|
} else {
|
|
@@ -388,7 +457,8 @@ globalThis.vi_delete_line = function (): void {
|
|
|
388
457
|
};
|
|
389
458
|
|
|
390
459
|
globalThis.vi_change_line = function (): void {
|
|
391
|
-
consumeCount();
|
|
460
|
+
const count = consumeCount();
|
|
461
|
+
state.lastChange = { type: "line-op", action: "change_line", count };
|
|
392
462
|
editor.executeAction("move_line_start");
|
|
393
463
|
const start = editor.getCursorPosition();
|
|
394
464
|
editor.executeAction("move_line_end");
|
|
@@ -413,32 +483,43 @@ globalThis.vi_yank_line = function (): void {
|
|
|
413
483
|
editor.executeAction("move_up");
|
|
414
484
|
editor.executeAction("move_line_start");
|
|
415
485
|
state.lastYankWasLinewise = true;
|
|
416
|
-
editor.setStatus(
|
|
486
|
+
editor.setStatus(editor.t("status.yanked_lines", { count: String(count) }));
|
|
417
487
|
switchMode("normal");
|
|
418
488
|
};
|
|
419
489
|
|
|
420
490
|
// Single character operations - support count prefix (3x = delete 3 chars)
|
|
421
491
|
globalThis.vi_delete_char = function (): void {
|
|
422
|
-
|
|
492
|
+
const count = consumeCount();
|
|
493
|
+
state.lastChange = { type: "simple", action: "delete_forward", count };
|
|
494
|
+
executeWithCount("delete_forward", count);
|
|
423
495
|
};
|
|
424
496
|
|
|
425
497
|
globalThis.vi_delete_char_before = function (): void {
|
|
426
|
-
|
|
498
|
+
const count = consumeCount();
|
|
499
|
+
state.lastChange = { type: "simple", action: "delete_backward", count };
|
|
500
|
+
executeWithCount("delete_backward", count);
|
|
427
501
|
};
|
|
428
502
|
|
|
429
503
|
globalThis.vi_replace_char = function (): void {
|
|
430
504
|
// TODO: implement character replacement (need to read next char)
|
|
431
|
-
editor.setStatus("
|
|
505
|
+
editor.setStatus(editor.t("status.replace_not_implemented"));
|
|
432
506
|
};
|
|
433
507
|
|
|
434
508
|
// Substitute (delete char and enter insert mode)
|
|
435
509
|
globalThis.vi_substitute = function (): void {
|
|
436
|
-
|
|
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
|
+
}
|
|
437
517
|
switchMode("insert");
|
|
438
518
|
};
|
|
439
519
|
|
|
440
520
|
// Delete to end of line
|
|
441
521
|
globalThis.vi_delete_to_end = function (): void {
|
|
522
|
+
state.lastChange = { type: "operator-motion", operator: "d", motion: "move_line_end" };
|
|
442
523
|
const start = editor.getCursorPosition();
|
|
443
524
|
editor.executeAction("move_line_end");
|
|
444
525
|
const end = editor.getCursorPosition();
|
|
@@ -449,6 +530,7 @@ globalThis.vi_delete_to_end = function (): void {
|
|
|
449
530
|
|
|
450
531
|
// Change to end of line
|
|
451
532
|
globalThis.vi_change_to_end = function (): void {
|
|
533
|
+
state.lastChange = { type: "operator-motion", operator: "c", motion: "move_line_end" };
|
|
452
534
|
const start = editor.getCursorPosition();
|
|
453
535
|
editor.executeAction("move_line_end");
|
|
454
536
|
const end = editor.getCursorPosition();
|
|
@@ -498,6 +580,104 @@ globalThis.vi_redo = function (): void {
|
|
|
498
580
|
editor.executeAction("redo");
|
|
499
581
|
};
|
|
500
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
|
+
|
|
501
681
|
// Join lines
|
|
502
682
|
globalThis.vi_join = function (): void {
|
|
503
683
|
editor.executeAction("move_line_end");
|
|
@@ -610,6 +790,95 @@ globalThis.vi_visual_toggle_line = function (): void {
|
|
|
610
790
|
}
|
|
611
791
|
};
|
|
612
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
|
+
|
|
613
882
|
// Visual mode motions - these extend the selection
|
|
614
883
|
globalThis.vi_vis_left = function (): void {
|
|
615
884
|
executeWithCount("select_left");
|
|
@@ -727,12 +996,18 @@ globalThis.vi_text_object_around = function (): void {
|
|
|
727
996
|
async function applyTextObject(objectType: string): Promise<void> {
|
|
728
997
|
const operator = state.pendingOperator;
|
|
729
998
|
const isInner = state.pendingTextObject === "inner";
|
|
999
|
+
const modifier = state.pendingTextObject;
|
|
730
1000
|
|
|
731
1001
|
if (!operator) {
|
|
732
1002
|
switchMode("normal");
|
|
733
1003
|
return;
|
|
734
1004
|
}
|
|
735
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
|
+
|
|
736
1011
|
const bufferId = editor.getActiveBufferId();
|
|
737
1012
|
const cursorPos = editor.getCursorPosition();
|
|
738
1013
|
if (cursorPos === null) {
|
|
@@ -1269,12 +1544,19 @@ editor.defineMode("vi-normal", null, [
|
|
|
1269
1544
|
["u", "vi_undo"],
|
|
1270
1545
|
["C-r", "vi_redo"],
|
|
1271
1546
|
|
|
1547
|
+
// Repeat last change
|
|
1548
|
+
[".", "vi_repeat"],
|
|
1549
|
+
|
|
1272
1550
|
// Visual mode
|
|
1273
1551
|
["v", "vi_visual_char"],
|
|
1274
1552
|
["V", "vi_visual_line"],
|
|
1553
|
+
["C-v", "vi_visual_block"],
|
|
1275
1554
|
|
|
1276
1555
|
// Other
|
|
1277
1556
|
["J", "vi_join"],
|
|
1557
|
+
|
|
1558
|
+
// Command mode
|
|
1559
|
+
[":", "vi_command_mode"],
|
|
1278
1560
|
], true); // read_only = true to prevent character insertion
|
|
1279
1561
|
|
|
1280
1562
|
// Define vi-insert mode - only Escape is special, other keys insert text
|
|
@@ -1519,83 +1801,918 @@ editor.defineMode("vi-visual-line", null, [
|
|
|
1519
1801
|
["V", "vi_vis_escape"], // V again exits visual-line mode
|
|
1520
1802
|
], true);
|
|
1521
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
|
+
|
|
1522
1842
|
// ============================================================================
|
|
1523
1843
|
// Register Commands
|
|
1524
1844
|
// ============================================================================
|
|
1525
1845
|
|
|
1526
1846
|
// Navigation commands
|
|
1527
1847
|
const navCommands = [
|
|
1528
|
-
["vi_left", "
|
|
1529
|
-
["vi_down", "
|
|
1530
|
-
["vi_up", "
|
|
1531
|
-
["vi_right", "
|
|
1532
|
-
["vi_word", "
|
|
1533
|
-
["vi_word_back", "
|
|
1534
|
-
["vi_word_end", "
|
|
1535
|
-
["vi_line_start", "
|
|
1536
|
-
["vi_line_end", "
|
|
1537
|
-
["vi_doc_start", "
|
|
1538
|
-
["vi_doc_end", "
|
|
1539
|
-
["vi_page_down", "
|
|
1540
|
-
["vi_page_up", "
|
|
1541
|
-
["vi_half_page_down", "
|
|
1542
|
-
["vi_half_page_up", "
|
|
1543
|
-
["vi_center_cursor", "
|
|
1544
|
-
["vi_search_forward", "
|
|
1545
|
-
["vi_search_backward", "
|
|
1546
|
-
["vi_find_next", "
|
|
1547
|
-
["vi_find_prev", "
|
|
1548
|
-
["vi_find_char_f", "
|
|
1549
|
-
["vi_find_char_t", "
|
|
1550
|
-
["vi_find_char_F", "
|
|
1551
|
-
["vi_find_char_T", "
|
|
1552
|
-
["vi_find_char_repeat", "
|
|
1553
|
-
["vi_find_char_repeat_reverse", "
|
|
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"],
|
|
1554
1874
|
];
|
|
1555
1875
|
|
|
1556
|
-
for (const [name,
|
|
1557
|
-
editor.registerCommand(
|
|
1876
|
+
for (const [name, key] of navCommands) {
|
|
1877
|
+
editor.registerCommand(`%cmd.${key}`, `%cmd.${key}`, name, "vi-normal");
|
|
1558
1878
|
}
|
|
1559
1879
|
|
|
1560
1880
|
// Mode commands
|
|
1561
1881
|
const modeCommands = [
|
|
1562
|
-
["vi_insert_before", "
|
|
1563
|
-
["vi_insert_after", "
|
|
1564
|
-
["vi_insert_line_start", "
|
|
1565
|
-
["vi_insert_line_end", "
|
|
1566
|
-
["vi_open_below", "
|
|
1567
|
-
["vi_open_above", "
|
|
1568
|
-
["vi_escape", "
|
|
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"],
|
|
1569
1889
|
];
|
|
1570
1890
|
|
|
1571
|
-
for (const [name,
|
|
1572
|
-
editor.registerCommand(
|
|
1891
|
+
for (const [name, key] of modeCommands) {
|
|
1892
|
+
editor.registerCommand(`%cmd.${key}`, `%cmd.${key}`, name, "vi-normal");
|
|
1573
1893
|
}
|
|
1574
1894
|
|
|
1575
1895
|
// Operator commands
|
|
1576
1896
|
const opCommands = [
|
|
1577
|
-
["vi_delete_operator", "
|
|
1578
|
-
["vi_change_operator", "
|
|
1579
|
-
["vi_yank_operator", "
|
|
1580
|
-
["vi_delete_line", "
|
|
1581
|
-
["vi_change_line", "
|
|
1582
|
-
["vi_yank_line", "
|
|
1583
|
-
["vi_delete_char", "
|
|
1584
|
-
["vi_delete_char_before", "
|
|
1585
|
-
["vi_substitute", "
|
|
1586
|
-
["vi_delete_to_end", "
|
|
1587
|
-
["vi_change_to_end", "
|
|
1588
|
-
["vi_paste_after", "
|
|
1589
|
-
["vi_paste_before", "
|
|
1590
|
-
["vi_undo", "
|
|
1591
|
-
["vi_redo", "
|
|
1592
|
-
["vi_join", "
|
|
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"],
|
|
1593
1913
|
];
|
|
1594
1914
|
|
|
1595
|
-
for (const [name,
|
|
1596
|
-
editor.registerCommand(
|
|
1915
|
+
for (const [name, key] of opCommands) {
|
|
1916
|
+
editor.registerCommand(`%cmd.${key}`, `%cmd.${key}`, name, "vi-normal");
|
|
1597
1917
|
}
|
|
1598
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
|
+
|
|
1599
2716
|
// ============================================================================
|
|
1600
2717
|
// Toggle Command
|
|
1601
2718
|
// ============================================================================
|
|
@@ -1607,18 +2724,18 @@ globalThis.vi_mode_toggle = function (): void {
|
|
|
1607
2724
|
|
|
1608
2725
|
if (viModeEnabled) {
|
|
1609
2726
|
switchMode("normal");
|
|
1610
|
-
editor.setStatus("
|
|
2727
|
+
editor.setStatus(editor.t("status.enabled"));
|
|
1611
2728
|
} else {
|
|
1612
2729
|
editor.setEditorMode(null);
|
|
1613
2730
|
state.mode = "normal";
|
|
1614
2731
|
state.pendingOperator = null;
|
|
1615
|
-
editor.setStatus("
|
|
2732
|
+
editor.setStatus(editor.t("status.disabled"));
|
|
1616
2733
|
}
|
|
1617
2734
|
};
|
|
1618
2735
|
|
|
1619
2736
|
editor.registerCommand(
|
|
1620
|
-
"
|
|
1621
|
-
"
|
|
2737
|
+
"%cmd.toggle_vi_mode",
|
|
2738
|
+
"%cmd.toggle_vi_mode_desc",
|
|
1622
2739
|
"vi_mode_toggle",
|
|
1623
2740
|
"normal",
|
|
1624
2741
|
);
|
|
@@ -1627,4 +2744,4 @@ editor.registerCommand(
|
|
|
1627
2744
|
// Initialization
|
|
1628
2745
|
// ============================================================================
|
|
1629
2746
|
|
|
1630
|
-
editor.setStatus("
|
|
2747
|
+
editor.setStatus(editor.t("status.loaded"));
|