@vybestack/llxprt-code 0.1.14-nightly.250729.2076f7c6 → 0.1.15

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 (116) hide show
  1. package/README.md +53 -0
  2. package/dist/package.json +4 -5
  3. package/dist/src/config/extension.d.ts +1 -0
  4. package/dist/src/config/extension.js +4 -0
  5. package/dist/src/config/extension.js.map +1 -1
  6. package/dist/src/config/settings.d.ts +2 -1
  7. package/dist/src/config/settings.js.map +1 -1
  8. package/dist/src/gemini.d.ts +1 -0
  9. package/dist/src/gemini.js +35 -15
  10. package/dist/src/gemini.js.map +1 -1
  11. package/dist/src/generated/git-commit.d.ts +1 -1
  12. package/dist/src/generated/git-commit.js +1 -1
  13. package/dist/src/services/BuiltinCommandLoader.js +2 -0
  14. package/dist/src/services/BuiltinCommandLoader.js.map +1 -1
  15. package/dist/src/services/CommandService.d.ts +8 -4
  16. package/dist/src/services/CommandService.js +24 -8
  17. package/dist/src/services/CommandService.js.map +1 -1
  18. package/dist/src/services/FileCommandLoader.d.ts +15 -3
  19. package/dist/src/services/FileCommandLoader.js +94 -42
  20. package/dist/src/services/FileCommandLoader.js.map +1 -1
  21. package/dist/src/services/McpPromptLoader.d.ts +25 -0
  22. package/dist/src/services/McpPromptLoader.js +192 -0
  23. package/dist/src/services/McpPromptLoader.js.map +1 -0
  24. package/dist/src/services/prompt-processors/shellProcessor.d.ts +32 -0
  25. package/dist/src/services/prompt-processors/shellProcessor.js +77 -0
  26. package/dist/src/services/prompt-processors/shellProcessor.js.map +1 -0
  27. package/dist/src/services/prompt-processors/types.d.ts +4 -0
  28. package/dist/src/services/prompt-processors/types.js +4 -0
  29. package/dist/src/services/prompt-processors/types.js.map +1 -1
  30. package/dist/src/ui/App.js +312 -195
  31. package/dist/src/ui/App.js.map +1 -1
  32. package/dist/src/ui/commands/chatCommand.js +39 -1
  33. package/dist/src/ui/commands/chatCommand.js.map +1 -1
  34. package/dist/src/ui/commands/initCommand.d.ts +7 -0
  35. package/dist/src/ui/commands/initCommand.js +76 -0
  36. package/dist/src/ui/commands/initCommand.js.map +1 -0
  37. package/dist/src/ui/commands/mcpCommand.js +53 -8
  38. package/dist/src/ui/commands/mcpCommand.js.map +1 -1
  39. package/dist/src/ui/commands/types.d.ts +22 -3
  40. package/dist/src/ui/commands/types.js +1 -0
  41. package/dist/src/ui/commands/types.js.map +1 -1
  42. package/dist/src/ui/commands/vimCommand.js +0 -7
  43. package/dist/src/ui/commands/vimCommand.js.map +1 -1
  44. package/dist/src/ui/components/ContextSummaryDisplay.d.ts +3 -3
  45. package/dist/src/ui/components/ContextSummaryDisplay.js +8 -8
  46. package/dist/src/ui/components/ContextSummaryDisplay.js.map +1 -1
  47. package/dist/src/ui/components/Footer.d.ts +1 -0
  48. package/dist/src/ui/components/Footer.js +2 -2
  49. package/dist/src/ui/components/Footer.js.map +1 -1
  50. package/dist/src/ui/components/Header.js +1 -1
  51. package/dist/src/ui/components/Header.js.map +1 -1
  52. package/dist/src/ui/components/Help.js +2 -2
  53. package/dist/src/ui/components/Help.js.map +1 -1
  54. package/dist/src/ui/components/IDEContextDetailDisplay.d.ts +4 -4
  55. package/dist/src/ui/components/IDEContextDetailDisplay.js +5 -7
  56. package/dist/src/ui/components/IDEContextDetailDisplay.js.map +1 -1
  57. package/dist/src/ui/components/InputPrompt.d.ts +2 -0
  58. package/dist/src/ui/components/InputPrompt.js +5 -1
  59. package/dist/src/ui/components/InputPrompt.js.map +1 -1
  60. package/dist/src/ui/components/ShellConfirmationDialog.d.ts +15 -0
  61. package/dist/src/ui/components/ShellConfirmationDialog.js +45 -0
  62. package/dist/src/ui/components/ShellConfirmationDialog.js.map +1 -0
  63. package/dist/src/ui/components/Tips.js +1 -1
  64. package/dist/src/ui/components/Tips.js.map +1 -1
  65. package/dist/src/ui/components/shared/text-buffer.d.ts +270 -2
  66. package/dist/src/ui/components/shared/text-buffer.js +415 -70
  67. package/dist/src/ui/components/shared/text-buffer.js.map +1 -1
  68. package/dist/src/ui/components/shared/vim-buffer-actions.d.ts +72 -0
  69. package/dist/src/ui/components/shared/vim-buffer-actions.js +565 -0
  70. package/dist/src/ui/components/shared/vim-buffer-actions.js.map +1 -0
  71. package/dist/src/ui/contexts/VimModeContext.js +2 -2
  72. package/dist/src/ui/contexts/VimModeContext.js.map +1 -1
  73. package/dist/src/ui/hooks/shellCommandProcessor.d.ts +1 -0
  74. package/dist/src/ui/hooks/shellCommandProcessor.js +139 -200
  75. package/dist/src/ui/hooks/shellCommandProcessor.js.map +1 -1
  76. package/dist/src/ui/hooks/slashCommandProcessor.d.ts +7 -3
  77. package/dist/src/ui/hooks/slashCommandProcessor.js +208 -129
  78. package/dist/src/ui/hooks/slashCommandProcessor.js.map +1 -1
  79. package/dist/src/ui/hooks/useCompletion.js +7 -2
  80. package/dist/src/ui/hooks/useCompletion.js.map +1 -1
  81. package/dist/src/ui/hooks/useConsoleMessages.js +53 -37
  82. package/dist/src/ui/hooks/useConsoleMessages.js.map +1 -1
  83. package/dist/src/ui/hooks/useGeminiStream.js +14 -2
  84. package/dist/src/ui/hooks/useGeminiStream.js.map +1 -1
  85. package/dist/src/ui/hooks/useKeypress.js +5 -2
  86. package/dist/src/ui/hooks/useKeypress.js.map +1 -1
  87. package/dist/src/ui/hooks/useReactToolScheduler.js +0 -1
  88. package/dist/src/ui/hooks/useReactToolScheduler.js.map +1 -1
  89. package/dist/src/ui/hooks/vim.d.ts +28 -0
  90. package/dist/src/ui/hooks/vim.js +630 -0
  91. package/dist/src/ui/hooks/vim.js.map +1 -0
  92. package/dist/src/ui/themes/theme-manager.js +10 -1
  93. package/dist/src/ui/themes/theme-manager.js.map +1 -1
  94. package/dist/src/ui/themes/theme.d.ts +1 -0
  95. package/dist/src/ui/themes/theme.js +19 -4
  96. package/dist/src/ui/themes/theme.js.map +1 -1
  97. package/dist/src/ui/utils/textUtils.d.ts +0 -8
  98. package/dist/src/ui/utils/textUtils.js +0 -22
  99. package/dist/src/ui/utils/textUtils.js.map +1 -1
  100. package/dist/src/ui/utils/updateCheck.d.ts +7 -1
  101. package/dist/src/ui/utils/updateCheck.js +17 -28
  102. package/dist/src/ui/utils/updateCheck.js.map +1 -1
  103. package/dist/src/utils/events.d.ts +11 -0
  104. package/dist/src/utils/events.js +13 -0
  105. package/dist/src/utils/events.js.map +1 -0
  106. package/dist/src/utils/handleAutoUpdate.d.ts +10 -0
  107. package/dist/src/utils/handleAutoUpdate.js +97 -0
  108. package/dist/src/utils/handleAutoUpdate.js.map +1 -0
  109. package/dist/src/utils/installationInfo.d.ts +23 -0
  110. package/dist/src/utils/installationInfo.js +154 -0
  111. package/dist/src/utils/installationInfo.js.map +1 -0
  112. package/dist/src/utils/updateEventEmitter.d.ts +11 -0
  113. package/dist/src/utils/updateEventEmitter.js +12 -0
  114. package/dist/src/utils/updateEventEmitter.js.map +1 -0
  115. package/dist/tsconfig.tsbuildinfo +1 -1
  116. package/package.json +4 -5
