@abacus-ai/cli 1.106.25007 → 2.0.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.oxlintrc.json +8 -0
- package/dist/index.mjs +12603 -0
- package/package.json +7 -39
- package/resources/abacus.ico +0 -0
- package/resources/entitlements.plist +9 -0
- package/src/__e2e__/README.md +196 -0
- package/src/__e2e__/agent-interactions.e2e.test.tsx +61 -0
- package/src/__e2e__/cli-commands.e2e.test.tsx +77 -0
- package/src/__e2e__/conversation-throttle.e2e.test.ts +453 -0
- package/src/__e2e__/conversation.e2e.test.tsx +56 -0
- package/src/__e2e__/diff-preview.e2e.test.tsx +3399 -0
- package/src/__e2e__/file-creation.e2e.test.tsx +149 -0
- package/src/__e2e__/helpers/test-helpers.ts +450 -0
- package/src/__e2e__/keyboard-navigation.e2e.test.tsx +34 -0
- package/src/__e2e__/llm-models.e2e.test.ts +402 -0
- package/src/__e2e__/mcp/mcp-callback-flow.e2e.test.tsx +71 -0
- package/src/__e2e__/mcp/mcp-full-app-ui.e2e.test.tsx +167 -0
- package/src/__e2e__/mcp/mcp-ui-rendering.e2e.test.tsx +185 -0
- package/src/__e2e__/repl.e2e.test.tsx +78 -0
- package/src/__e2e__/shell-compatibility.e2e.test.tsx +76 -0
- package/src/__e2e__/theme-mcp.e2e.test.tsx +98 -0
- package/src/__e2e__/tool-permissions.e2e.test.tsx +66 -0
- package/src/args.ts +22 -0
- package/src/components/__tests__/react-compiler.test.tsx +78 -0
- package/src/components/__tests__/status-indicator.test.tsx +403 -0
- package/src/components/composer/__tests__/bash-runner.test.tsx +263 -0
- package/src/components/composer/agent-mode-indicator.tsx +63 -0
- package/src/components/composer/bash-runner.tsx +54 -0
- package/src/components/composer/commands/default-commands.tsx +615 -0
- package/src/components/composer/commands/handler.tsx +59 -0
- package/src/components/composer/commands/picker.tsx +273 -0
- package/src/components/composer/commands/registry.ts +233 -0
- package/src/components/composer/commands/types.ts +33 -0
- package/src/components/composer/context.tsx +88 -0
- package/src/components/composer/file-mention-picker.tsx +83 -0
- package/src/components/composer/help.tsx +44 -0
- package/src/components/composer/index.tsx +1006 -0
- package/src/components/composer/mentions.ts +57 -0
- package/src/components/composer/message-queue.tsx +70 -0
- package/src/components/composer/mode-panel.tsx +35 -0
- package/src/components/composer/modes/__tests__/bash-handler.test.tsx +755 -0
- package/src/components/composer/modes/__tests__/bash-renderer.test.tsx +1108 -0
- package/src/components/composer/modes/bash-handler.tsx +132 -0
- package/src/components/composer/modes/bash-renderer.tsx +175 -0
- package/src/components/composer/modes/default-handlers.tsx +33 -0
- package/src/components/composer/modes/index.ts +41 -0
- package/src/components/composer/modes/types.ts +21 -0
- package/src/components/composer/persistent-shell.ts +283 -0
- package/src/components/composer/process.ts +65 -0
- package/src/components/composer/types.ts +9 -0
- package/src/components/composer/use-mention-search.ts +68 -0
- package/src/components/error-boundry.tsx +60 -0
- package/src/components/exit-message.tsx +29 -0
- package/src/components/expanded-view.tsx +74 -0
- package/src/components/file-completion.tsx +127 -0
- package/src/components/header.tsx +47 -0
- package/src/components/logo.tsx +37 -0
- package/src/components/segments.tsx +356 -0
- package/src/components/status-indicator.tsx +306 -0
- package/src/components/tool-group-summary.tsx +263 -0
- package/src/components/tool-permissions/ask-user-question-permission-ui.tsx +312 -0
- package/src/components/tool-permissions/diff-preview.tsx +355 -0
- package/src/components/tool-permissions/index.ts +5 -0
- package/src/components/tool-permissions/permission-options.tsx +375 -0
- package/src/components/tool-permissions/permission-preview-header.tsx +57 -0
- package/src/components/tool-permissions/tool-permission-ui.tsx +398 -0
- package/src/components/tools/agent/ask-user-question.tsx +101 -0
- package/src/components/tools/agent/enter-plan-mode.tsx +49 -0
- package/src/components/tools/agent/exit-plan-mode.tsx +75 -0
- package/src/components/tools/agent/handoff-to-main.tsx +27 -0
- package/src/components/tools/agent/subagent.tsx +37 -0
- package/src/components/tools/agent/todo-write.tsx +104 -0
- package/src/components/tools/browser/close-tab.tsx +58 -0
- package/src/components/tools/browser/computer.tsx +70 -0
- package/src/components/tools/browser/get-interactive-elements.tsx +54 -0
- package/src/components/tools/browser/get-tab-content.tsx +51 -0
- package/src/components/tools/browser/navigate-to.tsx +59 -0
- package/src/components/tools/browser/new-tab.tsx +60 -0
- package/src/components/tools/browser/perform-action.tsx +63 -0
- package/src/components/tools/browser/refresh-tab.tsx +43 -0
- package/src/components/tools/browser/switch-tab.tsx +58 -0
- package/src/components/tools/filesystem/delete-file.tsx +104 -0
- package/src/components/tools/filesystem/edit.tsx +220 -0
- package/src/components/tools/filesystem/list-dir.tsx +78 -0
- package/src/components/tools/filesystem/read-file.tsx +180 -0
- package/src/components/tools/filesystem/upload-image.tsx +76 -0
- package/src/components/tools/ide/ide-diagnostics.tsx +62 -0
- package/src/components/tools/index.ts +91 -0
- package/src/components/tools/mcp/mcp-tool.tsx +158 -0
- package/src/components/tools/search/fetch-url.tsx +73 -0
- package/src/components/tools/search/file-search.tsx +78 -0
- package/src/components/tools/search/grep.tsx +90 -0
- package/src/components/tools/search/semantic-search.tsx +66 -0
- package/src/components/tools/search/web-search.tsx +71 -0
- package/src/components/tools/shared/index.tsx +48 -0
- package/src/components/tools/shared/zod-coercion.ts +35 -0
- package/src/components/tools/terminal/bash-tool-output.tsx +174 -0
- package/src/components/tools/terminal/get-terminal-output.tsx +85 -0
- package/src/components/tools/terminal/run-in-terminal.tsx +106 -0
- package/src/components/tools/types.ts +16 -0
- package/src/components/tools.tsx +66 -0
- package/src/components/ui/__tests__/divider.test.tsx +61 -0
- package/src/components/ui/__tests__/gradient.test.tsx +125 -0
- package/src/components/ui/__tests__/input.test.tsx +166 -0
- package/src/components/ui/__tests__/select.test.tsx +273 -0
- package/src/components/ui/__tests__/shimmer.test.tsx +99 -0
- package/src/components/ui/blinking-indicator.tsx +25 -0
- package/src/components/ui/divider.tsx +162 -0
- package/src/components/ui/gradient.tsx +56 -0
- package/src/components/ui/input.tsx +228 -0
- package/src/components/ui/select.tsx +151 -0
- package/src/components/ui/shimmer.tsx +84 -0
- package/src/context/agent-mode.tsx +95 -0
- package/src/context/extension-file.tsx +136 -0
- package/src/context/network-activity.tsx +45 -0
- package/src/context/notification.tsx +62 -0
- package/src/context/shell-size.tsx +49 -0
- package/src/context/shell-title.tsx +38 -0
- package/src/entrypoints/print-mode.ts +312 -0
- package/src/entrypoints/repl.tsx +401 -0
- package/src/hooks/use-agent.ts +15 -0
- package/src/hooks/use-api-client.ts +1 -0
- package/src/hooks/use-available-height.ts +8 -0
- package/src/hooks/use-cleanup.ts +29 -0
- package/src/hooks/use-interrupt-manager.ts +242 -0
- package/src/hooks/use-models.ts +22 -0
- package/src/index.ts +217 -0
- package/src/lib/__tests__/ansi.test.ts +255 -0
- package/src/lib/__tests__/cli.test.ts +122 -0
- package/src/lib/__tests__/commands.test.ts +325 -0
- package/src/lib/__tests__/constants.test.ts +15 -0
- package/src/lib/__tests__/focusables.test.ts +25 -0
- package/src/lib/__tests__/fs.test.ts +231 -0
- package/src/lib/__tests__/markdown.test.tsx +348 -0
- package/src/lib/__tests__/mcpCommandHandler.test.ts +173 -0
- package/src/lib/__tests__/mcpManagement.test.ts +38 -0
- package/src/lib/__tests__/path-paste.test.ts +144 -0
- package/src/lib/__tests__/path.test.ts +300 -0
- package/src/lib/__tests__/queries.test.ts +39 -0
- package/src/lib/__tests__/standaloneMcpService.test.ts +71 -0
- package/src/lib/__tests__/text-buffer.test.ts +328 -0
- package/src/lib/__tests__/text-utils.test.ts +32 -0
- package/src/lib/__tests__/timing.test.ts +78 -0
- package/src/lib/__tests__/utils.test.ts +238 -0
- package/src/lib/__tests__/vim-buffer-actions.test.ts +154 -0
- package/src/lib/ansi.ts +150 -0
- package/src/lib/cli-push-server.ts +112 -0
- package/src/lib/cli.ts +44 -0
- package/src/lib/clipboard.ts +226 -0
- package/src/lib/command-utils.ts +93 -0
- package/src/lib/commands.ts +270 -0
- package/src/lib/constants.ts +3 -0
- package/src/lib/extension-connection.ts +181 -0
- package/src/lib/focusables.ts +7 -0
- package/src/lib/fs.ts +533 -0
- package/src/lib/markdown/code-block.tsx +63 -0
- package/src/lib/markdown/index.ts +4 -0
- package/src/lib/markdown/link.tsx +19 -0
- package/src/lib/markdown/markdown.tsx +372 -0
- package/src/lib/markdown/types.ts +15 -0
- package/src/lib/mcpCommandHandler.ts +121 -0
- package/src/lib/mcpManagement.ts +44 -0
- package/src/lib/path-paste.ts +185 -0
- package/src/lib/path.ts +179 -0
- package/src/lib/queries.ts +15 -0
- package/src/lib/standaloneMcpService.ts +688 -0
- package/src/lib/status-utils.ts +237 -0
- package/src/lib/test-utils.tsx +72 -0
- package/src/lib/text-buffer.ts +2415 -0
- package/src/lib/text-utils.ts +272 -0
- package/src/lib/timing.ts +63 -0
- package/src/lib/types.ts +295 -0
- package/src/lib/utils.ts +182 -0
- package/src/lib/vim-buffer-actions.ts +732 -0
- package/src/providers/agent.tsx +1075 -0
- package/src/providers/api-client.tsx +43 -0
- package/src/services/logger.ts +85 -0
- package/src/terminal/detection.ts +187 -0
- package/src/terminal/exit.ts +279 -0
- package/src/terminal/notification.ts +83 -0
- package/src/terminal/progress.ts +201 -0
- package/src/terminal/setup.ts +797 -0
- package/src/terminal/suspend.ts +58 -0
- package/src/terminal/types.ts +51 -0
- package/src/theme/context.tsx +57 -0
- package/src/theme/index.ts +4 -0
- package/src/theme/themed.tsx +35 -0
- package/src/theme/themes.json +546 -0
- package/src/theme/types.ts +110 -0
- package/src/tools/types.ts +59 -0
- package/src/tools/utils/__tests__/zod-coercion.test.ts +33 -0
- package/src/tools/utils/tool-ui-components.tsx +631 -0
- package/src/tools/utils/zod-coercion.ts +35 -0
- package/tsconfig.json +11 -0
- package/tsconfig.node.json +29 -0
- package/tsconfig.test.json +27 -0
- package/tsdown.config.ts +17 -0
- package/vitest.config.ts +76 -0
- package/README.md +0 -28
- package/dist/index.js +0 -26
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findNextWordAcrossLines,
|
|
3
|
+
findPrevWordAcrossLines,
|
|
4
|
+
findWordEndInLine,
|
|
5
|
+
getLineRangeOffsets,
|
|
6
|
+
getPositionFromOffsets,
|
|
7
|
+
isCombiningMark,
|
|
8
|
+
isWordCharStrict,
|
|
9
|
+
isWordCharWithCombining,
|
|
10
|
+
pushUndo,
|
|
11
|
+
replaceRangeInternal,
|
|
12
|
+
TextBufferAction,
|
|
13
|
+
TextBufferState,
|
|
14
|
+
} from "./text-buffer.js";
|
|
15
|
+
import { cpLen, toCodePoints } from "./text-utils.js";
|
|
16
|
+
|
|
17
|
+
/* Fail to compile on unexpected values. */
|
|
18
|
+
export function assumeExhaustive(_value: never): void {}
|
|
19
|
+
|
|
20
|
+
// Check if we're at the end of a base word (on the last base character)
|
|
21
|
+
// Returns true if current position has a base character followed only by combining marks until non-word
|
|
22
|
+
function isAtEndOfBaseWord(lineCodePoints: string[], col: number): boolean {
|
|
23
|
+
if (!isWordCharStrict(lineCodePoints[col])) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Look ahead to see if we have only combining marks followed by non-word
|
|
28
|
+
let i = col + 1;
|
|
29
|
+
|
|
30
|
+
// Skip any combining marks
|
|
31
|
+
while (i < lineCodePoints.length && isCombiningMark(lineCodePoints[i])) {
|
|
32
|
+
i++;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// If we hit end of line or non-word character, we were at end of base word
|
|
36
|
+
return i >= lineCodePoints.length || !isWordCharStrict(lineCodePoints[i]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type VimAction = Extract<
|
|
40
|
+
TextBufferAction,
|
|
41
|
+
| { type: "vim_delete_word_forward" }
|
|
42
|
+
| { type: "vim_delete_word_backward" }
|
|
43
|
+
| { type: "vim_delete_word_end" }
|
|
44
|
+
| { type: "vim_change_word_forward" }
|
|
45
|
+
| { type: "vim_change_word_backward" }
|
|
46
|
+
| { type: "vim_change_word_end" }
|
|
47
|
+
| { type: "vim_delete_line" }
|
|
48
|
+
| { type: "vim_change_line" }
|
|
49
|
+
| { type: "vim_delete_to_end_of_line" }
|
|
50
|
+
| { type: "vim_change_to_end_of_line" }
|
|
51
|
+
| { type: "vim_change_movement" }
|
|
52
|
+
| { type: "vim_move_left" }
|
|
53
|
+
| { type: "vim_move_right" }
|
|
54
|
+
| { type: "vim_move_up" }
|
|
55
|
+
| { type: "vim_move_down" }
|
|
56
|
+
| { type: "vim_move_word_forward" }
|
|
57
|
+
| { type: "vim_move_word_backward" }
|
|
58
|
+
| { type: "vim_move_word_end" }
|
|
59
|
+
| { type: "vim_delete_char" }
|
|
60
|
+
| { type: "vim_insert_at_cursor" }
|
|
61
|
+
| { type: "vim_append_at_cursor" }
|
|
62
|
+
| { type: "vim_open_line_below" }
|
|
63
|
+
| { type: "vim_open_line_above" }
|
|
64
|
+
| { type: "vim_append_at_line_end" }
|
|
65
|
+
| { type: "vim_insert_at_line_start" }
|
|
66
|
+
| { type: "vim_move_to_line_start" }
|
|
67
|
+
| { type: "vim_move_to_line_end" }
|
|
68
|
+
| { type: "vim_move_to_first_nonwhitespace" }
|
|
69
|
+
| { type: "vim_move_to_first_line" }
|
|
70
|
+
| { type: "vim_move_to_last_line" }
|
|
71
|
+
| { type: "vim_move_to_line" }
|
|
72
|
+
| { type: "vim_escape_insert_mode" }
|
|
73
|
+
>;
|
|
74
|
+
|
|
75
|
+
export function handleVimAction(state: TextBufferState, action: VimAction): TextBufferState {
|
|
76
|
+
const { lines, cursorRow, cursorCol } = state;
|
|
77
|
+
|
|
78
|
+
switch (action.type) {
|
|
79
|
+
case "vim_delete_word_forward":
|
|
80
|
+
case "vim_change_word_forward": {
|
|
81
|
+
const { count } = action.payload;
|
|
82
|
+
let endRow = cursorRow;
|
|
83
|
+
let endCol = cursorCol;
|
|
84
|
+
|
|
85
|
+
for (let i = 0; i < count; i++) {
|
|
86
|
+
const nextWord = findNextWordAcrossLines(lines, endRow, endCol, true);
|
|
87
|
+
if (nextWord) {
|
|
88
|
+
endRow = nextWord.row;
|
|
89
|
+
endCol = nextWord.col;
|
|
90
|
+
} else {
|
|
91
|
+
// No more words, delete/change to end of current word or line
|
|
92
|
+
const currentLine = lines[endRow] || "";
|
|
93
|
+
const wordEnd = findWordEndInLine(currentLine, endCol);
|
|
94
|
+
if (wordEnd !== null) {
|
|
95
|
+
endCol = wordEnd + 1; // Include the character at word end
|
|
96
|
+
} else {
|
|
97
|
+
endCol = cpLen(currentLine);
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (endRow !== cursorRow || endCol !== cursorCol) {
|
|
104
|
+
const nextState = pushUndo(state);
|
|
105
|
+
return replaceRangeInternal(nextState, cursorRow, cursorCol, endRow, endCol, "");
|
|
106
|
+
}
|
|
107
|
+
return state;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
case "vim_delete_word_backward":
|
|
111
|
+
case "vim_change_word_backward": {
|
|
112
|
+
const { count } = action.payload;
|
|
113
|
+
let startRow = cursorRow;
|
|
114
|
+
let startCol = cursorCol;
|
|
115
|
+
|
|
116
|
+
for (let i = 0; i < count; i++) {
|
|
117
|
+
const prevWord = findPrevWordAcrossLines(lines, startRow, startCol);
|
|
118
|
+
if (prevWord) {
|
|
119
|
+
startRow = prevWord.row;
|
|
120
|
+
startCol = prevWord.col;
|
|
121
|
+
} else {
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (startRow !== cursorRow || startCol !== cursorCol) {
|
|
127
|
+
const nextState = pushUndo(state);
|
|
128
|
+
return replaceRangeInternal(nextState, startRow, startCol, cursorRow, cursorCol, "");
|
|
129
|
+
}
|
|
130
|
+
return state;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
case "vim_delete_word_end":
|
|
134
|
+
case "vim_change_word_end": {
|
|
135
|
+
const { count } = action.payload;
|
|
136
|
+
let row = cursorRow;
|
|
137
|
+
let col = cursorCol;
|
|
138
|
+
let endRow = cursorRow;
|
|
139
|
+
let endCol = cursorCol;
|
|
140
|
+
|
|
141
|
+
for (let i = 0; i < count; i++) {
|
|
142
|
+
const wordEnd = findNextWordAcrossLines(lines, row, col, false);
|
|
143
|
+
if (wordEnd) {
|
|
144
|
+
endRow = wordEnd.row;
|
|
145
|
+
endCol = wordEnd.col + 1; // Include the character at word end
|
|
146
|
+
// For next iteration, move to start of next word
|
|
147
|
+
if (i < count - 1) {
|
|
148
|
+
const nextWord = findNextWordAcrossLines(lines, wordEnd.row, wordEnd.col + 1, true);
|
|
149
|
+
if (nextWord) {
|
|
150
|
+
row = nextWord.row;
|
|
151
|
+
col = nextWord.col;
|
|
152
|
+
} else {
|
|
153
|
+
break; // No more words
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Ensure we don't go past the end of the last line
|
|
162
|
+
if (endRow < lines.length) {
|
|
163
|
+
const lineLen = cpLen(lines[endRow] || "");
|
|
164
|
+
endCol = Math.min(endCol, lineLen);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (endRow !== cursorRow || endCol !== cursorCol) {
|
|
168
|
+
const nextState = pushUndo(state);
|
|
169
|
+
return replaceRangeInternal(nextState, cursorRow, cursorCol, endRow, endCol, "");
|
|
170
|
+
}
|
|
171
|
+
return state;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
case "vim_delete_line": {
|
|
175
|
+
const { count } = action.payload;
|
|
176
|
+
if (lines.length === 0) {
|
|
177
|
+
return state;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const linesToDelete = Math.min(count, lines.length - cursorRow);
|
|
181
|
+
const totalLines = lines.length;
|
|
182
|
+
|
|
183
|
+
if (totalLines === 1 || linesToDelete >= totalLines) {
|
|
184
|
+
// If there's only one line, or we're deleting all remaining lines,
|
|
185
|
+
// clear the content but keep one empty line (text editors should never be completely empty)
|
|
186
|
+
const nextState = pushUndo(state);
|
|
187
|
+
return {
|
|
188
|
+
...nextState,
|
|
189
|
+
lines: [""],
|
|
190
|
+
cursorRow: 0,
|
|
191
|
+
cursorCol: 0,
|
|
192
|
+
preferredCol: null,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const nextState = pushUndo(state);
|
|
197
|
+
const newLines = [...nextState.lines];
|
|
198
|
+
newLines.splice(cursorRow, linesToDelete);
|
|
199
|
+
|
|
200
|
+
// Adjust cursor position
|
|
201
|
+
const newCursorRow = Math.min(cursorRow, newLines.length - 1);
|
|
202
|
+
const newCursorCol = 0; // Vim places cursor at beginning of line after dd
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
...nextState,
|
|
206
|
+
lines: newLines,
|
|
207
|
+
cursorRow: newCursorRow,
|
|
208
|
+
cursorCol: newCursorCol,
|
|
209
|
+
preferredCol: null,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
case "vim_change_line": {
|
|
214
|
+
const { count } = action.payload;
|
|
215
|
+
if (lines.length === 0) {
|
|
216
|
+
return state;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const linesToChange = Math.min(count, lines.length - cursorRow);
|
|
220
|
+
const nextState = pushUndo(state);
|
|
221
|
+
|
|
222
|
+
const { startOffset, endOffset } = getLineRangeOffsets(
|
|
223
|
+
cursorRow,
|
|
224
|
+
linesToChange,
|
|
225
|
+
nextState.lines,
|
|
226
|
+
);
|
|
227
|
+
const { startRow, startCol, endRow, endCol } = getPositionFromOffsets(
|
|
228
|
+
startOffset,
|
|
229
|
+
endOffset,
|
|
230
|
+
nextState.lines,
|
|
231
|
+
);
|
|
232
|
+
return replaceRangeInternal(nextState, startRow, startCol, endRow, endCol, "");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
case "vim_delete_to_end_of_line":
|
|
236
|
+
case "vim_change_to_end_of_line": {
|
|
237
|
+
const currentLine = lines[cursorRow] || "";
|
|
238
|
+
if (cursorCol < cpLen(currentLine)) {
|
|
239
|
+
const nextState = pushUndo(state);
|
|
240
|
+
return replaceRangeInternal(
|
|
241
|
+
nextState,
|
|
242
|
+
cursorRow,
|
|
243
|
+
cursorCol,
|
|
244
|
+
cursorRow,
|
|
245
|
+
cpLen(currentLine),
|
|
246
|
+
"",
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return state;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
case "vim_change_movement": {
|
|
253
|
+
const { movement, count } = action.payload;
|
|
254
|
+
const totalLines = lines.length;
|
|
255
|
+
|
|
256
|
+
switch (movement) {
|
|
257
|
+
case "h": {
|
|
258
|
+
// Left
|
|
259
|
+
// Change N characters to the left
|
|
260
|
+
const startCol = Math.max(0, cursorCol - count);
|
|
261
|
+
return replaceRangeInternal(
|
|
262
|
+
pushUndo(state),
|
|
263
|
+
cursorRow,
|
|
264
|
+
startCol,
|
|
265
|
+
cursorRow,
|
|
266
|
+
cursorCol,
|
|
267
|
+
"",
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
case "j": {
|
|
272
|
+
// Down
|
|
273
|
+
const linesToChange = Math.min(count, totalLines - cursorRow);
|
|
274
|
+
if (linesToChange > 0) {
|
|
275
|
+
if (totalLines === 1) {
|
|
276
|
+
const currentLine = state.lines[0] || "";
|
|
277
|
+
return replaceRangeInternal(pushUndo(state), 0, 0, 0, cpLen(currentLine), "");
|
|
278
|
+
} else {
|
|
279
|
+
const nextState = pushUndo(state);
|
|
280
|
+
const { startOffset, endOffset } = getLineRangeOffsets(
|
|
281
|
+
cursorRow,
|
|
282
|
+
linesToChange,
|
|
283
|
+
nextState.lines,
|
|
284
|
+
);
|
|
285
|
+
const { startRow, startCol, endRow, endCol } = getPositionFromOffsets(
|
|
286
|
+
startOffset,
|
|
287
|
+
endOffset,
|
|
288
|
+
nextState.lines,
|
|
289
|
+
);
|
|
290
|
+
return replaceRangeInternal(nextState, startRow, startCol, endRow, endCol, "");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return state;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
case "k": {
|
|
297
|
+
// Up
|
|
298
|
+
const upLines = Math.min(count, cursorRow + 1);
|
|
299
|
+
if (upLines > 0) {
|
|
300
|
+
if (state.lines.length === 1) {
|
|
301
|
+
const currentLine = state.lines[0] || "";
|
|
302
|
+
return replaceRangeInternal(pushUndo(state), 0, 0, 0, cpLen(currentLine), "");
|
|
303
|
+
} else {
|
|
304
|
+
const startRow = Math.max(0, cursorRow - count + 1);
|
|
305
|
+
const linesToChange = cursorRow - startRow + 1;
|
|
306
|
+
const nextState = pushUndo(state);
|
|
307
|
+
const { startOffset, endOffset } = getLineRangeOffsets(
|
|
308
|
+
startRow,
|
|
309
|
+
linesToChange,
|
|
310
|
+
nextState.lines,
|
|
311
|
+
);
|
|
312
|
+
const {
|
|
313
|
+
startRow: newStartRow,
|
|
314
|
+
startCol,
|
|
315
|
+
endRow,
|
|
316
|
+
endCol,
|
|
317
|
+
} = getPositionFromOffsets(startOffset, endOffset, nextState.lines);
|
|
318
|
+
const resultState = replaceRangeInternal(
|
|
319
|
+
nextState,
|
|
320
|
+
newStartRow,
|
|
321
|
+
startCol,
|
|
322
|
+
endRow,
|
|
323
|
+
endCol,
|
|
324
|
+
"",
|
|
325
|
+
);
|
|
326
|
+
return {
|
|
327
|
+
...resultState,
|
|
328
|
+
cursorRow: startRow,
|
|
329
|
+
cursorCol: 0,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return state;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
case "l": {
|
|
337
|
+
// Right
|
|
338
|
+
// Change N characters to the right
|
|
339
|
+
return replaceRangeInternal(
|
|
340
|
+
pushUndo(state),
|
|
341
|
+
cursorRow,
|
|
342
|
+
cursorCol,
|
|
343
|
+
cursorRow,
|
|
344
|
+
Math.min(cpLen(lines[cursorRow] || ""), cursorCol + count),
|
|
345
|
+
"",
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
default:
|
|
350
|
+
return state;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
case "vim_move_left": {
|
|
355
|
+
const { count } = action.payload;
|
|
356
|
+
const { cursorRow, cursorCol, lines } = state;
|
|
357
|
+
let newRow = cursorRow;
|
|
358
|
+
let newCol = cursorCol;
|
|
359
|
+
|
|
360
|
+
for (let i = 0; i < count; i++) {
|
|
361
|
+
if (newCol > 0) {
|
|
362
|
+
newCol--;
|
|
363
|
+
} else if (newRow > 0) {
|
|
364
|
+
// Move to end of previous line
|
|
365
|
+
newRow--;
|
|
366
|
+
const prevLine = lines[newRow] || "";
|
|
367
|
+
const prevLineLength = cpLen(prevLine);
|
|
368
|
+
// Position on last character, or column 0 for empty lines
|
|
369
|
+
newCol = prevLineLength === 0 ? 0 : prevLineLength - 1;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
...state,
|
|
375
|
+
cursorRow: newRow,
|
|
376
|
+
cursorCol: newCol,
|
|
377
|
+
preferredCol: null,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
case "vim_move_right": {
|
|
382
|
+
const { count } = action.payload;
|
|
383
|
+
const { cursorRow, cursorCol, lines } = state;
|
|
384
|
+
let newRow = cursorRow;
|
|
385
|
+
let newCol = cursorCol;
|
|
386
|
+
|
|
387
|
+
for (let i = 0; i < count; i++) {
|
|
388
|
+
const currentLine = lines[newRow] || "";
|
|
389
|
+
const lineLength = cpLen(currentLine);
|
|
390
|
+
// Don't move past the last character of the line
|
|
391
|
+
// For empty lines, stay at column 0; for non-empty lines, don't go past last character
|
|
392
|
+
if (lineLength === 0) {
|
|
393
|
+
// Empty line - try to move to next line
|
|
394
|
+
if (newRow < lines.length - 1) {
|
|
395
|
+
newRow++;
|
|
396
|
+
newCol = 0;
|
|
397
|
+
}
|
|
398
|
+
} else if (newCol < lineLength - 1) {
|
|
399
|
+
newCol++;
|
|
400
|
+
|
|
401
|
+
// Skip over combining marks - don't let cursor land on them
|
|
402
|
+
const currentLinePoints = toCodePoints(currentLine);
|
|
403
|
+
while (
|
|
404
|
+
newCol < currentLinePoints.length &&
|
|
405
|
+
isCombiningMark(currentLinePoints[newCol]) &&
|
|
406
|
+
newCol < lineLength - 1
|
|
407
|
+
) {
|
|
408
|
+
newCol++;
|
|
409
|
+
}
|
|
410
|
+
} else if (newRow < lines.length - 1) {
|
|
411
|
+
// At end of line - move to beginning of next line
|
|
412
|
+
newRow++;
|
|
413
|
+
newCol = 0;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
...state,
|
|
419
|
+
cursorRow: newRow,
|
|
420
|
+
cursorCol: newCol,
|
|
421
|
+
preferredCol: null,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
case "vim_move_up": {
|
|
426
|
+
const { count } = action.payload;
|
|
427
|
+
const { cursorRow, cursorCol, lines } = state;
|
|
428
|
+
const newRow = Math.max(0, cursorRow - count);
|
|
429
|
+
const targetLine = lines[newRow] || "";
|
|
430
|
+
const targetLineLength = cpLen(targetLine);
|
|
431
|
+
const newCol = Math.min(cursorCol, targetLineLength > 0 ? targetLineLength - 1 : 0);
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
...state,
|
|
435
|
+
cursorRow: newRow,
|
|
436
|
+
cursorCol: newCol,
|
|
437
|
+
preferredCol: null,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
case "vim_move_down": {
|
|
442
|
+
const { count } = action.payload;
|
|
443
|
+
const { cursorRow, cursorCol, lines } = state;
|
|
444
|
+
const newRow = Math.min(lines.length - 1, cursorRow + count);
|
|
445
|
+
const targetLine = lines[newRow] || "";
|
|
446
|
+
const targetLineLength = cpLen(targetLine);
|
|
447
|
+
const newCol = Math.min(cursorCol, targetLineLength > 0 ? targetLineLength - 1 : 0);
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
...state,
|
|
451
|
+
cursorRow: newRow,
|
|
452
|
+
cursorCol: newCol,
|
|
453
|
+
preferredCol: null,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
case "vim_move_word_forward": {
|
|
458
|
+
const { count } = action.payload;
|
|
459
|
+
let row = cursorRow;
|
|
460
|
+
let col = cursorCol;
|
|
461
|
+
|
|
462
|
+
for (let i = 0; i < count; i++) {
|
|
463
|
+
const nextWord = findNextWordAcrossLines(lines, row, col, true);
|
|
464
|
+
if (nextWord) {
|
|
465
|
+
row = nextWord.row;
|
|
466
|
+
col = nextWord.col;
|
|
467
|
+
} else {
|
|
468
|
+
// No more words to move to
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
...state,
|
|
475
|
+
cursorRow: row,
|
|
476
|
+
cursorCol: col,
|
|
477
|
+
preferredCol: null,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
case "vim_move_word_backward": {
|
|
482
|
+
const { count } = action.payload;
|
|
483
|
+
let row = cursorRow;
|
|
484
|
+
let col = cursorCol;
|
|
485
|
+
|
|
486
|
+
for (let i = 0; i < count; i++) {
|
|
487
|
+
const prevWord = findPrevWordAcrossLines(lines, row, col);
|
|
488
|
+
if (prevWord) {
|
|
489
|
+
row = prevWord.row;
|
|
490
|
+
col = prevWord.col;
|
|
491
|
+
} else {
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
...state,
|
|
498
|
+
cursorRow: row,
|
|
499
|
+
cursorCol: col,
|
|
500
|
+
preferredCol: null,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
case "vim_move_word_end": {
|
|
505
|
+
const { count } = action.payload;
|
|
506
|
+
let row = cursorRow;
|
|
507
|
+
let col = cursorCol;
|
|
508
|
+
|
|
509
|
+
for (let i = 0; i < count; i++) {
|
|
510
|
+
// Special handling for the first iteration when we're at end of word
|
|
511
|
+
if (i === 0) {
|
|
512
|
+
const currentLine = lines[row] || "";
|
|
513
|
+
const lineCodePoints = toCodePoints(currentLine);
|
|
514
|
+
|
|
515
|
+
// Check if we're at the end of a word (on the last base character)
|
|
516
|
+
const atEndOfWord =
|
|
517
|
+
col < lineCodePoints.length &&
|
|
518
|
+
isWordCharStrict(lineCodePoints[col]) &&
|
|
519
|
+
(col + 1 >= lineCodePoints.length ||
|
|
520
|
+
!isWordCharWithCombining(lineCodePoints[col + 1]) ||
|
|
521
|
+
// Or if we're on a base char followed only by combining marks until non-word
|
|
522
|
+
(isWordCharStrict(lineCodePoints[col]) && isAtEndOfBaseWord(lineCodePoints, col)));
|
|
523
|
+
|
|
524
|
+
if (atEndOfWord) {
|
|
525
|
+
// We're already at end of word, find next word end
|
|
526
|
+
const nextWord = findNextWordAcrossLines(lines, row, col + 1, false);
|
|
527
|
+
if (nextWord) {
|
|
528
|
+
row = nextWord.row;
|
|
529
|
+
col = nextWord.col;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const wordEnd = findNextWordAcrossLines(lines, row, col, false);
|
|
536
|
+
if (wordEnd) {
|
|
537
|
+
row = wordEnd.row;
|
|
538
|
+
col = wordEnd.col;
|
|
539
|
+
} else {
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return {
|
|
545
|
+
...state,
|
|
546
|
+
cursorRow: row,
|
|
547
|
+
cursorCol: col,
|
|
548
|
+
preferredCol: null,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
case "vim_delete_char": {
|
|
553
|
+
const { count } = action.payload;
|
|
554
|
+
const { cursorRow, cursorCol, lines } = state;
|
|
555
|
+
const currentLine = lines[cursorRow] || "";
|
|
556
|
+
const lineLength = cpLen(currentLine);
|
|
557
|
+
|
|
558
|
+
if (cursorCol < lineLength) {
|
|
559
|
+
const deleteCount = Math.min(count, lineLength - cursorCol);
|
|
560
|
+
const nextState = pushUndo(state);
|
|
561
|
+
return replaceRangeInternal(
|
|
562
|
+
nextState,
|
|
563
|
+
cursorRow,
|
|
564
|
+
cursorCol,
|
|
565
|
+
cursorRow,
|
|
566
|
+
cursorCol + deleteCount,
|
|
567
|
+
"",
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
return state;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
case "vim_insert_at_cursor": {
|
|
574
|
+
// Just return state - mode change is handled elsewhere
|
|
575
|
+
return state;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
case "vim_append_at_cursor": {
|
|
579
|
+
const { cursorRow, cursorCol, lines } = state;
|
|
580
|
+
const currentLine = lines[cursorRow] || "";
|
|
581
|
+
const newCol = cursorCol < cpLen(currentLine) ? cursorCol + 1 : cursorCol;
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
...state,
|
|
585
|
+
cursorCol: newCol,
|
|
586
|
+
preferredCol: null,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
case "vim_open_line_below": {
|
|
591
|
+
const { cursorRow, lines } = state;
|
|
592
|
+
const nextState = pushUndo(state);
|
|
593
|
+
|
|
594
|
+
// Insert newline at end of current line
|
|
595
|
+
const endOfLine = cpLen(lines[cursorRow] || "");
|
|
596
|
+
return replaceRangeInternal(nextState, cursorRow, endOfLine, cursorRow, endOfLine, "\n");
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
case "vim_open_line_above": {
|
|
600
|
+
const { cursorRow } = state;
|
|
601
|
+
const nextState = pushUndo(state);
|
|
602
|
+
|
|
603
|
+
// Insert newline at beginning of current line
|
|
604
|
+
const resultState = replaceRangeInternal(nextState, cursorRow, 0, cursorRow, 0, "\n");
|
|
605
|
+
|
|
606
|
+
// Move cursor to the new line above
|
|
607
|
+
return {
|
|
608
|
+
...resultState,
|
|
609
|
+
cursorRow,
|
|
610
|
+
cursorCol: 0,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
case "vim_append_at_line_end": {
|
|
615
|
+
const { cursorRow, lines } = state;
|
|
616
|
+
const lineLength = cpLen(lines[cursorRow] || "");
|
|
617
|
+
|
|
618
|
+
return {
|
|
619
|
+
...state,
|
|
620
|
+
cursorCol: lineLength,
|
|
621
|
+
preferredCol: null,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
case "vim_insert_at_line_start": {
|
|
626
|
+
const { cursorRow, lines } = state;
|
|
627
|
+
const currentLine = lines[cursorRow] || "";
|
|
628
|
+
let col = 0;
|
|
629
|
+
|
|
630
|
+
// Find first non-whitespace character using proper Unicode handling
|
|
631
|
+
const lineCodePoints = toCodePoints(currentLine);
|
|
632
|
+
while (col < lineCodePoints.length && /\s/.test(lineCodePoints[col])) {
|
|
633
|
+
col++;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return {
|
|
637
|
+
...state,
|
|
638
|
+
cursorCol: col,
|
|
639
|
+
preferredCol: null,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
case "vim_move_to_line_start": {
|
|
644
|
+
return {
|
|
645
|
+
...state,
|
|
646
|
+
cursorCol: 0,
|
|
647
|
+
preferredCol: null,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
case "vim_move_to_line_end": {
|
|
652
|
+
const { cursorRow, lines } = state;
|
|
653
|
+
const lineLength = cpLen(lines[cursorRow] || "");
|
|
654
|
+
|
|
655
|
+
return {
|
|
656
|
+
...state,
|
|
657
|
+
cursorCol: lineLength > 0 ? lineLength - 1 : 0,
|
|
658
|
+
preferredCol: null,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
case "vim_move_to_first_nonwhitespace": {
|
|
663
|
+
const { cursorRow, lines } = state;
|
|
664
|
+
const currentLine = lines[cursorRow] || "";
|
|
665
|
+
let col = 0;
|
|
666
|
+
|
|
667
|
+
// Find first non-whitespace character using proper Unicode handling
|
|
668
|
+
const lineCodePoints = toCodePoints(currentLine);
|
|
669
|
+
while (col < lineCodePoints.length && /\s/.test(lineCodePoints[col])) {
|
|
670
|
+
col++;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return {
|
|
674
|
+
...state,
|
|
675
|
+
cursorCol: col,
|
|
676
|
+
preferredCol: null,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
case "vim_move_to_first_line": {
|
|
681
|
+
return {
|
|
682
|
+
...state,
|
|
683
|
+
cursorRow: 0,
|
|
684
|
+
cursorCol: 0,
|
|
685
|
+
preferredCol: null,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
case "vim_move_to_last_line": {
|
|
690
|
+
const { lines } = state;
|
|
691
|
+
const lastRow = lines.length - 1;
|
|
692
|
+
|
|
693
|
+
return {
|
|
694
|
+
...state,
|
|
695
|
+
cursorRow: lastRow,
|
|
696
|
+
cursorCol: 0,
|
|
697
|
+
preferredCol: null,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
case "vim_move_to_line": {
|
|
702
|
+
const { lineNumber } = action.payload;
|
|
703
|
+
const { lines } = state;
|
|
704
|
+
const targetRow = Math.min(Math.max(0, lineNumber - 1), lines.length - 1);
|
|
705
|
+
|
|
706
|
+
return {
|
|
707
|
+
...state,
|
|
708
|
+
cursorRow: targetRow,
|
|
709
|
+
cursorCol: 0,
|
|
710
|
+
preferredCol: null,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
case "vim_escape_insert_mode": {
|
|
715
|
+
// Move cursor left if not at beginning of line (vim behavior when exiting insert mode)
|
|
716
|
+
const { cursorCol } = state;
|
|
717
|
+
const newCol = cursorCol > 0 ? cursorCol - 1 : 0;
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
...state,
|
|
721
|
+
cursorCol: newCol,
|
|
722
|
+
preferredCol: null,
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
default: {
|
|
727
|
+
// This should never happen if TypeScript is working correctly
|
|
728
|
+
assumeExhaustive(action);
|
|
729
|
+
return state;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|