ink-prompt 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/MultilineInput/KeyHandler.d.ts +1 -1
- package/dist/components/MultilineInput/__tests__/integration.test.js +10 -1
- package/dist/components/MultilineInput/index.js +4 -14
- package/dist/components/MultilineInput/useTextInput.d.ts +4 -0
- package/dist/components/MultilineInput/useTextInput.js +28 -2
- package/package.json +7 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Key, type Buffer, type Cursor } from './types.js';
|
|
2
2
|
import { type UseTextInputResult } from './useTextInput.js';
|
|
3
|
-
export interface KeyHandlerActions extends Omit<UseTextInputResult, 'value' | 'cursor' | 'cursorOffset' | 'setCursorOffset' | 'buffer' | 'blockState' | 'insertImage' | 'images' | 'getImages' | 'setImages'> {
|
|
3
|
+
export interface KeyHandlerActions extends Omit<UseTextInputResult, 'value' | 'cursor' | 'cursorOffset' | 'setCursorOffset' | 'buffer' | 'blockState' | 'insertImage' | 'images' | 'getImages' | 'setImages' | 'syncExternalState'> {
|
|
4
4
|
submit: () => void;
|
|
5
5
|
onBoundaryArrow?: (direction: 'up' | 'down' | 'left' | 'right') => void;
|
|
6
6
|
paste?: () => void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
-
import { render } from '@testing-library/react';
|
|
3
|
+
import { render, waitFor } from '@testing-library/react';
|
|
4
4
|
import { MultilineInputCore } from '../index.js';
|
|
5
5
|
/**
|
|
6
6
|
* Integration tests for MultilineInputCore.
|
|
@@ -66,6 +66,15 @@ describe('MultilineInputCore', () => {
|
|
|
66
66
|
});
|
|
67
67
|
});
|
|
68
68
|
describe('Controlled component behavior', () => {
|
|
69
|
+
it('applies simultaneous value and cursorOverride updates against the new value', async () => {
|
|
70
|
+
const onCursorChange = vi.fn();
|
|
71
|
+
const { rerender } = render(_jsx(MultilineInputCore, { value: "x", cursorOverride: 1, onCursorChange: onCursorChange }));
|
|
72
|
+
onCursorChange.mockClear();
|
|
73
|
+
rerender(_jsx(MultilineInputCore, { value: "abcde", cursorOverride: 4, onCursorChange: onCursorChange }));
|
|
74
|
+
await waitFor(() => {
|
|
75
|
+
expect(onCursorChange).toHaveBeenLastCalledWith(4);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
69
78
|
it('does NOT call onChange when value prop is updated by parent', () => {
|
|
70
79
|
// This tests the controlled component pattern:
|
|
71
80
|
// onChange should only fire for user-initiated changes, not prop updates
|
|
@@ -16,11 +16,6 @@ export const MultilineInputCore = ({ value, onChange, placeholder, showCursor =
|
|
|
16
16
|
formatPastePlaceholder,
|
|
17
17
|
});
|
|
18
18
|
const isSyncingFromProps = useRef(false);
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
if (cursorOverride !== undefined) {
|
|
21
|
-
textInput.setCursorOffset(cursorOverride);
|
|
22
|
-
}
|
|
23
|
-
}, [cursorOverride]);
|
|
24
19
|
const onCursorChangeRef = useRef(onCursorChange);
|
|
25
20
|
useEffect(() => {
|
|
26
21
|
onCursorChangeRef.current = onCursorChange;
|
|
@@ -33,9 +28,9 @@ export const MultilineInputCore = ({ value, onChange, placeholder, showCursor =
|
|
|
33
28
|
useEffect(() => {
|
|
34
29
|
if (value !== undefined && value !== textInput.value) {
|
|
35
30
|
isSyncingFromProps.current = true;
|
|
36
|
-
textInput.setText(value);
|
|
37
31
|
}
|
|
38
|
-
|
|
32
|
+
textInput.syncExternalState({ value, cursorOffset: cursorOverride });
|
|
33
|
+
}, [value, cursorOverride]);
|
|
39
34
|
const onChangeRef = useRef(onChange);
|
|
40
35
|
useEffect(() => {
|
|
41
36
|
onChangeRef.current = onChange;
|
|
@@ -145,11 +140,6 @@ export const MultilineInput = ({ value, onChange, onSubmit, placeholder, showCur
|
|
|
145
140
|
existingImages: textInput.images,
|
|
146
141
|
onPasteError,
|
|
147
142
|
});
|
|
148
|
-
useEffect(() => {
|
|
149
|
-
if (cursorOverride !== undefined) {
|
|
150
|
-
textInput.setCursorOffset(cursorOverride);
|
|
151
|
-
}
|
|
152
|
-
}, [cursorOverride]);
|
|
153
143
|
const onCursorChangeRef = useRef(onCursorChange);
|
|
154
144
|
useEffect(() => {
|
|
155
145
|
onCursorChangeRef.current = onCursorChange;
|
|
@@ -161,9 +151,9 @@ export const MultilineInput = ({ value, onChange, onSubmit, placeholder, showCur
|
|
|
161
151
|
useEffect(() => {
|
|
162
152
|
if (value !== undefined && value !== textInput.value) {
|
|
163
153
|
isSyncingFromProps.current = true;
|
|
164
|
-
textInput.setText(value);
|
|
165
154
|
}
|
|
166
|
-
|
|
155
|
+
textInput.syncExternalState({ value, cursorOffset: cursorOverride });
|
|
156
|
+
}, [value, cursorOverride]);
|
|
167
157
|
const onChangeRef = useRef(onChange);
|
|
168
158
|
useEffect(() => {
|
|
169
159
|
onChangeRef.current = onChange;
|
|
@@ -22,6 +22,10 @@ export interface UseTextInputResult {
|
|
|
22
22
|
undo: () => void;
|
|
23
23
|
redo: () => void;
|
|
24
24
|
setText: (text: string) => void;
|
|
25
|
+
syncExternalState: (state: {
|
|
26
|
+
value?: string;
|
|
27
|
+
cursorOffset?: number;
|
|
28
|
+
}) => void;
|
|
25
29
|
cursorOffset: number;
|
|
26
30
|
setCursorOffset: (offset: number) => void;
|
|
27
31
|
blockState: BlockState;
|
|
@@ -4,6 +4,13 @@ import { createBlockState, createPasteBlockEntry, createImageBlockEntry, removeB
|
|
|
4
4
|
import { findAtomicBlockBefore, findAtomicBlockAfter } from './AtomicBlocks.js';
|
|
5
5
|
import { log } from '../../utils/logger.js';
|
|
6
6
|
const defaultFormatPlaceholder = (displayNumber) => `[Paste text #${displayNumber}]`;
|
|
7
|
+
function getEndCursor(text) {
|
|
8
|
+
const lines = text.split('\n');
|
|
9
|
+
return {
|
|
10
|
+
line: lines.length - 1,
|
|
11
|
+
column: lines[lines.length - 1].length,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
7
14
|
export function useTextInput({ initialValue = '', width, historyLimit = 100, undoDebounceMs = 200, pasteThreshold, formatPastePlaceholder = defaultFormatPlaceholder, } = {}) {
|
|
8
15
|
const [buffer, setBuffer] = useState(() => createBuffer(initialValue));
|
|
9
16
|
const [cursor, setCursor] = useState(() => {
|
|
@@ -188,8 +195,7 @@ export function useTextInput({ initialValue = '', width, historyLimit = 100, und
|
|
|
188
195
|
applyEdit(() => {
|
|
189
196
|
setBlockState(createBlockState());
|
|
190
197
|
const newBuffer = createBuffer(text);
|
|
191
|
-
const
|
|
192
|
-
const newCursor = { line: lines.length - 1, column: lines[lines.length - 1].length };
|
|
198
|
+
const newCursor = getEndCursor(text);
|
|
193
199
|
return { buffer: newBuffer, cursor: newCursor };
|
|
194
200
|
});
|
|
195
201
|
}, [applyEdit]);
|
|
@@ -202,6 +208,25 @@ export function useTextInput({ initialValue = '', width, historyLimit = 100, und
|
|
|
202
208
|
}, [applyEdit, buffer, cursor, blockState]);
|
|
203
209
|
const value = useMemo(() => getValue(buffer.lines, blockState.entries), [buffer.lines, blockState.entries]);
|
|
204
210
|
const cursorOffset = useMemo(() => getValueCursorOffset(buffer.lines, cursor, blockState.entries), [buffer.lines, cursor, blockState.entries]);
|
|
211
|
+
const syncExternalState = useCallback(({ value: externalValue, cursorOffset: externalCursorOffset }) => {
|
|
212
|
+
const shouldSyncValue = externalValue !== undefined && externalValue !== value;
|
|
213
|
+
const shouldSyncCursor = externalCursorOffset !== undefined && externalCursorOffset !== cursorOffset;
|
|
214
|
+
if (!shouldSyncValue && !shouldSyncCursor)
|
|
215
|
+
return;
|
|
216
|
+
flushPendingInsertBatch();
|
|
217
|
+
const nextBlockState = shouldSyncValue ? createBlockState() : blockState;
|
|
218
|
+
const nextBuffer = shouldSyncValue ? createBuffer(externalValue) : buffer;
|
|
219
|
+
const nextCursor = externalCursorOffset !== undefined
|
|
220
|
+
? getCursorFromValueOffset(nextBuffer.lines, externalCursorOffset, nextBlockState.entries)
|
|
221
|
+
: shouldSyncValue
|
|
222
|
+
? getEndCursor(externalValue)
|
|
223
|
+
: cursor;
|
|
224
|
+
if (shouldSyncValue) {
|
|
225
|
+
setBlockState(nextBlockState);
|
|
226
|
+
setBuffer(nextBuffer);
|
|
227
|
+
}
|
|
228
|
+
setCursor(nextCursor);
|
|
229
|
+
}, [blockState, buffer, cursor, cursorOffset, flushPendingInsertBatch, value]);
|
|
205
230
|
const imagesList = useMemo(() => {
|
|
206
231
|
const result = [];
|
|
207
232
|
for (const entry of blockState.entries.values()) {
|
|
@@ -256,6 +281,7 @@ export function useTextInput({ initialValue = '', width, historyLimit = 100, und
|
|
|
256
281
|
undo,
|
|
257
282
|
redo,
|
|
258
283
|
setText,
|
|
284
|
+
syncExternalState,
|
|
259
285
|
cursorOffset,
|
|
260
286
|
setCursorOffset: useCallback((offset) => {
|
|
261
287
|
flushPendingInsertBatch();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ink-prompt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A React Ink component for prompts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"dev": "tsc --watch",
|
|
22
22
|
"type-check": "tsc --noEmit",
|
|
23
23
|
"clean": "rm -rf dist",
|
|
24
|
-
"test": "vitest run",
|
|
24
|
+
"test": "vitest run --reporter=minimal",
|
|
25
25
|
"test:ui": "vitest --ui",
|
|
26
26
|
"test:watch": "vitest",
|
|
27
27
|
"release": "bash scripts/release.sh"
|
|
@@ -34,6 +34,9 @@
|
|
|
34
34
|
],
|
|
35
35
|
"author": "Duc Nguyen",
|
|
36
36
|
"license": "MIT",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20 <=24"
|
|
39
|
+
},
|
|
37
40
|
"repository": "github:qduc/ink-prompt",
|
|
38
41
|
"publishConfig": {
|
|
39
42
|
"access": "public"
|
|
@@ -46,12 +49,12 @@
|
|
|
46
49
|
"@testing-library/react": "^16.3.0",
|
|
47
50
|
"@types/node": "^20.19.41",
|
|
48
51
|
"@types/react": "^19.0.0",
|
|
49
|
-
"@vitest/ui": "^4.
|
|
52
|
+
"@vitest/ui": "^4.1.9",
|
|
50
53
|
"happy-dom": "^20.0.11",
|
|
51
54
|
"ink": "^7.0.0",
|
|
52
55
|
"react": "^19.2.1",
|
|
53
56
|
"react-dom": "^19.2.1",
|
|
54
57
|
"typescript": "^5.0.0",
|
|
55
|
-
"vitest": "^4.
|
|
58
|
+
"vitest": "^4.1.9"
|
|
56
59
|
}
|
|
57
60
|
}
|