This commit is contained in:
Sijie Wang
2025-07-25 15:36:42 -07:00
committed by GitHub
parent aa71438684
commit fbdc8d5ab3
21 changed files with 5324 additions and 191 deletions

View File

@@ -13,6 +13,7 @@ import { useState, useCallback, useEffect, useMemo, useReducer } from 'react';
import stringWidth from 'string-width';
import { unescapePath } from '@google/gemini-cli-core';
import { toCodePoints, cpLen, cpSlice } from '../../utils/textUtils.js';
import { handleVimAction, VimAction } from './vim-buffer-actions.js';
export type Direction =
| 'left'
@@ -32,6 +33,283 @@ function isWordChar(ch: string | undefined): boolean {
return !/[\s,.;!?]/.test(ch);
}
// Vim-specific word boundary functions
export const findNextWordStart = (
text: string,
currentOffset: number,
): number => {
let i = currentOffset;
if (i >= text.length) return i;
const currentChar = text[i];
// Skip current word/sequence based on character type
if (/\w/.test(currentChar)) {
// Skip current word characters
while (i < text.length && /\w/.test(text[i])) {
i++;
}
} else if (!/\s/.test(currentChar)) {
// Skip current non-word, non-whitespace characters (like "/", ".", etc.)
while (i < text.length && !/\w/.test(text[i]) && !/\s/.test(text[i])) {
i++;
}
}
// Skip whitespace
while (i < text.length && /\s/.test(text[i])) {
i++;
}
// If we reached the end of text and there's no next word,
// vim behavior for dw is to delete to the end of the current word
if (i >= text.length) {
// Go back to find the end of the last word
let endOfLastWord = text.length - 1;
while (endOfLastWord >= 0 && /\s/.test(text[endOfLastWord])) {
endOfLastWord--;
}
// For dw on last word, return position AFTER the last character to delete entire word
return Math.max(currentOffset + 1, endOfLastWord + 1);
}
return i;
};
export const findPrevWordStart = (
text: string,
currentOffset: number,
): number => {
let i = currentOffset;
// If at beginning of text, return current position
if (i <= 0) {
return currentOffset;
}
// Move back one character to start searching
i--;
// Skip whitespace moving backwards
while (i >= 0 && (text[i] === ' ' || text[i] === '\t' || text[i] === '\n')) {
i--;
}
if (i < 0) {
return 0; // Reached beginning of text
}
const charAtI = text[i];
if (/\w/.test(charAtI)) {
// We're in a word, move to its beginning
while (i >= 0 && /\w/.test(text[i])) {
i--;
}
return i + 1; // Return first character of word
} else {
// We're in punctuation, move to its beginning
while (
i >= 0 &&
!/\w/.test(text[i]) &&
text[i] !== ' ' &&
text[i] !== '\t' &&
text[i] !== '\n'
) {
i--;
}
return i + 1; // Return first character of punctuation sequence
}
};
export const findWordEnd = (text: string, currentOffset: number): number => {
let i = currentOffset;
// If we're already at the end of a word, advance to next word
if (
i < text.length &&
/\w/.test(text[i]) &&
(i + 1 >= text.length || !/\w/.test(text[i + 1]))
) {
// We're at the end of a word, move forward to find next word
i++;
// Skip whitespace/punctuation to find next word
while (i < text.length && !/\w/.test(text[i])) {
i++;
}
}
// If we're not on a word character, find the next word
if (i < text.length && !/\w/.test(text[i])) {
while (i < text.length && !/\w/.test(text[i])) {
i++;
}
}
// Move to end of current word
while (i < text.length && /\w/.test(text[i])) {
i++;
}
// Move back one to be on the last character of the word
return Math.max(currentOffset, i - 1);
};
// Helper functions for vim operations
export const getOffsetFromPosition = (
row: number,
col: number,
lines: string[],
): number => {
let offset = 0;
for (let i = 0; i < row; i++) {
offset += lines[i].length + 1; // +1 for newline
}
offset += col;
return offset;
};
export const getPositionFromOffsets = (
startOffset: number,
endOffset: number,
lines: string[],
) => {
let offset = 0;
let startRow = 0;
let startCol = 0;
let endRow = 0;
let endCol = 0;
// Find start position
for (let i = 0; i < lines.length; i++) {
const lineLength = lines[i].length + 1; // +1 for newline
if (offset + lineLength > startOffset) {
startRow = i;
startCol = startOffset - offset;
break;
}
offset += lineLength;
}
// Find end position
offset = 0;
for (let i = 0; i < lines.length; i++) {
const lineLength = lines[i].length + (i < lines.length - 1 ? 1 : 0); // +1 for newline except last line
if (offset + lineLength >= endOffset) {
endRow = i;
endCol = endOffset - offset;
break;
}
offset += lineLength;
}
return { startRow, startCol, endRow, endCol };
};
export const getLineRangeOffsets = (
startRow: number,
lineCount: number,
lines: string[],
) => {
let startOffset = 0;
// Calculate start offset
for (let i = 0; i < startRow; i++) {
startOffset += lines[i].length + 1; // +1 for newline
}
// Calculate end offset
let endOffset = startOffset;
for (let i = 0; i < lineCount; i++) {
const lineIndex = startRow + i;
if (lineIndex < lines.length) {
endOffset += lines[lineIndex].length;
if (lineIndex < lines.length - 1) {
endOffset += 1; // +1 for newline
}
}
}
return { startOffset, endOffset };
};
export const replaceRangeInternal = (
state: TextBufferState,
startRow: number,
startCol: number,
endRow: number,
endCol: number,
text: string,
): TextBufferState => {
const currentLine = (row: number) => state.lines[row] || '';
const currentLineLen = (row: number) => cpLen(currentLine(row));
const clamp = (value: number, min: number, max: number) =>
Math.min(Math.max(value, min), max);
if (
startRow > endRow ||
(startRow === endRow && startCol > endCol) ||
startRow < 0 ||
startCol < 0 ||
endRow >= state.lines.length ||
(endRow < state.lines.length && endCol > currentLineLen(endRow))
) {
return state; // Invalid range
}
const newLines = [...state.lines];
const sCol = clamp(startCol, 0, currentLineLen(startRow));
const eCol = clamp(endCol, 0, currentLineLen(endRow));
const prefix = cpSlice(currentLine(startRow), 0, sCol);
const suffix = cpSlice(currentLine(endRow), eCol);
const normalisedReplacement = text
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n');
const replacementParts = normalisedReplacement.split('\n');
// Replace the content
if (startRow === endRow) {
newLines[startRow] = prefix + normalisedReplacement + suffix;
} else {
const firstLine = prefix + replacementParts[0];
if (replacementParts.length === 1) {
// Single line of replacement text, but spanning multiple original lines
newLines.splice(startRow, endRow - startRow + 1, firstLine + suffix);
} else {
// Multi-line replacement text
const lastLine = replacementParts[replacementParts.length - 1] + suffix;
const middleLines = replacementParts.slice(1, -1);
newLines.splice(
startRow,
endRow - startRow + 1,
firstLine,
...middleLines,
lastLine,
);
}
}
const finalCursorRow = startRow + replacementParts.length - 1;
const finalCursorCol =
(replacementParts.length > 1 ? 0 : sCol) +
cpLen(replacementParts[replacementParts.length - 1]);
return {
...state,
lines: newLines,
cursorRow: Math.min(Math.max(finalCursorRow, 0), newLines.length - 1),
cursorCol: Math.max(
0,
Math.min(finalCursorCol, cpLen(newLines[finalCursorRow] || '')),
),
preferredCol: null,
};
};
/**
* Strip characters that can break terminal rendering.
*
@@ -158,6 +436,33 @@ export function offsetToLogicalPos(
return [row, col];
}
/**
* Converts logical row/col position to absolute text offset
* Inverse operation of offsetToLogicalPos
*/
export function logicalPosToOffset(
lines: string[],
row: number,
col: number,
): number {
let offset = 0;
// Clamp row to valid range
const actualRow = Math.min(row, lines.length - 1);
// Add lengths of all lines before the target row
for (let i = 0; i < actualRow; i++) {
offset += cpLen(lines[i]) + 1; // +1 for newline
}
// Add column offset within the target row
if (actualRow >= 0 && actualRow < lines.length) {
offset += Math.min(col, cpLen(lines[actualRow]));
}
return offset;
}
// Helper to calculate visual lines and map cursor positions
function calculateVisualLayout(
logicalLines: string[],
@@ -376,7 +681,7 @@ function calculateVisualLayout(
// --- Start of reducer logic ---
interface TextBufferState {
export interface TextBufferState {
lines: string[];
cursorRow: number;
cursorCol: number;
@@ -390,7 +695,20 @@ interface TextBufferState {
const historyLimit = 100;
type TextBufferAction =
export const pushUndo = (currentState: TextBufferState): TextBufferState => {
const snapshot = {
lines: [...currentState.lines],
cursorRow: currentState.cursorRow,
cursorCol: currentState.cursorCol,
};
const newStack = [...currentState.undoStack, snapshot];
if (newStack.length > historyLimit) {
newStack.shift();
}
return { ...currentState, undoStack: newStack, redoStack: [] };
};
export type TextBufferAction =
| { type: 'set_text'; payload: string; pushToUndo?: boolean }
| { type: 'insert'; payload: string }
| { type: 'backspace' }
@@ -419,24 +737,49 @@ type TextBufferAction =
}
| { type: 'move_to_offset'; payload: { offset: number } }
| { type: 'create_undo_snapshot' }
| { type: 'set_viewport_width'; payload: number };
| { type: 'set_viewport_width'; payload: number }
| { type: 'vim_delete_word_forward'; payload: { count: number } }
| { type: 'vim_delete_word_backward'; payload: { count: number } }
| { type: 'vim_delete_word_end'; payload: { count: number } }
| { type: 'vim_change_word_forward'; payload: { count: number } }
| { type: 'vim_change_word_backward'; payload: { count: number } }
| { type: 'vim_change_word_end'; payload: { count: number } }
| { type: 'vim_delete_line'; payload: { count: number } }
| { type: 'vim_change_line'; payload: { count: number } }
| { type: 'vim_delete_to_end_of_line' }
| { type: 'vim_change_to_end_of_line' }
| {
type: 'vim_change_movement';
payload: { movement: 'h' | 'j' | 'k' | 'l'; count: number };
}
// New vim actions for stateless command handling
| { type: 'vim_move_left'; payload: { count: number } }
| { type: 'vim_move_right'; payload: { count: number } }
| { type: 'vim_move_up'; payload: { count: number } }
| { type: 'vim_move_down'; payload: { count: number } }
| { type: 'vim_move_word_forward'; payload: { count: number } }
| { type: 'vim_move_word_backward'; payload: { count: number } }
| { type: 'vim_move_word_end'; payload: { count: number } }
| { type: 'vim_delete_char'; payload: { count: number } }
| { type: 'vim_insert_at_cursor' }
| { type: 'vim_append_at_cursor' }
| { type: 'vim_open_line_below' }
| { type: 'vim_open_line_above' }
| { type: 'vim_append_at_line_end' }
| { type: 'vim_insert_at_line_start' }
| { type: 'vim_move_to_line_start' }
| { type: 'vim_move_to_line_end' }
| { type: 'vim_move_to_first_nonwhitespace' }
| { type: 'vim_move_to_first_line' }
| { type: 'vim_move_to_last_line' }
| { type: 'vim_move_to_line'; payload: { lineNumber: number } }
| { type: 'vim_escape_insert_mode' };
export function textBufferReducer(
state: TextBufferState,
action: TextBufferAction,
): TextBufferState {
const pushUndo = (currentState: TextBufferState): TextBufferState => {
const snapshot = {
lines: [...currentState.lines],
cursorRow: currentState.cursorRow,
cursorCol: currentState.cursorCol,
};
const newStack = [...currentState.undoStack, snapshot];
if (newStack.length > historyLimit) {
newStack.shift();
}
return { ...currentState, undoStack: newStack, redoStack: [] };
};
const pushUndoLocal = pushUndo;
const currentLine = (r: number): string => state.lines[r] ?? '';
const currentLineLen = (r: number): number => cpLen(currentLine(r));
@@ -445,7 +788,7 @@ export function textBufferReducer(
case 'set_text': {
let nextState = state;
if (action.pushToUndo !== false) {
nextState = pushUndo(state);
nextState = pushUndoLocal(state);
}
const newContentLines = action.payload
.replace(/\r\n?/g, '\n')
@@ -462,7 +805,7 @@ export function textBufferReducer(
}
case 'insert': {
const nextState = pushUndo(state);
const nextState = pushUndoLocal(state);
const newLines = [...nextState.lines];
let newCursorRow = nextState.cursorRow;
let newCursorCol = nextState.cursorCol;
@@ -504,7 +847,7 @@ export function textBufferReducer(
}
case 'backspace': {
const nextState = pushUndo(state);
const nextState = pushUndoLocal(state);
const newLines = [...nextState.lines];
let newCursorRow = nextState.cursorRow;
let newCursorCol = nextState.cursorCol;
@@ -700,14 +1043,14 @@ export function textBufferReducer(
const { cursorRow, cursorCol, lines } = state;
const lineContent = currentLine(cursorRow);
if (cursorCol < currentLineLen(cursorRow)) {
const nextState = pushUndo(state);
const nextState = pushUndoLocal(state);
const newLines = [...nextState.lines];
newLines[cursorRow] =
cpSlice(lineContent, 0, cursorCol) +
cpSlice(lineContent, cursorCol + 1);
return { ...nextState, lines: newLines, preferredCol: null };
} else if (cursorRow < lines.length - 1) {
const nextState = pushUndo(state);
const nextState = pushUndoLocal(state);
const nextLineContent = currentLine(cursorRow + 1);
const newLines = [...nextState.lines];
newLines[cursorRow] = lineContent + nextLineContent;
@@ -722,7 +1065,7 @@ export function textBufferReducer(
if (cursorCol === 0 && cursorRow === 0) return state;
if (cursorCol === 0) {
// Act as a backspace
const nextState = pushUndo(state);
const nextState = pushUndoLocal(state);
const prevLineContent = currentLine(cursorRow - 1);
const currentLineContentVal = currentLine(cursorRow);
const newCol = cpLen(prevLineContent);
@@ -737,7 +1080,7 @@ export function textBufferReducer(
preferredCol: null,
};
}
const nextState = pushUndo(state);
const nextState = pushUndoLocal(state);
const lineContent = currentLine(cursorRow);
const arr = toCodePoints(lineContent);
let start = cursorCol;
@@ -773,14 +1116,14 @@ export function textBufferReducer(
return state;
if (cursorCol >= arr.length) {
// Act as a delete
const nextState = pushUndo(state);
const nextState = pushUndoLocal(state);
const nextLineContent = currentLine(cursorRow + 1);
const newLines = [...nextState.lines];
newLines[cursorRow] = lineContent + nextLineContent;
newLines.splice(cursorRow + 1, 1);
return { ...nextState, lines: newLines, preferredCol: null };
}
const nextState = pushUndo(state);
const nextState = pushUndoLocal(state);
let end = cursorCol;
while (end < arr.length && !isWordChar(arr[end])) end++;
while (end < arr.length && isWordChar(arr[end])) end++;
@@ -794,13 +1137,13 @@ export function textBufferReducer(
const { cursorRow, cursorCol, lines } = state;
const lineContent = currentLine(cursorRow);
if (cursorCol < currentLineLen(cursorRow)) {
const nextState = pushUndo(state);
const nextState = pushUndoLocal(state);
const newLines = [...nextState.lines];
newLines[cursorRow] = cpSlice(lineContent, 0, cursorCol);
return { ...nextState, lines: newLines };
} else if (cursorRow < lines.length - 1) {
// Act as a delete
const nextState = pushUndo(state);
const nextState = pushUndoLocal(state);
const nextLineContent = currentLine(cursorRow + 1);
const newLines = [...nextState.lines];
newLines[cursorRow] = lineContent + nextLineContent;
@@ -813,7 +1156,7 @@ export function textBufferReducer(
case 'kill_line_left': {
const { cursorRow, cursorCol } = state;
if (cursorCol > 0) {
const nextState = pushUndo(state);
const nextState = pushUndoLocal(state);
const lineContent = currentLine(cursorRow);
const newLines = [...nextState.lines];
newLines[cursorRow] = cpSlice(lineContent, cursorCol);
@@ -863,66 +1206,15 @@ export function textBufferReducer(
case 'replace_range': {
const { startRow, startCol, endRow, endCol, text } = action.payload;
if (
startRow > endRow ||
(startRow === endRow && startCol > endCol) ||
startRow < 0 ||
startCol < 0 ||
endRow >= state.lines.length ||
(endRow < state.lines.length && endCol > currentLineLen(endRow))
) {
return state; // Invalid range
}
const nextState = pushUndo(state);
const newLines = [...nextState.lines];
const sCol = clamp(startCol, 0, currentLineLen(startRow));
const eCol = clamp(endCol, 0, currentLineLen(endRow));
const prefix = cpSlice(currentLine(startRow), 0, sCol);
const suffix = cpSlice(currentLine(endRow), eCol);
const normalisedReplacement = text
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n');
const replacementParts = normalisedReplacement.split('\n');
// Replace the content
if (startRow === endRow) {
newLines[startRow] = prefix + normalisedReplacement + suffix;
} else {
const firstLine = prefix + replacementParts[0];
if (replacementParts.length === 1) {
// Single line of replacement text, but spanning multiple original lines
newLines.splice(startRow, endRow - startRow + 1, firstLine + suffix);
} else {
// Multi-line replacement text
const lastLine =
replacementParts[replacementParts.length - 1] + suffix;
const middleLines = replacementParts.slice(1, -1);
newLines.splice(
startRow,
endRow - startRow + 1,
firstLine,
...middleLines,
lastLine,
);
}
}
const finalCursorRow = startRow + replacementParts.length - 1;
const finalCursorCol =
(replacementParts.length > 1 ? 0 : sCol) +
cpLen(replacementParts[replacementParts.length - 1]);
return {
...nextState,
lines: newLines,
cursorRow: finalCursorRow,
cursorCol: finalCursorCol,
preferredCol: null,
};
const nextState = pushUndoLocal(state);
return replaceRangeInternal(
nextState,
startRow,
startCol,
endRow,
endCol,
text,
);
}
case 'move_to_offset': {
@@ -940,9 +1232,44 @@ export function textBufferReducer(
}
case 'create_undo_snapshot': {
return pushUndo(state);
return pushUndoLocal(state);
}
// Vim-specific operations
case 'vim_delete_word_forward':
case 'vim_delete_word_backward':
case 'vim_delete_word_end':
case 'vim_change_word_forward':
case 'vim_change_word_backward':
case 'vim_change_word_end':
case 'vim_delete_line':
case 'vim_change_line':
case 'vim_delete_to_end_of_line':
case 'vim_change_to_end_of_line':
case 'vim_change_movement':
case 'vim_move_left':
case 'vim_move_right':
case 'vim_move_up':
case 'vim_move_down':
case 'vim_move_word_forward':
case 'vim_move_word_backward':
case 'vim_move_word_end':
case 'vim_delete_char':
case 'vim_insert_at_cursor':
case 'vim_append_at_cursor':
case 'vim_open_line_below':
case 'vim_open_line_above':
case 'vim_append_at_line_end':
case 'vim_insert_at_line_start':
case 'vim_move_to_line_start':
case 'vim_move_to_line_end':
case 'vim_move_to_first_nonwhitespace':
case 'vim_move_to_first_line':
case 'vim_move_to_last_line':
case 'vim_move_to_line':
case 'vim_escape_insert_mode':
return handleVimAction(state, action as VimAction);
default: {
const exhaustiveCheck: never = action;
console.error(`Unknown action encountered: ${exhaustiveCheck}`);
@@ -1110,6 +1437,139 @@ export function useTextBuffer({
dispatch({ type: 'kill_line_left' });
}, []);
// Vim-specific operations
const vimDeleteWordForward = useCallback((count: number): void => {
dispatch({ type: 'vim_delete_word_forward', payload: { count } });
}, []);
const vimDeleteWordBackward = useCallback((count: number): void => {
dispatch({ type: 'vim_delete_word_backward', payload: { count } });
}, []);
const vimDeleteWordEnd = useCallback((count: number): void => {
dispatch({ type: 'vim_delete_word_end', payload: { count } });
}, []);
const vimChangeWordForward = useCallback((count: number): void => {
dispatch({ type: 'vim_change_word_forward', payload: { count } });
}, []);
const vimChangeWordBackward = useCallback((count: number): void => {
dispatch({ type: 'vim_change_word_backward', payload: { count } });
}, []);
const vimChangeWordEnd = useCallback((count: number): void => {
dispatch({ type: 'vim_change_word_end', payload: { count } });
}, []);
const vimDeleteLine = useCallback((count: number): void => {
dispatch({ type: 'vim_delete_line', payload: { count } });
}, []);
const vimChangeLine = useCallback((count: number): void => {
dispatch({ type: 'vim_change_line', payload: { count } });
}, []);
const vimDeleteToEndOfLine = useCallback((): void => {
dispatch({ type: 'vim_delete_to_end_of_line' });
}, []);
const vimChangeToEndOfLine = useCallback((): void => {
dispatch({ type: 'vim_change_to_end_of_line' });
}, []);
const vimChangeMovement = useCallback(
(movement: 'h' | 'j' | 'k' | 'l', count: number): void => {
dispatch({ type: 'vim_change_movement', payload: { movement, count } });
},
[],
);
// New vim navigation and operation methods
const vimMoveLeft = useCallback((count: number): void => {
dispatch({ type: 'vim_move_left', payload: { count } });
}, []);
const vimMoveRight = useCallback((count: number): void => {
dispatch({ type: 'vim_move_right', payload: { count } });
}, []);
const vimMoveUp = useCallback((count: number): void => {
dispatch({ type: 'vim_move_up', payload: { count } });
}, []);
const vimMoveDown = useCallback((count: number): void => {
dispatch({ type: 'vim_move_down', payload: { count } });
}, []);
const vimMoveWordForward = useCallback((count: number): void => {
dispatch({ type: 'vim_move_word_forward', payload: { count } });
}, []);
const vimMoveWordBackward = useCallback((count: number): void => {
dispatch({ type: 'vim_move_word_backward', payload: { count } });
}, []);
const vimMoveWordEnd = useCallback((count: number): void => {
dispatch({ type: 'vim_move_word_end', payload: { count } });
}, []);
const vimDeleteChar = useCallback((count: number): void => {
dispatch({ type: 'vim_delete_char', payload: { count } });
}, []);
const vimInsertAtCursor = useCallback((): void => {
dispatch({ type: 'vim_insert_at_cursor' });
}, []);
const vimAppendAtCursor = useCallback((): void => {
dispatch({ type: 'vim_append_at_cursor' });
}, []);
const vimOpenLineBelow = useCallback((): void => {
dispatch({ type: 'vim_open_line_below' });
}, []);
const vimOpenLineAbove = useCallback((): void => {
dispatch({ type: 'vim_open_line_above' });
}, []);
const vimAppendAtLineEnd = useCallback((): void => {
dispatch({ type: 'vim_append_at_line_end' });
}, []);
const vimInsertAtLineStart = useCallback((): void => {
dispatch({ type: 'vim_insert_at_line_start' });
}, []);
const vimMoveToLineStart = useCallback((): void => {
dispatch({ type: 'vim_move_to_line_start' });
}, []);
const vimMoveToLineEnd = useCallback((): void => {
dispatch({ type: 'vim_move_to_line_end' });
}, []);
const vimMoveToFirstNonWhitespace = useCallback((): void => {
dispatch({ type: 'vim_move_to_first_nonwhitespace' });
}, []);
const vimMoveToFirstLine = useCallback((): void => {
dispatch({ type: 'vim_move_to_first_line' });
}, []);
const vimMoveToLastLine = useCallback((): void => {
dispatch({ type: 'vim_move_to_last_line' });
}, []);
const vimMoveToLine = useCallback((lineNumber: number): void => {
dispatch({ type: 'vim_move_to_line', payload: { lineNumber } });
}, []);
const vimEscapeInsertMode = useCallback((): void => {
dispatch({ type: 'vim_escape_insert_mode' });
}, []);
const openInExternalEditor = useCallback(
async (opts: { editor?: string } = {}): Promise<void> => {
const editor =
@@ -1273,6 +1733,39 @@ export function useTextBuffer({
killLineLeft,
handleInput,
openInExternalEditor,
// Vim-specific operations
vimDeleteWordForward,
vimDeleteWordBackward,
vimDeleteWordEnd,
vimChangeWordForward,
vimChangeWordBackward,
vimChangeWordEnd,
vimDeleteLine,
vimChangeLine,
vimDeleteToEndOfLine,
vimChangeToEndOfLine,
vimChangeMovement,
vimMoveLeft,
vimMoveRight,
vimMoveUp,
vimMoveDown,
vimMoveWordForward,
vimMoveWordBackward,
vimMoveWordEnd,
vimDeleteChar,
vimInsertAtCursor,
vimAppendAtCursor,
vimOpenLineBelow,
vimOpenLineAbove,
vimAppendAtLineEnd,
vimInsertAtLineStart,
vimMoveToLineStart,
vimMoveToLineEnd,
vimMoveToFirstNonWhitespace,
vimMoveToFirstLine,
vimMoveToLastLine,
vimMoveToLine,
vimEscapeInsertMode,
};
return returnValue;
}
@@ -1387,4 +1880,134 @@ export interface TextBuffer {
replacementText: string,
) => void;
moveToOffset(offset: number): void;
// Vim-specific operations
/**
* Delete N words forward from cursor position (vim 'dw' command)
*/
vimDeleteWordForward: (count: number) => void;
/**
* Delete N words backward from cursor position (vim 'db' command)
*/
vimDeleteWordBackward: (count: number) => void;
/**
* Delete to end of N words from cursor position (vim 'de' command)
*/
vimDeleteWordEnd: (count: number) => void;
/**
* Change N words forward from cursor position (vim 'cw' command)
*/
vimChangeWordForward: (count: number) => void;
/**
* Change N words backward from cursor position (vim 'cb' command)
*/
vimChangeWordBackward: (count: number) => void;
/**
* Change to end of N words from cursor position (vim 'ce' command)
*/
vimChangeWordEnd: (count: number) => void;
/**
* Delete N lines from cursor position (vim 'dd' command)
*/
vimDeleteLine: (count: number) => void;
/**
* Change N lines from cursor position (vim 'cc' command)
*/
vimChangeLine: (count: number) => void;
/**
* Delete from cursor to end of line (vim 'D' command)
*/
vimDeleteToEndOfLine: () => void;
/**
* Change from cursor to end of line (vim 'C' command)
*/
vimChangeToEndOfLine: () => void;
/**
* Change movement operations (vim 'ch', 'cj', 'ck', 'cl' commands)
*/
vimChangeMovement: (movement: 'h' | 'j' | 'k' | 'l', count: number) => void;
/**
* Move cursor left N times (vim 'h' command)
*/
vimMoveLeft: (count: number) => void;
/**
* Move cursor right N times (vim 'l' command)
*/
vimMoveRight: (count: number) => void;
/**
* Move cursor up N times (vim 'k' command)
*/
vimMoveUp: (count: number) => void;
/**
* Move cursor down N times (vim 'j' command)
*/
vimMoveDown: (count: number) => void;
/**
* Move cursor forward N words (vim 'w' command)
*/
vimMoveWordForward: (count: number) => void;
/**
* Move cursor backward N words (vim 'b' command)
*/
vimMoveWordBackward: (count: number) => void;
/**
* Move cursor to end of Nth word (vim 'e' command)
*/
vimMoveWordEnd: (count: number) => void;
/**
* Delete N characters at cursor (vim 'x' command)
*/
vimDeleteChar: (count: number) => void;
/**
* Enter insert mode at cursor (vim 'i' command)
*/
vimInsertAtCursor: () => void;
/**
* Enter insert mode after cursor (vim 'a' command)
*/
vimAppendAtCursor: () => void;
/**
* Open new line below and enter insert mode (vim 'o' command)
*/
vimOpenLineBelow: () => void;
/**
* Open new line above and enter insert mode (vim 'O' command)
*/
vimOpenLineAbove: () => void;
/**
* Move to end of line and enter insert mode (vim 'A' command)
*/
vimAppendAtLineEnd: () => void;
/**
* Move to first non-whitespace and enter insert mode (vim 'I' command)
*/
vimInsertAtLineStart: () => void;
/**
* Move cursor to beginning of line (vim '0' command)
*/
vimMoveToLineStart: () => void;
/**
* Move cursor to end of line (vim '$' command)
*/
vimMoveToLineEnd: () => void;
/**
* Move cursor to first non-whitespace character (vim '^' command)
*/
vimMoveToFirstNonWhitespace: () => void;
/**
* Move cursor to first line (vim 'gg' command)
*/
vimMoveToFirstLine: () => void;
/**
* Move cursor to last line (vim 'G' command)
*/
vimMoveToLastLine: () => void;
/**
* Move cursor to specific line number (vim '[N]G' command)
*/
vimMoveToLine: (lineNumber: number) => void;
/**
* Handle escape from insert mode (moves cursor left if not at line start)
*/
vimEscapeInsertMode: () => void;
}