@@ -12,6 +12,7 @@ import { useState, useCallback, useEffect, useMemo, useReducer } from 'react';
12
12
  import stringWidth from 'string-width';
13
13
  import { unescapePath } from '@vybestack/llxprt-code-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,209 @@ 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
+ // Replace the content
199
+ if (startRow === endRow) {
200
+ newLines[startRow] = prefix + normalisedReplacement + suffix;
201
+ }
202
+ else {
203
+ const firstLine = prefix + replacementParts[0];
204
+ if (replacementParts.length === 1) {
205
+ // Single line of replacement text, but spanning multiple original lines
206
+ newLines.splice(startRow, endRow - startRow + 1, firstLine + suffix);
207
+ }
208
+ else {
209
+ // Multi-line replacement text
210
+ const lastLine = replacementParts[replacementParts.length - 1] + suffix;
211
+ const middleLines = replacementParts.slice(1, -1);
212
+ newLines.splice(startRow, endRow - startRow + 1, firstLine, ...middleLines, lastLine);
213
+ }
214
+ }
215
+ const finalCursorRow = startRow + replacementParts.length - 1;
216
+ const finalCursorCol = (replacementParts.length > 1 ? 0 : sCol) +
217
+ cpLen(replacementParts[replacementParts.length - 1]);
218
+ return {
219
+ ...state,
220
+ lines: newLines,
221
+ cursorRow: Math.min(Math.max(finalCursorRow, 0), newLines.length - 1),
222
+ cursorCol: Math.max(0, Math.min(finalCursorCol, cpLen(newLines[finalCursorRow] || ''))),
223
+ preferredCol: null,
224
+ };
225
+ };
22
226
  /**
23
227
  * Strip characters that can break terminal rendering.
24
228
  *
@@ -106,6 +310,24 @@ export function offsetToLogicalPos(text, offset) {
106
310
  }
107
311
  return [row, col];
108
312
  }
313
+ /**
314
+ * Converts logical row/col position to absolute text offset
315
+ * Inverse operation of offsetToLogicalPos
316
+ */
317
+ export function logicalPosToOffset(lines, row, col) {
318
+ let offset = 0;
319
+ // Clamp row to valid range
320
+ const actualRow = Math.min(row, lines.length - 1);
321
+ // Add lengths of all lines before the target row
322
+ for (let i = 0; i < actualRow; i++) {
323
+ offset += cpLen(lines[i]) + 1; // +1 for newline
324
+ }
325
+ // Add column offset within the target row
326
+ if (actualRow >= 0 && actualRow < lines.length) {
327
+ offset += Math.min(col, cpLen(lines[actualRow]));
328
+ }
329
+ return offset;
330
+ }
109
331
  // Helper to calculate visual lines and map cursor positions
