Refactor text-buffer to use reducer (#2652)
This commit is contained in:
parent
8d3fec08e5
commit
32db5ba0e1
|
@ -11,8 +11,150 @@ import {
|
|||
Viewport,
|
||||
TextBuffer,
|
||||
offsetToLogicalPos,
|
||||
textBufferReducer,
|
||||
TextBufferState,
|
||||
TextBufferAction,
|
||||
} from './text-buffer.js';
|
||||
|
||||
const initialState: TextBufferState = {
|
||||
lines: [''],
|
||||
cursorRow: 0,
|
||||
cursorCol: 0,
|
||||
preferredCol: null,
|
||||
undoStack: [],
|
||||
redoStack: [],
|
||||
clipboard: null,
|
||||
selectionAnchor: null,
|
||||
};
|
||||
|
||||
describe('textBufferReducer', () => {
|
||||
it('should return the initial state if state is undefined', () => {
|
||||
const action = { type: 'unknown_action' } as unknown as TextBufferAction;
|
||||
const state = textBufferReducer(initialState, action);
|
||||
expect(state).toEqual(initialState);
|
||||
});
|
||||
|
||||
describe('set_text action', () => {
|
||||
it('should set new text and move cursor to the end', () => {
|
||||
const action: TextBufferAction = {
|
||||
type: 'set_text',
|
||||
payload: 'hello\nworld',
|
||||
};
|
||||
const state = textBufferReducer(initialState, action);
|
||||
expect(state.lines).toEqual(['hello', 'world']);
|
||||
expect(state.cursorRow).toBe(1);
|
||||
expect(state.cursorCol).toBe(5);
|
||||
expect(state.undoStack.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should not create an undo snapshot if pushToUndo is false', () => {
|
||||
const action: TextBufferAction = {
|
||||
type: 'set_text',
|
||||
payload: 'no undo',
|
||||
pushToUndo: false,
|
||||
};
|
||||
const state = textBufferReducer(initialState, action);
|
||||
expect(state.lines).toEqual(['no undo']);
|
||||
expect(state.undoStack.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('insert action', () => {
|
||||
it('should insert a character', () => {
|
||||
const action: TextBufferAction = { type: 'insert', payload: 'a' };
|
||||
const state = textBufferReducer(initialState, action);
|
||||
expect(state.lines).toEqual(['a']);
|
||||
expect(state.cursorCol).toBe(1);
|
||||
});
|
||||
|
||||
it('should insert a newline', () => {
|
||||
const stateWithText = { ...initialState, lines: ['hello'] };
|
||||
const action: TextBufferAction = { type: 'insert', payload: '\n' };
|
||||
const state = textBufferReducer(stateWithText, action);
|
||||
expect(state.lines).toEqual(['', 'hello']);
|
||||
expect(state.cursorRow).toBe(1);
|
||||
expect(state.cursorCol).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('backspace action', () => {
|
||||
it('should remove a character', () => {
|
||||
const stateWithText: TextBufferState = {
|
||||
...initialState,
|
||||
lines: ['a'],
|
||||
cursorRow: 0,
|
||||
cursorCol: 1,
|
||||
};
|
||||
const action: TextBufferAction = { type: 'backspace' };
|
||||
const state = textBufferReducer(stateWithText, action);
|
||||
expect(state.lines).toEqual(['']);
|
||||
expect(state.cursorCol).toBe(0);
|
||||
});
|
||||
|
||||
it('should join lines if at the beginning of a line', () => {
|
||||
const stateWithText: TextBufferState = {
|
||||
...initialState,
|
||||
lines: ['hello', 'world'],
|
||||
cursorRow: 1,
|
||||
cursorCol: 0,
|
||||
};
|
||||
const action: TextBufferAction = { type: 'backspace' };
|
||||
const state = textBufferReducer(stateWithText, action);
|
||||
expect(state.lines).toEqual(['helloworld']);
|
||||
expect(state.cursorRow).toBe(0);
|
||||
expect(state.cursorCol).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('undo/redo actions', () => {
|
||||
it('should undo and redo a change', () => {
|
||||
// 1. Insert text
|
||||
const insertAction: TextBufferAction = {
|
||||
type: 'insert',
|
||||
payload: 'test',
|
||||
};
|
||||
const stateAfterInsert = textBufferReducer(initialState, insertAction);
|
||||
expect(stateAfterInsert.lines).toEqual(['test']);
|
||||
expect(stateAfterInsert.undoStack.length).toBe(1);
|
||||
|
||||
// 2. Undo
|
||||
const undoAction: TextBufferAction = { type: 'undo' };
|
||||
const stateAfterUndo = textBufferReducer(stateAfterInsert, undoAction);
|
||||
expect(stateAfterUndo.lines).toEqual(['']);
|
||||
expect(stateAfterUndo.undoStack.length).toBe(0);
|
||||
expect(stateAfterUndo.redoStack.length).toBe(1);
|
||||
|
||||
// 3. Redo
|
||||
const redoAction: TextBufferAction = { type: 'redo' };
|
||||
const stateAfterRedo = textBufferReducer(stateAfterUndo, redoAction);
|
||||
expect(stateAfterRedo.lines).toEqual(['test']);
|
||||
expect(stateAfterRedo.undoStack.length).toBe(1);
|
||||
expect(stateAfterRedo.redoStack.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create_undo_snapshot action', () => {
|
||||
it('should create a snapshot without changing state', () => {
|
||||
const stateWithText: TextBufferState = {
|
||||
...initialState,
|
||||
lines: ['hello'],
|
||||
cursorRow: 0,
|
||||
cursorCol: 5,
|
||||
};
|
||||
const action: TextBufferAction = { type: 'create_undo_snapshot' };
|
||||
const state = textBufferReducer(stateWithText, action);
|
||||
|
||||
expect(state.lines).toEqual(['hello']);
|
||||
expect(state.cursorRow).toBe(0);
|
||||
expect(state.cursorCol).toBe(5);
|
||||
expect(state.undoStack.length).toBe(1);
|
||||
expect(state.undoStack[0].lines).toEqual(['hello']);
|
||||
expect(state.undoStack[0].cursorRow).toBe(0);
|
||||
expect(state.undoStack[0].cursorCol).toBe(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Helper to get the state from the hook
|
||||
const getBufferState = (result: { current: TextBuffer }) => ({
|
||||
text: result.current.text,
|
||||
|
@ -644,11 +786,27 @@ describe('useTextBuffer', () => {
|
|||
expect(getBufferState(result).cursor).toEqual([0, 5]);
|
||||
|
||||
act(() => {
|
||||
result.current.applyOperations([
|
||||
{ type: 'backspace' },
|
||||
{ type: 'backspace' },
|
||||
{ type: 'backspace' },
|
||||
]);
|
||||
result.current.handleInput({
|
||||
name: 'backspace',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
sequence: '\x7f',
|
||||
});
|
||||
result.current.handleInput({
|
||||
name: 'backspace',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
sequence: '\x7f',
|
||||
});
|
||||
result.current.handleInput({
|
||||
name: 'backspace',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
sequence: '\x7f',
|
||||
});
|
||||
});
|
||||
expect(getBufferState(result).text).toBe('ab');
|
||||
expect(getBufferState(result).cursor).toEqual([0, 2]);
|
||||
|
@ -666,9 +824,7 @@ describe('useTextBuffer', () => {
|
|||
expect(getBufferState(result).cursor).toEqual([0, 5]);
|
||||
|
||||
act(() => {
|
||||
result.current.applyOperations([
|
||||
{ type: 'insert', payload: '\x7f\x7f\x7f' },
|
||||
]);
|
||||
result.current.insert('\x7f\x7f\x7f');
|
||||
});
|
||||
expect(getBufferState(result).text).toBe('ab');
|
||||
expect(getBufferState(result).cursor).toEqual([0, 2]);
|
||||
|
@ -686,9 +842,7 @@ describe('useTextBuffer', () => {
|
|||
expect(getBufferState(result).cursor).toEqual([0, 5]);
|
||||
|
||||
act(() => {
|
||||
result.current.applyOperations([
|
||||
{ type: 'insert', payload: '\x7fI\x7f\x7fNEW' },
|
||||
]);
|
||||
result.current.insert('\x7fI\x7f\x7fNEW');
|
||||
});
|
||||
expect(getBufferState(result).text).toBe('abcNEW');
|
||||
expect(getBufferState(result).cursor).toEqual([0, 6]);
|
||||
|
@ -774,11 +928,9 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
|
|||
|
||||
// Simulate pasting the long text multiple times
|
||||
act(() => {
|
||||
result.current.applyOperations([
|
||||
{ type: 'insert', payload: longText },
|
||||
{ type: 'insert', payload: longText },
|
||||
{ type: 'insert', payload: longText },
|
||||
]);
|
||||
result.current.insert(longText);
|
||||
result.current.insert(longText);
|
||||
result.current.insert(longText);
|
||||
});
|
||||
|
||||
const state = getBufferState(result);
|
||||
|
@ -909,17 +1061,15 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
|
|||
isValidPath: () => false,
|
||||
}),
|
||||
);
|
||||
let success = true;
|
||||
act(() => {
|
||||
success = result.current.replaceRange(0, 5, 0, 3, 'fail'); // startCol > endCol in same line
|
||||
result.current.replaceRange(0, 5, 0, 3, 'fail'); // startCol > endCol in same line
|
||||
});
|
||||
expect(success).toBe(false);
|
||||
|
||||
expect(getBufferState(result).text).toBe('test');
|
||||
|
||||
act(() => {
|
||||
success = result.current.replaceRange(1, 0, 0, 0, 'fail'); // startRow > endRow
|
||||
result.current.replaceRange(1, 0, 0, 0, 'fail'); // startRow > endRow
|
||||
});
|
||||
expect(success).toBe(false);
|
||||
expect(getBufferState(result).text).toBe('test');
|
||||
});
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue