Fix characters being dropped in text-buffer (#2504)
Co-authored-by: Sandy Tao <sandytao520@icloud.com> Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
parent
82afc75350
commit
3a995305c0
|
@ -420,6 +420,7 @@ export function useTextBuffer({
|
||||||
const [undoStack, setUndoStack] = useState<UndoHistoryEntry[]>([]);
|
const [undoStack, setUndoStack] = useState<UndoHistoryEntry[]>([]);
|
||||||
const [redoStack, setRedoStack] = useState<UndoHistoryEntry[]>([]);
|
const [redoStack, setRedoStack] = useState<UndoHistoryEntry[]>([]);
|
||||||
const historyLimit = 100;
|
const historyLimit = 100;
|
||||||
|
const [opQueue, setOpQueue] = useState<UpdateOperation[]>([]);
|
||||||
|
|
||||||
const [clipboard, setClipboard] = useState<string | null>(null);
|
const [clipboard, setClipboard] = useState<string | null>(null);
|
||||||
const [selectionAnchor, setSelectionAnchor] = useState<
|
const [selectionAnchor, setSelectionAnchor] = useState<
|
||||||
|
@ -526,148 +527,110 @@ export function useTextBuffer({
|
||||||
return _restoreState(state);
|
return _restoreState(state);
|
||||||
}, [redoStack, lines, cursorRow, cursorCol, _restoreState]);
|
}, [redoStack, lines, cursorRow, cursorCol, _restoreState]);
|
||||||
|
|
||||||
const insertStr = useCallback(
|
const applyOperations = useCallback((ops: UpdateOperation[]) => {
|
||||||
(str: string): boolean => {
|
if (ops.length === 0) return;
|
||||||
dbg('insertStr', { str, beforeCursor: [cursorRow, cursorCol] });
|
setOpQueue((prev) => [...prev, ...ops]);
|
||||||
if (str === '') return false;
|
}, []);
|
||||||
|
|
||||||
pushUndo();
|
useEffect(() => {
|
||||||
let normalised = str.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
if (opQueue.length === 0) return;
|
||||||
normalised = stripUnsafeCharacters(normalised);
|
|
||||||
|
|
||||||
const parts = normalised.split('\n');
|
const expandedOps: UpdateOperation[] = [];
|
||||||
|
for (const op of opQueue) {
|
||||||
const newLines = [...lines];
|
if (op.type === 'insert') {
|
||||||
const lineContent = currentLine(cursorRow);
|
let currentText = '';
|
||||||
const before = cpSlice(lineContent, 0, cursorCol);
|
for (const char of toCodePoints(op.payload)) {
|
||||||
const after = cpSlice(lineContent, cursorCol);
|
if (char.codePointAt(0) === 127) {
|
||||||
newLines[cursorRow] = before + parts[0];
|
// \x7f
|
||||||
|
if (currentText.length > 0) {
|
||||||
if (parts.length > 1) {
|
expandedOps.push({ type: 'insert', payload: currentText });
|
||||||
// Adjusted condition for inserting multiple lines
|
currentText = '';
|
||||||
const remainingParts = parts.slice(1);
|
|
||||||
const lastPartOriginal = remainingParts.pop() ?? '';
|
|
||||||
newLines.splice(cursorRow + 1, 0, ...remainingParts);
|
|
||||||
newLines.splice(
|
|
||||||
cursorRow + parts.length - 1,
|
|
||||||
0,
|
|
||||||
lastPartOriginal + after,
|
|
||||||
);
|
|
||||||
setCursorRow(cursorRow + parts.length - 1);
|
|
||||||
setCursorCol(cpLen(lastPartOriginal));
|
|
||||||
} else {
|
|
||||||
setCursorCol(cpLen(before) + cpLen(parts[0]));
|
|
||||||
}
|
|
||||||
setLines(newLines);
|
|
||||||
setPreferredCol(null);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
[pushUndo, cursorRow, cursorCol, lines, currentLine, setPreferredCol],
|
|
||||||
);
|
|
||||||
|
|
||||||
const applyOperations = useCallback(
|
|
||||||
(ops: UpdateOperation[]) => {
|
|
||||||
if (ops.length === 0) return;
|
|
||||||
|
|
||||||
const expandedOps: UpdateOperation[] = [];
|
|
||||||
for (const op of ops) {
|
|
||||||
if (op.type === 'insert') {
|
|
||||||
let currentText = '';
|
|
||||||
for (const char of toCodePoints(op.payload)) {
|
|
||||||
if (char.codePointAt(0) === 127) {
|
|
||||||
// \x7f
|
|
||||||
if (currentText.length > 0) {
|
|
||||||
expandedOps.push({ type: 'insert', payload: currentText });
|
|
||||||
currentText = '';
|
|
||||||
}
|
|
||||||
expandedOps.push({ type: 'backspace' });
|
|
||||||
} else {
|
|
||||||
currentText += char;
|
|
||||||
}
|
}
|
||||||
}
|
expandedOps.push({ type: 'backspace' });
|
||||||
if (currentText.length > 0) {
|
|
||||||
expandedOps.push({ type: 'insert', payload: currentText });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
expandedOps.push(op);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expandedOps.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pushUndo(); // Snapshot before applying batch of updates
|
|
||||||
|
|
||||||
const newLines = [...lines];
|
|
||||||
let newCursorRow = cursorRow;
|
|
||||||
let newCursorCol = cursorCol;
|
|
||||||
|
|
||||||
const currentLine = (r: number) => newLines[r] ?? '';
|
|
||||||
|
|
||||||
for (const op of expandedOps) {
|
|
||||||
if (op.type === 'insert') {
|
|
||||||
const str = stripUnsafeCharacters(
|
|
||||||
op.payload.replace(/\r\n/g, '\n').replace(/\r/g, '\n'),
|
|
||||||
);
|
|
||||||
const parts = str.split('\n');
|
|
||||||
const lineContent = currentLine(newCursorRow);
|
|
||||||
const before = cpSlice(lineContent, 0, newCursorCol);
|
|
||||||
const after = cpSlice(lineContent, newCursorCol);
|
|
||||||
|
|
||||||
if (parts.length > 1) {
|
|
||||||
newLines[newCursorRow] = before + parts[0];
|
|
||||||
const remainingParts = parts.slice(1);
|
|
||||||
const lastPartOriginal = remainingParts.pop() ?? '';
|
|
||||||
newLines.splice(newCursorRow + 1, 0, ...remainingParts);
|
|
||||||
newLines.splice(
|
|
||||||
newCursorRow + parts.length - 1,
|
|
||||||
0,
|
|
||||||
lastPartOriginal + after,
|
|
||||||
);
|
|
||||||
newCursorRow = newCursorRow + parts.length - 1;
|
|
||||||
newCursorCol = cpLen(lastPartOriginal);
|
|
||||||
} else {
|
} else {
|
||||||
newLines[newCursorRow] = before + parts[0] + after;
|
currentText += char;
|
||||||
|
|
||||||
newCursorCol = cpLen(before) + cpLen(parts[0]);
|
|
||||||
}
|
|
||||||
} else if (op.type === 'backspace') {
|
|
||||||
if (newCursorCol === 0 && newCursorRow === 0) continue;
|
|
||||||
|
|
||||||
if (newCursorCol > 0) {
|
|
||||||
const lineContent = currentLine(newCursorRow);
|
|
||||||
newLines[newCursorRow] =
|
|
||||||
cpSlice(lineContent, 0, newCursorCol - 1) +
|
|
||||||
cpSlice(lineContent, newCursorCol);
|
|
||||||
newCursorCol--;
|
|
||||||
} else if (newCursorRow > 0) {
|
|
||||||
const prevLineContent = currentLine(newCursorRow - 1);
|
|
||||||
const currentLineContentVal = currentLine(newCursorRow);
|
|
||||||
const newCol = cpLen(prevLineContent);
|
|
||||||
newLines[newCursorRow - 1] =
|
|
||||||
prevLineContent + currentLineContentVal;
|
|
||||||
newLines.splice(newCursorRow, 1);
|
|
||||||
newCursorRow--;
|
|
||||||
newCursorCol = newCol;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (currentText.length > 0) {
|
||||||
|
expandedOps.push({ type: 'insert', payload: currentText });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expandedOps.push(op);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setLines(newLines);
|
if (expandedOps.length === 0) {
|
||||||
setCursorRow(newCursorRow);
|
setOpQueue([]); // Clear queue even if ops were no-ops
|
||||||
setCursorCol(newCursorCol);
|
return;
|
||||||
setPreferredCol(null);
|
}
|
||||||
},
|
|
||||||
[lines, cursorRow, cursorCol, pushUndo, setPreferredCol],
|
pushUndo(); // Snapshot before applying batch of updates
|
||||||
);
|
|
||||||
|
const newLines = [...lines];
|
||||||
|
let newCursorRow = cursorRow;
|
||||||
|
let newCursorCol = cursorCol;
|
||||||
|
|
||||||
|
const currentLine = (r: number) => newLines[r] ?? '';
|
||||||
|
|
||||||
|
for (const op of expandedOps) {
|
||||||
|
if (op.type === 'insert') {
|
||||||
|
const str = stripUnsafeCharacters(
|
||||||
|
op.payload.replace(/\r\n/g, '\n').replace(/\r/g, '\n'),
|
||||||
|
);
|
||||||
|
const parts = str.split('\n');
|
||||||
|
const lineContent = currentLine(newCursorRow);
|
||||||
|
const before = cpSlice(lineContent, 0, newCursorCol);
|
||||||
|
const after = cpSlice(lineContent, newCursorCol);
|
||||||
|
|
||||||
|
if (parts.length > 1) {
|
||||||
|
newLines[newCursorRow] = before + parts[0];
|
||||||
|
const remainingParts = parts.slice(1);
|
||||||
|
const lastPartOriginal = remainingParts.pop() ?? '';
|
||||||
|
newLines.splice(newCursorRow + 1, 0, ...remainingParts);
|
||||||
|
newLines.splice(
|
||||||
|
newCursorRow + parts.length - 1,
|
||||||
|
0,
|
||||||
|
lastPartOriginal + after,
|
||||||
|
);
|
||||||
|
newCursorRow = newCursorRow + parts.length - 1;
|
||||||
|
newCursorCol = cpLen(lastPartOriginal);
|
||||||
|
} else {
|
||||||
|
newLines[newCursorRow] = before + parts[0] + after;
|
||||||
|
|
||||||
|
newCursorCol = cpLen(before) + cpLen(parts[0]);
|
||||||
|
}
|
||||||
|
} else if (op.type === 'backspace') {
|
||||||
|
if (newCursorCol === 0 && newCursorRow === 0) continue;
|
||||||
|
|
||||||
|
if (newCursorCol > 0) {
|
||||||
|
const lineContent = currentLine(newCursorRow);
|
||||||
|
newLines[newCursorRow] =
|
||||||
|
cpSlice(lineContent, 0, newCursorCol - 1) +
|
||||||
|
cpSlice(lineContent, newCursorCol);
|
||||||
|
newCursorCol--;
|
||||||
|
} else if (newCursorRow > 0) {
|
||||||
|
const prevLineContent = currentLine(newCursorRow - 1);
|
||||||
|
const currentLineContentVal = currentLine(newCursorRow);
|
||||||
|
const newCol = cpLen(prevLineContent);
|
||||||
|
newLines[newCursorRow - 1] = prevLineContent + currentLineContentVal;
|
||||||
|
newLines.splice(newCursorRow, 1);
|
||||||
|
newCursorRow--;
|
||||||
|
newCursorCol = newCol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLines(newLines);
|
||||||
|
setCursorRow(newCursorRow);
|
||||||
|
setCursorCol(newCursorCol);
|
||||||
|
setPreferredCol(null);
|
||||||
|
|
||||||
|
// Clear the queue after processing
|
||||||
|
setOpQueue((prev) => prev.slice(opQueue.length));
|
||||||
|
}, [opQueue, lines, cursorRow, cursorCol, pushUndo, setPreferredCol]);
|
||||||
|
|
||||||
const insert = useCallback(
|
const insert = useCallback(
|
||||||
(ch: string): void => {
|
(ch: string): void => {
|
||||||
if (/[\n\r]/.test(ch)) {
|
|
||||||
insertStr(ch);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dbg('insert', { ch, beforeCursor: [cursorRow, cursorCol] });
|
dbg('insert', { ch, beforeCursor: [cursorRow, cursorCol] });
|
||||||
|
|
||||||
ch = stripUnsafeCharacters(ch);
|
ch = stripUnsafeCharacters(ch);
|
||||||
|
@ -694,7 +657,7 @@ export function useTextBuffer({
|
||||||
}
|
}
|
||||||
applyOperations([{ type: 'insert', payload: ch }]);
|
applyOperations([{ type: 'insert', payload: ch }]);
|
||||||
},
|
},
|
||||||
[applyOperations, cursorRow, cursorCol, isValidPath, insertStr],
|
[applyOperations, cursorRow, cursorCol, isValidPath],
|
||||||
);
|
);
|
||||||
|
|
||||||
const newline = useCallback((): void => {
|
const newline = useCallback((): void => {
|
||||||
|
@ -1397,8 +1360,9 @@ export function useTextBuffer({
|
||||||
}, [selectionAnchor, cursorRow, cursorCol, currentLine, setClipboard]),
|
}, [selectionAnchor, cursorRow, cursorCol, currentLine, setClipboard]),
|
||||||
paste: useCallback(() => {
|
paste: useCallback(() => {
|
||||||
if (clipboard === null) return false;
|
if (clipboard === null) return false;
|
||||||
return insertStr(clipboard);
|
applyOperations([{ type: 'insert', payload: clipboard }]);
|
||||||
}, [clipboard, insertStr]),
|
return true;
|
||||||
|
}, [clipboard, applyOperations]),
|
||||||
startSelection: useCallback(
|
startSelection: useCallback(
|
||||||
() => setSelectionAnchor([cursorRow, cursorCol]),
|
() => setSelectionAnchor([cursorRow, cursorCol]),
|
||||||
[cursorRow, cursorCol, setSelectionAnchor],
|
[cursorRow, cursorCol, setSelectionAnchor],
|
||||||
|
|
Loading…
Reference in New Issue