@google/gemini-cli 0.1.14 → 0.1.15-nightly.250801.6f7beb41

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 (161) hide show
  1. package/README.md +28 -0
  2. package/dist/google-gemini-cli-0.1.15.tgz +0 -0
  3. package/dist/package.json +2 -2
  4. package/dist/src/config/config.d.ts +4 -2
  5. package/dist/src/config/config.js +25 -12
  6. package/dist/src/config/config.js.map +1 -1
  7. package/dist/src/config/extension.d.ts +1 -0
  8. package/dist/src/config/extension.js +4 -0
  9. package/dist/src/config/extension.js.map +1 -1
  10. package/dist/src/config/settings.d.ts +4 -0
  11. package/dist/src/config/settings.js.map +1 -1
  12. package/dist/src/gemini.d.ts +1 -0
  13. package/dist/src/gemini.js +34 -15
  14. package/dist/src/gemini.js.map +1 -1
  15. package/dist/src/generated/git-commit.d.ts +1 -1
  16. package/dist/src/generated/git-commit.js +1 -1
  17. package/dist/src/nonInteractiveCli.js +14 -37
  18. package/dist/src/nonInteractiveCli.js.map +1 -1
  19. package/dist/src/services/BuiltinCommandLoader.js +9 -0
  20. package/dist/src/services/BuiltinCommandLoader.js.map +1 -1
  21. package/dist/src/services/CommandService.d.ts +8 -4
  22. package/dist/src/services/CommandService.js +24 -8
  23. package/dist/src/services/CommandService.js.map +1 -1
  24. package/dist/src/services/FileCommandLoader.d.ts +15 -3
  25. package/dist/src/services/FileCommandLoader.js +94 -42
  26. package/dist/src/services/FileCommandLoader.js.map +1 -1
  27. package/dist/src/services/McpPromptLoader.d.ts +25 -0
  28. package/dist/src/services/McpPromptLoader.js +192 -0
  29. package/dist/src/services/McpPromptLoader.js.map +1 -0
  30. package/dist/src/services/prompt-processors/shellProcessor.d.ts +32 -0
  31. package/dist/src/services/prompt-processors/shellProcessor.js +77 -0
  32. package/dist/src/services/prompt-processors/shellProcessor.js.map +1 -0
  33. package/dist/src/services/prompt-processors/types.d.ts +4 -0
  34. package/dist/src/services/prompt-processors/types.js +4 -0
  35. package/dist/src/services/prompt-processors/types.js.map +1 -1
  36. package/dist/src/ui/App.js +89 -46
  37. package/dist/src/ui/App.js.map +1 -1
  38. package/dist/src/ui/commands/chatCommand.js +39 -1
  39. package/dist/src/ui/commands/chatCommand.js.map +1 -1
  40. package/dist/src/ui/commands/directoryCommand.d.ts +8 -0
  41. package/dist/src/ui/commands/directoryCommand.js +116 -0
  42. package/dist/src/ui/commands/directoryCommand.js.map +1 -0
  43. package/dist/src/ui/commands/ideCommand.js +101 -105
  44. package/dist/src/ui/commands/ideCommand.js.map +1 -1
  45. package/dist/src/ui/commands/initCommand.d.ts +7 -0
  46. package/dist/src/ui/commands/initCommand.js +76 -0
  47. package/dist/src/ui/commands/initCommand.js.map +1 -0
  48. package/dist/src/ui/commands/mcpCommand.js +53 -8
  49. package/dist/src/ui/commands/mcpCommand.js.map +1 -1
  50. package/dist/src/ui/commands/memoryCommand.js +2 -1
  51. package/dist/src/ui/commands/memoryCommand.js.map +1 -1
  52. package/dist/src/ui/commands/setupGithubCommand.d.ts +7 -0
  53. package/dist/src/ui/commands/setupGithubCommand.js +49 -0
  54. package/dist/src/ui/commands/setupGithubCommand.js.map +1 -0
  55. package/dist/src/ui/commands/types.d.ts +20 -2
  56. package/dist/src/ui/commands/types.js +1 -0
  57. package/dist/src/ui/commands/types.js.map +1 -1
  58. package/dist/src/ui/commands/vimCommand.d.ts +7 -0
  59. package/dist/src/ui/commands/vimCommand.js +23 -0
  60. package/dist/src/ui/commands/vimCommand.js.map +1 -0
  61. package/dist/src/ui/components/ContextSummaryDisplay.d.ts +2 -2
  62. package/dist/src/ui/components/ContextSummaryDisplay.js +8 -8
  63. package/dist/src/ui/components/ContextSummaryDisplay.js.map +1 -1
  64. package/dist/src/ui/components/DebugProfiler.d.ts +6 -0
  65. package/dist/src/ui/components/DebugProfiler.js +26 -0
  66. package/dist/src/ui/components/DebugProfiler.js.map +1 -0
  67. package/dist/src/ui/components/Footer.d.ts +1 -0
  68. package/dist/src/ui/components/Footer.js +4 -3
  69. package/dist/src/ui/components/Footer.js.map +1 -1
  70. package/dist/src/ui/components/Header.js +1 -1
  71. package/dist/src/ui/components/Header.js.map +1 -1
  72. package/dist/src/ui/components/Help.js +2 -2
  73. package/dist/src/ui/components/Help.js.map +1 -1
  74. package/dist/src/ui/components/IDEContextDetailDisplay.d.ts +4 -3
  75. package/dist/src/ui/components/IDEContextDetailDisplay.js +6 -13
  76. package/dist/src/ui/components/IDEContextDetailDisplay.js.map +1 -1
  77. package/dist/src/ui/components/InputPrompt.d.ts +2 -0
  78. package/dist/src/ui/components/InputPrompt.js +13 -2
  79. package/dist/src/ui/components/InputPrompt.js.map +1 -1
  80. package/dist/src/ui/components/ShellConfirmationDialog.d.ts +15 -0
  81. package/dist/src/ui/components/ShellConfirmationDialog.js +44 -0
  82. package/dist/src/ui/components/ShellConfirmationDialog.js.map +1 -0
  83. package/dist/src/ui/components/Tips.js +1 -1
  84. package/dist/src/ui/components/Tips.js.map +1 -1
  85. package/dist/src/ui/components/messages/ToolConfirmationMessage.js +15 -4
  86. package/dist/src/ui/components/messages/ToolConfirmationMessage.js.map +1 -1
  87. package/dist/src/ui/components/messages/UserMessage.js +4 -1
  88. package/dist/src/ui/components/messages/UserMessage.js.map +1 -1
  89. package/dist/src/ui/components/shared/text-buffer.d.ts +270 -2
  90. package/dist/src/ui/components/shared/text-buffer.js +410 -70
  91. package/dist/src/ui/components/shared/text-buffer.js.map +1 -1
  92. package/dist/src/ui/components/shared/vim-buffer-actions.d.ts +72 -0
  93. package/dist/src/ui/components/shared/vim-buffer-actions.js +565 -0
  94. package/dist/src/ui/components/shared/vim-buffer-actions.js.map +1 -0
  95. package/dist/src/ui/contexts/VimModeContext.d.ts +19 -0
  96. package/dist/src/ui/contexts/VimModeContext.js +48 -0
  97. package/dist/src/ui/contexts/VimModeContext.js.map +1 -0
  98. package/dist/src/ui/editors/editorSettingsManager.js +1 -0
  99. package/dist/src/ui/editors/editorSettingsManager.js.map +1 -1
  100. package/dist/src/ui/hooks/atCommandProcessor.js +56 -48
  101. package/dist/src/ui/hooks/atCommandProcessor.js.map +1 -1
  102. package/dist/src/ui/hooks/shellCommandProcessor.d.ts +1 -0
  103. package/dist/src/ui/hooks/shellCommandProcessor.js +139 -200
  104. package/dist/src/ui/hooks/shellCommandProcessor.js.map +1 -1
  105. package/dist/src/ui/hooks/slashCommandProcessor.d.ts +7 -3
  106. package/dist/src/ui/hooks/slashCommandProcessor.js +198 -127
  107. package/dist/src/ui/hooks/slashCommandProcessor.js.map +1 -1
  108. package/dist/src/ui/hooks/useCompletion.d.ts +1 -1
  109. package/dist/src/ui/hooks/useCompletion.js +108 -76
  110. package/dist/src/ui/hooks/useCompletion.js.map +1 -1
  111. package/dist/src/ui/hooks/useConsoleMessages.js +53 -37
  112. package/dist/src/ui/hooks/useConsoleMessages.js.map +1 -1
  113. package/dist/src/ui/hooks/useGeminiStream.js +5 -2
  114. package/dist/src/ui/hooks/useGeminiStream.js.map +1 -1
  115. package/dist/src/ui/hooks/useKeypress.js +5 -2
  116. package/dist/src/ui/hooks/useKeypress.js.map +1 -1
  117. package/dist/src/ui/hooks/usePhraseCycler.js +1 -0
  118. package/dist/src/ui/hooks/usePhraseCycler.js.map +1 -1
  119. package/dist/src/ui/hooks/useReactToolScheduler.js +0 -1
  120. package/dist/src/ui/hooks/useReactToolScheduler.js.map +1 -1
  121. package/dist/src/ui/hooks/vim.d.ts +28 -0
  122. package/dist/src/ui/hooks/vim.js +630 -0
  123. package/dist/src/ui/hooks/vim.js.map +1 -0
  124. package/dist/src/ui/themes/theme-manager.js +10 -1
  125. package/dist/src/ui/themes/theme-manager.js.map +1 -1
  126. package/dist/src/ui/themes/theme.d.ts +1 -0
  127. package/dist/src/ui/themes/theme.js +19 -4
  128. package/dist/src/ui/themes/theme.js.map +1 -1
  129. package/dist/src/ui/utils/textUtils.d.ts +0 -8
  130. package/dist/src/ui/utils/textUtils.js +0 -22
  131. package/dist/src/ui/utils/textUtils.js.map +1 -1
  132. package/dist/src/ui/utils/updateCheck.d.ts +7 -1
  133. package/dist/src/ui/utils/updateCheck.js +9 -3
  134. package/dist/src/ui/utils/updateCheck.js.map +1 -1
  135. package/dist/src/utils/events.d.ts +11 -0
  136. package/dist/src/utils/events.js +13 -0
  137. package/dist/src/utils/events.js.map +1 -0
  138. package/dist/src/utils/gitUtils.d.ts +10 -0
  139. package/dist/src/utils/gitUtils.js +24 -0
  140. package/dist/src/utils/gitUtils.js.map +1 -0
  141. package/dist/src/utils/handleAutoUpdate.d.ts +10 -0
  142. package/dist/src/utils/handleAutoUpdate.js +97 -0
  143. package/dist/src/utils/handleAutoUpdate.js.map +1 -0
  144. package/dist/src/utils/installationInfo.d.ts +23 -0
  145. package/dist/src/utils/installationInfo.js +154 -0
  146. package/dist/src/utils/installationInfo.js.map +1 -0
  147. package/dist/src/utils/sandbox-macos-permissive-closed.sb +6 -0
  148. package/dist/src/utils/sandbox-macos-permissive-open.sb +6 -0
  149. package/dist/src/utils/sandbox-macos-permissive-proxied.sb +6 -0
  150. package/dist/src/utils/sandbox-macos-restrictive-closed.sb +6 -0
  151. package/dist/src/utils/sandbox-macos-restrictive-open.sb +6 -0
  152. package/dist/src/utils/sandbox-macos-restrictive-proxied.sb +6 -0
  153. package/dist/src/utils/sandbox.d.ts +2 -2
  154. package/dist/src/utils/sandbox.js +29 -10
  155. package/dist/src/utils/sandbox.js.map +1 -1
  156. package/dist/src/utils/updateEventEmitter.d.ts +11 -0
  157. package/dist/src/utils/updateEventEmitter.js +12 -0
  158. package/dist/src/utils/updateEventEmitter.js.map +1 -0
  159. package/dist/tsconfig.tsbuildinfo +1 -1
  160. package/package.json +3 -3
  161. package/dist/google-gemini-cli-0.1.13.tgz +0 -0