110
332
  function calculateVisualLayout(logicalLines, logicalCursor, viewportWidth) {
111
333
  const visualLines = [];
@@ -279,26 +501,27 @@ function calculateVisualLayout(logicalLines, logicalCursor, viewportWidth) {
279
501
  };
280
502
  }
281
503
  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: [] };
504
+ export const pushUndo = (currentState) => {
505
+ const snapshot = {
506
+ lines: [...currentState.lines],
507
+ cursorRow: currentState.cursorRow,
508
+ cursorCol: currentState.cursorCol,
294
509
  };
510
+ const newStack = [...currentState.undoStack, snapshot];
511
+ if (newStack.length > historyLimit) {
512
+ newStack.shift();
513
+ }
514
+ return { ...currentState, undoStack: newStack, redoStack: [] };
515
+ };
516
+ export function textBufferReducer(state, action) {
517
+ const pushUndoLocal = pushUndo;
295
518
  const currentLine = (r) => state.lines[r] ?? '';
296
519
  const currentLineLen = (r) => cpLen(currentLine(r));
297
520
  switch (action.type) {
298
521
  case 'set_text': {
299
522
  let nextState = state;
300
523
  if (action.pushToUndo !== false) {
301
- nextState = pushUndo(state);
524
+ nextState = pushUndoLocal(state);
302
525
  }
303
526
  const newContentLines = action.payload
304
527
  .replace(/\r\n?/g, '\n')
@@ -314,7 +537,7 @@ export function textBufferReducer(state, action) {
314
537
  };
315
538
  }
316
539
  case 'insert': {
317
- const nextState = pushUndo(state);
540
+ const nextState = pushUndoLocal(state);
318
541
  const newLines = [...nextState.lines];
319
542
  let newCursorRow = nextState.cursorRow;
320
543
  let newCursorCol = nextState.cursorCol;
@@ -351,7 +574,7 @@ export function textBufferReducer(state, action) {
351
574
  };
352
575
  }
353
576
  case 'backspace': {
354
- const nextState = pushUndo(state);
577
+ const nextState = pushUndoLocal(state);
355
578
  const newLines = [...nextState.lines];
356
579
  let newCursorRow = nextState.cursorRow;
357
580
  let newCursorCol = nextState.cursorCol;
@@ -528,7 +751,7 @@ export function textBufferReducer(state, action) {
528
751
  const { cursorRow, cursorCol, lines } = state;
529
752
  const lineContent = currentLine(cursorRow);
530
753
  if (cursorCol < currentLineLen(cursorRow)) {
531
- const nextState = pushUndo(state);
754
+ const nextState = pushUndoLocal(state);
532
755
  const newLines = [...nextState.lines];
533
756
  newLines[cursorRow] =
534
757
  cpSlice(lineContent, 0, cursorCol) +
@@ -536,7 +759,7 @@ export function textBufferReducer(state, action) {
536
759
  return { ...nextState, lines: newLines, preferredCol: null };
537
760
  }
538
761
  else if (cursorRow < lines.length - 1) {
539
- const nextState = pushUndo(state);
762
+ const nextState = pushUndoLocal(state);
540
763
  const nextLineContent = currentLine(cursorRow + 1);
541
764
  const newLines = [...nextState.lines];
542
765
  newLines[cursorRow] = lineContent + nextLineContent;
@@ -551,7 +774,7 @@ export function textBufferReducer(state, action) {
551
774
  return state;
552
775
  if (cursorCol === 0) {
553
776
  // Act as a backspace
554
- const nextState = pushUndo(state);
777
+ const nextState = pushUndoLocal(state);
555
778
  const prevLineContent = currentLine(cursorRow - 1);
556
779
  const currentLineContentVal = currentLine(cursorRow);
557
780
  const newCol = cpLen(prevLineContent);
@@ -566,7 +789,7 @@ export function textBufferReducer(state, action) {
566
789
  preferredCol: null,
567
790
  };
568
791
  }
569
- const nextState = pushUndo(state);
792
+ const nextState = pushUndoLocal(state);
570
793
  const lineContent = currentLine(cursorRow);
571
794
  const arr = toCodePoints(lineContent);
572
795
  let start = cursorCol;
@@ -604,14 +827,14 @@ export function textBufferReducer(state, action) {
604
827
  return state;
605
828
  if (cursorCol >= arr.length) {
606
829
  // Act as a delete
607
- const nextState = pushUndo(state);
830
+ const nextState = pushUndoLocal(state);
608
831
  const nextLineContent = currentLine(cursorRow + 1);
609
832
  const newLines = [...nextState.lines];
610
833
  newLines[cursorRow] = lineContent + nextLineContent;
611
834
  newLines.splice(cursorRow + 1, 1);
612
835
  return { ...nextState, lines: newLines, preferredCol: null };
613
836
  }
614
- const nextState = pushUndo(state);
837
+ const nextState = pushUndoLocal(state);
615
838
  let end = cursorCol;
616
839
  while (end < arr.length && !isWordChar(arr[end]))
617
840
  end++;
@@ -626,14 +849,14 @@ export function textBufferReducer(state, action) {
626
849
  const { cursorRow, cursorCol, lines } = state;
627
850
  const lineContent = currentLine(cursorRow);
628
851
  if (cursorCol < currentLineLen(cursorRow)) {
629
- const nextState = pushUndo(state);
852
+ const nextState = pushUndoLocal(state);
630
853
  const newLines = [...nextState.lines];
631
854
  newLines[cursorRow] = cpSlice(lineContent, 0, cursorCol);
632
855
  return { ...nextState, lines: newLines };
633
856
  }
634
857
  else if (cursorRow < lines.length - 1) {
635
858
  // Act as a delete
636
- const nextState = pushUndo(state);
859
+ const nextState = pushUndoLocal(state);
637
860
  const nextLineContent = currentLine(cursorRow + 1);
638
861
  const newLines = [...nextState.lines];
639
862
  newLines[cursorRow] = lineContent + nextLineContent;
@@ -645,7 +868,7 @@ export function textBufferReducer(state, action) {
645
868
  case 'kill_line_left': {
646
869
  const { cursorRow, cursorCol } = state;
647
870
  if (cursorCol > 0) {
648
- const nextState = pushUndo(state);
871
+ const nextState = pushUndoLocal(state);
649
872
  const lineContent = currentLine(cursorRow);
650
873
  const newLines = [...nextState.lines];
651
874
  newLines[cursorRow] = cpSlice(lineContent, cursorCol);
@@ -692,51 +915,8 @@ export function textBufferReducer(state, action) {
692
915
  }
693
916
  case 'replace_range': {
694
917
  const { startRow, startCol, endRow, endCol, text } = action.payload;
695
- if (startRow > endRow ||
696
- (startRow === endRow && startCol > endCol) ||
697
- startRow < 0 ||
698
- startCol < 0 ||
699
- endRow >= state.lines.length ||
700
- (endRow < state.lines.length && endCol > currentLineLen(endRow))) {
701
- return state; // Invalid range
702
- }
703
- const nextState = pushUndo(state);
704
- const newLines = [...nextState.lines];
705
- const sCol = clamp(startCol, 0, currentLineLen(startRow));
706
- const eCol = clamp(endCol, 0, currentLineLen(endRow));
707
- const prefix = cpSlice(currentLine(startRow), 0, sCol);
708
- const suffix = cpSlice(currentLine(endRow), eCol);
709
- const normalisedReplacement = text
710
- .replace(/\r\n/g, '\n')
711
- .replace(/\r/g, '\n');
712
- const replacementParts = normalisedReplacement.split('\n');
713
- // Replace the content
714
- if (startRow === endRow) {
715
- newLines[startRow] = prefix + normalisedReplacement + suffix;
716
- }
717
- else {
718
- const firstLine = prefix + replacementParts[0];
719
- if (replacementParts.length === 1) {
720
- // Single line of replacement text, but spanning multiple original lines
721
- newLines.splice(startRow, endRow - startRow + 1, firstLine + suffix);
722
- }
723
- else {
724
- // Multi-line replacement text
725
- const lastLine = replacementParts[replacementParts.length - 1] + suffix;
726
- const middleLines = replacementParts.slice(1, -1);
727
- newLines.splice(startRow, endRow - startRow + 1, firstLine, ...middleLines, lastLine);
728
- }
729
- }
730
- const finalCursorRow = startRow + replacementParts.length - 1;
731
- const finalCursorCol = (replacementParts.length > 1 ? 0 : sCol) +
732
- cpLen(replacementParts[replacementParts.length - 1]);
733
- return {
734
- ...nextState,
735
- lines: newLines,
736
- cursorRow: finalCursorRow,
737
- cursorCol: finalCursorCol,
738
- preferredCol: null,
739
- };
918
+ const nextState = pushUndoLocal(state);
919
+ return replaceRangeInternal(nextState, startRow, startCol, endRow, endCol, text);
740
920
  }
741
921
  case 'move_to_offset': {
742
922
  const { offset } = action.payload;
@@ -749,8 +929,42 @@ export function textBufferReducer(state, action) {
749
929
  };
750
930
  }
751
931
  case 'create_undo_snapshot': {
752
- return pushUndo(state);
932
+ return pushUndoLocal(state);
753
933
  }
934
+ // Vim-specific operations
935
+ case 'vim_delete_word_forward':
936
+ case 'vim_delete_word_backward':
937
+ case 'vim_delete_word_end':
938
+ case 'vim_change_word_forward':
939
+ case 'vim_change_word_backward':
940
+ case 'vim_change_word_end':
941
+ case 'vim_delete_line':
942
+ case 'vim_change_line':
943
+ case 'vim_delete_to_end_of_line':
944
+ case 'vim_change_to_end_of_line':
945
+ case 'vim_change_movement':
946
+ case 'vim_move_left':
947
+ case 'vim_move_right':
948
+ case 'vim_move_up':
949
+ case 'vim_move_down':
950
+ case 'vim_move_word_forward':
951
+ case 'vim_move_word_backward':
952
+ case 'vim_move_word_end':
953
+ case 'vim_delete_char':
954
+ case 'vim_insert_at_cursor':
955
+ case 'vim_append_at_cursor':
956
+ case 'vim_open_line_below':
957
+ case 'vim_open_line_above':
958
+ case 'vim_append_at_line_end':
959
+ case 'vim_insert_at_line_start':
960
+ case 'vim_move_to_line_start':
961
+ case 'vim_move_to_line_end':
962
+ case 'vim_move_to_first_nonwhitespace':
963
+ case 'vim_move_to_first_line':
964
+ case 'vim_move_to_last_line':
965
+ case 'vim_move_to_line':
966
+ case 'vim_escape_insert_mode':
967
+ return handleVimAction(state, action);
754
968
  default: {
755
969
  const exhaustiveCheck = action;
756
970
  console.error(`Unknown action encountered: ${exhaustiveCheck}`);
@@ -872,6 +1086,104 @@ export function useTextBuffer({ initialText = '', initialCursorOffset = 0, viewp
872
1086
  const killLineLeft = useCallback(() => {
873
1087
  dispatch({ type: 'kill_line_left' });
874
1088
  }, []);
1089
+ // Vim-specific operations
1090
+ const vimDeleteWordForward = useCallback((count) => {
1091
+ dispatch({ type: 'vim_delete_word_forward', payload: { count } });
1092
+ }, []);
1093
+ const vimDeleteWordBackward = useCallback((count) => {
1094
+ dispatch({ type: 'vim_delete_word_backward', payload: { count } });
1095
+ }, []);
1096
+ const vimDeleteWordEnd = useCallback((count) => {
1097
+ dispatch({ type: 'vim_delete_word_end', payload: { count } });
1098
+ }, []);
1099
+ const vimChangeWordForward = useCallback((count) => {
1100
+ dispatch({ type: 'vim_change_word_forward', payload: { count } });
1101
+ }, []);
1102
+ const vimChangeWordBackward = useCallback((count) => {
1103
+ dispatch({ type: 'vim_change_word_backward', payload: { count } });
1104
+ }, []);
1105
+ const vimChangeWordEnd = useCallback((count) => {
1106
+ dispatch({ type: 'vim_change_word_end', payload: { count } });
1107
+ }, []);
1108
+ const vimDeleteLine = useCallback((count) => {
1109
+ dispatch({ type: 'vim_delete_line', payload: { count } });
1110
+ }, []);
1111
+ const vimChangeLine = useCallback((count) => {
1112
+ dispatch({ type: 'vim_change_line', payload: { count } });
1113
+ }, []);
1114
+ const vimDeleteToEndOfLine = useCallback(() => {
1115
+ dispatch({ type: 'vim_delete_to_end_of_line' });
1116
+ }, []);
1117
+ const vimChangeToEndOfLine = useCallback(() => {
1118
+ dispatch({ type: 'vim_change_to_end_of_line' });
1119
+ }, []);
1120
+ const vimChangeMovement = useCallback((movement, count) => {
1121
+ dispatch({ type: 'vim_change_movement', payload: { movement, count } });
1122
+ }, []);
1123
+ // New vim navigation and operation methods
1124
+ const vimMoveLeft = useCallback((count) => {
1125
+ dispatch({ type: 'vim_move_left', payload: { count } });
1126
+ }, []);
1127
+ const vimMoveRight = useCallback((count) => {
1128
+ dispatch({ type: 'vim_move_right', payload: { count } });
1129
+ }, []);
1130
+ const vimMoveUp = useCallback((count) => {
1131
+ dispatch({ type: 'vim_move_up', payload: { count } });
1132
+ }, []);
1133
+ const vimMoveDown = useCallback((count) => {
1134
+ dispatch({ type: 'vim_move_down', payload: { count } });
1135
+ }, []);
1136
+ const vimMoveWordForward = useCallback((count) => {
1137
+ dispatch({ type: 'vim_move_word_forward', payload: { count } });
1138
+ }, []);
1139
+ const vimMoveWordBackward = useCallback((count) => {
1140
+ dispatch({ type: 'vim_move_word_backward', payload: { count } });
1141
+ }, []);
1142
+ const vimMoveWordEnd = useCallback((count) => {
1143
+ dispatch({ type: 'vim_move_word_end', payload: { count } });
1144
+ }, []);
1145
+ const vimDeleteChar = useCallback((count) => {
1146
+ dispatch({ type: 'vim_delete_char', payload: { count } });
1147
+ }, []);
1148
+ const vimInsertAtCursor = useCallback(() => {
1149
+ dispatch({ type: 'vim_insert_at_cursor' });
1150
+ }, []);
1151
+ const vimAppendAtCursor = useCallback(() => {
1152
+ dispatch({ type: 'vim_append_at_cursor' });
1153
+ }, []);
1154
+ const vimOpenLineBelow = useCallback(() => {
1155
+ dispatch({ type: 'vim_open_line_below' });
1156
+ }, []);
1157
+ const vimOpenLineAbove = useCallback(() => {
1158
+ dispatch({ type: 'vim_open_line_above' });
1159
+ }, []);
1160
+ const vimAppendAtLineEnd = useCallback(() => {
1161
+ dispatch({ type: 'vim_append_at_line_end' });
1162
+ }, []);
1163
+ const vimInsertAtLineStart = useCallback(() => {
1164
+ dispatch({ type: 'vim_insert_at_line_start' });
1165
+ }, []);
1166
+ const vimMoveToLineStart = useCallback(() => {
1167
+ dispatch({ type: 'vim_move_to_line_start' });
1168
+ }, []);
1169
+ const vimMoveToLineEnd = useCallback(() => {
1170
+ dispatch({ type: 'vim_move_to_line_end' });
1171
+ }, []);
1172
+ const vimMoveToFirstNonWhitespace = useCallback(() => {
1173
+ dispatch({ type: 'vim_move_to_first_nonwhitespace' });
1174
+ }, []);
1175
+ const vimMoveToFirstLine = useCallback(() => {
1176
+ dispatch({ type: 'vim_move_to_first_line' });
1177
+ }, []);
1178
+ const vimMoveToLastLine = useCallback(() => {
1179
+ dispatch({ type: 'vim_move_to_last_line' });
1180
+ }, []);
1181
+ const vimMoveToLine = useCallback((lineNumber) => {
1182
+ dispatch({ type: 'vim_move_to_line', payload: { lineNumber } });
1183
+ }, []);
1184
+ const vimEscapeInsertMode = useCallback(() => {
1185
+ dispatch({ type: 'vim_escape_insert_mode' });
1186
+ }, []);
875
1187
  const openInExternalEditor = useCallback(async (opts = {}) => {
876
1188
  const editor = opts.editor ??
877
1189
  process.env['VISUAL'] ??
@@ -1016,6 +1328,39 @@ export function useTextBuffer({ initialText = '', initialCursorOffset = 0, viewp
1016
1328
  killLineLeft,
1017
1329
  handleInput,
1018
1330
  openInExternalEditor,
1331
+ // Vim-specific operations
1332
+ vimDeleteWordForward,
1333
+ vimDeleteWordBackward,
1334
+ vimDeleteWordEnd,
1335
+ vimChangeWordForward,
1336
+ vimChangeWordBackward,
1337
+ vimChangeWordEnd,
1338
+ vimDeleteLine,
1339
+ vimChangeLine,
1340
+ vimDeleteToEndOfLine,
1341
+ vimChangeToEndOfLine,
1342
+ vimChangeMovement,
1343
+ vimMoveLeft,
1344
+ vimMoveRight,
1345
+ vimMoveUp,
1346
+ vimMoveDown,
1347
+ vimMoveWordForward,
1348
+ vimMoveWordBackward,
1349
+ vimMoveWordEnd,
1350
+ vimDeleteChar,
1351
+ vimInsertAtCursor,
1352
+ vimAppendAtCursor,
1353
+ vimOpenLineBelow,
1354
+ vimOpenLineAbove,
1355
+ vimAppendAtLineEnd,
1356
+ vimInsertAtLineStart,
1357
+ vimMoveToLineStart,
1358
+ vimMoveToLineEnd,
1359
+ vimMoveToFirstNonWhitespace,
1360
+ vimMoveToFirstLine,
1361
+ vimMoveToLastLine,
1362
+ vimMoveToLine,
1363
+ vimEscapeInsertMode,
1019
1364
  };
1020
1365
  return returnValue;
1021
1366
  }