ink-prompt 0.2.3 → 0.2.4
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 +2 -2
- 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/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__/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/Placeholder.d.ts +0 -30
- package/dist/components/MultilineInput/Placeholder.js +0 -200
- 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.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__/Placeholder.test.d.ts → BlockTypes.js} +0 -0
- /package/dist/{examples/examples/basic.d.ts → components/MultilineInput/__tests__/BlockMarker.test.d.ts} +0 -0
- /package/dist/{examples/src/components/MultilineInput/__tests__/KeyHandler.test.d.ts → components/MultilineInput/__tests__/BlockRegistry.test.d.ts} +0 -0
|
@@ -17,19 +17,15 @@ export function insertText(buffer, cursor, text) {
|
|
|
17
17
|
return { buffer, cursor };
|
|
18
18
|
const { line, column } = cursor;
|
|
19
19
|
const currentLine = buffer.lines[line];
|
|
20
|
-
// Insert text and handle newlines
|
|
21
20
|
const beforeCursor = currentLine.slice(0, column);
|
|
22
21
|
const afterCursor = currentLine.slice(column);
|
|
23
22
|
const fullText = beforeCursor + text + afterCursor;
|
|
24
|
-
// Split into lines
|
|
25
23
|
const newLines = fullText.split('\n');
|
|
26
|
-
// Calculate new cursor position
|
|
27
24
|
const textLines = text.split('\n');
|
|
28
25
|
const cursorLine = line + (textLines.length - 1);
|
|
29
26
|
const cursorColumn = textLines.length === 1
|
|
30
27
|
? column + text.length
|
|
31
28
|
: textLines[textLines.length - 1].length;
|
|
32
|
-
// Rebuild buffer
|
|
33
29
|
const resultLines = [
|
|
34
30
|
...buffer.lines.slice(0, line),
|
|
35
31
|
...newLines,
|
|
@@ -43,13 +39,11 @@ export function insertText(buffer, cursor, text) {
|
|
|
43
39
|
/**
|
|
44
40
|
* Delete character before cursor (backspace)
|
|
45
41
|
*/
|
|
46
|
-
export function deleteChar(buffer, cursor,
|
|
42
|
+
export function deleteChar(buffer, cursor, entries) {
|
|
47
43
|
const { line, column } = cursor;
|
|
48
|
-
// At the very start of the buffer - nothing to delete
|
|
49
44
|
if (line === 0 && column === 0) {
|
|
50
45
|
return { buffer, cursor };
|
|
51
46
|
}
|
|
52
|
-
// At the start of a line - merge with previous line
|
|
53
47
|
if (column === 0) {
|
|
54
48
|
const previousLine = buffer.lines[line - 1];
|
|
55
49
|
const currentLine = buffer.lines[line];
|
|
@@ -62,9 +56,8 @@ export function deleteChar(buffer, cursor, placeholders) {
|
|
|
62
56
|
cursor: { line: line - 1, column: previousLine.length },
|
|
63
57
|
};
|
|
64
58
|
}
|
|
65
|
-
// Atomic-block deletion (sentinel or placeholder marker) \u2014 remove the whole block
|
|
66
59
|
const currentLine = buffer.lines[line];
|
|
67
|
-
const block = findAtomicBlockBefore(currentLine, column,
|
|
60
|
+
const block = findAtomicBlockBefore(currentLine, column, entries);
|
|
68
61
|
if (block) {
|
|
69
62
|
const newLine = currentLine.slice(0, block.start) + currentLine.slice(block.end);
|
|
70
63
|
const newLines = [...buffer.lines];
|
|
@@ -85,15 +78,13 @@ export function deleteChar(buffer, cursor, placeholders) {
|
|
|
85
78
|
/**
|
|
86
79
|
* Delete character after cursor (forward delete / Delete key)
|
|
87
80
|
*/
|
|
88
|
-
export function deleteCharForward(buffer, cursor,
|
|
81
|
+
export function deleteCharForward(buffer, cursor, entries) {
|
|
89
82
|
const { line, column } = cursor;
|
|
90
83
|
const currentLine = buffer.lines[line];
|
|
91
84
|
const lineCount = buffer.lines.length;
|
|
92
|
-
// At the very end of the buffer - nothing to delete
|
|
93
85
|
if (line === lineCount - 1 && column >= currentLine.length) {
|
|
94
86
|
return { buffer, cursor };
|
|
95
87
|
}
|
|
96
|
-
// At the end of a line - merge with next line
|
|
97
88
|
if (column >= currentLine.length) {
|
|
98
89
|
const nextLine = buffer.lines[line + 1];
|
|
99
90
|
const mergedLine = currentLine + nextLine;
|
|
@@ -102,11 +93,10 @@ export function deleteCharForward(buffer, cursor, placeholders) {
|
|
|
102
93
|
newLines.splice(line + 1, 1);
|
|
103
94
|
return {
|
|
104
95
|
buffer: { lines: newLines },
|
|
105
|
-
cursor,
|
|
96
|
+
cursor,
|
|
106
97
|
};
|
|
107
98
|
}
|
|
108
|
-
|
|
109
|
-
const block = findAtomicBlockAfter(currentLine, column, placeholders);
|
|
99
|
+
const block = findAtomicBlockAfter(currentLine, column, entries);
|
|
110
100
|
if (block) {
|
|
111
101
|
const newLine = currentLine.slice(0, block.start) + currentLine.slice(block.end);
|
|
112
102
|
const newLines = [...buffer.lines];
|
|
@@ -116,13 +106,12 @@ export function deleteCharForward(buffer, cursor, placeholders) {
|
|
|
116
106
|
cursor,
|
|
117
107
|
};
|
|
118
108
|
}
|
|
119
|
-
// Delete character after cursor within the line
|
|
120
109
|
const newLine = currentLine.slice(0, column) + currentLine.slice(column + 1);
|
|
121
110
|
const newLines = [...buffer.lines];
|
|
122
111
|
newLines[line] = newLine;
|
|
123
112
|
return {
|
|
124
113
|
buffer: { lines: newLines },
|
|
125
|
-
cursor,
|
|
114
|
+
cursor,
|
|
126
115
|
};
|
|
127
116
|
}
|
|
128
117
|
/**
|
|
@@ -144,46 +133,82 @@ export function insertNewLine(buffer, cursor) {
|
|
|
144
133
|
function getVisualWidth(char) {
|
|
145
134
|
return 1;
|
|
146
135
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
136
|
+
function getVisualRowCount(line, width, entries) {
|
|
137
|
+
return getVisualRows(line, width, entries).length;
|
|
138
|
+
}
|
|
139
|
+
function visualToBufferColumn(visualRow, visualCol, line, width, entries) {
|
|
140
|
+
const rows = getVisualRows(line, width, entries);
|
|
141
|
+
if (visualRow >= rows.length) {
|
|
142
|
+
return line.length;
|
|
143
|
+
}
|
|
144
|
+
const row = rows[visualRow];
|
|
145
|
+
return Math.min(row.start + visualCol, line.length);
|
|
146
|
+
}
|
|
147
|
+
function getVisualRowLength(line, visualRow, width, entries) {
|
|
148
|
+
const rows = getVisualRows(line, width, entries);
|
|
149
|
+
if (visualRow >= rows.length)
|
|
150
|
+
return 0;
|
|
151
|
+
return rows[visualRow].length;
|
|
152
|
+
}
|
|
153
|
+
function getVisualPosition(bufferColumn, line, width, entries) {
|
|
154
|
+
const rows = getVisualRows(line, width, entries);
|
|
155
|
+
for (let i = 0; i < rows.length; i++) {
|
|
156
|
+
const row = rows[i];
|
|
157
|
+
const rowEnd = row.start + row.length;
|
|
158
|
+
if (bufferColumn >= row.start && bufferColumn < rowEnd) {
|
|
159
|
+
return { visualRow: i, visualCol: bufferColumn - row.start };
|
|
160
|
+
}
|
|
161
|
+
if (bufferColumn === rowEnd && i === rows.length - 1) {
|
|
162
|
+
return { visualRow: i, visualCol: row.length };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (let i = 0; i < rows.length; i++) {
|
|
166
|
+
const row = rows[i];
|
|
167
|
+
if (bufferColumn === row.start + row.length && i < rows.length - 1) {
|
|
168
|
+
return { visualRow: i + 1, visualCol: 0 };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const lastRow = rows[rows.length - 1];
|
|
172
|
+
return { visualRow: rows.length - 1, visualCol: lastRow.length };
|
|
173
|
+
}
|
|
174
|
+
export function getVisualRows(line, width, entries) {
|
|
155
175
|
const safeWidth = Math.max(1, width);
|
|
156
176
|
const rows = [];
|
|
157
|
-
|
|
158
|
-
|
|
177
|
+
const blocks = findAtomicBlocks(line, entries);
|
|
178
|
+
if (blocks.length > 0) {
|
|
179
|
+
return getVisualRowsWithBlocks(line, safeWidth, rows, blocks);
|
|
159
180
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
let
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (remaining[i] === ' ') {
|
|
174
|
-
splitIndex = i;
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (splitIndex !== -1) {
|
|
179
|
-
chunkLength = splitIndex + 1;
|
|
181
|
+
let offset = 0;
|
|
182
|
+
let remaining = line;
|
|
183
|
+
while (remaining.length > 0) {
|
|
184
|
+
let chunkLength = safeWidth;
|
|
185
|
+
if (remaining.length <= safeWidth) {
|
|
186
|
+
chunkLength = remaining.length;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
let splitIndex = -1;
|
|
190
|
+
for (let i = safeWidth - 1; i >= 0; i--) {
|
|
191
|
+
if (remaining[i] === ' ') {
|
|
192
|
+
splitIndex = i;
|
|
193
|
+
break;
|
|
180
194
|
}
|
|
181
195
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
196
|
+
if (splitIndex !== -1) {
|
|
197
|
+
chunkLength = splitIndex + 1;
|
|
198
|
+
}
|
|
185
199
|
}
|
|
186
|
-
|
|
200
|
+
rows.push({ start: offset, length: chunkLength });
|
|
201
|
+
remaining = remaining.slice(chunkLength);
|
|
202
|
+
offset += chunkLength;
|
|
203
|
+
}
|
|
204
|
+
if (rows.length === 0) {
|
|
205
|
+
return [{ start: 0, length: 0 }];
|
|
206
|
+
}
|
|
207
|
+
return rows;
|
|
208
|
+
}
|
|
209
|
+
function getVisualRowsWithBlocks(line, safeWidth, rows, blocks) {
|
|
210
|
+
if (line.length === 0) {
|
|
211
|
+
return [{ start: 0, length: 0 }];
|
|
187
212
|
}
|
|
188
213
|
let charPos = 0;
|
|
189
214
|
let rowStart = 0;
|
|
@@ -249,83 +274,18 @@ export function getVisualRows(line, width, placeholders) {
|
|
|
249
274
|
}
|
|
250
275
|
return rows;
|
|
251
276
|
}
|
|
252
|
-
|
|
253
|
-
* Calculate which visual row (within a buffer line) the cursor is on,
|
|
254
|
-
* and the column within that visual row.
|
|
255
|
-
* Uses word-aware wrapping.
|
|
256
|
-
*/
|
|
257
|
-
function getVisualPosition(bufferColumn, line, width, placeholders) {
|
|
258
|
-
const rows = getVisualRows(line, width, placeholders);
|
|
259
|
-
for (let i = 0; i < rows.length; i++) {
|
|
260
|
-
const row = rows[i];
|
|
261
|
-
const rowEnd = row.start + row.length;
|
|
262
|
-
if (bufferColumn >= row.start && bufferColumn < rowEnd) {
|
|
263
|
-
return { visualRow: i, visualCol: bufferColumn - row.start };
|
|
264
|
-
}
|
|
265
|
-
// Handle cursor at the very end of this row
|
|
266
|
-
if (bufferColumn === rowEnd && i === rows.length - 1) {
|
|
267
|
-
return { visualRow: i, visualCol: row.length };
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
// Cursor at wrap point - belongs to the next row
|
|
271
|
-
for (let i = 0; i < rows.length; i++) {
|
|
272
|
-
const row = rows[i];
|
|
273
|
-
if (bufferColumn === row.start + row.length && i < rows.length - 1) {
|
|
274
|
-
return { visualRow: i + 1, visualCol: 0 };
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
// Fallback: cursor at end of line
|
|
278
|
-
const lastRow = rows[rows.length - 1];
|
|
279
|
-
return { visualRow: rows.length - 1, visualCol: lastRow.length };
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* Calculate how many visual rows a buffer line takes.
|
|
283
|
-
* Uses word-aware wrapping.
|
|
284
|
-
*/
|
|
285
|
-
function getVisualRowCount(line, width, placeholders) {
|
|
286
|
-
return getVisualRows(line, width, placeholders).length;
|
|
287
|
-
}
|
|
288
|
-
/**
|
|
289
|
-
* Convert visual position back to buffer column.
|
|
290
|
-
* Uses word-aware wrapping.
|
|
291
|
-
*/
|
|
292
|
-
function visualToBufferColumn(visualRow, visualCol, line, width, placeholders) {
|
|
293
|
-
const rows = getVisualRows(line, width, placeholders);
|
|
294
|
-
if (visualRow >= rows.length) {
|
|
295
|
-
return line.length;
|
|
296
|
-
}
|
|
297
|
-
const row = rows[visualRow];
|
|
298
|
-
return Math.min(row.start + visualCol, line.length);
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* Get the length of a specific visual row within a buffer line.
|
|
302
|
-
* Uses word-aware wrapping.
|
|
303
|
-
*/
|
|
304
|
-
function getVisualRowLength(line, visualRow, width, placeholders) {
|
|
305
|
-
const rows = getVisualRows(line, width, placeholders);
|
|
306
|
-
if (visualRow >= rows.length)
|
|
307
|
-
return 0;
|
|
308
|
-
return rows[visualRow].length;
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Move cursor in specified direction with bounds checking.
|
|
312
|
-
* When width is provided, up/down movement is based on visual lines (accounting for wrapping).
|
|
313
|
-
* When width is not provided, up/down movement is based on buffer lines.
|
|
314
|
-
*/
|
|
315
|
-
export function moveCursor(buffer, cursor, direction, width, placeholders) {
|
|
277
|
+
export function moveCursor(buffer, cursor, direction, width, entries) {
|
|
316
278
|
const { line, column } = cursor;
|
|
317
279
|
const currentLine = buffer.lines[line];
|
|
318
280
|
const lineCount = buffer.lines.length;
|
|
319
281
|
switch (direction) {
|
|
320
282
|
case 'left': {
|
|
321
283
|
if (column > 0) {
|
|
322
|
-
|
|
323
|
-
const block = findAtomicBlockBefore(currentLine, column, placeholders);
|
|
284
|
+
const block = findAtomicBlockBefore(currentLine, column, entries);
|
|
324
285
|
if (block)
|
|
325
286
|
return { line, column: block.start };
|
|
326
287
|
return { line, column: column - 1 };
|
|
327
288
|
}
|
|
328
|
-
// Wrap to end of previous line
|
|
329
289
|
if (line > 0) {
|
|
330
290
|
return { line: line - 1, column: buffer.lines[line - 1].length };
|
|
331
291
|
}
|
|
@@ -333,13 +293,11 @@ export function moveCursor(buffer, cursor, direction, width, placeholders) {
|
|
|
333
293
|
}
|
|
334
294
|
case 'right': {
|
|
335
295
|
if (column < currentLine.length) {
|
|
336
|
-
|
|
337
|
-
const block = findAtomicBlockAfter(currentLine, column, placeholders);
|
|
296
|
+
const block = findAtomicBlockAfter(currentLine, column, entries);
|
|
338
297
|
if (block)
|
|
339
298
|
return { line, column: block.end };
|
|
340
299
|
return { line, column: column + 1 };
|
|
341
300
|
}
|
|
342
|
-
// Wrap to start of next line
|
|
343
301
|
if (line < lineCount - 1) {
|
|
344
302
|
return { line: line + 1, column: 0 };
|
|
345
303
|
}
|
|
@@ -347,20 +305,20 @@ export function moveCursor(buffer, cursor, direction, width, placeholders) {
|
|
|
347
305
|
}
|
|
348
306
|
case 'up':
|
|
349
307
|
if (width !== undefined) {
|
|
350
|
-
const { visualRow, visualCol } = getVisualPosition(column, currentLine, width,
|
|
308
|
+
const { visualRow, visualCol } = getVisualPosition(column, currentLine, width, entries);
|
|
351
309
|
if (visualRow > 0) {
|
|
352
310
|
const targetVisualRow = visualRow - 1;
|
|
353
|
-
const targetVisualRowLength = getVisualRowLength(currentLine, targetVisualRow, width,
|
|
311
|
+
const targetVisualRowLength = getVisualRowLength(currentLine, targetVisualRow, width, entries);
|
|
354
312
|
const targetVisualCol = Math.min(visualCol, targetVisualRowLength);
|
|
355
|
-
return { line, column: visualToBufferColumn(targetVisualRow, targetVisualCol, currentLine, width,
|
|
313
|
+
return { line, column: visualToBufferColumn(targetVisualRow, targetVisualCol, currentLine, width, entries) };
|
|
356
314
|
}
|
|
357
315
|
if (line > 0) {
|
|
358
316
|
const prevLine = buffer.lines[line - 1];
|
|
359
|
-
const prevLineVisualRows = getVisualRowCount(prevLine, width,
|
|
317
|
+
const prevLineVisualRows = getVisualRowCount(prevLine, width, entries);
|
|
360
318
|
const targetVisualRow = prevLineVisualRows - 1;
|
|
361
|
-
const targetVisualRowLength = getVisualRowLength(prevLine, targetVisualRow, width,
|
|
319
|
+
const targetVisualRowLength = getVisualRowLength(prevLine, targetVisualRow, width, entries);
|
|
362
320
|
const targetVisualCol = Math.min(visualCol, targetVisualRowLength);
|
|
363
|
-
return { line: line - 1, column: visualToBufferColumn(targetVisualRow, targetVisualCol, prevLine, width,
|
|
321
|
+
return { line: line - 1, column: visualToBufferColumn(targetVisualRow, targetVisualCol, prevLine, width, entries) };
|
|
364
322
|
}
|
|
365
323
|
return cursor;
|
|
366
324
|
}
|
|
@@ -371,23 +329,22 @@ export function moveCursor(buffer, cursor, direction, width, placeholders) {
|
|
|
371
329
|
return cursor;
|
|
372
330
|
case 'down':
|
|
373
331
|
if (width !== undefined) {
|
|
374
|
-
const { visualRow, visualCol } = getVisualPosition(column, currentLine, width,
|
|
375
|
-
const currentLineVisualRows = getVisualRowCount(currentLine, width,
|
|
332
|
+
const { visualRow, visualCol } = getVisualPosition(column, currentLine, width, entries);
|
|
333
|
+
const currentLineVisualRows = getVisualRowCount(currentLine, width, entries);
|
|
376
334
|
if (visualRow < currentLineVisualRows - 1) {
|
|
377
335
|
const targetVisualRow = visualRow + 1;
|
|
378
|
-
const targetVisualRowLength = getVisualRowLength(currentLine, targetVisualRow, width,
|
|
336
|
+
const targetVisualRowLength = getVisualRowLength(currentLine, targetVisualRow, width, entries);
|
|
379
337
|
const targetVisualCol = Math.min(visualCol, targetVisualRowLength);
|
|
380
|
-
return { line, column: visualToBufferColumn(targetVisualRow, targetVisualCol, currentLine, width,
|
|
338
|
+
return { line, column: visualToBufferColumn(targetVisualRow, targetVisualCol, currentLine, width, entries) };
|
|
381
339
|
}
|
|
382
340
|
if (line < lineCount - 1) {
|
|
383
341
|
const nextLine = buffer.lines[line + 1];
|
|
384
|
-
const targetVisualRowLength = getVisualRowLength(nextLine, 0, width,
|
|
342
|
+
const targetVisualRowLength = getVisualRowLength(nextLine, 0, width, entries);
|
|
385
343
|
const targetVisualCol = Math.min(visualCol, targetVisualRowLength);
|
|
386
344
|
return { line: line + 1, column: Math.min(targetVisualCol, nextLine.length) };
|
|
387
345
|
}
|
|
388
346
|
return cursor;
|
|
389
347
|
}
|
|
390
|
-
// Buffer-line movement (no width provided)
|
|
391
348
|
if (line < lineCount - 1) {
|
|
392
349
|
const targetLine = buffer.lines[line + 1];
|
|
393
350
|
return { line: line + 1, column: Math.min(column, targetLine.length) };
|
|
@@ -401,62 +358,35 @@ export function moveCursor(buffer, cursor, direction, width, placeholders) {
|
|
|
401
358
|
return cursor;
|
|
402
359
|
}
|
|
403
360
|
}
|
|
404
|
-
/**
|
|
405
|
-
* Get the full text content from buffer (lines joined with newlines)
|
|
406
|
-
*/
|
|
407
361
|
export function getTextContent(buffer) {
|
|
408
|
-
// Single empty line is considered empty buffer
|
|
409
362
|
if (buffer.lines.length === 1 && buffer.lines[0] === '') {
|
|
410
363
|
return '';
|
|
411
364
|
}
|
|
412
365
|
return buffer.lines.join('\n');
|
|
413
366
|
}
|
|
414
|
-
/**
|
|
415
|
-
* Get the flat offset (index) from a cursor position.
|
|
416
|
-
*/
|
|
417
367
|
export function getOffset(buffer, cursor) {
|
|
418
368
|
let offset = 0;
|
|
419
369
|
for (let i = 0; i < cursor.line; i++) {
|
|
420
|
-
offset += buffer.lines[i].length + 1;
|
|
370
|
+
offset += buffer.lines[i].length + 1;
|
|
421
371
|
}
|
|
422
372
|
offset += cursor.column;
|
|
423
373
|
return offset;
|
|
424
374
|
}
|
|
425
|
-
/**
|
|
426
|
-
* Get the cursor position from a flat offset.
|
|
427
|
-
*/
|
|
428
375
|
export function getCursor(buffer, offset) {
|
|
429
376
|
let currentOffset = 0;
|
|
430
377
|
for (let i = 0; i < buffer.lines.length; i++) {
|
|
431
378
|
const lineLength = buffer.lines[i].length;
|
|
432
|
-
// Check if the offset falls within this line (including the newline character unless it's the last line)
|
|
433
|
-
// We treat the position *after* the newline as the start of the next line.
|
|
434
|
-
// Position *at* the end of line (before newline) is valid cursor column.
|
|
435
|
-
// If we are on the last line, we accept up to lineLength
|
|
436
379
|
if (i === buffer.lines.length - 1) {
|
|
437
380
|
if (offset <= currentOffset + lineLength) {
|
|
438
381
|
return { line: i, column: Math.max(0, offset - currentOffset) };
|
|
439
382
|
}
|
|
440
|
-
// If offset is beyond the content, clamp to end
|
|
441
383
|
return { line: i, column: lineLength };
|
|
442
384
|
}
|
|
443
|
-
// For non-last lines, we account for newline character (+1)
|
|
444
385
|
if (offset <= currentOffset + lineLength) {
|
|
445
386
|
return { line: i, column: Math.max(0, offset - currentOffset) };
|
|
446
387
|
}
|
|
447
|
-
// If offset is exactly at the newline character, it depends on interpretation.
|
|
448
|
-
// Usually, cursor at the newline means it's at the end of the line.
|
|
449
|
-
// But if we are "past" the newline, we are on the start of next line.
|
|
450
|
-
// Logic:
|
|
451
|
-
// Line 0: "abc" (len 3). Newline at index 3.
|
|
452
|
-
// Index 0='a', 1='b', 2='c', 3='\n'.
|
|
453
|
-
// If target offset is 3: That's end of line 0.
|
|
454
|
-
// If target offset is 4: That's start of line 1.
|
|
455
|
-
// currentOffset + lineLength is index of newline.
|
|
456
|
-
// If offset == currentOffset + lineLength + 1, we move to next loop
|
|
457
388
|
currentOffset += lineLength + 1;
|
|
458
389
|
}
|
|
459
|
-
// Fallback (should have returned in loop)
|
|
460
390
|
const lastLineIdx = buffer.lines.length - 1;
|
|
461
391
|
return { line: lastLineIdx, column: buffer.lines[lastLineIdx].length };
|
|
462
392
|
}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type { Buffer, Cursor, WrapResult
|
|
3
|
-
import type {
|
|
2
|
+
import type { Buffer, Cursor, WrapResult } from './types.js';
|
|
3
|
+
import type { BlockState } from './BlockTypes.js';
|
|
4
4
|
export interface TextRendererProps {
|
|
5
5
|
buffer: Buffer;
|
|
6
6
|
cursor: Cursor;
|
|
7
7
|
width?: number;
|
|
8
8
|
showCursor?: boolean;
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
images?: Record<string, ImageRef>;
|
|
9
|
+
/** Block state for expanding markers into display text */
|
|
10
|
+
blockState?: BlockState;
|
|
12
11
|
}
|
|
13
12
|
interface VisualSegment {
|
|
14
13
|
text: string;
|
|
@@ -19,8 +18,8 @@ interface VisualRow {
|
|
|
19
18
|
/** Total visual length (sum of segment text lengths). */
|
|
20
19
|
visualLength: number;
|
|
21
20
|
}
|
|
22
|
-
export declare function wrapLines(buffer: Buffer, cursor: Cursor, width: number,
|
|
21
|
+
export declare function wrapLines(buffer: Buffer, cursor: Cursor, width: number, entries?: Map<string, import('./BlockTypes.js').BlockEntry>): WrapResult & {
|
|
23
22
|
rows: VisualRow[];
|
|
24
23
|
};
|
|
25
|
-
export declare function TextRenderer({ buffer, cursor, width: propWidth, showCursor,
|
|
24
|
+
export declare function TextRenderer({ buffer, cursor, width: propWidth, showCursor, blockState, }: TextRendererProps): React.ReactElement;
|
|
26
25
|
export {};
|
|
@@ -14,7 +14,7 @@ function expandRange(line, start, end, blocks) {
|
|
|
14
14
|
if (raw > plainStart) {
|
|
15
15
|
segments.push({ text: line.slice(plainStart, raw), dim: false });
|
|
16
16
|
}
|
|
17
|
-
segments.push({ text: block.displayText, dim: block.
|
|
17
|
+
segments.push({ text: block.displayText, dim: block.dim });
|
|
18
18
|
raw = block.end;
|
|
19
19
|
plainStart = raw;
|
|
20
20
|
}
|
|
@@ -46,7 +46,7 @@ function visualColumnForRawColumn(line, rowStart, column, blocks) {
|
|
|
46
46
|
}
|
|
47
47
|
return visualCol;
|
|
48
48
|
}
|
|
49
|
-
export function wrapLines(buffer, cursor, width,
|
|
49
|
+
export function wrapLines(buffer, cursor, width, entries) {
|
|
50
50
|
const visualLines = [];
|
|
51
51
|
const rowsOut = [];
|
|
52
52
|
let cursorVisualRow = 0;
|
|
@@ -56,8 +56,8 @@ export function wrapLines(buffer, cursor, width, placeholders) {
|
|
|
56
56
|
for (let lineIndex = 0; lineIndex < buffer.lines.length; lineIndex++) {
|
|
57
57
|
const rawLine = buffer.lines[lineIndex];
|
|
58
58
|
const isCursorLine = lineIndex === cursor.line;
|
|
59
|
-
const blocks = findAtomicBlocks(rawLine,
|
|
60
|
-
const rows = getVisualRows(rawLine, safeWidth,
|
|
59
|
+
const blocks = findAtomicBlocks(rawLine, entries);
|
|
60
|
+
const rows = getVisualRows(rawLine, safeWidth, entries);
|
|
61
61
|
if (rawLine.length === 0) {
|
|
62
62
|
visualLines.push('');
|
|
63
63
|
rowsOut.push({ segments: [], visualLength: 0 });
|
|
@@ -140,10 +140,10 @@ function renderVisualRow(row, isCursorRow, cursorCol, showCursor) {
|
|
|
140
140
|
: [];
|
|
141
141
|
return (_jsxs(_Fragment, { children: [renderSegments(before, 'b'), _jsx(Text, { inverse: true, dimColor: under.dim, children: under.ch }), renderSegments(after, 'a')] }));
|
|
142
142
|
}
|
|
143
|
-
export function TextRenderer({ buffer, cursor, width: propWidth, showCursor = true,
|
|
143
|
+
export function TextRenderer({ buffer, cursor, width: propWidth, showCursor = true, blockState, }) {
|
|
144
144
|
const width = useTerminalWidth(propWidth);
|
|
145
|
-
const
|
|
146
|
-
const { rows, cursorVisualRow, cursorVisualCol } = wrapLines(buffer, cursor, width,
|
|
145
|
+
const entries = blockState?.entries;
|
|
146
|
+
const { rows, cursorVisualRow, cursorVisualCol } = wrapLines(buffer, cursor, width, entries);
|
|
147
147
|
return (_jsx(Box, { flexDirection: "column", children: rows.map((row, index) => {
|
|
148
148
|
const isCursorRow = index === cursorVisualRow;
|
|
149
149
|
return (_jsx(Box, { children: renderVisualRow(row, isCursorRow, cursorVisualCol, showCursor) }, index));
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { BLOCK_OPEN, BLOCK_CLOSE, createBlockMarker, parseBlockMarkers, findBlockMarkerAt, findBlockMarkerBefore, findBlockMarkerAfter, removeBlockMarker, blockMarkerVisualWidth, getBlockPlaceholderText, generateBlockId, } from '../BlockMarker.js';
|
|
3
|
+
describe('generateBlockId', () => {
|
|
4
|
+
it('generates a non-empty string', () => {
|
|
5
|
+
const id = generateBlockId();
|
|
6
|
+
expect(id.length).toBeGreaterThan(0);
|
|
7
|
+
});
|
|
8
|
+
it('generates unique IDs', () => {
|
|
9
|
+
const ids = new Set(Array.from({ length: 100 }, () => generateBlockId()));
|
|
10
|
+
expect(ids.size).toBe(100);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
describe('createBlockMarker', () => {
|
|
14
|
+
it('creates a paste marker', () => {
|
|
15
|
+
const marker = createBlockMarker('p', 'abc123', 1);
|
|
16
|
+
expect(marker).toBe(`${BLOCK_OPEN}p:abc123:1${BLOCK_CLOSE}`);
|
|
17
|
+
});
|
|
18
|
+
it('creates an image marker', () => {
|
|
19
|
+
const marker = createBlockMarker('i', 'xyz789', 2);
|
|
20
|
+
expect(marker).toBe(`${BLOCK_OPEN}i:xyz789:2${BLOCK_CLOSE}`);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
describe('parseBlockMarkers', () => {
|
|
24
|
+
it('finds a single paste marker', () => {
|
|
25
|
+
const text = `hello ${BLOCK_OPEN}p:abc123:1${BLOCK_CLOSE} world`;
|
|
26
|
+
const markers = parseBlockMarkers(text);
|
|
27
|
+
expect(markers).toHaveLength(1);
|
|
28
|
+
expect(markers[0]).toMatchObject({ kind: 'p', id: 'abc123', displayNumber: 1, start: 6, end: 18 });
|
|
29
|
+
});
|
|
30
|
+
it('finds a single image marker', () => {
|
|
31
|
+
const text = `${BLOCK_OPEN}i:xyz789:2${BLOCK_CLOSE}`;
|
|
32
|
+
const markers = parseBlockMarkers(text);
|
|
33
|
+
expect(markers).toHaveLength(1);
|
|
34
|
+
expect(markers[0]).toMatchObject({ kind: 'i', id: 'xyz789', displayNumber: 2, start: 0, end: 12 });
|
|
35
|
+
});
|
|
36
|
+
it('finds multiple markers', () => {
|
|
37
|
+
const text = `${BLOCK_OPEN}p:id1:1${BLOCK_CLOSE}ab${BLOCK_OPEN}i:id2:2${BLOCK_CLOSE}`;
|
|
38
|
+
const markers = parseBlockMarkers(text);
|
|
39
|
+
expect(markers).toHaveLength(2);
|
|
40
|
+
expect(markers[0]).toMatchObject({ kind: 'p', id: 'id1', displayNumber: 1, start: 0 });
|
|
41
|
+
expect(markers[1]).toMatchObject({ kind: 'i', id: 'id2', displayNumber: 2 });
|
|
42
|
+
});
|
|
43
|
+
it('returns empty array for text without markers', () => {
|
|
44
|
+
expect(parseBlockMarkers('plain text')).toEqual([]);
|
|
45
|
+
});
|
|
46
|
+
it('returns empty array for empty text', () => {
|
|
47
|
+
expect(parseBlockMarkers('')).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
it('skips malformed markers', () => {
|
|
50
|
+
const text = `${BLOCK_OPEN}bad${BLOCK_CLOSE}`;
|
|
51
|
+
expect(parseBlockMarkers(text)).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
it('skips markers with unknown kind', () => {
|
|
54
|
+
const text = `${BLOCK_OPEN}x:abc:1${BLOCK_CLOSE}`;
|
|
55
|
+
expect(parseBlockMarkers(text)).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('findBlockMarkerAt', () => {
|
|
59
|
+
it('finds marker containing the offset', () => {
|
|
60
|
+
const text = `a${BLOCK_OPEN}p:id:1${BLOCK_CLOSE}b`;
|
|
61
|
+
const result = findBlockMarkerAt(text, 2);
|
|
62
|
+
expect(result).toMatchObject({ kind: 'p', id: 'id', displayNumber: 1 });
|
|
63
|
+
});
|
|
64
|
+
it('returns null for offset at marker start', () => {
|
|
65
|
+
const text = `${BLOCK_OPEN}p:id:1${BLOCK_CLOSE}`;
|
|
66
|
+
expect(findBlockMarkerAt(text, 0)).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
it('returns null for offset at marker end', () => {
|
|
69
|
+
const text = `${BLOCK_OPEN}p:id:1${BLOCK_CLOSE}`;
|
|
70
|
+
const end = text.length;
|
|
71
|
+
expect(findBlockMarkerAt(text, end)).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
it('returns null outside markers', () => {
|
|
74
|
+
const text = `a${BLOCK_OPEN}p:id:1${BLOCK_CLOSE}b`;
|
|
75
|
+
expect(findBlockMarkerAt(text, 0)).toBeNull();
|
|
76
|
+
expect(findBlockMarkerAt(text, text.length - 1)).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe('findBlockMarkerBefore', () => {
|
|
80
|
+
it('finds marker ending at offset', () => {
|
|
81
|
+
const text = `a${BLOCK_OPEN}p:id:1${BLOCK_CLOSE}b`;
|
|
82
|
+
const end = text.indexOf(BLOCK_CLOSE) + 1;
|
|
83
|
+
const result = findBlockMarkerBefore(text, end);
|
|
84
|
+
expect(result).toMatchObject({ kind: 'p', id: 'id' });
|
|
85
|
+
});
|
|
86
|
+
it('returns null when no marker ends at offset', () => {
|
|
87
|
+
const text = `a${BLOCK_OPEN}p:id:1${BLOCK_CLOSE}b`;
|
|
88
|
+
expect(findBlockMarkerBefore(text, 1)).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe('findBlockMarkerAfter', () => {
|
|
92
|
+
it('finds marker starting at offset', () => {
|
|
93
|
+
const text = `a${BLOCK_OPEN}p:id:1${BLOCK_CLOSE}b`;
|
|
94
|
+
const start = text.indexOf(BLOCK_OPEN);
|
|
95
|
+
const result = findBlockMarkerAfter(text, start);
|
|
96
|
+
expect(result).toMatchObject({ kind: 'p', id: 'id' });
|
|
97
|
+
});
|
|
98
|
+
it('returns null when no marker starts at offset', () => {
|
|
99
|
+
const text = `a${BLOCK_OPEN}p:id:1${BLOCK_CLOSE}b`;
|
|
100
|
+
expect(findBlockMarkerAfter(text, 0)).toBeNull();
|
|
101
|
+
expect(findBlockMarkerAfter(text, 2)).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('removeBlockMarker', () => {
|
|
105
|
+
it('removes marker at offset', () => {
|
|
106
|
+
const text = `a${BLOCK_OPEN}p:id:1${BLOCK_CLOSE}b`;
|
|
107
|
+
const result = removeBlockMarker(text, 2);
|
|
108
|
+
expect(result).toBe('ab');
|
|
109
|
+
});
|
|
110
|
+
it('returns original text if no marker at offset', () => {
|
|
111
|
+
const text = 'abc';
|
|
112
|
+
expect(removeBlockMarker(text, 1)).toBe('abc');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
describe('getBlockPlaceholderText', () => {
|
|
116
|
+
it('formats image display text', () => {
|
|
117
|
+
expect(getBlockPlaceholderText('i', 1)).toBe('[Pasted Image #1]');
|
|
118
|
+
expect(getBlockPlaceholderText('i', 42)).toBe('[Pasted Image #42]');
|
|
119
|
+
});
|
|
120
|
+
it('formats paste display text', () => {
|
|
121
|
+
expect(getBlockPlaceholderText('p', 1)).toBe('[Paste text #1]');
|
|
122
|
+
expect(getBlockPlaceholderText('p', 5)).toBe('[Paste text #5]');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
describe('blockMarkerVisualWidth', () => {
|
|
126
|
+
it('returns correct width', () => {
|
|
127
|
+
expect(blockMarkerVisualWidth(1)).toBe('[Pasted Image #1]'.length);
|
|
128
|
+
expect(blockMarkerVisualWidth(100)).toBe('[Pasted Image #100]'.length);
|
|
129
|
+
});
|
|
130
|
+
});
|