mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-20 11:00:40 -07:00
feat(cli): Add support for Ctrl+Backspace to delete a word backward (#7162)
This commit is contained in:
@@ -172,6 +172,117 @@ describe('textBufferReducer', () => {
|
||||
expect(state.undoStack[0].cursorCol).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete_word_left action', () => {
|
||||
it('should delete a simple word', () => {
|
||||
const stateWithText: TextBufferState = {
|
||||
...initialState,
|
||||
lines: ['hello world'],
|
||||
cursorRow: 0,
|
||||
cursorCol: 11,
|
||||
};
|
||||
const action: TextBufferAction = { type: 'delete_word_left' };
|
||||
const state = textBufferReducer(stateWithText, action);
|
||||
expect(state.lines).toEqual(['hello ']);
|
||||
expect(state.cursorCol).toBe(6);
|
||||
});
|
||||
|
||||
it('should delete a path segment', () => {
|
||||
const stateWithText: TextBufferState = {
|
||||
...initialState,
|
||||
lines: ['path/to/file'],
|
||||
cursorRow: 0,
|
||||
cursorCol: 12,
|
||||
};
|
||||
const action: TextBufferAction = { type: 'delete_word_left' };
|
||||
const state = textBufferReducer(stateWithText, action);
|
||||
expect(state.lines).toEqual(['path/to/']);
|
||||
expect(state.cursorCol).toBe(8);
|
||||
});
|
||||
|
||||
it('should delete variable_name parts', () => {
|
||||
const stateWithText: TextBufferState = {
|
||||
...initialState,
|
||||
lines: ['variable_name'],
|
||||
cursorRow: 0,
|
||||
cursorCol: 13,
|
||||
};
|
||||
const action: TextBufferAction = { type: 'delete_word_left' };
|
||||
const state = textBufferReducer(stateWithText, action);
|
||||
expect(state.lines).toEqual(['variable_']);
|
||||
expect(state.cursorCol).toBe(9);
|
||||
});
|
||||
|
||||
it('should act like backspace at the beginning of a line', () => {
|
||||
const stateWithText: TextBufferState = {
|
||||
...initialState,
|
||||
lines: ['hello', 'world'],
|
||||
cursorRow: 1,
|
||||
cursorCol: 0,
|
||||
};
|
||||
const action: TextBufferAction = { type: 'delete_word_left' };
|
||||
const state = textBufferReducer(stateWithText, action);
|
||||
expect(state.lines).toEqual(['helloworld']);
|
||||
expect(state.cursorRow).toBe(0);
|
||||
expect(state.cursorCol).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete_word_right action', () => {
|
||||
it('should delete a simple word', () => {
|
||||
const stateWithText: TextBufferState = {
|
||||
...initialState,
|
||||
lines: ['hello world'],
|
||||
cursorRow: 0,
|
||||
cursorCol: 0,
|
||||
};
|
||||
const action: TextBufferAction = { type: 'delete_word_right' };
|
||||
const state = textBufferReducer(stateWithText, action);
|
||||
expect(state.lines).toEqual(['world']);
|
||||
expect(state.cursorCol).toBe(0);
|
||||
});
|
||||
|
||||
it('should delete a path segment', () => {
|
||||
const stateWithText: TextBufferState = {
|
||||
...initialState,
|
||||
lines: ['path/to/file'],
|
||||
cursorRow: 0,
|
||||
cursorCol: 0,
|
||||
};
|
||||
const action: TextBufferAction = { type: 'delete_word_right' };
|
||||
let state = textBufferReducer(stateWithText, action);
|
||||
expect(state.lines).toEqual(['/to/file']);
|
||||
state = textBufferReducer(state, action);
|
||||
expect(state.lines).toEqual(['to/file']);
|
||||
});
|
||||
|
||||
it('should delete variable_name parts', () => {
|
||||
const stateWithText: TextBufferState = {
|
||||
...initialState,
|
||||
lines: ['variable_name'],
|
||||
cursorRow: 0,
|
||||
cursorCol: 0,
|
||||
};
|
||||
const action: TextBufferAction = { type: 'delete_word_right' };
|
||||
const state = textBufferReducer(stateWithText, action);
|
||||
expect(state.lines).toEqual(['_name']);
|
||||
expect(state.cursorCol).toBe(0);
|
||||
});
|
||||
|
||||
it('should act like delete at the end of a line', () => {
|
||||
const stateWithText: TextBufferState = {
|
||||
...initialState,
|
||||
lines: ['hello', 'world'],
|
||||
cursorRow: 0,
|
||||
cursorCol: 5,
|
||||
};
|
||||
const action: TextBufferAction = { type: 'delete_word_right' };
|
||||
const state = textBufferReducer(stateWithText, action);
|
||||
expect(state.lines).toEqual(['helloworld']);
|
||||
expect(state.cursorRow).toBe(0);
|
||||
expect(state.cursorCol).toBe(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Helper to get the state from the hook
|
||||
|
||||
@@ -1229,47 +1229,38 @@ export function textBufferReducer(
|
||||
case 'delete_word_left': {
|
||||
const { cursorRow, cursorCol } = state;
|
||||
if (cursorCol === 0 && cursorRow === 0) return state;
|
||||
if (cursorCol === 0) {
|
||||
|
||||
const nextState = pushUndoLocal(state);
|
||||
const newLines = [...nextState.lines];
|
||||
let newCursorRow = cursorRow;
|
||||
let newCursorCol = cursorCol;
|
||||
|
||||
if (newCursorCol > 0) {
|
||||
const lineContent = currentLine(newCursorRow);
|
||||
const prevWordStart = findPrevWordStartInLine(
|
||||
lineContent,
|
||||
newCursorCol,
|
||||
);
|
||||
const start = prevWordStart === null ? 0 : prevWordStart;
|
||||
newLines[newCursorRow] =
|
||||
cpSlice(lineContent, 0, start) + cpSlice(lineContent, newCursorCol);
|
||||
newCursorCol = start;
|
||||
} else {
|
||||
// Act as a backspace
|
||||
const nextState = pushUndoLocal(state);
|
||||
const prevLineContent = currentLine(cursorRow - 1);
|
||||
const currentLineContentVal = currentLine(cursorRow);
|
||||
const newCol = cpLen(prevLineContent);
|
||||
const newLines = [...nextState.lines];
|
||||
newLines[cursorRow - 1] = prevLineContent + currentLineContentVal;
|
||||
newLines.splice(cursorRow, 1);
|
||||
return {
|
||||
...nextState,
|
||||
lines: newLines,
|
||||
cursorRow: cursorRow - 1,
|
||||
cursorCol: newCol,
|
||||
preferredCol: null,
|
||||
};
|
||||
newCursorRow--;
|
||||
newCursorCol = newCol;
|
||||
}
|
||||
const nextState = pushUndoLocal(state);
|
||||
const lineContent = currentLine(cursorRow);
|
||||
const arr = toCodePoints(lineContent);
|
||||
let start = cursorCol;
|
||||
let onlySpaces = true;
|
||||
for (let i = 0; i < start; i++) {
|
||||
if (isWordChar(arr[i])) {
|
||||
onlySpaces = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (onlySpaces && start > 0) {
|
||||
start--;
|
||||
} else {
|
||||
while (start > 0 && !isWordChar(arr[start - 1])) start--;
|
||||
while (start > 0 && isWordChar(arr[start - 1])) start--;
|
||||
}
|
||||
const newLines = [...nextState.lines];
|
||||
newLines[cursorRow] =
|
||||
cpSlice(lineContent, 0, start) + cpSlice(lineContent, cursorCol);
|
||||
|
||||
return {
|
||||
...nextState,
|
||||
lines: newLines,
|
||||
cursorCol: start,
|
||||
cursorRow: newCursorRow,
|
||||
cursorCol: newCursorCol,
|
||||
preferredCol: null,
|
||||
};
|
||||
}
|
||||
@@ -1277,26 +1268,32 @@ export function textBufferReducer(
|
||||
case 'delete_word_right': {
|
||||
const { cursorRow, cursorCol, lines } = state;
|
||||
const lineContent = currentLine(cursorRow);
|
||||
const arr = toCodePoints(lineContent);
|
||||
if (cursorCol >= arr.length && cursorRow === lines.length - 1)
|
||||
const lineLen = cpLen(lineContent);
|
||||
|
||||
if (cursorCol >= lineLen && cursorRow === lines.length - 1) {
|
||||
return state;
|
||||
if (cursorCol >= arr.length) {
|
||||
// Act as a delete
|
||||
const nextState = pushUndoLocal(state);
|
||||
}
|
||||
|
||||
const nextState = pushUndoLocal(state);
|
||||
const newLines = [...nextState.lines];
|
||||
|
||||
if (cursorCol >= lineLen) {
|
||||
// Act as a delete, joining with the next line
|
||||
const nextLineContent = currentLine(cursorRow + 1);
|
||||
const newLines = [...nextState.lines];
|
||||
newLines[cursorRow] = lineContent + nextLineContent;
|
||||
newLines.splice(cursorRow + 1, 1);
|
||||
return { ...nextState, lines: newLines, preferredCol: null };
|
||||
} else {
|
||||
const nextWordStart = findNextWordStartInLine(lineContent, cursorCol);
|
||||
const end = nextWordStart === null ? lineLen : nextWordStart;
|
||||
newLines[cursorRow] =
|
||||
cpSlice(lineContent, 0, cursorCol) + cpSlice(lineContent, end);
|
||||
}
|
||||
const nextState = pushUndoLocal(state);
|
||||
let end = cursorCol;
|
||||
while (end < arr.length && !isWordChar(arr[end])) end++;
|
||||
while (end < arr.length && isWordChar(arr[end])) end++;
|
||||
const newLines = [...nextState.lines];
|
||||
newLines[cursorRow] =
|
||||
cpSlice(lineContent, 0, cursorCol) + cpSlice(lineContent, end);
|
||||
return { ...nextState, lines: newLines, preferredCol: null };
|
||||
|
||||
return {
|
||||
...nextState,
|
||||
lines: newLines,
|
||||
preferredCol: null,
|
||||
};
|
||||
}
|
||||
|
||||
case 'kill_line_right': {
|
||||
@@ -1902,6 +1899,7 @@ export function useTextBuffer({
|
||||
moveToOffset,
|
||||
deleteWordLeft,
|
||||
deleteWordRight,
|
||||
|
||||
killLineRight,
|
||||
killLineLeft,
|
||||
handleInput,
|
||||
@@ -2011,6 +2009,7 @@ export interface TextBuffer {
|
||||
* follows the caret and the next contiguous run of word characters.
|
||||
*/
|
||||
deleteWordRight: () => void;
|
||||
|
||||
/**
|
||||
* Deletes text from the cursor to the end of the current line.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user