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.
Files changed (61) hide show
  1. package/README.md +2 -2
  2. package/dist/components/MultilineInput/AtomicBlocks.d.ts +10 -20
  3. package/dist/components/MultilineInput/AtomicBlocks.js +33 -42
  4. package/dist/components/MultilineInput/BlockMarker.d.ts +19 -0
  5. package/dist/components/MultilineInput/BlockMarker.js +83 -0
  6. package/dist/components/MultilineInput/BlockRegistry.d.ts +39 -0
  7. package/dist/components/MultilineInput/BlockRegistry.js +236 -0
  8. package/dist/components/MultilineInput/BlockTypes.d.ts +22 -0
  9. package/dist/components/MultilineInput/ImageTypes.d.ts +0 -2
  10. package/dist/components/MultilineInput/ImageTypes.js +1 -2
  11. package/dist/components/MultilineInput/ImageValidator.js +1 -1
  12. package/dist/components/MultilineInput/KeyHandler.d.ts +1 -1
  13. package/dist/components/MultilineInput/TextBuffer.d.ts +5 -31
  14. package/dist/components/MultilineInput/TextBuffer.js +91 -161
  15. package/dist/components/MultilineInput/TextRenderer.d.ts +6 -7
  16. package/dist/components/MultilineInput/TextRenderer.js +7 -7
  17. package/dist/components/MultilineInput/__tests__/BlockMarker.test.js +130 -0
  18. package/dist/components/MultilineInput/__tests__/BlockRegistry.test.js +225 -0
  19. package/dist/components/MultilineInput/__tests__/Placeholder.integration.test.js +44 -65
  20. package/dist/components/MultilineInput/__tests__/TextBuffer_images.test.js +10 -31
  21. package/dist/components/MultilineInput/__tests__/TextRenderer_images.test.js +27 -13
  22. package/dist/components/MultilineInput/__tests__/integration_images.test.js +2 -4
  23. package/dist/components/MultilineInput/__tests__/useTextInput_images.test.js +30 -29
  24. package/dist/components/MultilineInput/index.d.ts +6 -6
  25. package/dist/components/MultilineInput/index.js +2 -11
  26. package/dist/components/MultilineInput/types.d.ts +0 -20
  27. package/dist/components/MultilineInput/useTextInput.d.ts +4 -11
  28. package/dist/components/MultilineInput/useTextInput.js +79 -76
  29. package/package.json +1 -1
  30. package/dist/components/MultilineInput/Placeholder.d.ts +0 -30
  31. package/dist/components/MultilineInput/Placeholder.js +0 -200
  32. package/dist/components/MultilineInput/__tests__/Placeholder.test.js +0 -235
  33. package/dist/examples/examples/basic.js +0 -9
  34. package/dist/examples/src/components/MultilineInput/KeyHandler.d.ts +0 -15
  35. package/dist/examples/src/components/MultilineInput/KeyHandler.js +0 -97
  36. package/dist/examples/src/components/MultilineInput/TextBuffer.d.ts +0 -34
  37. package/dist/examples/src/components/MultilineInput/TextBuffer.js +0 -127
  38. package/dist/examples/src/components/MultilineInput/TextRenderer.d.ts +0 -24
  39. package/dist/examples/src/components/MultilineInput/TextRenderer.js +0 -72
  40. package/dist/examples/src/components/MultilineInput/__tests__/KeyHandler.test.js +0 -115
  41. package/dist/examples/src/components/MultilineInput/__tests__/TextBuffer.test.d.ts +0 -1
  42. package/dist/examples/src/components/MultilineInput/__tests__/TextBuffer.test.js +0 -254
  43. package/dist/examples/src/components/MultilineInput/__tests__/TextRenderer.test.d.ts +0 -1
  44. package/dist/examples/src/components/MultilineInput/__tests__/TextRenderer.test.js +0 -176
  45. package/dist/examples/src/components/MultilineInput/__tests__/integration.test.d.ts +0 -1
  46. package/dist/examples/src/components/MultilineInput/__tests__/integration.test.js +0 -71
  47. package/dist/examples/src/components/MultilineInput/__tests__/useTextInput.test.d.ts +0 -1
  48. package/dist/examples/src/components/MultilineInput/__tests__/useTextInput.test.js +0 -65
  49. package/dist/examples/src/components/MultilineInput/index.d.ts +0 -39
  50. package/dist/examples/src/components/MultilineInput/index.js +0 -82
  51. package/dist/examples/src/components/MultilineInput/types.d.ts +0 -55
  52. package/dist/examples/src/components/MultilineInput/types.js +0 -1
  53. package/dist/examples/src/components/MultilineInput/useTextInput.d.ts +0 -16
  54. package/dist/examples/src/components/MultilineInput/useTextInput.js +0 -82
  55. package/dist/examples/src/hello.test.d.ts +0 -1
  56. package/dist/examples/src/hello.test.js +0 -13
  57. package/dist/examples/src/index.d.ts +0 -2
  58. package/dist/examples/src/index.js +0 -2
  59. /package/dist/components/MultilineInput/{__tests__/Placeholder.test.d.ts → BlockTypes.js} +0 -0
  60. /package/dist/{examples/examples/basic.d.ts → components/MultilineInput/__tests__/BlockMarker.test.d.ts} +0 -0
  61. /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, placeholders) {
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, placeholders);
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, placeholders) {
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, // Cursor stays in place
96
+ cursor,
106
97
  };
