Refactor TextBuffer to be a React hook (#340)

This commit is contained in:
Jacob Richman 2025-05-13 19:55:31 -07:00 committed by GitHub
parent 7116ab9c29
commit bfda4295c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 819 additions and 1018 deletions

View File

@ -4,10 +4,10 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { TextBuffer } from './text-buffer.js'; import { useTextBuffer } from './text-buffer.js';
import chalk from 'chalk'; import chalk from 'chalk';
import { Box, Text, useInput, useStdin, Key } from 'ink'; import { Box, Text, useInput, useStdin, Key } from 'ink';
import React, { useState, useCallback } from 'react'; import React from 'react';
import { useTerminalSize } from '../../hooks/useTerminalSize.js'; import { useTerminalSize } from '../../hooks/useTerminalSize.js';
import { Colors } from '../../colors.js'; import { Colors } from '../../colors.js';
@ -68,10 +68,6 @@ export const MultilineTextEditor = ({
navigateDown, navigateDown,
inputPreprocessor, inputPreprocessor,
}: MultilineTextEditorProps): React.ReactElement => { }: MultilineTextEditorProps): React.ReactElement => {
const [buffer, setBuffer] = useState(
() => new TextBuffer(initialText, initialCursorOffset),
);
const terminalSize = useTerminalSize(); const terminalSize = useTerminalSize();
const effectiveWidth = Math.max( const effectiveWidth = Math.max(
20, 20,
@ -81,35 +77,14 @@ export const MultilineTextEditor = ({
const { stdin, setRawMode } = useStdin(); const { stdin, setRawMode } = useStdin();
// TODO(jacobr): make TextBuffer immutable rather than this hack to act const buffer = useTextBuffer({
// like it is immutable. initialText,
const updateBufferState = useCallback( initialCursorOffset,
(mutator: (currentBuffer: TextBuffer) => void) => { viewport: { height, width: effectiveWidth },
setBuffer((currentBuffer) => { stdin,
mutator(currentBuffer); setRawMode,
// Create a new instance from the mutated buffer to trigger re-render onChange, // Pass onChange to the hook
return TextBuffer.fromBuffer(currentBuffer);
}); });
},
[],
);
const openExternalEditor = useCallback(async () => {
const wasRaw = stdin?.isRaw ?? false;
try {
setRawMode?.(false);
// openInExternalEditor mutates the buffer instance
await buffer.openInExternalEditor();
} catch (err) {
console.error('[MultilineTextEditor] external editor error', err);
} finally {
if (wasRaw) {
setRawMode?.(true);
}
// Update state with the mutated buffer to trigger re-render
setBuffer(TextBuffer.fromBuffer(buffer));
}
}, [buffer, stdin, setRawMode, setBuffer]);
useInput( useInput(
(input, key) => { (input, key) => {
@ -131,7 +106,7 @@ export const MultilineTextEditor = ({
input.length === 1 && input.length === 1 &&
input.charCodeAt(0) === 5); input.charCodeAt(0) === 5);
if (isCtrlX || isCtrlE) { if (isCtrlX || isCtrlE) {
openExternalEditor(); buffer.openInExternalEditor();
return; return;
} }
@ -142,8 +117,6 @@ export const MultilineTextEditor = ({
console.log('[MultilineTextEditor] event', { input, key }); console.log('[MultilineTextEditor] event', { input, key });
} }
let bufferMutated = false;
if (input.startsWith('[') && input.endsWith('u')) { if (input.startsWith('[') && input.endsWith('u')) {
const m = input.match(/^\[([0-9]+);([0-9]+)u$/); const m = input.match(/^\[([0-9]+);([0-9]+)u$/);
if (m && m[1] === '13') { if (m && m[1] === '13') {
@ -151,14 +124,10 @@ export const MultilineTextEditor = ({
const hasCtrl = Math.floor(mod / 4) % 2 === 1; const hasCtrl = Math.floor(mod / 4) % 2 === 1;
if (hasCtrl) { if (hasCtrl) {
if (onSubmit) { if (onSubmit) {
onSubmit(buffer.getText()); onSubmit(buffer.text);
} }
} else { } else {
buffer.newline(); buffer.newline();
bufferMutated = true;
}
if (bufferMutated) {
updateBufferState((_) => {}); // Trigger re-render if mutated
} }
return; return;
} }
@ -171,14 +140,10 @@ export const MultilineTextEditor = ({
const hasCtrl = Math.floor(mod / 4) % 2 === 1; const hasCtrl = Math.floor(mod / 4) % 2 === 1;
if (hasCtrl) { if (hasCtrl) {
if (onSubmit) { if (onSubmit) {
onSubmit(buffer.getText()); onSubmit(buffer.text);
} }
} else { } else {
buffer.newline(); buffer.newline();
bufferMutated = true;
}
if (bufferMutated) {
updateBufferState((_) => {}); // Trigger re-render if mutated
} }
return; return;
} }
@ -186,63 +151,42 @@ export const MultilineTextEditor = ({
if (input === '\n') { if (input === '\n') {
buffer.newline(); buffer.newline();
updateBufferState((_) => {});
return; return;
} }
if (input === '\r') { if (input === '\r') {
if (onSubmit) { if (onSubmit) {
onSubmit(buffer.getText()); onSubmit(buffer.text);
} }
return; return;
} }
if (key.upArrow) { if (key.upArrow) {
if (buffer.getCursor()[0] === 0 && navigateUp) { if (buffer.cursor[0] === 0 && navigateUp) {
navigateUp(); navigateUp();
return; return;
} }
} }
if (key.downArrow) { if (key.downArrow) {
if ( if (buffer.cursor[0] === buffer.lines.length - 1 && navigateDown) {
buffer.getCursor()[0] === buffer.getText().split('\n').length - 1 &&
navigateDown
) {
navigateDown(); navigateDown();
return; return;
} }
} }
const modifiedByHandleInput = buffer.handleInput( buffer.handleInput(input, key as Record<string, boolean>);
input,
key as Record<string, boolean>,
{ height, width: effectiveWidth },
);
if (modifiedByHandleInput) {
updateBufferState((_) => {});
}
const newText = buffer.getText();
if (onChange) {
onChange(newText);
}
}, },
{ isActive: focus }, { isActive: focus },
); );
const visibleLines = buffer.getVisibleLines({ const visibleLines = buffer.visibleLines;
height, const [cursorRow, cursorCol] = buffer.cursor;
width: effectiveWidth, const [scrollRow, scrollCol] = buffer.scroll;
});
const [cursorRow, cursorCol] = buffer.getCursor();
const scrollRow = buffer.getScrollRow();
const scrollCol = buffer.getScrollCol();
return ( return (
<Box flexDirection="column"> <Box flexDirection="column">
{buffer.getText().length === 0 && placeholder ? ( {buffer.text.length === 0 && placeholder ? (
<Text color={Colors.SubtleComment}>{placeholder}</Text> <Text color={Colors.SubtleComment}>{placeholder}</Text>
) : ( ) : (
visibleLines.map((lineText, idx) => { visibleLines.map((lineText, idx) => {

File diff suppressed because it is too large Load Diff