@abacus-ai/cli 2.0.0-canary.1 → 2.0.0-canary.11

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.
Files changed (198) hide show
  1. package/README.md +25 -0
  2. package/dist/index.mjs +466 -438
  3. package/package.json +4 -1
  4. package/.oxlintrc.json +0 -8
  5. package/resources/abacus.ico +0 -0
  6. package/resources/entitlements.plist +0 -9
  7. package/src/__e2e__/README.md +0 -196
  8. package/src/__e2e__/agent-interactions.e2e.test.tsx +0 -61
  9. package/src/__e2e__/cli-commands.e2e.test.tsx +0 -77
  10. package/src/__e2e__/conversation-throttle.e2e.test.ts +0 -453
  11. package/src/__e2e__/conversation.e2e.test.tsx +0 -56
  12. package/src/__e2e__/diff-preview.e2e.test.tsx +0 -3399
  13. package/src/__e2e__/file-creation.e2e.test.tsx +0 -149
  14. package/src/__e2e__/helpers/test-helpers.ts +0 -449
  15. package/src/__e2e__/keyboard-navigation.e2e.test.tsx +0 -34
  16. package/src/__e2e__/llm-models.e2e.test.ts +0 -402
  17. package/src/__e2e__/mcp/mcp-callback-flow.e2e.test.tsx +0 -71
  18. package/src/__e2e__/mcp/mcp-full-app-ui.e2e.test.tsx +0 -167
  19. package/src/__e2e__/mcp/mcp-ui-rendering.e2e.test.tsx +0 -185
  20. package/src/__e2e__/repl.e2e.test.tsx +0 -78
  21. package/src/__e2e__/shell-compatibility.e2e.test.tsx +0 -76
  22. package/src/__e2e__/theme-mcp.e2e.test.tsx +0 -98
  23. package/src/__e2e__/tool-permissions.e2e.test.tsx +0 -66
  24. package/src/args.ts +0 -22
  25. package/src/components/__tests__/react-compiler.test.tsx +0 -78
  26. package/src/components/__tests__/status-indicator.test.tsx +0 -403
  27. package/src/components/composer/__tests__/bash-runner.test.tsx +0 -263
  28. package/src/components/composer/agent-mode-indicator.tsx +0 -63
  29. package/src/components/composer/bash-runner.tsx +0 -54
  30. package/src/components/composer/commands/default-commands.tsx +0 -615
  31. package/src/components/composer/commands/handler.tsx +0 -59
  32. package/src/components/composer/commands/picker.tsx +0 -273
  33. package/src/components/composer/commands/registry.ts +0 -233
  34. package/src/components/composer/commands/types.ts +0 -33
  35. package/src/components/composer/context.tsx +0 -88
  36. package/src/components/composer/file-mention-picker.tsx +0 -83
  37. package/src/components/composer/help.tsx +0 -44
  38. package/src/components/composer/index.tsx +0 -1007
  39. package/src/components/composer/mentions.ts +0 -57
  40. package/src/components/composer/message-queue.tsx +0 -70
  41. package/src/components/composer/mode-panel.tsx +0 -35
  42. package/src/components/composer/modes/__tests__/bash-handler.test.tsx +0 -755
  43. package/src/components/composer/modes/__tests__/bash-renderer.test.tsx +0 -1108
  44. package/src/components/composer/modes/bash-handler.tsx +0 -132
  45. package/src/components/composer/modes/bash-renderer.tsx +0 -175
  46. package/src/components/composer/modes/default-handlers.tsx +0 -33
  47. package/src/components/composer/modes/index.ts +0 -41
  48. package/src/components/composer/modes/types.ts +0 -21
  49. package/src/components/composer/persistent-shell.ts +0 -283
  50. package/src/components/composer/process.ts +0 -65
  51. package/src/components/composer/types.ts +0 -9
  52. package/src/components/composer/use-mention-search.ts +0 -68
  53. package/src/components/error-boundry.tsx +0 -60
  54. package/src/components/exit-message.tsx +0 -29
  55. package/src/components/expanded-view.tsx +0 -74
  56. package/src/components/file-completion.tsx +0 -127
  57. package/src/components/header.tsx +0 -47
  58. package/src/components/logo.tsx +0 -37
  59. package/src/components/segments.tsx +0 -356
  60. package/src/components/status-indicator.tsx +0 -306
  61. package/src/components/tool-group-summary.tsx +0 -263
  62. package/src/components/tool-permissions/ask-user-question-permission-ui.tsx +0 -319
  63. package/src/components/tool-permissions/diff-preview.tsx +0 -359
  64. package/src/components/tool-permissions/index.ts +0 -5
  65. package/src/components/tool-permissions/permission-options.tsx +0 -401
  66. package/src/components/tool-permissions/permission-preview-header.tsx +0 -57
  67. package/src/components/tool-permissions/tool-permission-ui.tsx +0 -420
  68. package/src/components/tools/agent/ask-user-question.tsx +0 -107
  69. package/src/components/tools/agent/enter-plan-mode.tsx +0 -55
  70. package/src/components/tools/agent/exit-plan-mode.tsx +0 -83
  71. package/src/components/tools/agent/handoff-to-main.tsx +0 -27
  72. package/src/components/tools/agent/subagent.tsx +0 -37
  73. package/src/components/tools/agent/todo-write.tsx +0 -104
  74. package/src/components/tools/browser/close-tab.tsx +0 -58
  75. package/src/components/tools/browser/computer.tsx +0 -70
  76. package/src/components/tools/browser/get-interactive-elements.tsx +0 -54
  77. package/src/components/tools/browser/get-tab-content.tsx +0 -51
  78. package/src/components/tools/browser/navigate-to.tsx +0 -59
  79. package/src/components/tools/browser/new-tab.tsx +0 -60
  80. package/src/components/tools/browser/perform-action.tsx +0 -63
  81. package/src/components/tools/browser/refresh-tab.tsx +0 -43
  82. package/src/components/tools/browser/switch-tab.tsx +0 -58
  83. package/src/components/tools/filesystem/delete-file.tsx +0 -104
  84. package/src/components/tools/filesystem/edit.tsx +0 -220
  85. package/src/components/tools/filesystem/list-dir.tsx +0 -78
  86. package/src/components/tools/filesystem/read-file.tsx +0 -180
  87. package/src/components/tools/filesystem/upload-image.tsx +0 -76
  88. package/src/components/tools/ide/ide-diagnostics.tsx +0 -62
  89. package/src/components/tools/index.ts +0 -91
  90. package/src/components/tools/mcp/mcp-tool.tsx +0 -158
  91. package/src/components/tools/search/fetch-url.tsx +0 -73
  92. package/src/components/tools/search/file-search.tsx +0 -78
  93. package/src/components/tools/search/grep.tsx +0 -90
  94. package/src/components/tools/search/semantic-search.tsx +0 -66
  95. package/src/components/tools/search/web-search.tsx +0 -71
  96. package/src/components/tools/shared/index.tsx +0 -48
  97. package/src/components/tools/shared/zod-coercion.ts +0 -35
  98. package/src/components/tools/terminal/bash-tool-output.tsx +0 -188
  99. package/src/components/tools/terminal/get-terminal-output.tsx +0 -91
  100. package/src/components/tools/terminal/run-in-terminal.tsx +0 -131
  101. package/src/components/tools/types.ts +0 -16
  102. package/src/components/tools.tsx +0 -68
  103. package/src/components/ui/__tests__/divider.test.tsx +0 -61
  104. package/src/components/ui/__tests__/gradient.test.tsx +0 -125
  105. package/src/components/ui/__tests__/input.test.tsx +0 -166
  106. package/src/components/ui/__tests__/select.test.tsx +0 -273
  107. package/src/components/ui/__tests__/shimmer.test.tsx +0 -99
  108. package/src/components/ui/blinking-indicator.tsx +0 -27
  109. package/src/components/ui/divider.tsx +0 -162
  110. package/src/components/ui/gradient.tsx +0 -56
  111. package/src/components/ui/input.tsx +0 -228
  112. package/src/components/ui/select.tsx +0 -151
  113. package/src/components/ui/shimmer.tsx +0 -76
  114. package/src/context/agent-mode.tsx +0 -95
  115. package/src/context/extension-file.tsx +0 -136
  116. package/src/context/network-activity.tsx +0 -45
  117. package/src/context/notification.tsx +0 -62
  118. package/src/context/shell-size.tsx +0 -49
  119. package/src/context/shell-title.tsx +0 -38
  120. package/src/entrypoints/print-mode.ts +0 -312
  121. package/src/entrypoints/repl.tsx +0 -389
  122. package/src/hooks/use-agent.ts +0 -15
  123. package/src/hooks/use-api-client.ts +0 -1
  124. package/src/hooks/use-available-height.ts +0 -8
  125. package/src/hooks/use-cleanup.ts +0 -29
  126. package/src/hooks/use-interrupt-manager.ts +0 -242
  127. package/src/hooks/use-models.ts +0 -22
  128. package/src/index.ts +0 -217
  129. package/src/lib/__tests__/ansi.test.ts +0 -255
  130. package/src/lib/__tests__/cli.test.ts +0 -122
  131. package/src/lib/__tests__/commands.test.ts +0 -325
  132. package/src/lib/__tests__/constants.test.ts +0 -15
  133. package/src/lib/__tests__/focusables.test.ts +0 -25
  134. package/src/lib/__tests__/fs.test.ts +0 -231
  135. package/src/lib/__tests__/markdown.test.tsx +0 -348
  136. package/src/lib/__tests__/mcpCommandHandler.test.ts +0 -173
  137. package/src/lib/__tests__/mcpManagement.test.ts +0 -38
  138. package/src/lib/__tests__/path-paste.test.ts +0 -144
  139. package/src/lib/__tests__/path.test.ts +0 -300
  140. package/src/lib/__tests__/queries.test.ts +0 -39
  141. package/src/lib/__tests__/standaloneMcpService.test.ts +0 -71
  142. package/src/lib/__tests__/text-buffer.test.ts +0 -328
  143. package/src/lib/__tests__/text-utils.test.ts +0 -32
  144. package/src/lib/__tests__/timing.test.ts +0 -78
  145. package/src/lib/__tests__/utils.test.ts +0 -238
  146. package/src/lib/__tests__/vim-buffer-actions.test.ts +0 -154
  147. package/src/lib/ansi.ts +0 -150
  148. package/src/lib/cli-push-server.ts +0 -112
  149. package/src/lib/cli.ts +0 -44
  150. package/src/lib/clipboard.ts +0 -226
  151. package/src/lib/command-utils.ts +0 -93
  152. package/src/lib/commands.ts +0 -270
  153. package/src/lib/constants.ts +0 -3
  154. package/src/lib/extension-connection.ts +0 -181
  155. package/src/lib/focusables.ts +0 -7
  156. package/src/lib/fs.ts +0 -533
  157. package/src/lib/markdown/code-block.tsx +0 -63
  158. package/src/lib/markdown/index.ts +0 -4
  159. package/src/lib/markdown/link.tsx +0 -19
  160. package/src/lib/markdown/markdown.tsx +0 -372
  161. package/src/lib/markdown/types.ts +0 -15
  162. package/src/lib/mcpCommandHandler.ts +0 -121
  163. package/src/lib/mcpManagement.ts +0 -44
  164. package/src/lib/path-paste.ts +0 -185
  165. package/src/lib/path.ts +0 -179
  166. package/src/lib/queries.ts +0 -15
  167. package/src/lib/standaloneMcpService.ts +0 -688
  168. package/src/lib/status-utils.ts +0 -237
  169. package/src/lib/test-utils.tsx +0 -72
  170. package/src/lib/text-buffer.ts +0 -2415
  171. package/src/lib/text-utils.ts +0 -272
  172. package/src/lib/timing.ts +0 -63
  173. package/src/lib/types.ts +0 -295
  174. package/src/lib/utils.ts +0 -182
  175. package/src/lib/vim-buffer-actions.ts +0 -732
  176. package/src/providers/agent.tsx +0 -1063
  177. package/src/providers/api-client.tsx +0 -43
  178. package/src/services/logger.ts +0 -85
  179. package/src/terminal/detection.ts +0 -187
  180. package/src/terminal/exit.ts +0 -279
  181. package/src/terminal/notification.ts +0 -83
  182. package/src/terminal/progress.ts +0 -201
  183. package/src/terminal/setup.ts +0 -797
  184. package/src/terminal/types.ts +0 -51
  185. package/src/theme/context.tsx +0 -57
  186. package/src/theme/index.ts +0 -4
  187. package/src/theme/themed.tsx +0 -35
  188. package/src/theme/themes.json +0 -546
  189. package/src/theme/types.ts +0 -110
  190. package/src/tools/types.ts +0 -59
  191. package/src/tools/utils/__tests__/zod-coercion.test.ts +0 -33
  192. package/src/tools/utils/tool-ui-components.tsx +0 -649
  193. package/src/tools/utils/zod-coercion.ts +0 -35
  194. package/tsconfig.json +0 -16
  195. package/tsconfig.node.json +0 -29
  196. package/tsconfig.test.json +0 -27
  197. package/tsdown.config.ts +0 -17
  198. package/vitest.config.ts +0 -76
@@ -1,732 +0,0 @@
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
- }