107
98
  }
108
- // Atomic-block deletion forward (sentinel or placeholder marker) \u2014 remove the whole block
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, // Cursor stays in place
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
- * Break a line into visual rows using word-aware wrapping.
149
- * Words are kept intact when possible, breaking at spaces.
150
- * Long words that exceed width are hard-wrapped.
151
- * Sentinel blocks are atomic: never split, and occupy visual width
152
- * equal to their placeholder text length.
153
- */
154
- export function getVisualRows(line, width, placeholders) {
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
- if (line.length === 0) {
158
- return [{ start: 0, length: 0 }];
177
+ const blocks = findAtomicBlocks(line, entries);
178
+ if (blocks.length > 0) {
179
+ return getVisualRowsWithBlocks(line, safeWidth, rows, blocks);
159
180
  }
160
- const blocks = findAtomicBlocks(line, placeholders);
161
- // Fast path: no atomic blocks
162
- if (blocks.length === 0) {
163
- let offset = 0;
164
- let remaining = line;
165
- while (remaining.length > 0) {
166
- let chunkLength = safeWidth;
167
- if (remaining.length <= safeWidth) {
168
- chunkLength = remaining.length;
169
- }
170
- else {
171
- let splitIndex = -1;
172
- for (let i = safeWidth - 1; i >= 0; i--) {
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
- rows.push({ start: offset, length: chunkLength });
183
- remaining = remaining.slice(chunkLength);
184
- offset += chunkLength;
196
+ if (splitIndex !== -1) {
197
+ chunkLength = splitIndex + 1;
198
+ }
185
199
  }
186
- return rows;
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
- // If position before cursor is the end of an atomic block, jump to its start
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
- // If cursor is at the start of an atomic block, jump past its end
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, placeholders);
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, placeholders);
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, placeholders) };
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, placeholders);
317
+ const prevLineVisualRows = getVisualRowCount(prevLine, width, entries);
360
318
  const targetVisualRow = prevLineVisualRows - 1;
361
- const targetVisualRowLength = getVisualRowLength(prevLine, targetVisualRow, width, placeholders);
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, placeholders) };
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, placeholders);
375
- const currentLineVisualRows = getVisualRowCount(currentLine, width, placeholders);
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, placeholders);
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, placeholders) };
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, placeholders);
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; // +1 for the newline
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, PlaceholderState } from './types.js';
3
- import type { ImageRef } from './ImageTypes.js';
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
- /** Placeholder state for expanding paste markers into display text */
10
- placeholderState?: PlaceholderState;
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, placeholders?: PlaceholderState['placeholders']): WrapResult & {
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, placeholderState, }: TextRendererProps): React.ReactElement;
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.kind === 'sentinel' });
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, placeholders) {
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, placeholders);
60
- const rows = getVisualRows(rawLine, safeWidth, placeholders);
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, placeholderState, }) {
143
+ export function TextRenderer({ buffer, cursor, width: propWidth, showCursor = true, blockState, }) {
144
144
  const width = useTerminalWidth(propWidth);
145
- const placeholders = placeholderState?.placeholders;
146
- const { rows, cursorVisualRow, cursorVisualCol } = wrapLines(buffer, cursor, width, placeholders);
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
+ });