@@ -12,6 +12,7 @@ import { useState, useCallback, useEffect, useMemo, useReducer } from 'react';
12
12
  import stringWidth from 'string-width';
13
13
  import { unescapePath } from '@google/gemini-cli-core';
14
14
  import { toCodePoints, cpLen, cpSlice } from '../../utils/textUtils.js';
15
+ import { handleVimAction } from './vim-buffer-actions.js';
15
16
  // Simple helper for word‑wise ops.
16
17
  function isWordChar(ch) {
17
18
  if (ch === undefined) {
@@ -19,6 +20,204 @@ function isWordChar(ch) {
19
20
  }
20
21
  return !/[\s,.;!?]/.test(ch);
21
22
  }
23
+ // Vim-specific word boundary functions
24
+ export const findNextWordStart = (text, currentOffset) => {
25
+ let i = currentOffset;
26
+ if (i >= text.length)
27
+ return i;
28
+ const currentChar = text[i];
29
+ // Skip current word/sequence based on character type
30
+ if (/\w/.test(currentChar)) {
31
+ // Skip current word characters
32
+ while (i < text.length && /\w/.test(text[i])) {
33
+ i++;
34
+ }
35
+ }
36
+ else if (!/\s/.test(currentChar)) {
37
+ // Skip current non-word, non-whitespace characters (like "/", ".", etc.)
38
+ while (i < text.length && !/\w/.test(text[i]) && !/\s/.test(text[i])) {
39
+ i++;
40
+ }
41
+ }
42
+ // Skip whitespace
43
+ while (i < text.length && /\s/.test(text[i])) {
44
+ i++;
45
+ }
46
+ // If we reached the end of text and there's no next word,
47
+ // vim behavior for dw is to delete to the end of the current word
48
+ if (i >= text.length) {
49
+ // Go back to find the end of the last word
50
+ let endOfLastWord = text.length - 1;
51
+ while (endOfLastWord >= 0 && /\s/.test(text[endOfLastWord])) {
52
+ endOfLastWord--;
53
+ }
54
+ // For dw on last word, return position AFTER the last character to delete entire word
55
+ return Math.max(currentOffset + 1, endOfLastWord + 1);
56
+ }
57
+ return i;
58
+ };
59
+ export const findPrevWordStart = (text, currentOffset) => {
60
+ let i = currentOffset;
61
+ // If at beginning of text, return current position
62
+ if (i <= 0) {
63
+ return currentOffset;
64
+ }
65
+ // Move back one character to start searching
66
+ i--;
67
+ // Skip whitespace moving backwards
68
+ while (i >= 0 && (text[i] === ' ' || text[i] === '\t' || text[i] === '\n')) {
69
+ i--;
70
+ }
71
+ if (i < 0) {
72
+ return 0; // Reached beginning of text
73
+ }
74
+ const charAtI = text[i];
75
+ if (/\w/.test(charAtI)) {
76
+ // We're in a word, move to its beginning
77
+ while (i >= 0 && /\w/.test(text[i])) {
78
+ i--;
79
+ }
80
+ return i + 1; // Return first character of word
81
+ }
82
+ else {
83
+ // We're in punctuation, move to its beginning
84
+ while (i >= 0 &&
85
+ !/\w/.test(text[i]) &&
86
+ text[i] !== ' ' &&
87
+ text[i] !== '\t' &&
88
+ text[i] !== '\n') {
89
+ i--;
90
+ }
91
+ return i + 1; // Return first character of punctuation sequence
92
+ }
93
+ };
94
+ export const findWordEnd = (text, currentOffset) => {
95
+ let i = currentOffset;
96
+ // If we're already at the end of a word, advance to next word
97
+ if (i < text.length &&
98
+ /\w/.test(text[i]) &&
99
+ (i + 1 >= text.length || !/\w/.test(text[i + 1]))) {
100
+ // We're at the end of a word, move forward to find next word
101
+ i++;
102
+ // Skip whitespace/punctuation to find next word
103
+ while (i < text.length && !/\w/.test(text[i])) {
104
+ i++;
105
+ }
106
+ }
107
+ // If we're not on a word character, find the next word
108
+ if (i < text.length && !/\w/.test(text[i])) {
109
+ while (i < text.length && !/\w/.test(text[i])) {
110
+ i++;
111
+ }
112
+ }
113
+ // Move to end of current word
114
+ while (i < text.length && /\w/.test(text[i])) {
115
+ i++;
116
+ }
117
+ // Move back one to be on the last character of the word
118
+ return Math.max(currentOffset, i - 1);
119
+ };
120
+ // Helper functions for vim operations
121
+ export const getOffsetFromPosition = (row, col, lines) => {
122
+ let offset = 0;
123
+ for (let i = 0; i < row; i++) {
124
+ offset += lines[i].length + 1; // +1 for newline
125
+ }
126
+ offset += col;
127
+ return offset;
128
+ };
129
+ export const getPositionFromOffsets = (startOffset, endOffset, lines) => {
130
+ let offset = 0;
131
+ let startRow = 0;
132
+ let startCol = 0;
133
+ let endRow = 0;
134
+ let endCol = 0;
135
+ // Find start position
136
+ for (let i = 0; i < lines.length; i++) {
137
+ const lineLength = lines[i].length + 1; // +1 for newline
138
+ if (offset + lineLength > startOffset) {
139
+ startRow = i;
140
+ startCol = startOffset - offset;
141
+ break;
142
+ }
143
+ offset += lineLength;
144
+ }
145
+ // Find end position
146
+ offset = 0;
147
+ for (let i = 0; i < lines.length; i++) {
148
+ const lineLength = lines[i].length + (i < lines.length - 1 ? 1 : 0); // +1 for newline except last line
149
+ if (offset + lineLength >= endOffset) {
150
+ endRow = i;
151
+ endCol = endOffset - offset;
152
+ break;
153
+ }
154
+ offset += lineLength;
155
+ }
156
+ return { startRow, startCol, endRow, endCol };
157
+ };
158
+ export const getLineRangeOffsets = (startRow, lineCount, lines) => {
159
+ let startOffset = 0;
160
+ // Calculate start offset
161
+ for (let i = 0; i < startRow; i++) {
162
+ startOffset += lines[i].length + 1; // +1 for newline
163
+ }
164
+ // Calculate end offset
165
+ let endOffset = startOffset;
166
+ for (let i = 0; i < lineCount; i++) {
167
+ const lineIndex = startRow + i;
168
+ if (lineIndex < lines.length) {
169
+ endOffset += lines[lineIndex].length;
170
+ if (lineIndex < lines.length - 1) {
171
+ endOffset += 1; // +1 for newline
172
+ }
173
+ }
174
+ }
175
+ return { startOffset, endOffset };
176
+ };
177
+ export const replaceRangeInternal = (state, startRow, startCol, endRow, endCol, text) => {
178
+ const currentLine = (row) => state.lines[row] || '';
179
+ const currentLineLen = (row) => cpLen(currentLine(row));
180
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
181
+ if (startRow > endRow ||
182
+ (startRow === endRow && startCol > endCol) ||
183
+ startRow < 0 ||
184
+ startCol < 0 ||
185
+ endRow >= state.lines.length ||
186
+ (endRow < state.lines.length && endCol > currentLineLen(endRow))) {
187
+ return state; // Invalid range
188
+ }
189
+ const newLines = [...state.lines];
190
+ const sCol = clamp(startCol, 0, currentLineLen(startRow));
191
+ const eCol = clamp(endCol, 0, currentLineLen(endRow));
192
+ const prefix = cpSlice(currentLine(startRow), 0, sCol);
193
+ const suffix = cpSlice(currentLine(endRow), eCol);
194
+ const normalisedReplacement = text
195
+ .replace(/\r\n/g, '\n')
196
+ .replace(/\r/g, '\n');
197
+ const replacementParts = normalisedReplacement.split('\n');
198
+ // The combined first line of the new text
199
+ const firstLine = prefix + replacementParts[0];
200
+ if (replacementParts.length === 1) {
201
+ // No newlines in replacement: combine prefix, replacement, and suffix on one line.
202
+ newLines.splice(startRow, endRow - startRow + 1, firstLine + suffix);
203
+ }
204
+ else {
205
+ // Newlines in replacement: create new lines.
206
+ const lastLine = replacementParts[replacementParts.length - 1] + suffix;
207
+ const middleLines = replacementParts.slice(1, -1);
208
+ newLines.splice(startRow, endRow - startRow + 1, firstLine, ...middleLines, lastLine);
209
+ }
210
+ const finalCursorRow = startRow + replacementParts.length - 1;
211
+ const finalCursorCol = (replacementParts.length > 1 ? 0 : sCol) +
212
+ cpLen(replacementParts[replacementParts.length - 1]);
213
+ return {
214
+ ...state,
215
+ lines: newLines,
216
+ cursorRow: Math.min(Math.max(finalCursorRow, 0), newLines.length - 1),
217
+ cursorCol: Math.max(0, Math.min(finalCursorCol, cpLen(newLines[finalCursorRow] || ''))),
218
+ preferredCol: null,
219
+ };
220
+ };
22
221
  /**
23
222
  * Strip characters that can break terminal rendering.
24
223
  *
@@ -106,6 +305,24 @@ export function offsetToLogicalPos(text, offset) {
106
305
  }
107
306
  return [row, col];
108
307
  }
308
+ /**
309
+ * Converts logical row/col position to absolute text offset
310
+ * Inverse operation of offsetToLogicalPos
311
+ */
312
+ export function logicalPosToOffset(lines, row, col) {
313
+ let offset = 0;
314
+ // Clamp row to valid range
315
+ const actualRow = Math.min(row, lines.length - 1);
316
+ // Add lengths of all lines before the target row
317
+ for (let i = 0; i < actualRow; i++) {
318
+ offset += cpLen(lines[i]) + 1; // +1 for newline
319
+ }
320
+ // Add column offset within the target row
321
+ if (actualRow >= 0 && actualRow < lines.length) {
322
+ offset += Math.min(col, cpLen(lines[actualRow]));
323
+ }
324
+ return offset;
325
+ }
109
326
  // Helper to calculate visual lines and map cursor positions
110
327
  function calculateVisualLayout(logicalLines, logicalCursor, viewportWidth) {
111
328
  const visualLines = [];
@@ -279,26 +496,27 @@ function calculateVisualLayout(logicalLines, logicalCursor, viewportWidth) {
279
496
  };
280
497
  }
281
498
  const historyLimit = 100;
282
- export function textBufferReducer(state, action) {
283
- const pushUndo = (currentState) => {
284
- const snapshot = {
285
- lines: [...currentState.lines],
286
- cursorRow: currentState.cursorRow,
287
- cursorCol: currentState.cursorCol,
288
- };
289
- const newStack = [...currentState.undoStack, snapshot];
290
- if (newStack.length > historyLimit) {
291
- newStack.shift();
292
- }
293
- return { ...currentState, undoStack: newStack, redoStack: [] };
499
+ export const pushUndo = (currentState) => {
500
+ const snapshot = {
501
+ lines: [...currentState.lines],
502
+ cursorRow: currentState.cursorRow,
503
+ cursorCol: currentState.cursorCol,
294
504
  };
505
+ const newStack = [...currentState.undoStack, snapshot];
506
+ if (newStack.length > historyLimit) {
507
+ newStack.shift();
508
+ }
509
+ return { ...currentState, undoStack: newStack, redoStack: [] };
510
+ };
511
+ export function textBufferReducer(state, action) {
512
+ const pushUndoLocal = pushUndo;
295
513
  const currentLine = (r) => state.lines[r] ?? '';
296
514
  const currentLineLen = (r) => cpLen(currentLine(r));
297
515
  switch (action.type) {
298
516
  case 'set_text': {
299
517
  let nextState = state;
300
518
  if (action.pushToUndo !== false) {
301
- nextState = pushUndo(state);
519
+ nextState = pushUndoLocal(state);
302
520
  }
303
521
  const newContentLines = action.payload
304
522
  .replace(/\r\n?/g, '\n')
@@ -314,7 +532,7 @@ export function textBufferReducer(state, action) {
314
532
  };
315
533
  }
316
534
  case 'insert': {
317
- const nextState = pushUndo(state);
535
+ const nextState = pushUndoLocal(state);
318
536
  const newLines = [...nextState.lines];
319
537
  let newCursorRow = nextState.cursorRow;
320
538
  let newCursorCol = nextState.cursorCol;
@@ -346,7 +564,7 @@ export function textBufferReducer(state, action) {
346
564
  };
347
565
  }
348
566
  case 'backspace': {
349
- const nextState = pushUndo(state);
567
+ const nextState = pushUndoLocal(state);
350
568
  const newLines = [...nextState.lines];
351
569
  let newCursorRow = nextState.cursorRow;
352
570
  let newCursorCol = nextState.cursorCol;
@@ -523,7 +741,7 @@ export function textBufferReducer(state, action) {
523
741
  const { cursorRow, cursorCol, lines } = state;
524
742
  const lineContent = currentLine(cursorRow);
525
743
  if (cursorCol < currentLineLen(cursorRow)) {
526
- const nextState = pushUndo(state);
744
+ const nextState = pushUndoLocal(state);
527
745
  const newLines = [...nextState.lines];
528
746
  newLines[cursorRow] =
529
747
  cpSlice(lineContent, 0, cursorCol) +
@@ -531,7 +749,7 @@ export function textBufferReducer(state, action) {
531
749
  return { ...nextState, lines: newLines, preferredCol: null };
532
750
  }
533
751
  else if (cursorRow < lines.length - 1) {
534
- const nextState = pushUndo(state);
752
+ const nextState = pushUndoLocal(state);
535
753
  const nextLineContent = currentLine(cursorRow + 1);
536
754
  const newLines = [...nextState.lines];
537
755
  newLines[cursorRow] = lineContent + nextLineContent;
@@ -546,7 +764,7 @@ export function textBufferReducer(state, action) {
546
764
  return state;
547
765
  if (cursorCol === 0) {
548
766
  // Act as a backspace
549
- const nextState = pushUndo(state);
767
+ const nextState = pushUndoLocal(state);
550
768
  const prevLineContent = currentLine(cursorRow - 1);
551
769
  const currentLineContentVal = currentLine(cursorRow);
552
770
  const newCol = cpLen(prevLineContent);
@@ -561,7 +779,7 @@ export function textBufferReducer(state, action) {
561
779
  preferredCol: null,
562
780
  };
563
781
  }
564
- const nextState = pushUndo(state);
782
+ const nextState = pushUndoLocal(state);
565
783
  const lineContent = currentLine(cursorRow);
566
784
  const arr = toCodePoints(lineContent);
567
785
  let start = cursorCol;
@@ -599,14 +817,14 @@ export function textBufferReducer(state, action) {
599
817
  return state;
600
818
  if (cursorCol >= arr.length) {
601
819
  // Act as a delete
602
- const nextState = pushUndo(state);
820
+ const nextState = pushUndoLocal(state);
603
821
  const nextLineContent = currentLine(cursorRow + 1);
604
822
  const newLines = [...nextState.lines];
605
823
  newLines[cursorRow] = lineContent + nextLineContent;
606
824
  newLines.splice(cursorRow + 1, 1);
607
825
  return { ...nextState, lines: newLines, preferredCol: null };
608
826
  }
609
- const nextState = pushUndo(state);
827
+ const nextState = pushUndoLocal(state);
610
828
  let end = cursorCol;
611
829
  while (end < arr.length && !isWordChar(arr[end]))
612
830
  end++;
@@ -621,14 +839,14 @@ export function textBufferReducer(state, action) {
621
839
  const { cursorRow, cursorCol, lines } = state;
622
840
  const lineContent = currentLine(cursorRow);
623
841
  if (cursorCol < currentLineLen(cursorRow)) {
624
- const nextState = pushUndo(state);
842
+ const nextState = pushUndoLocal(state);
625
843
  const newLines = [...nextState.lines];
626
844
  newLines[cursorRow] = cpSlice(lineContent, 0, cursorCol);
627
845
  return { ...nextState, lines: newLines };
628
846
  }
629
847
  else if (cursorRow < lines.length - 1) {
630
848
  // Act as a delete
631
- const nextState = pushUndo(state);
849
+ const nextState = pushUndoLocal(state);
632
850
  const nextLineContent = currentLine(cursorRow + 1);
633
851
  const newLines = [...nextState.lines];
634
852
  newLines[cursorRow] = lineContent + nextLineContent;
@@ -640,7 +858,7 @@ export function textBufferReducer(state, action) {
640
858
  case 'kill_line_left': {
641
859
  const { cursorRow, cursorCol } = state;
642
860
  if (cursorCol > 0) {
643
- const nextState = pushUndo(state);
861
+ const nextState = pushUndoLocal(state);
644
862
  const lineContent = currentLine(cursorRow);
645
863
  const newLines = [...nextState.lines];
646
864
  newLines[cursorRow] = cpSlice(lineContent, cursorCol);
@@ -687,51 +905,8 @@ export function textBufferReducer(state, action) {
687
905
  }
688
906
  case 'replace_range': {
689
907
  const { startRow, startCol, endRow, endCol, text } = action.payload;
690
- if (startRow > endRow ||
691
- (startRow === endRow && startCol > endCol) ||
692
- startRow < 0 ||
693
- startCol < 0 ||
694
- endRow >= state.lines.length ||
695
- (endRow < state.lines.length && endCol > currentLineLen(endRow))) {
696
- return state; // Invalid range
697
- }
698
- const nextState = pushUndo(state);
699
- const newLines = [...nextState.lines];
700
- const sCol = clamp(startCol, 0, currentLineLen(startRow));
701
- const eCol = clamp(endCol, 0, currentLineLen(endRow));
702
- const prefix = cpSlice(currentLine(startRow), 0, sCol);
703
- const suffix = cpSlice(currentLine(endRow), eCol);
704
- const normalisedReplacement = text
705
- .replace(/\r\n/g, '\n')
706
- .replace(/\r/g, '\n');
707
- const replacementParts = normalisedReplacement.split('\n');
708
- // Replace the content
709
- if (startRow === endRow) {
710
- newLines[startRow] = prefix + normalisedReplacement + suffix;
711
- }
712
- else {
713
- const firstLine = prefix + replacementParts[0];
714
- if (replacementParts.length === 1) {
715
- // Single line of replacement text, but spanning multiple original lines
716
- newLines.splice(startRow, endRow - startRow + 1, firstLine + suffix);
717
- }
718
- else {
719
- // Multi-line replacement text
720
- const lastLine = replacementParts[replacementParts.length - 1] + suffix;
721
- const middleLines = replacementParts.slice(1, -1);
722
- newLines.splice(startRow, endRow - startRow + 1, firstLine, ...middleLines, lastLine);
723
- }
724
- }
725
- const finalCursorRow = startRow + replacementParts.length - 1;
726
- const finalCursorCol = (replacementParts.length > 1 ? 0 : sCol) +
727
- cpLen(replacementParts[replacementParts.length - 1]);
728
- return {
729
- ...nextState,
730
- lines: newLines,
731
- cursorRow: finalCursorRow,
732
- cursorCol: finalCursorCol,
733
- preferredCol: null,
734
- };
908
+ const nextState = pushUndoLocal(state);
909
+ return replaceRangeInternal(nextState, startRow, startCol, endRow, endCol, text);
735
910
  }
736
911
  case 'move_to_offset': {
737
912
  const { offset } = action.payload;
@@ -744,8 +919,42 @@ export function textBufferReducer(state, action) {
744
919
  };
745
920
  }
746
921
  case 'create_undo_snapshot': {
747
- return pushUndo(state);
922
+ return pushUndoLocal(state);
748
923
  }
924
+ // Vim-specific operations
925
+ case 'vim_delete_word_forward':
926
+ case 'vim_delete_word_backward':
927
+ case 'vim_delete_word_end':
928
+ case 'vim_change_word_forward':
929
+ case 'vim_change_word_backward':
930
+ case 'vim_change_word_end':
931
+ case 'vim_delete_line':
932
+ case 'vim_change_line':
933
+ case 'vim_delete_to_end_of_line':
934
+ case 'vim_change_to_end_of_line':
935
+ case 'vim_change_movement':
936
+ case 'vim_move_left':
937
+ case 'vim_move_right':
938
+ case 'vim_move_up':
939
+ case 'vim_move_down':
940
+ case 'vim_move_word_forward':
941
+ case 'vim_move_word_backward':
942
+ case 'vim_move_word_end':
943
+ case 'vim_delete_char':
944
+ case 'vim_insert_at_cursor':
945
+ case 'vim_append_at_cursor':
946
+ case 'vim_open_line_below':
947
+ case 'vim_open_line_above':
948
+ case 'vim_append_at_line_end':
949
+ case 'vim_insert_at_line_start':
950
+ case 'vim_move_to_line_start':
951
+ case 'vim_move_to_line_end':
952
+ case 'vim_move_to_first_nonwhitespace':
953
+ case 'vim_move_to_first_line':
954
+ case 'vim_move_to_last_line':
955
+ case 'vim_move_to_line':
956
+ case 'vim_escape_insert_mode':
957
+ return handleVimAction(state, action);
749
958
  default: {
750
959
  const exhaustiveCheck = action;
751
960
  console.error(`Unknown action encountered: ${exhaustiveCheck}`);
@@ -867,6 +1076,104 @@ export function useTextBuffer({ initialText = '', initialCursorOffset = 0, viewp
867
1076
  const killLineLeft = useCallback(() => {
868
1077
  dispatch({ type: 'kill_line_left' });
869
1078
  }, []);
1079
+ // Vim-specific operations
1080
+ const vimDeleteWordForward = useCallback((count) => {
1081
+ dispatch({ type: 'vim_delete_word_forward', payload: { count } });
1082
+ }, []);
1083
+ const vimDeleteWordBackward = useCallback((count) => {
1084
+ dispatch({ type: 'vim_delete_word_backward', payload: { count } });
1085
+ }, []);
1086
+ const vimDeleteWordEnd = useCallback((count) => {
1087
+ dispatch({ type: 'vim_delete_word_end', payload: { count } });
1088
+ }, []);
1089
+ const vimChangeWordForward = useCallback((count) => {
1090
+ dispatch({ type: 'vim_change_word_forward', payload: { count } });
1091
+ }, []);
1092
+ const vimChangeWordBackward = useCallback((count) => {
1093
+ dispatch({ type: 'vim_change_word_backward', payload: { count } });
1094
+ }, []);
1095
+ const vimChangeWordEnd = useCallback((count) => {
1096
+ dispatch({ type: 'vim_change_word_end', payload: { count } });
1097
+ }, []);
1098
+ const vimDeleteLine = useCallback((count) => {
1099
+ dispatch({ type: 'vim_delete_line', payload: { count } });
1100
+ }, []);
1101
+ const vimChangeLine = useCallback((count) => {
1102
+ dispatch({ type: 'vim_change_line', payload: { count } });
1103
+ }, []);
1104
+ const vimDeleteToEndOfLine = useCallback(() => {
1105
+ dispatch({ type: 'vim_delete_to_end_of_line' });
1106
+ }, []);
1107
+ const vimChangeToEndOfLine = useCallback(() => {
1108
+ dispatch({ type: 'vim_change_to_end_of_line' });
1109
+ }, []);
1110
+ const vimChangeMovement = useCallback((movement, count) => {
1111
+ dispatch({ type: 'vim_change_movement', payload: { movement, count } });
1112
+ }, []);
1113
+ // New vim navigation and operation methods
1114
+ const vimMoveLeft = useCallback((count) => {
1115
+ dispatch({ type: 'vim_move_left', payload: { count } });
1116
+ }, []);
1117
+ const vimMoveRight = useCallback((count) => {
1118
+ dispatch({ type: 'vim_move_right', payload: { count } });
1119
+ }, []);
1120
+ const vimMoveUp = useCallback((count) => {
1121
+ dispatch({ type: 'vim_move_up', payload: { count } });
1122
+ }, []);
1123
+ const vimMoveDown = useCallback((count) => {
1124
+ dispatch({ type: 'vim_move_down', payload: { count } });
1125
+ }, []);
1126
+ const vimMoveWordForward = useCallback((count) => {
1127
+ dispatch({ type: 'vim_move_word_forward', payload: { count } });
1128
+ }, []);
1129
+ const vimMoveWordBackward = useCallback((count) => {
1130
+ dispatch({ type: 'vim_move_word_backward', payload: { count } });
1131
+ }, []);
1132
+ const vimMoveWordEnd = useCallback((count) => {
1133
+ dispatch({ type: 'vim_move_word_end', payload: { count } });
1134
+ }, []);
1135
+ const vimDeleteChar = useCallback((count) => {
1136
+ dispatch({ type: 'vim_delete_char', payload: { count } });
1137
+ }, []);
1138
+ const vimInsertAtCursor = useCallback(() => {
1139
+ dispatch({ type: 'vim_insert_at_cursor' });
1140
+ }, []);
1141
+ const vimAppendAtCursor = useCallback(() => {
1142
+ dispatch({ type: 'vim_append_at_cursor' });
1143
+ }, []);
1144
+ const vimOpenLineBelow = useCallback(() => {
1145
+ dispatch({ type: 'vim_open_line_below' });
1146
+ }, []);
1147
+ const vimOpenLineAbove = useCallback(() => {
1148
+ dispatch({ type: 'vim_open_line_above' });
1149
+ }, []);
1150
+ const vimAppendAtLineEnd = useCallback(() => {
1151
+ dispatch({ type: 'vim_append_at_line_end' });
1152
+ }, []);
1153
+ const vimInsertAtLineStart = useCallback(() => {
1154
+ dispatch({ type: 'vim_insert_at_line_start' });
1155
+ }, []);
1156
+ const vimMoveToLineStart = useCallback(() => {
1157
+ dispatch({ type: 'vim_move_to_line_start' });
1158
+ }, []);
1159
+ const vimMoveToLineEnd = useCallback(() => {
1160
+ dispatch({ type: 'vim_move_to_line_end' });
1161
+ }, []);
1162
+ const vimMoveToFirstNonWhitespace = useCallback(() => {
1163
+ dispatch({ type: 'vim_move_to_first_nonwhitespace' });
1164
+ }, []);
1165
+ const vimMoveToFirstLine = useCallback(() => {
1166
+ dispatch({ type: 'vim_move_to_first_line' });
1167
+ }, []);
1168
+ const vimMoveToLastLine = useCallback(() => {
1169
+ dispatch({ type: 'vim_move_to_last_line' });
1170
+ }, []);
1171
+ const vimMoveToLine = useCallback((lineNumber) => {
1172
+ dispatch({ type: 'vim_move_to_line', payload: { lineNumber } });
1173
+ }, []);
1174
+ const vimEscapeInsertMode = useCallback(() => {
1175
+ dispatch({ type: 'vim_escape_insert_mode' });
1176
+ }, []);
870
1177
  const openInExternalEditor = useCallback(async (opts = {}) => {
871
1178
  const editor = opts.editor ??
872
1179
  process.env['VISUAL'] ??
@@ -1005,6 +1312,39 @@ export function useTextBuffer({ initialText = '', initialCursorOffset = 0, viewp
1005
1312
  killLineLeft,
1006
1313
  handleInput,
1007
1314
  openInExternalEditor,
1315
+ // Vim-specific operations
1316
+ vimDeleteWordForward,
1317
+ vimDeleteWordBackward,
1318
+ vimDeleteWordEnd,
1319
+ vimChangeWordForward,
1320
+ vimChangeWordBackward,
1321
+ vimChangeWordEnd,
1322
+ vimDeleteLine,
1323
+ vimChangeLine,
1324
+ vimDeleteToEndOfLine,
1325
+ vimChangeToEndOfLine,
1326
+ vimChangeMovement,
1327
+ vimMoveLeft,
1328
+ vimMoveRight,
1329
+ vimMoveUp,
1330
+ vimMoveDown,
1331
+ vimMoveWordForward,
1332
+ vimMoveWordBackward,
1333
+ vimMoveWordEnd,
1334
+ vimDeleteChar,
1335
+ vimInsertAtCursor,
1336
+ vimAppendAtCursor,
1337
+ vimOpenLineBelow,
1338
+ vimOpenLineAbove,
1339
+ vimAppendAtLineEnd,
1340
+ vimInsertAtLineStart,
1341
+ vimMoveToLineStart,
1342
+ vimMoveToLineEnd,
1343
+ vimMoveToFirstNonWhitespace,
1344
+ vimMoveToFirstLine,
1345
+ vimMoveToLastLine,
1346
+ vimMoveToLine,
1347
+ vimEscapeInsertMode,
1008
1348
  };
1009
1349
  return returnValue;
1010
1350
  }