ink-prompt 0.2.3 → 0.2.5
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/README.md +3 -3
- package/dist/components/MultilineInput/AtomicBlocks.d.ts +10 -20
- package/dist/components/MultilineInput/AtomicBlocks.js +33 -42
- package/dist/components/MultilineInput/BlockMarker.d.ts +19 -0
- package/dist/components/MultilineInput/BlockMarker.js +83 -0
- package/dist/components/MultilineInput/BlockRegistry.d.ts +39 -0
- package/dist/components/MultilineInput/BlockRegistry.js +236 -0
- package/dist/components/MultilineInput/BlockTypes.d.ts +22 -0
- package/dist/components/MultilineInput/ImageTypes.d.ts +0 -2
- package/dist/components/MultilineInput/ImageTypes.js +1 -2
- package/dist/components/MultilineInput/ImageValidator.js +1 -1
- package/dist/components/MultilineInput/KeyHandler.d.ts +1 -1
- package/dist/components/MultilineInput/KeyHandler.js +18 -0
- package/dist/components/MultilineInput/TextBuffer.d.ts +5 -31
- package/dist/components/MultilineInput/TextBuffer.js +91 -161
- package/dist/components/MultilineInput/TextRenderer.d.ts +6 -7
- package/dist/components/MultilineInput/TextRenderer.js +7 -7
- package/dist/components/MultilineInput/__tests__/BlockMarker.test.js +130 -0
- package/dist/components/MultilineInput/__tests__/BlockRegistry.test.js +225 -0
- package/dist/components/MultilineInput/__tests__/KeyHandler.test.js +18 -0
- package/dist/components/MultilineInput/__tests__/Placeholder.integration.test.js +44 -65
- package/dist/components/MultilineInput/__tests__/TextBuffer_images.test.js +10 -31
- package/dist/components/MultilineInput/__tests__/TextRenderer_images.test.js +27 -13
- package/dist/components/MultilineInput/__tests__/integration_images.test.js +2 -4
- package/dist/components/MultilineInput/__tests__/useTextInput_images.test.js +30 -29
- package/dist/components/MultilineInput/index.d.ts +6 -6
- package/dist/components/MultilineInput/index.js +2 -11
- package/dist/components/MultilineInput/types.d.ts +0 -20
- package/dist/components/MultilineInput/useTextInput.d.ts +4 -11
- package/dist/components/MultilineInput/useTextInput.js +79 -76
- package/package.json +1 -1
- package/dist/components/MultilineInput/ImageSentinel.d.ts +0 -15
- package/dist/components/MultilineInput/ImageSentinel.js +0 -62
- package/dist/components/MultilineInput/Placeholder.d.ts +0 -30
- package/dist/components/MultilineInput/Placeholder.js +0 -200
- package/dist/components/MultilineInput/__tests__/ImageSentinel.test.js +0 -154
- package/dist/components/MultilineInput/__tests__/Placeholder.test.js +0 -235
- package/dist/examples/examples/basic.js +0 -9
- package/dist/examples/src/components/MultilineInput/KeyHandler.d.ts +0 -15
- package/dist/examples/src/components/MultilineInput/KeyHandler.js +0 -97
- package/dist/examples/src/components/MultilineInput/TextBuffer.d.ts +0 -34
- package/dist/examples/src/components/MultilineInput/TextBuffer.js +0 -127
- package/dist/examples/src/components/MultilineInput/TextRenderer.d.ts +0 -24
- package/dist/examples/src/components/MultilineInput/TextRenderer.js +0 -72
- package/dist/examples/src/components/MultilineInput/__tests__/KeyHandler.test.d.ts +0 -1
- package/dist/examples/src/components/MultilineInput/__tests__/KeyHandler.test.js +0 -115
- package/dist/examples/src/components/MultilineInput/__tests__/TextBuffer.test.d.ts +0 -1
- package/dist/examples/src/components/MultilineInput/__tests__/TextBuffer.test.js +0 -254
- package/dist/examples/src/components/MultilineInput/__tests__/TextRenderer.test.d.ts +0 -1
- package/dist/examples/src/components/MultilineInput/__tests__/TextRenderer.test.js +0 -176
- package/dist/examples/src/components/MultilineInput/__tests__/integration.test.d.ts +0 -1
- package/dist/examples/src/components/MultilineInput/__tests__/integration.test.js +0 -71
- package/dist/examples/src/components/MultilineInput/__tests__/useTextInput.test.d.ts +0 -1
- package/dist/examples/src/components/MultilineInput/__tests__/useTextInput.test.js +0 -65
- package/dist/examples/src/components/MultilineInput/index.d.ts +0 -39
- package/dist/examples/src/components/MultilineInput/index.js +0 -82
- package/dist/examples/src/components/MultilineInput/types.d.ts +0 -55
- package/dist/examples/src/components/MultilineInput/types.js +0 -1
- package/dist/examples/src/components/MultilineInput/useTextInput.d.ts +0 -16
- package/dist/examples/src/components/MultilineInput/useTextInput.js +0 -82
- package/dist/examples/src/hello.test.d.ts +0 -1
- package/dist/examples/src/hello.test.js +0 -13
- package/dist/examples/src/index.d.ts +0 -2
- package/dist/examples/src/index.js +0 -2
- /package/dist/components/MultilineInput/{__tests__/ImageSentinel.test.d.ts → BlockTypes.js} +0 -0
- /package/dist/components/MultilineInput/__tests__/{Placeholder.test.d.ts → BlockMarker.test.d.ts} +0 -0
- /package/dist/{examples/examples/basic.d.ts → components/MultilineInput/__tests__/BlockRegistry.test.d.ts} +0 -0
|
@@ -20,10 +20,10 @@ export interface MultilineInputProps {
|
|
|
20
20
|
pasteThreshold?: number;
|
|
21
21
|
/**
|
|
22
22
|
* Custom formatter for the placeholder display text.
|
|
23
|
-
* Receives the
|
|
24
|
-
* Default: (
|
|
23
|
+
* Receives the display number (1-based) and should return the display string.
|
|
24
|
+
* Default: (n) => `[Paste text #${n}]`
|
|
25
25
|
*/
|
|
26
|
-
formatPastePlaceholder?: (
|
|
26
|
+
formatPastePlaceholder?: (displayNumber: number) => string;
|
|
27
27
|
images?: ImageRef[];
|
|
28
28
|
onImagesChange?: (images: ImageRef[]) => void;
|
|
29
29
|
onPasteError?: (reason: PasteErrorReason) => void;
|
|
@@ -51,10 +51,10 @@ export interface MultilineInputCoreProps {
|
|
|
51
51
|
pasteThreshold?: number;
|
|
52
52
|
/**
|
|
53
53
|
* Custom formatter for the placeholder display text.
|
|
54
|
-
* Receives the
|
|
55
|
-
* Default: (
|
|
54
|
+
* Receives the display number (1-based) and should return the display string.
|
|
55
|
+
* Default: (n) => `[Paste text #${n}]`
|
|
56
56
|
*/
|
|
57
|
-
formatPastePlaceholder?: (
|
|
57
|
+
formatPastePlaceholder?: (displayNumber: number) => string;
|
|
58
58
|
images?: ImageRef[];
|
|
59
59
|
onImagesChange?: (images: ImageRef[]) => void;
|
|
60
60
|
}
|
|
@@ -7,15 +7,6 @@ import { handleKey } from './KeyHandler.js';
|
|
|
7
7
|
import { TextRenderer } from './TextRenderer.js';
|
|
8
8
|
import { useClipboardPaste } from './useClipboardPaste.js';
|
|
9
9
|
import { log } from '../../utils/logger.js';
|
|
10
|
-
function imagesToRecord(images) {
|
|
11
|
-
if (!images || images.length === 0)
|
|
12
|
-
return {};
|
|
13
|
-
const record = {};
|
|
14
|
-
for (const img of images) {
|
|
15
|
-
record[img.id] = img;
|
|
16
|
-
}
|
|
17
|
-
return record;
|
|
18
|
-
}
|
|
19
10
|
export const MultilineInputCore = ({ value, onChange, placeholder, showCursor = true, width = 80, onCursorChange, cursorOverride, undoDebounceMs, pasteThreshold, formatPastePlaceholder, images, onImagesChange, }) => {
|
|
20
11
|
const textInput = useTextInput({ initialValue: value ?? '', undoDebounceMs, pasteThreshold, formatPastePlaceholder });
|
|
21
12
|
const isSyncingFromProps = useRef(false);
|
|
@@ -67,7 +58,7 @@ export const MultilineInputCore = ({ value, onChange, placeholder, showCursor =
|
|
|
67
58
|
if (showPlaceholder) {
|
|
68
59
|
return _jsx("div", { style: { opacity: 0.5 }, children: placeholder });
|
|
69
60
|
}
|
|
70
|
-
return (_jsx(TextRenderer, { buffer: textInput.buffer, cursor: textInput.cursor, width: width, showCursor: showCursor,
|
|
61
|
+
return (_jsx(TextRenderer, { buffer: textInput.buffer, cursor: textInput.cursor, width: width, showCursor: showCursor, blockState: textInput.blockState }));
|
|
71
62
|
};
|
|
72
63
|
export const MultilineInput = ({ value, onChange, onSubmit, placeholder, showCursor = true, width, isActive = true, onCursorChange, cursorOverride, onBoundaryArrow, undoDebounceMs, pasteThreshold, formatPastePlaceholder, images, onImagesChange, onPasteError, enableImagePaste = false, maxImageSizeBytes, maxImageCount, acceptedMimeTypes, }) => {
|
|
73
64
|
const terminalWidth = useTerminalWidth(width);
|
|
@@ -228,5 +219,5 @@ export const MultilineInput = ({ value, onChange, onSubmit, placeholder, showCur
|
|
|
228
219
|
if (showPlaceholder && !isPasting) {
|
|
229
220
|
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: placeholder }) }));
|
|
230
221
|
}
|
|
231
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(TextRenderer, { buffer: textInput.buffer, cursor: textInput.cursor, width: terminalWidth, showCursor: showCursor,
|
|
222
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(TextRenderer, { buffer: textInput.buffer, cursor: textInput.cursor, width: terminalWidth, showCursor: showCursor, blockState: textInput.blockState }), isPasting && (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "Reading clipboard..." }) }))] }));
|
|
232
223
|
};
|
|
@@ -33,26 +33,6 @@ export interface WrapResult {
|
|
|
33
33
|
/** Column in that visual row where cursor appears */
|
|
34
34
|
cursorVisualCol: number;
|
|
35
35
|
}
|
|
36
|
-
/**
|
|
37
|
-
* Information about a paste placeholder: its identifier, original text, and display label.
|
|
38
|
-
*/
|
|
39
|
-
export interface PlaceholderInfo {
|
|
40
|
-
/** Unique placeholder identifier */
|
|
41
|
-
id: number;
|
|
42
|
-
/** The original pasted text (may contain newlines) */
|
|
43
|
-
originalText: string;
|
|
44
|
-
/** The display label shown in place of the original text (e.g., "[Paste text #1]") */
|
|
45
|
-
displayText: string;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Registry state for paste placeholders.
|
|
49
|
-
*/
|
|
50
|
-
export interface PlaceholderState {
|
|
51
|
-
/** Map of placeholder ID to info */
|
|
52
|
-
placeholders: Map<number, PlaceholderInfo>;
|
|
53
|
-
/** Next auto-incrementing ID */
|
|
54
|
-
nextId: number;
|
|
55
|
-
}
|
|
56
36
|
/**
|
|
57
37
|
* Keyboard key state (mirrors Ink's Key interface)
|
|
58
38
|
* Defined locally to avoid ESM/CJS import issues with Ink
|
|
@@ -1,20 +1,13 @@
|
|
|
1
|
-
import type { Buffer, Cursor, Direction
|
|
1
|
+
import type { Buffer, Cursor, Direction } from './types.js';
|
|
2
2
|
import type { ImageRef } from './ImageTypes.js';
|
|
3
|
+
import type { BlockState } from './BlockTypes.js';
|
|
3
4
|
export interface UseTextInputProps {
|
|
4
5
|
initialValue?: string;
|
|
5
6
|
width?: number;
|
|
6
7
|
historyLimit?: number;
|
|
7
8
|
undoDebounceMs?: number;
|
|
8
|
-
/**
|
|
9
|
-
* When set, pasted text exceeding this character count is replaced
|
|
10
|
-
* with a placeholder for cleaner display.
|
|
11
|
-
*/
|
|
12
9
|
pasteThreshold?: number;
|
|
13
|
-
|
|
14
|
-
* Custom formatter for placeholder display text.
|
|
15
|
-
* Default: (id) => `[Paste text #${id}]`
|
|
16
|
-
*/
|
|
17
|
-
formatPastePlaceholder?: (id: number) => string;
|
|
10
|
+
formatPastePlaceholder?: (displayNumber: number) => string;
|
|
18
11
|
}
|
|
19
12
|
export interface UseTextInputResult {
|
|
20
13
|
value: string;
|
|
@@ -31,7 +24,7 @@ export interface UseTextInputResult {
|
|
|
31
24
|
setText: (text: string) => void;
|
|
32
25
|
cursorOffset: number;
|
|
33
26
|
setCursorOffset: (offset: number) => void;
|
|
34
|
-
|
|
27
|
+
blockState: BlockState;
|
|
35
28
|
insertImage: (imageRef: ImageRef) => void;
|
|
36
29
|
images: ImageRef[];
|
|
37
30
|
getImages: () => ImageRef[];
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
2
|
-
import { createBuffer, insertText as bufferInsertText, deleteChar as bufferDeleteChar, deleteCharForward as bufferDeleteCharForward, insertNewLine as bufferInsertNewLine, moveCursor as bufferMoveCursor,
|
|
3
|
-
import {
|
|
4
|
-
import { createSentinel, parseSentinels } from './ImageSentinel.js';
|
|
2
|
+
import { createBuffer, insertText as bufferInsertText, deleteChar as bufferDeleteChar, deleteCharForward as bufferDeleteCharForward, insertNewLine as bufferInsertNewLine, moveCursor as bufferMoveCursor, } from './TextBuffer.js';
|
|
3
|
+
import { createBlockState, createPasteBlockEntry, createImageBlockEntry, removeBlock, getValue, getValueCursorOffset, getCursorFromValueOffset, } from './BlockRegistry.js';
|
|
5
4
|
import { findAtomicBlockBefore, findAtomicBlockAfter } from './AtomicBlocks.js';
|
|
6
5
|
import { log } from '../../utils/logger.js';
|
|
7
|
-
const defaultFormatPlaceholder = (
|
|
6
|
+
const defaultFormatPlaceholder = (displayNumber) => `[Paste text #${displayNumber}]`;
|
|
8
7
|
export function useTextInput({ initialValue = '', width, historyLimit = 100, undoDebounceMs = 200, pasteThreshold, formatPastePlaceholder = defaultFormatPlaceholder, } = {}) {
|
|
9
8
|
const [buffer, setBuffer] = useState(() => createBuffer(initialValue));
|
|
10
9
|
const [cursor, setCursor] = useState(() => {
|
|
@@ -14,9 +13,7 @@ export function useTextInput({ initialValue = '', width, historyLimit = 100, und
|
|
|
14
13
|
column: lines[lines.length - 1].length,
|
|
15
14
|
};
|
|
16
15
|
});
|
|
17
|
-
const [
|
|
18
|
-
const [images, setImages] = useState({});
|
|
19
|
-
const nextDisplayNumberRef = useRef(1);
|
|
16
|
+
const [blockState, setBlockState] = useState(() => createBlockState());
|
|
20
17
|
const [undoStack, setUndoStack] = useState([]);
|
|
21
18
|
const [redoStack, setRedoStack] = useState([]);
|
|
22
19
|
const pendingInsertBatchRef = useRef({});
|
|
@@ -56,13 +53,12 @@ export function useTextInput({ initialValue = '', width, historyLimit = 100, und
|
|
|
56
53
|
pendingInsertBatchRef.current.startState = {
|
|
57
54
|
buffer: currentBuffer,
|
|
58
55
|
cursor: currentCursor,
|
|
59
|
-
|
|
60
|
-
images,
|
|
56
|
+
blockState,
|
|
61
57
|
};
|
|
62
58
|
setRedoStack([]);
|
|
63
59
|
}
|
|
64
60
|
schedulePendingInsertCommit();
|
|
65
|
-
}, [schedulePendingInsertCommit,
|
|
61
|
+
}, [schedulePendingInsertCommit, blockState]);
|
|
66
62
|
const flushPendingInsertBatch = useCallback(() => {
|
|
67
63
|
commitPendingInsertBatch();
|
|
68
64
|
}, [commitPendingInsertBatch]);
|
|
@@ -70,11 +66,10 @@ export function useTextInput({ initialValue = '', width, historyLimit = 100, und
|
|
|
70
66
|
appendUndoState({
|
|
71
67
|
buffer: currentBuffer,
|
|
72
68
|
cursor: currentCursor,
|
|
73
|
-
|
|
74
|
-
images,
|
|
69
|
+
blockState,
|
|
75
70
|
});
|
|
76
71
|
setRedoStack([]);
|
|
77
|
-
}, [appendUndoState,
|
|
72
|
+
}, [appendUndoState, blockState]);
|
|
78
73
|
useEffect(() => {
|
|
79
74
|
return () => {
|
|
80
75
|
clearPendingInsertTimer();
|
|
@@ -92,16 +87,16 @@ export function useTextInput({ initialValue = '', width, historyLimit = 100, und
|
|
|
92
87
|
}
|
|
93
88
|
}, [buffer, cursor, flushPendingInsertBatch, pushToHistory]);
|
|
94
89
|
const insert = useCallback((char) => {
|
|
95
|
-
log(`[INSERT] char="${char.replace(/[\x00-\x1F\x7F
|
|
90
|
+
log(`[INSERT] char="${char.replace(/[\x00-\x1F\x7F-\uFFFF]/g, c => `\\x${c.charCodeAt(0).toString(16)}`)}" len=${char.length} cursor={line:${cursor.line},col:${cursor.column}} linesBefore=${buffer.lines.length}`);
|
|
96
91
|
const normalized = char.replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\x00/g, '');
|
|
97
92
|
if (normalized.length === 0)
|
|
98
93
|
return;
|
|
99
|
-
// Check if this is a paste exceeding the threshold
|
|
100
94
|
if (pasteThreshold !== undefined && pasteThreshold > 0 && normalized.length > pasteThreshold) {
|
|
101
95
|
flushPendingInsertBatch();
|
|
102
96
|
pushToHistory(buffer, cursor);
|
|
103
|
-
const
|
|
104
|
-
|
|
97
|
+
const displayText = formatPastePlaceholder(blockState.nextPasteNumber);
|
|
98
|
+
const { marker, state: newBlockState } = createPasteBlockEntry(blockState, normalized, displayText);
|
|
99
|
+
setBlockState(newBlockState);
|
|
105
100
|
const result = bufferInsertText(buffer, cursor, marker);
|
|
106
101
|
setBuffer(result.buffer);
|
|
107
102
|
setCursor(result.cursor);
|
|
@@ -120,70 +115,61 @@ export function useTextInput({ initialValue = '', width, historyLimit = 100, und
|
|
|
120
115
|
const result = bufferInsertText(buffer, cursor, normalized);
|
|
121
116
|
setBuffer(result.buffer);
|
|
122
117
|
setCursor(result.cursor);
|
|
123
|
-
}, [beginOrRefreshInsertBatch, buffer, cursor, flushPendingInsertBatch, pushToHistory, undoDebounceMs, pasteThreshold,
|
|
118
|
+
}, [beginOrRefreshInsertBatch, buffer, cursor, flushPendingInsertBatch, pushToHistory, undoDebounceMs, pasteThreshold, blockState, formatPastePlaceholder]);
|
|
124
119
|
const cleanupBlockRegistry = useCallback((block) => {
|
|
125
120
|
if (!block)
|
|
126
121
|
return;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
else if (block.kind === 'sentinel' && images[block.id]) {
|
|
131
|
-
const next = { ...images };
|
|
132
|
-
delete next[block.id];
|
|
133
|
-
setImages(next);
|
|
134
|
-
}
|
|
135
|
-
}, [images]);
|
|
122
|
+
setBlockState((prev) => removeBlock(prev, block.id));
|
|
123
|
+
}, []);
|
|
136
124
|
const deleteChar = useCallback(() => {
|
|
137
125
|
applyEdit(() => {
|
|
138
126
|
const line = buffer.lines[cursor.line];
|
|
139
|
-
cleanupBlockRegistry(findAtomicBlockBefore(line, cursor.column,
|
|
140
|
-
return bufferDeleteChar(buffer, cursor,
|
|
127
|
+
cleanupBlockRegistry(findAtomicBlockBefore(line, cursor.column, blockState.entries));
|
|
128
|
+
return bufferDeleteChar(buffer, cursor, blockState.entries);
|
|
141
129
|
});
|
|
142
|
-
}, [applyEdit, buffer, cursor,
|
|
130
|
+
}, [applyEdit, buffer, cursor, blockState, cleanupBlockRegistry]);
|
|
143
131
|
const deleteCharForward = useCallback(() => {
|
|
144
132
|
applyEdit(() => {
|
|
145
133
|
const line = buffer.lines[cursor.line];
|
|
146
|
-
cleanupBlockRegistry(findAtomicBlockAfter(line, cursor.column,
|
|
147
|
-
return bufferDeleteCharForward(buffer, cursor,
|
|
134
|
+
cleanupBlockRegistry(findAtomicBlockAfter(line, cursor.column, blockState.entries));
|
|
135
|
+
return bufferDeleteCharForward(buffer, cursor, blockState.entries);
|
|
148
136
|
});
|
|
149
|
-
}, [applyEdit, buffer, cursor,
|
|
137
|
+
}, [applyEdit, buffer, cursor, blockState, cleanupBlockRegistry]);
|
|
150
138
|
const newLine = useCallback(() => {
|
|
151
139
|
applyEdit(() => bufferInsertNewLine(buffer, cursor));
|
|
152
140
|
}, [applyEdit, buffer, cursor]);
|
|
153
141
|
const deleteAndNewLine = useCallback(() => {
|
|
154
142
|
applyEdit(() => {
|
|
155
|
-
const afterDelete = bufferDeleteChar(buffer, cursor,
|
|
143
|
+
const afterDelete = bufferDeleteChar(buffer, cursor, blockState.entries);
|
|
156
144
|
return bufferInsertNewLine(afterDelete.buffer, afterDelete.cursor);
|
|
157
145
|
});
|
|
158
|
-
}, [applyEdit, buffer, cursor,
|
|
146
|
+
}, [applyEdit, buffer, cursor, blockState]);
|
|
159
147
|
const moveCursor = useCallback((direction) => {
|
|
160
148
|
flushPendingInsertBatch();
|
|
161
|
-
const newCursor = bufferMoveCursor(buffer, cursor, direction, width,
|
|
149
|
+
const newCursor = bufferMoveCursor(buffer, cursor, direction, width, blockState.entries);
|
|
162
150
|
setCursor(newCursor);
|
|
163
|
-
}, [buffer, cursor, flushPendingInsertBatch, width,
|
|
151
|
+
}, [buffer, cursor, flushPendingInsertBatch, width, blockState]);
|
|
164
152
|
const undo = useCallback(() => {
|
|
165
153
|
const pendingStartState = pendingInsertBatchRef.current.startState;
|
|
166
154
|
if (pendingStartState) {
|
|
167
155
|
clearPendingInsertTimer();
|
|
168
156
|
pendingInsertBatchRef.current.startState = undefined;
|
|
169
|
-
setRedoStack((prev) => [...prev, { buffer, cursor,
|
|
157
|
+
setRedoStack((prev) => [...prev, { buffer, cursor, blockState }]);
|
|
170
158
|
setBuffer(pendingStartState.buffer);
|
|
171
159
|
setCursor(pendingStartState.cursor);
|
|
172
|
-
|
|
173
|
-
setImages(pendingStartState.images);
|
|
160
|
+
setBlockState(pendingStartState.blockState);
|
|
174
161
|
return;
|
|
175
162
|
}
|
|
176
163
|
if (undoStack.length === 0)
|
|
177
164
|
return;
|
|
178
165
|
const previousState = undoStack[undoStack.length - 1];
|
|
179
166
|
const newUndoStack = undoStack.slice(0, -1);
|
|
180
|
-
setRedoStack((prev) => [...prev, { buffer, cursor,
|
|
167
|
+
setRedoStack((prev) => [...prev, { buffer, cursor, blockState }]);
|
|
181
168
|
setBuffer(previousState.buffer);
|
|
182
169
|
setCursor(previousState.cursor);
|
|
183
|
-
|
|
184
|
-
setImages(previousState.images);
|
|
170
|
+
setBlockState(previousState.blockState);
|
|
185
171
|
setUndoStack(newUndoStack);
|
|
186
|
-
}, [buffer, clearPendingInsertTimer, cursor, undoStack,
|
|
172
|
+
}, [buffer, clearPendingInsertTimer, cursor, undoStack, blockState]);
|
|
187
173
|
const redo = useCallback(() => {
|
|
188
174
|
if (pendingInsertBatchRef.current.startState) {
|
|
189
175
|
return;
|
|
@@ -192,53 +178,70 @@ export function useTextInput({ initialValue = '', width, historyLimit = 100, und
|
|
|
192
178
|
return;
|
|
193
179
|
const nextState = redoStack[redoStack.length - 1];
|
|
194
180
|
const newRedoStack = redoStack.slice(0, -1);
|
|
195
|
-
setUndoStack((prev) => [...prev, { buffer, cursor,
|
|
181
|
+
setUndoStack((prev) => [...prev, { buffer, cursor, blockState }]);
|
|
196
182
|
setBuffer(nextState.buffer);
|
|
197
183
|
setCursor(nextState.cursor);
|
|
198
|
-
|
|
199
|
-
setImages(nextState.images);
|
|
184
|
+
setBlockState(nextState.blockState);
|
|
200
185
|
setRedoStack(newRedoStack);
|
|
201
|
-
}, [buffer, cursor, redoStack,
|
|
186
|
+
}, [buffer, cursor, redoStack, blockState]);
|
|
202
187
|
const setText = useCallback((text) => {
|
|
203
188
|
applyEdit(() => {
|
|
204
|
-
|
|
189
|
+
setBlockState(createBlockState());
|
|
205
190
|
const newBuffer = createBuffer(text);
|
|
206
191
|
const lines = text.split('\n');
|
|
207
192
|
const newCursor = { line: lines.length - 1, column: lines[lines.length - 1].length };
|
|
208
|
-
// Clean up orphaned images
|
|
209
|
-
const usedIds = new Set(parseSentinels(getTextContent(newBuffer)).map((s) => s.id));
|
|
210
|
-
setImages((prev) => {
|
|
211
|
-
const next = {};
|
|
212
|
-
for (const [id, ref] of Object.entries(prev)) {
|
|
213
|
-
if (usedIds.has(id))
|
|
214
|
-
next[id] = ref;
|
|
215
|
-
}
|
|
216
|
-
return next;
|
|
217
|
-
});
|
|
218
193
|
return { buffer: newBuffer, cursor: newCursor };
|
|
219
194
|
});
|
|
220
195
|
}, [applyEdit]);
|
|
221
196
|
const insertImage = useCallback((imageRef) => {
|
|
222
197
|
applyEdit(() => {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
return bufferInsertText(buffer, cursor, createSentinel(imageRef.id, imageRef.displayNumber));
|
|
198
|
+
const { marker, state: newBlockState } = createImageBlockEntry(blockState, imageRef, imageRef.id);
|
|
199
|
+
setBlockState(newBlockState);
|
|
200
|
+
return bufferInsertText(buffer, cursor, marker);
|
|
228
201
|
});
|
|
229
|
-
}, [applyEdit, buffer, cursor]);
|
|
230
|
-
const value = useMemo(() => getValue(buffer.lines,
|
|
231
|
-
const cursorOffset = useMemo(() => getValueCursorOffset(buffer.lines, cursor,
|
|
232
|
-
const imagesList = useMemo(() =>
|
|
202
|
+
}, [applyEdit, buffer, cursor, blockState]);
|
|
203
|
+
const value = useMemo(() => getValue(buffer.lines, blockState.entries), [buffer.lines, blockState.entries]);
|
|
204
|
+
const cursorOffset = useMemo(() => getValueCursorOffset(buffer.lines, cursor, blockState.entries), [buffer.lines, cursor, blockState.entries]);
|
|
205
|
+
const imagesList = useMemo(() => {
|
|
206
|
+
const result = [];
|
|
207
|
+
for (const entry of blockState.entries.values()) {
|
|
208
|
+
if (entry.kind === 'image') {
|
|
209
|
+
result.push({
|
|
210
|
+
id: entry.id,
|
|
211
|
+
data: entry.data,
|
|
212
|
+
mimeType: entry.mimeType,
|
|
213
|
+
byteSize: entry.byteSize,
|
|
214
|
+
displayNumber: entry.displayNumber,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
}, [blockState.entries]);
|
|
233
220
|
const getImagesCallback = useCallback(() => {
|
|
234
221
|
return imagesList;
|
|
235
222
|
}, [imagesList]);
|
|
236
223
|
const setImagesCallback = useCallback((newImages) => {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
224
|
+
setBlockState((prev) => {
|
|
225
|
+
const newEntries = new Map(prev.entries);
|
|
226
|
+
for (const [id, entry] of prev.entries) {
|
|
227
|
+
if (entry.kind === 'image') {
|
|
228
|
+
newEntries.delete(id);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
let nextImageNumber = prev.nextImageNumber;
|
|
232
|
+
for (const img of newImages) {
|
|
233
|
+
nextImageNumber = Math.max(nextImageNumber, img.displayNumber + 1);
|
|
234
|
+
newEntries.set(img.id, {
|
|
235
|
+
kind: 'image',
|
|
236
|
+
id: img.id,
|
|
237
|
+
displayNumber: img.displayNumber,
|
|
238
|
+
data: img.data,
|
|
239
|
+
mimeType: img.mimeType,
|
|
240
|
+
byteSize: img.byteSize,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return { ...prev, entries: newEntries, nextImageNumber };
|
|
244
|
+
});
|
|
242
245
|
}, []);
|
|
243
246
|
return {
|
|
244
247
|
value,
|
|
@@ -256,9 +259,9 @@ export function useTextInput({ initialValue = '', width, historyLimit = 100, und
|
|
|
256
259
|
cursorOffset,
|
|
257
260
|
setCursorOffset: useCallback((offset) => {
|
|
258
261
|
flushPendingInsertBatch();
|
|
259
|
-
setCursor(getCursorFromValueOffset(buffer.lines, offset,
|
|
260
|
-
}, [buffer.lines, flushPendingInsertBatch,
|
|
261
|
-
|
|
262
|
+
setCursor(getCursorFromValueOffset(buffer.lines, offset, blockState.entries));
|
|
263
|
+
}, [buffer.lines, flushPendingInsertBatch, blockState.entries]),
|
|
264
|
+
blockState,
|
|
262
265
|
insertImage,
|
|
263
266
|
images: imagesList,
|
|
264
267
|
getImages: getImagesCallback,
|
package/package.json
CHANGED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export declare function generateImageId(): string;
|
|
2
|
-
export declare function createSentinel(id: string, displayNumber: number): string;
|
|
3
|
-
export interface SentinelInfo {
|
|
4
|
-
id: string;
|
|
5
|
-
displayNumber: number;
|
|
6
|
-
start: number;
|
|
7
|
-
end: number;
|
|
8
|
-
}
|
|
9
|
-
export declare function parseSentinels(text: string): SentinelInfo[];
|
|
10
|
-
export declare function findSentinelAt(text: string, offset: number): SentinelInfo | null;
|
|
11
|
-
export declare function isInsideSentinel(text: string, offset: number): boolean;
|
|
12
|
-
export declare function removeSentinel(text: string, offset: number): string;
|
|
13
|
-
export declare function getPlaceholderText(displayNumber: number): string;
|
|
14
|
-
export declare function getPlaceholderVisualWidth(displayNumber: number): number;
|
|
15
|
-
export declare function getSentinelVisualWidthFromText(text: string, offset: number): number | null;
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { SENTINEL_OPEN, SENTINEL_CLOSE } from './ImageTypes.js';
|
|
2
|
-
export function generateImageId() {
|
|
3
|
-
return Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 6);
|
|
4
|
-
}
|
|
5
|
-
export function createSentinel(id, displayNumber) {
|
|
6
|
-
return `${SENTINEL_OPEN}${id}:${displayNumber}${SENTINEL_CLOSE}`;
|
|
7
|
-
}
|
|
8
|
-
export function parseSentinels(text) {
|
|
9
|
-
const result = [];
|
|
10
|
-
let i = 0;
|
|
11
|
-
while (i < text.length) {
|
|
12
|
-
const openIdx = text.indexOf(SENTINEL_OPEN, i);
|
|
13
|
-
if (openIdx === -1)
|
|
14
|
-
break;
|
|
15
|
-
const closeIdx = text.indexOf(SENTINEL_CLOSE, openIdx + 1);
|
|
16
|
-
if (closeIdx === -1)
|
|
17
|
-
break;
|
|
18
|
-
const raw = text.substring(openIdx + 1, closeIdx);
|
|
19
|
-
const colonIdx = raw.lastIndexOf(':');
|
|
20
|
-
const id = colonIdx >= 0 ? raw.substring(0, colonIdx) : raw;
|
|
21
|
-
const displayNumber = colonIdx >= 0 ? parseInt(raw.substring(colonIdx + 1), 10) || 1 : 1;
|
|
22
|
-
result.push({ id, displayNumber, start: openIdx, end: closeIdx + 1 });
|
|
23
|
-
i = closeIdx + 1;
|
|
24
|
-
}
|
|
25
|
-
return result;
|
|
26
|
-
}
|
|
27
|
-
export function findSentinelAt(text, offset) {
|
|
28
|
-
const sentinels = parseSentinels(text);
|
|
29
|
-
for (const s of sentinels) {
|
|
30
|
-
if (offset >= s.start && offset <= s.end) {
|
|
31
|
-
return s;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
export function isInsideSentinel(text, offset) {
|
|
37
|
-
const sentinels = parseSentinels(text);
|
|
38
|
-
for (const s of sentinels) {
|
|
39
|
-
if (offset >= s.start && offset < s.end) {
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
export function removeSentinel(text, offset) {
|
|
46
|
-
const sentinel = findSentinelAt(text, offset);
|
|
47
|
-
if (!sentinel)
|
|
48
|
-
return text;
|
|
49
|
-
return text.slice(0, sentinel.start) + text.slice(sentinel.end);
|
|
50
|
-
}
|
|
51
|
-
export function getPlaceholderText(displayNumber) {
|
|
52
|
-
return `[Pasted Image #${displayNumber}]`;
|
|
53
|
-
}
|
|
54
|
-
export function getPlaceholderVisualWidth(displayNumber) {
|
|
55
|
-
return getPlaceholderText(displayNumber).length;
|
|
56
|
-
}
|
|
57
|
-
export function getSentinelVisualWidthFromText(text, offset) {
|
|
58
|
-
const s = findSentinelAt(text, offset);
|
|
59
|
-
if (!s)
|
|
60
|
-
return null;
|
|
61
|
-
return getPlaceholderVisualWidth(s.displayNumber);
|
|
62
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { PlaceholderInfo, PlaceholderState } from './types.js';
|
|
2
|
-
export declare const MARKER_REGEX: RegExp;
|
|
3
|
-
export declare function createMarker(id: number): string;
|
|
4
|
-
export declare function createPlaceholderState(): PlaceholderState;
|
|
5
|
-
export declare function addPlaceholder(state: PlaceholderState, originalText: string, displayText: string): {
|
|
6
|
-
id: number;
|
|
7
|
-
marker: string;
|
|
8
|
-
state: PlaceholderState;
|
|
9
|
-
};
|
|
10
|
-
export declare function removePlaceholder(state: PlaceholderState, id: number): PlaceholderState;
|
|
11
|
-
export declare function getDisplayLine(line: string, placeholders: Map<number, PlaceholderInfo>): string;
|
|
12
|
-
export declare function getValue(lines: string[], placeholders: Map<number, PlaceholderInfo>): string;
|
|
13
|
-
export interface MarkerRange {
|
|
14
|
-
id: number;
|
|
15
|
-
start: number;
|
|
16
|
-
end: number;
|
|
17
|
-
}
|
|
18
|
-
export declare function findPlaceholderAt(line: string, column: number): MarkerRange | null;
|
|
19
|
-
export declare function findPlaceholderAfter(line: string, column: number): MarkerRange | null;
|
|
20
|
-
export declare function findPlaceholderBefore(line: string, column: number): MarkerRange | null;
|
|
21
|
-
export declare function bufferColToDisplayCol(line: string, column: number, placeholders: Map<number, PlaceholderInfo>): number;
|
|
22
|
-
export declare function displayColToBufferCol(line: string, displayColumn: number, placeholders: Map<number, PlaceholderInfo>): number;
|
|
23
|
-
export declare function getValueCursorOffset(lines: string[], cursor: {
|
|
24
|
-
line: number;
|
|
25
|
-
column: number;
|
|
26
|
-
}, placeholders: Map<number, PlaceholderInfo>): number;
|
|
27
|
-
export declare function getCursorFromValueOffset(lines: string[], offset: number, placeholders: Map<number, PlaceholderInfo>): {
|
|
28
|
-
line: number;
|
|
29
|
-
column: number;
|
|
30
|
-
};
|