@google/gemini-cli 0.1.14 → 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.
- package/dist/google-gemini-cli-0.1.13.tgz +0 -0
- package/dist/package.json +2 -2
- package/dist/src/config/settings.d.ts +1 -0
- package/dist/src/config/settings.js.map +1 -1
- package/dist/src/gemini.d.ts +1 -0
- package/dist/src/gemini.js +21 -14
- package/dist/src/gemini.js.map +1 -1
- package/dist/src/generated/git-commit.d.ts +1 -1
- package/dist/src/generated/git-commit.js +1 -1
- package/dist/src/services/BuiltinCommandLoader.js +3 -1
- package/dist/src/services/BuiltinCommandLoader.js.map +1 -1
- package/dist/src/services/FileCommandLoader.js +31 -8
- package/dist/src/services/FileCommandLoader.js.map +1 -1
- package/dist/src/services/McpPromptLoader.d.ts +25 -0
- package/dist/src/services/McpPromptLoader.js +192 -0
- package/dist/src/services/McpPromptLoader.js.map +1 -0
- package/dist/src/services/prompt-processors/shellProcessor.d.ts +32 -0
- package/dist/src/services/prompt-processors/shellProcessor.js +77 -0
- package/dist/src/services/prompt-processors/shellProcessor.js.map +1 -0
- package/dist/src/services/prompt-processors/types.d.ts +4 -0
- package/dist/src/services/prompt-processors/types.js +4 -0
- package/dist/src/services/prompt-processors/types.js.map +1 -1
- package/dist/src/ui/App.js +70 -35
- package/dist/src/ui/App.js.map +1 -1
- package/dist/src/ui/commands/mcpCommand.js +53 -8
- package/dist/src/ui/commands/mcpCommand.js.map +1 -1
- package/dist/src/ui/commands/types.d.ts +19 -2
- package/dist/src/ui/commands/types.js +1 -0
- package/dist/src/ui/commands/types.js.map +1 -1
- package/dist/src/ui/commands/vimCommand.d.ts +7 -0
- package/dist/src/ui/commands/vimCommand.js +23 -0
- package/dist/src/ui/commands/vimCommand.js.map +1 -0
- package/dist/src/ui/components/Footer.d.ts +1 -0
- package/dist/src/ui/components/Footer.js +2 -2
- package/dist/src/ui/components/Footer.js.map +1 -1
- package/dist/src/ui/components/Header.js +1 -1
- package/dist/src/ui/components/Header.js.map +1 -1
- package/dist/src/ui/components/InputPrompt.d.ts +2 -0
- package/dist/src/ui/components/InputPrompt.js +5 -1
- package/dist/src/ui/components/InputPrompt.js.map +1 -1
- package/dist/src/ui/components/ShellConfirmationDialog.d.ts +15 -0
- package/dist/src/ui/components/ShellConfirmationDialog.js +44 -0
- package/dist/src/ui/components/ShellConfirmationDialog.js.map +1 -0
- package/dist/src/ui/components/Tips.js +1 -1
- package/dist/src/ui/components/Tips.js.map +1 -1
- package/dist/src/ui/components/shared/text-buffer.d.ts +270 -2
- package/dist/src/ui/components/shared/text-buffer.js +415 -70
- package/dist/src/ui/components/shared/text-buffer.js.map +1 -1
- package/dist/src/ui/components/shared/vim-buffer-actions.d.ts +72 -0
- package/dist/src/ui/components/shared/vim-buffer-actions.js +565 -0
- package/dist/src/ui/components/shared/vim-buffer-actions.js.map +1 -0
- package/dist/src/ui/contexts/VimModeContext.d.ts +19 -0
- package/dist/src/ui/contexts/VimModeContext.js +48 -0
- package/dist/src/ui/contexts/VimModeContext.js.map +1 -0
- package/dist/src/ui/hooks/shellCommandProcessor.d.ts +1 -0
- package/dist/src/ui/hooks/shellCommandProcessor.js +139 -200
- package/dist/src/ui/hooks/shellCommandProcessor.js.map +1 -1
- package/dist/src/ui/hooks/slashCommandProcessor.d.ts +7 -3
- package/dist/src/ui/hooks/slashCommandProcessor.js +187 -126
- package/dist/src/ui/hooks/slashCommandProcessor.js.map +1 -1
- package/dist/src/ui/hooks/useCompletion.js +7 -2
- package/dist/src/ui/hooks/useCompletion.js.map +1 -1
- package/dist/src/ui/hooks/useConsoleMessages.js +53 -37
- package/dist/src/ui/hooks/useConsoleMessages.js.map +1 -1
- package/dist/src/ui/hooks/useKeypress.js +5 -2
- package/dist/src/ui/hooks/useKeypress.js.map +1 -1
- package/dist/src/ui/hooks/vim.d.ts +28 -0
- package/dist/src/ui/hooks/vim.js +630 -0
- package/dist/src/ui/hooks/vim.js.map +1 -0
- package/dist/src/ui/themes/theme-manager.js +10 -1
- package/dist/src/ui/themes/theme-manager.js.map +1 -1
- package/dist/src/ui/themes/theme.d.ts +1 -0
- package/dist/src/ui/themes/theme.js +19 -4
- package/dist/src/ui/themes/theme.js.map +1 -1
- package/dist/src/ui/utils/textUtils.d.ts +0 -8
- package/dist/src/ui/utils/textUtils.js +0 -22
- package/dist/src/ui/utils/textUtils.js.map +1 -1
- package/dist/src/utils/events.d.ts +11 -0
- package/dist/src/utils/events.js +13 -0
- package/dist/src/utils/events.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -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,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
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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 =
|
|
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 =
|
|
540
|
+
const nextState = pushUndoLocal(state);
|
|
318
541
|
const newLines = [...nextState.lines];
|
|
319
542
|
let newCursorRow = nextState.cursorRow;
|
|
320
543
|
let newCursorCol = nextState.cursorCol;
|
|
@@ -346,7 +569,7 @@ export function textBufferReducer(state, action) {
|
|
|
346
569
|
};
|
|
347
570
|
}
|
|
348
571
|
case 'backspace': {
|
|
349
|
-
const nextState =
|
|
572
|
+
const nextState = pushUndoLocal(state);
|
|
350
573
|
const newLines = [...nextState.lines];
|
|
351
574
|
let newCursorRow = nextState.cursorRow;
|
|
352
575
|
let newCursorCol = nextState.cursorCol;
|
|
@@ -523,7 +746,7 @@ export function textBufferReducer(state, action) {
|
|
|
523
746
|
const { cursorRow, cursorCol, lines } = state;
|
|
524
747
|
const lineContent = currentLine(cursorRow);
|
|
525
748
|
if (cursorCol < currentLineLen(cursorRow)) {
|
|
526
|
-
const nextState =
|
|
749
|
+
const nextState = pushUndoLocal(state);
|
|
527
750
|
const newLines = [...nextState.lines];
|
|
528
751
|
newLines[cursorRow] =
|
|
529
752
|
cpSlice(lineContent, 0, cursorCol) +
|
|
@@ -531,7 +754,7 @@ export function textBufferReducer(state, action) {
|
|
|
531
754
|
return { ...nextState, lines: newLines, preferredCol: null };
|
|
532
755
|
}
|
|
533
756
|
else if (cursorRow < lines.length - 1) {
|
|
534
|
-
const nextState =
|
|
757
|
+
const nextState = pushUndoLocal(state);
|
|
535
758
|
const nextLineContent = currentLine(cursorRow + 1);
|
|
536
759
|
const newLines = [...nextState.lines];
|
|
537
760
|
newLines[cursorRow] = lineContent + nextLineContent;
|
|
@@ -546,7 +769,7 @@ export function textBufferReducer(state, action) {
|
|
|
546
769
|
return state;
|
|
547
770
|
if (cursorCol === 0) {
|
|
548
771
|
// Act as a backspace
|
|
549
|
-
const nextState =
|
|
772
|
+
const nextState = pushUndoLocal(state);
|
|
550
773
|
const prevLineContent = currentLine(cursorRow - 1);
|
|
551
774
|
const currentLineContentVal = currentLine(cursorRow);
|
|
552
775
|
const newCol = cpLen(prevLineContent);
|
|
@@ -561,7 +784,7 @@ export function textBufferReducer(state, action) {
|
|
|
561
784
|
preferredCol: null,
|
|
562
785
|
};
|
|
563
786
|
}
|
|
564
|
-
const nextState =
|
|
787
|
+
const nextState = pushUndoLocal(state);
|
|
565
788
|
const lineContent = currentLine(cursorRow);
|
|
566
789
|
const arr = toCodePoints(lineContent);
|
|
567
790
|
let start = cursorCol;
|
|
@@ -599,14 +822,14 @@ export function textBufferReducer(state, action) {
|
|
|
599
822
|
return state;
|
|
600
823
|
if (cursorCol >= arr.length) {
|
|
601
824
|
// Act as a delete
|
|
602
|
-
const nextState =
|
|
825
|
+
const nextState = pushUndoLocal(state);
|
|
603
826
|
const nextLineContent = currentLine(cursorRow + 1);
|
|
604
827
|
const newLines = [...nextState.lines];
|
|
605
828
|
newLines[cursorRow] = lineContent + nextLineContent;
|
|
606
829
|
newLines.splice(cursorRow + 1, 1);
|
|
607
830
|
return { ...nextState, lines: newLines, preferredCol: null };
|
|
608
831
|
}
|
|
609
|
-
const nextState =
|
|
832
|
+
const nextState = pushUndoLocal(state);
|
|
610
833
|
let end = cursorCol;
|
|
611
834
|
while (end < arr.length && !isWordChar(arr[end]))
|
|
612
835
|
end++;
|
|
@@ -621,14 +844,14 @@ export function textBufferReducer(state, action) {
|
|
|
621
844
|
const { cursorRow, cursorCol, lines } = state;
|
|
622
845
|
const lineContent = currentLine(cursorRow);
|
|
623
846
|
if (cursorCol < currentLineLen(cursorRow)) {
|
|
624
|
-
const nextState =
|
|
847
|
+
const nextState = pushUndoLocal(state);
|
|
625
848
|
const newLines = [...nextState.lines];
|
|
626
849
|
newLines[cursorRow] = cpSlice(lineContent, 0, cursorCol);
|
|
627
850
|
return { ...nextState, lines: newLines };
|
|
628
851
|
}
|
|
629
852
|
else if (cursorRow < lines.length - 1) {
|
|
630
853
|
// Act as a delete
|
|
631
|
-
const nextState =
|
|
854
|
+
const nextState = pushUndoLocal(state);
|
|
632
855
|
const nextLineContent = currentLine(cursorRow + 1);
|
|
633
856
|
const newLines = [...nextState.lines];
|
|
634
857
|
newLines[cursorRow] = lineContent + nextLineContent;
|
|
@@ -640,7 +863,7 @@ export function textBufferReducer(state, action) {
|
|
|
640
863
|
case 'kill_line_left': {
|
|
641
864
|
const { cursorRow, cursorCol } = state;
|
|
642
865
|
if (cursorCol > 0) {
|
|
643
|
-
const nextState =
|
|
866
|
+
const nextState = pushUndoLocal(state);
|
|
644
867
|
const lineContent = currentLine(cursorRow);
|
|
645
868
|
const newLines = [...nextState.lines];
|
|
646
869
|
newLines[cursorRow] = cpSlice(lineContent, cursorCol);
|
|
@@ -687,51 +910,8 @@ export function textBufferReducer(state, action) {
|
|
|
687
910
|
}
|
|
688
911
|
case 'replace_range': {
|
|
689
912
|
const { startRow, startCol, endRow, endCol, text } = action.payload;
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
};
|
|
913
|
+
const nextState = pushUndoLocal(state);
|
|
914
|
+
return replaceRangeInternal(nextState, startRow, startCol, endRow, endCol, text);
|
|
735
915
|
}
|
|
736
916
|
case 'move_to_offset': {
|
|
737
917
|
const { offset } = action.payload;
|
|
@@ -744,8 +924,42 @@ export function textBufferReducer(state, action) {
|
|
|
744
924
|
};
|
|
745
925
|
}
|
|
746
926
|
case 'create_undo_snapshot': {
|
|
747
|
-
return
|
|
927
|
+
return pushUndoLocal(state);
|
|
748
928
|
}
|
|
929
|
+
// Vim-specific operations
|
|
930
|
+
case 'vim_delete_word_forward':
|
|
931
|
+
case 'vim_delete_word_backward':
|
|
932
|
+
case 'vim_delete_word_end':
|
|
933
|
+
case 'vim_change_word_forward':
|
|
934
|
+
case 'vim_change_word_backward':
|
|
935
|
+
case 'vim_change_word_end':
|
|
936
|
+
case 'vim_delete_line':
|
|
937
|
+
case 'vim_change_line':
|
|
938
|
+
case 'vim_delete_to_end_of_line':
|
|
939
|
+
case 'vim_change_to_end_of_line':
|
|
940
|
+
case 'vim_change_movement':
|
|
941
|
+
case 'vim_move_left':
|
|
942
|
+
case 'vim_move_right':
|
|
943
|
+
case 'vim_move_up':
|
|
944
|
+
case 'vim_move_down':
|
|
945
|
+
case 'vim_move_word_forward':
|
|
946
|
+
case 'vim_move_word_backward':
|
|
947
|
+
case 'vim_move_word_end':
|
|
948
|
+
case 'vim_delete_char':
|
|
949
|
+
case 'vim_insert_at_cursor':
|
|
950
|
+
case 'vim_append_at_cursor':
|
|
951
|
+
case 'vim_open_line_below':
|
|
952
|
+
case 'vim_open_line_above':
|
|
953
|
+
case 'vim_append_at_line_end':
|
|
954
|
+
case 'vim_insert_at_line_start':
|
|
955
|
+
case 'vim_move_to_line_start':
|
|
956
|
+
case 'vim_move_to_line_end':
|
|
957
|
+
case 'vim_move_to_first_nonwhitespace':
|
|
958
|
+
case 'vim_move_to_first_line':
|
|
959
|
+
case 'vim_move_to_last_line':
|
|
960
|
+
case 'vim_move_to_line':
|
|
961
|
+
case 'vim_escape_insert_mode':
|
|
962
|
+
return handleVimAction(state, action);
|
|
749
963
|
default: {
|
|
750
964
|
const exhaustiveCheck = action;
|
|
751
965
|
console.error(`Unknown action encountered: ${exhaustiveCheck}`);
|
|
@@ -867,6 +1081,104 @@ export function useTextBuffer({ initialText = '', initialCursorOffset = 0, viewp
|
|
|
867
1081
|
const killLineLeft = useCallback(() => {
|
|
868
1082
|
dispatch({ type: 'kill_line_left' });
|
|
869
1083
|
}, []);
|
|
1084
|
+
// Vim-specific operations
|
|
1085
|
+
const vimDeleteWordForward = useCallback((count) => {
|
|
1086
|
+
dispatch({ type: 'vim_delete_word_forward', payload: { count } });
|
|
1087
|
+
}, []);
|
|
1088
|
+
const vimDeleteWordBackward = useCallback((count) => {
|
|
1089
|
+
dispatch({ type: 'vim_delete_word_backward', payload: { count } });
|
|
1090
|
+
}, []);
|
|
1091
|
+
const vimDeleteWordEnd = useCallback((count) => {
|
|
1092
|
+
dispatch({ type: 'vim_delete_word_end', payload: { count } });
|
|
1093
|
+
}, []);
|
|
1094
|
+
const vimChangeWordForward = useCallback((count) => {
|
|
1095
|
+
dispatch({ type: 'vim_change_word_forward', payload: { count } });
|
|
1096
|
+
}, []);
|
|
1097
|
+
const vimChangeWordBackward = useCallback((count) => {
|
|
1098
|
+
dispatch({ type: 'vim_change_word_backward', payload: { count } });
|
|
1099
|
+
}, []);
|
|
1100
|
+
const vimChangeWordEnd = useCallback((count) => {
|
|
1101
|
+
dispatch({ type: 'vim_change_word_end', payload: { count } });
|
|
1102
|
+
}, []);
|
|
1103
|
+
const vimDeleteLine = useCallback((count) => {
|
|
1104
|
+
dispatch({ type: 'vim_delete_line', payload: { count } });
|
|
1105
|
+
}, []);
|
|
1106
|
+
const vimChangeLine = useCallback((count) => {
|
|
1107
|
+
dispatch({ type: 'vim_change_line', payload: { count } });
|
|
1108
|
+
}, []);
|
|
1109
|
+
const vimDeleteToEndOfLine = useCallback(() => {
|
|
1110
|
+
dispatch({ type: 'vim_delete_to_end_of_line' });
|
|
1111
|
+
}, []);
|
|
1112
|
+
const vimChangeToEndOfLine = useCallback(() => {
|
|
1113
|
+
dispatch({ type: 'vim_change_to_end_of_line' });
|
|
1114
|
+
}, []);
|
|
1115
|
+
const vimChangeMovement = useCallback((movement, count) => {
|
|
1116
|
+
dispatch({ type: 'vim_change_movement', payload: { movement, count } });
|
|
1117
|
+
}, []);
|
|
1118
|
+
// New vim navigation and operation methods
|
|
1119
|
+
const vimMoveLeft = useCallback((count) => {
|
|
1120
|
+
dispatch({ type: 'vim_move_left', payload: { count } });
|
|
1121
|
+
}, []);
|
|
1122
|
+
const vimMoveRight = useCallback((count) => {
|
|
1123
|
+
dispatch({ type: 'vim_move_right', payload: { count } });
|
|
1124
|
+
}, []);
|
|
1125
|
+
const vimMoveUp = useCallback((count) => {
|
|
1126
|
+
dispatch({ type: 'vim_move_up', payload: { count } });
|
|
1127
|
+
}, []);
|
|
1128
|
+
const vimMoveDown = useCallback((count) => {
|
|
1129
|
+
dispatch({ type: 'vim_move_down', payload: { count } });
|
|
1130
|
+
}, []);
|
|
1131
|
+
const vimMoveWordForward = useCallback((count) => {
|
|
1132
|
+
dispatch({ type: 'vim_move_word_forward', payload: { count } });
|
|
1133
|
+
}, []);
|
|
1134
|
+
const vimMoveWordBackward = useCallback((count) => {
|
|
1135
|
+
dispatch({ type: 'vim_move_word_backward', payload: { count } });
|
|
1136
|
+
}, []);
|
|
1137
|
+
const vimMoveWordEnd = useCallback((count) => {
|
|
1138
|
+
dispatch({ type: 'vim_move_word_end', payload: { count } });
|
|
1139
|
+
}, []);
|
|
1140
|
+
const vimDeleteChar = useCallback((count) => {
|
|
1141
|
+
dispatch({ type: 'vim_delete_char', payload: { count } });
|
|
1142
|
+
}, []);
|
|
1143
|
+
const vimInsertAtCursor = useCallback(() => {
|
|
1144
|
+
dispatch({ type: 'vim_insert_at_cursor' });
|
|
1145
|
+
}, []);
|
|
1146
|
+
const vimAppendAtCursor = useCallback(() => {
|
|
1147
|
+
dispatch({ type: 'vim_append_at_cursor' });
|
|
1148
|
+
}, []);
|
|
1149
|
+
const vimOpenLineBelow = useCallback(() => {
|
|
1150
|
+
dispatch({ type: 'vim_open_line_below' });
|
|
1151
|
+
}, []);
|
|
1152
|
+
const vimOpenLineAbove = useCallback(() => {
|
|
1153
|
+
dispatch({ type: 'vim_open_line_above' });
|
|
1154
|
+
}, []);
|
|
1155
|
+
const vimAppendAtLineEnd = useCallback(() => {
|
|
1156
|
+
dispatch({ type: 'vim_append_at_line_end' });
|
|
1157
|
+
}, []);
|
|
1158
|
+
const vimInsertAtLineStart = useCallback(() => {
|
|
1159
|
+
dispatch({ type: 'vim_insert_at_line_start' });
|
|
1160
|
+
}, []);
|
|
1161
|
+
const vimMoveToLineStart = useCallback(() => {
|
|
1162
|
+
dispatch({ type: 'vim_move_to_line_start' });
|
|
1163
|
+
}, []);
|
|
1164
|
+
const vimMoveToLineEnd = useCallback(() => {
|
|
1165
|
+
dispatch({ type: 'vim_move_to_line_end' });
|
|
1166
|
+
}, []);
|
|
1167
|
+
const vimMoveToFirstNonWhitespace = useCallback(() => {
|
|
1168
|
+
dispatch({ type: 'vim_move_to_first_nonwhitespace' });
|
|
1169
|
+
}, []);
|
|
1170
|
+
const vimMoveToFirstLine = useCallback(() => {
|
|
1171
|
+
dispatch({ type: 'vim_move_to_first_line' });
|
|
1172
|
+
}, []);
|
|
1173
|
+
const vimMoveToLastLine = useCallback(() => {
|
|
1174
|
+
dispatch({ type: 'vim_move_to_last_line' });
|
|
1175
|
+
}, []);
|
|
1176
|
+
const vimMoveToLine = useCallback((lineNumber) => {
|
|
1177
|
+
dispatch({ type: 'vim_move_to_line', payload: { lineNumber } });
|
|
1178
|
+
}, []);
|
|
1179
|
+
const vimEscapeInsertMode = useCallback(() => {
|
|
1180
|
+
dispatch({ type: 'vim_escape_insert_mode' });
|
|
1181
|
+
}, []);
|
|
870
1182
|
const openInExternalEditor = useCallback(async (opts = {}) => {
|
|
871
1183
|
const editor = opts.editor ??
|
|
872
1184
|
process.env['VISUAL'] ??
|
|
@@ -1005,6 +1317,39 @@ export function useTextBuffer({ initialText = '', initialCursorOffset = 0, viewp
|
|
|
1005
1317
|
killLineLeft,
|
|
1006
1318
|
handleInput,
|
|
1007
1319
|
openInExternalEditor,
|
|
1320
|
+
// Vim-specific operations
|
|
1321
|
+
vimDeleteWordForward,
|
|
1322
|
+
vimDeleteWordBackward,
|
|
1323
|
+
vimDeleteWordEnd,
|
|
1324
|
+
vimChangeWordForward,
|
|
1325
|
+
vimChangeWordBackward,
|
|
1326
|
+
vimChangeWordEnd,
|
|
1327
|
+
vimDeleteLine,
|
|
1328
|
+
vimChangeLine,
|
|
1329
|
+
vimDeleteToEndOfLine,
|
|
1330
|
+
vimChangeToEndOfLine,
|
|
1331
|
+
vimChangeMovement,
|
|
1332
|
+
vimMoveLeft,
|
|
1333
|
+
vimMoveRight,
|
|
1334
|
+
vimMoveUp,
|
|
1335
|
+
vimMoveDown,
|
|
1336
|
+
vimMoveWordForward,
|
|
1337
|
+
vimMoveWordBackward,
|
|
1338
|
+
vimMoveWordEnd,
|
|
1339
|
+
vimDeleteChar,
|
|
1340
|
+
vimInsertAtCursor,
|
|
1341
|
+
vimAppendAtCursor,
|
|
1342
|
+
vimOpenLineBelow,
|
|
1343
|
+
vimOpenLineAbove,
|
|
1344
|
+
vimAppendAtLineEnd,
|
|
1345
|
+
vimInsertAtLineStart,
|
|
1346
|
+
vimMoveToLineStart,
|
|
1347
|
+
vimMoveToLineEnd,
|
|
1348
|
+
vimMoveToFirstNonWhitespace,
|
|
1349
|
+
vimMoveToFirstLine,
|
|
1350
|
+
vimMoveToLastLine,
|
|
1351
|
+
vimMoveToLine,
|
|
1352
|
+
vimEscapeInsertMode,
|
|
1008
1353
|
};
|
|
1009
1354
|
return returnValue;
|
|
1010
1355
|
}
|