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.
Files changed (67) hide show
  1. package/README.md +3 -3
  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/KeyHandler.js +18 -0
  14. package/dist/components/MultilineInput/TextBuffer.d.ts +5 -31
  15. package/dist/components/MultilineInput/TextBuffer.js +91 -161
  16. package/dist/components/MultilineInput/TextRenderer.d.ts +6 -7
  17. package/dist/components/MultilineInput/TextRenderer.js +7 -7
  18. package/dist/components/MultilineInput/__tests__/BlockMarker.test.js +130 -0
  19. package/dist/components/MultilineInput/__tests__/BlockRegistry.test.js +225 -0
  20. package/dist/components/MultilineInput/__tests__/KeyHandler.test.js +18 -0
  21. package/dist/components/MultilineInput/__tests__/Placeholder.integration.test.js +44 -65
  22. package/dist/components/MultilineInput/__tests__/TextBuffer_images.test.js +10 -31
  23. package/dist/components/MultilineInput/__tests__/TextRenderer_images.test.js +27 -13
  24. package/dist/components/MultilineInput/__tests__/integration_images.test.js +2 -4
  25. package/dist/components/MultilineInput/__tests__/useTextInput_images.test.js +30 -29
  26. package/dist/components/MultilineInput/index.d.ts +6 -6
  27. package/dist/components/MultilineInput/index.js +2 -11
  28. package/dist/components/MultilineInput/types.d.ts +0 -20
  29. package/dist/components/MultilineInput/useTextInput.d.ts +4 -11
  30. package/dist/components/MultilineInput/useTextInput.js +79 -76
  31. package/package.json +1 -1
  32. package/dist/components/MultilineInput/ImageSentinel.d.ts +0 -15
  33. package/dist/components/MultilineInput/ImageSentinel.js +0 -62
  34. package/dist/components/MultilineInput/Placeholder.d.ts +0 -30
  35. package/dist/components/MultilineInput/Placeholder.js +0 -200
  36. package/dist/components/MultilineInput/__tests__/ImageSentinel.test.js +0 -154
  37. package/dist/components/MultilineInput/__tests__/Placeholder.test.js +0 -235
  38. package/dist/examples/examples/basic.js +0 -9
  39. package/dist/examples/src/components/MultilineInput/KeyHandler.d.ts +0 -15
  40. package/dist/examples/src/components/MultilineInput/KeyHandler.js +0 -97
  41. package/dist/examples/src/components/MultilineInput/TextBuffer.d.ts +0 -34
  42. package/dist/examples/src/components/MultilineInput/TextBuffer.js +0 -127
  43. package/dist/examples/src/components/MultilineInput/TextRenderer.d.ts +0 -24
  44. package/dist/examples/src/components/MultilineInput/TextRenderer.js +0 -72
  45. package/dist/examples/src/components/MultilineInput/__tests__/KeyHandler.test.d.ts +0 -1
  46. package/dist/examples/src/components/MultilineInput/__tests__/KeyHandler.test.js +0 -115
  47. package/dist/examples/src/components/MultilineInput/__tests__/TextBuffer.test.d.ts +0 -1
  48. package/dist/examples/src/components/MultilineInput/__tests__/TextBuffer.test.js +0 -254
  49. package/dist/examples/src/components/MultilineInput/__tests__/TextRenderer.test.d.ts +0 -1
  50. package/dist/examples/src/components/MultilineInput/__tests__/TextRenderer.test.js +0 -176
  51. package/dist/examples/src/components/MultilineInput/__tests__/integration.test.d.ts +0 -1
  52. package/dist/examples/src/components/MultilineInput/__tests__/integration.test.js +0 -71
  53. package/dist/examples/src/components/MultilineInput/__tests__/useTextInput.test.d.ts +0 -1
  54. package/dist/examples/src/components/MultilineInput/__tests__/useTextInput.test.js +0 -65
  55. package/dist/examples/src/components/MultilineInput/index.d.ts +0 -39
  56. package/dist/examples/src/components/MultilineInput/index.js +0 -82
  57. package/dist/examples/src/components/MultilineInput/types.d.ts +0 -55
  58. package/dist/examples/src/components/MultilineInput/types.js +0 -1
  59. package/dist/examples/src/components/MultilineInput/useTextInput.d.ts +0 -16
  60. package/dist/examples/src/components/MultilineInput/useTextInput.js +0 -82
  61. package/dist/examples/src/hello.test.d.ts +0 -1
  62. package/dist/examples/src/hello.test.js +0 -13
  63. package/dist/examples/src/index.d.ts +0 -2
  64. package/dist/examples/src/index.js +0 -2
  65. /package/dist/components/MultilineInput/{__tests__/ImageSentinel.test.d.ts → BlockTypes.js} +0 -0
  66. /package/dist/components/MultilineInput/__tests__/{Placeholder.test.d.ts → BlockMarker.test.d.ts} +0 -0
  67. /package/dist/{examples/examples/basic.d.ts → components/MultilineInput/__tests__/BlockRegistry.test.d.ts} +0 -0
package/README.md CHANGED
@@ -46,7 +46,7 @@ render(<App />);
46
46
  | `onBoundaryArrow` | `(direction: 'up' \| 'down' \| 'left' \| 'right') => void` | | Called when arrow key reaches a boundary |
47
47
  | `undoDebounceMs` | `number` | `200` | Milliseconds of inactivity to commit undo batch (`0` = disable) |
48
48
  | `pasteThreshold` | `number` | | Max paste length before text is replaced by a placeholder |
49
- | `formatPastePlaceholder` | `(id: number) => string` | | Custom placeholder display format |
49
+ | `formatPastePlaceholder` | `(displayNumber: number) => string` | | Custom placeholder display format (1-based) |
50
50
  | `enableImagePaste` | `boolean` | `false` | Enables image-aware Ctrl+V handling |
51
51
  | `images` | `ImageRef[]` | | Controlled image state for pasted images |
52
52
  | `onImagesChange` | `(images: ImageRef[]) => void` | | Called when images change |
@@ -60,7 +60,7 @@ render(<App />);
60
60
  `MultilineInput` supports typical editing controls:
61
61
 
62
62
  - **Arrow keys** for navigation
63
- - `Ctrl+J` or typing `\` before **Enter** to add a newline
63
+ - **Shift+Enter**, `Ctrl+J`, or typing `\` before **Enter** to add a newline (Shift+Enter requires a terminal that distinguishes it — most emit `ESC + CR` or the kitty `CSI 13;2u` sequence)
64
64
  - `Ctrl+Z` / `Ctrl+Y` for undo/redo
65
65
  - `Ctrl+A` / `Ctrl+E` for jump to line start/end
66
66
  - **Home** / **End** keys for line start/end
@@ -77,7 +77,7 @@ replace the pasted content with a compact placeholder for cleaner display.
77
77
  <MultilineInput
78
78
  onSubmit={(value) => console.log(value)}
79
79
  pasteThreshold={200} // Text >200 chars becomes a placeholder
80
- formatPastePlaceholder={(id) => `[Pasted block #${id}]`} // Optional formatter
80
+ formatPastePlaceholder={(n) => `[Pasted block #${n}]`} // Optional formatter
81
81
  />
82
82
  ```
83
83
 
@@ -1,25 +1,15 @@
1
- import type { PlaceholderInfo } from './types.js';
2
- export type AtomicBlock = {
3
- kind: 'sentinel';
1
+ import type { BlockEntry, BlockKind } from './BlockTypes.js';
2
+ export interface AtomicBlock {
3
+ kind: BlockKind;
4
4
  id: string;
5
- displayNumber: number;
6
5
  start: number;
7
6
  end: number;
8
7
  displayWidth: number;
9
8
  displayText: string;
10
- } | {
11
- kind: 'placeholder';
12
- id: number;
13
- start: number;
14
- end: number;
15
- displayWidth: number;
16
- displayText: string;
17
- };
18
- export type Placeholders = Map<number, PlaceholderInfo> | undefined;
19
- export declare function findAtomicBlocks(line: string, placeholders?: Placeholders): AtomicBlock[];
20
- /** Block that strictly contains offset (offset > start && offset < end) — cursor is in the interior. */
21
- export declare function findAtomicBlockSpanning(line: string, offset: number, placeholders?: Placeholders): AtomicBlock | null;
22
- /** Block whose end === offset (the block immediately to the left of the cursor). */
23
- export declare function findAtomicBlockBefore(line: string, offset: number, placeholders?: Placeholders): AtomicBlock | null;
24
- /** Block whose start === offset (the block immediately to the right of the cursor). */
25
- export declare function findAtomicBlockAfter(line: string, offset: number, placeholders?: Placeholders): AtomicBlock | null;
9
+ dim: boolean;
10
+ }
11
+ export type BlockEntries = Map<string, BlockEntry> | undefined;
12
+ export declare function findAtomicBlocks(line: string, entries?: BlockEntries): AtomicBlock[];
13
+ export declare function findAtomicBlockSpanning(line: string, offset: number, entries?: BlockEntries): AtomicBlock | null;
14
+ export declare function findAtomicBlockBefore(line: string, offset: number, entries?: BlockEntries): AtomicBlock | null;
15
+ export declare function findAtomicBlockAfter(line: string, offset: number, entries?: BlockEntries): AtomicBlock | null;
@@ -1,58 +1,49 @@
1
- import { parseSentinels, getPlaceholderText } from './ImageSentinel.js';
2
- import { MARKER_REGEX } from './Placeholder.js';
3
- export function findAtomicBlocks(line, placeholders) {
4
- const blocks = [];
5
- for (const s of parseSentinels(line)) {
6
- const displayText = getPlaceholderText(s.displayNumber);
7
- blocks.push({
8
- kind: 'sentinel',
9
- id: s.id,
10
- displayNumber: s.displayNumber,
11
- start: s.start,
12
- end: s.end,
13
- displayWidth: displayText.length,
14
- displayText,
15
- });
16
- }
17
- if (placeholders && placeholders.size > 0) {
18
- MARKER_REGEX.lastIndex = 0;
19
- let m;
20
- while ((m = MARKER_REGEX.exec(line)) !== null) {
21
- const id = Number(m[1]);
22
- const info = placeholders.get(id);
23
- const displayText = info ? info.displayText : '';
24
- blocks.push({
25
- kind: 'placeholder',
26
- id,
27
- start: m.index,
28
- end: m.index + m[0].length,
29
- displayWidth: displayText.length,
30
- displayText,
31
- });
1
+ import { parseBlockMarkers } from './BlockMarker.js';
2
+ function getDisplayInfo(marker, entries) {
3
+ if (entries) {
4
+ const entry = entries.get(marker.id);
5
+ if (entry && entry.kind === 'paste') {
6
+ return { displayWidth: entry.displayText.length, displayText: entry.displayText };
32
7
  }
33
8
  }
34
- blocks.sort((a, b) => a.start - b.start);
35
- return blocks;
9
+ if (marker.kind === 'p') {
10
+ const text = `[Paste text #${marker.displayNumber}]`;
11
+ return { displayWidth: text.length, displayText: text };
12
+ }
13
+ const text = `[Pasted Image #${marker.displayNumber}]`;
14
+ return { displayWidth: text.length, displayText: text };
15
+ }
16
+ export function findAtomicBlocks(line, entries) {
17
+ const markers = parseBlockMarkers(line);
18
+ return markers.map((m) => {
19
+ const { displayWidth, displayText } = getDisplayInfo(m, entries);
20
+ return {
21
+ kind: m.kind === 'p' ? 'paste' : 'image',
22
+ id: m.id,
23
+ start: m.start,
24
+ end: m.end,
25
+ displayWidth,
26
+ displayText,
27
+ dim: m.kind === 'i',
28
+ };
29
+ });
36
30
  }
37
- /** Block that strictly contains offset (offset > start && offset < end) — cursor is in the interior. */
38
- export function findAtomicBlockSpanning(line, offset, placeholders) {
39
- for (const b of findAtomicBlocks(line, placeholders)) {
31
+ export function findAtomicBlockSpanning(line, offset, entries) {
32
+ for (const b of findAtomicBlocks(line, entries)) {
40
33
  if (offset > b.start && offset < b.end)
41
34
  return b;
42
35
  }
43
36
  return null;
44
37
  }
45
- /** Block whose end === offset (the block immediately to the left of the cursor). */
46
- export function findAtomicBlockBefore(line, offset, placeholders) {
47
- for (const b of findAtomicBlocks(line, placeholders)) {
38
+ export function findAtomicBlockBefore(line, offset, entries) {
39
+ for (const b of findAtomicBlocks(line, entries)) {
48
40
  if (b.end === offset)
49
41
  return b;
50
42
  }
51
43
  return null;
52
44
  }
53
- /** Block whose start === offset (the block immediately to the right of the cursor). */
54
- export function findAtomicBlockAfter(line, offset, placeholders) {
55
- for (const b of findAtomicBlocks(line, placeholders)) {
45
+ export function findAtomicBlockAfter(line, offset, entries) {
46
+ for (const b of findAtomicBlocks(line, entries)) {
56
47
  if (b.start === offset)
57
48
  return b;
58
49
  }
@@ -0,0 +1,19 @@
1
+ export declare const BLOCK_OPEN = "\uE000";
2
+ export declare const BLOCK_CLOSE = "\uE001";
3
+ export type BlockMarkerKind = 'p' | 'i';
4
+ export interface BlockMarkerInfo {
5
+ kind: BlockMarkerKind;
6
+ id: string;
7
+ displayNumber: number;
8
+ start: number;
9
+ end: number;
10
+ }
11
+ export declare function generateBlockId(): string;
12
+ export declare function createBlockMarker(kind: BlockMarkerKind, id: string, displayNumber: number): string;
13
+ export declare function parseBlockMarkers(text: string): BlockMarkerInfo[];
14
+ export declare function findBlockMarkerAt(text: string, offset: number): BlockMarkerInfo | null;
15
+ export declare function findBlockMarkerBefore(text: string, offset: number): BlockMarkerInfo | null;
16
+ export declare function findBlockMarkerAfter(text: string, offset: number): BlockMarkerInfo | null;
17
+ export declare function removeBlockMarker(text: string, offset: number): string;
18
+ export declare function blockMarkerVisualWidth(displayNumber: number): number;
19
+ export declare function getBlockPlaceholderText(kind: BlockMarkerKind, displayNumber: number): string;
@@ -0,0 +1,83 @@
1
+ export const BLOCK_OPEN = '\uE000';
2
+ export const BLOCK_CLOSE = '\uE001';
3
+ export function generateBlockId() {
4
+ return Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 6);
5
+ }
6
+ export function createBlockMarker(kind, id, displayNumber) {
7
+ return `${BLOCK_OPEN}${kind}:${id}:${displayNumber}${BLOCK_CLOSE}`;
8
+ }
9
+ export function parseBlockMarkers(text) {
10
+ const result = [];
11
+ let i = 0;
12
+ while (i < text.length) {
13
+ const openIdx = text.indexOf(BLOCK_OPEN, i);
14
+ if (openIdx === -1)
15
+ break;
16
+ const closeIdx = text.indexOf(BLOCK_CLOSE, openIdx + 1);
17
+ if (closeIdx === -1)
18
+ break;
19
+ const raw = text.substring(openIdx + 1, closeIdx);
20
+ const kindChar = raw[0];
21
+ if (kindChar !== 'p' && kindChar !== 'i') {
22
+ i = closeIdx + 1;
23
+ continue;
24
+ }
25
+ const rest = raw.substring(2);
26
+ const colonIdx = rest.lastIndexOf(':');
27
+ if (colonIdx === -1) {
28
+ i = closeIdx + 1;
29
+ continue;
30
+ }
31
+ const id = rest.substring(0, colonIdx);
32
+ const displayNumber = parseInt(rest.substring(colonIdx + 1), 10);
33
+ if (isNaN(displayNumber)) {
34
+ i = closeIdx + 1;
35
+ continue;
36
+ }
37
+ result.push({
38
+ kind: kindChar,
39
+ id,
40
+ displayNumber,
41
+ start: openIdx,
42
+ end: closeIdx + 1,
43
+ });
44
+ i = closeIdx + 1;
45
+ }
46
+ return result;
47
+ }
48
+ export function findBlockMarkerAt(text, offset) {
49
+ for (const m of parseBlockMarkers(text)) {
50
+ if (offset > m.start && offset < m.end)
51
+ return m;
52
+ }
53
+ return null;
54
+ }
55
+ export function findBlockMarkerBefore(text, offset) {
56
+ for (const m of parseBlockMarkers(text)) {
57
+ if (m.end === offset)
58
+ return m;
59
+ }
60
+ return null;
61
+ }
62
+ export function findBlockMarkerAfter(text, offset) {
63
+ for (const m of parseBlockMarkers(text)) {
64
+ if (m.start === offset)
65
+ return m;
66
+ }
67
+ return null;
68
+ }
69
+ export function removeBlockMarker(text, offset) {
70
+ const marker = findBlockMarkerAt(text, offset);
71
+ if (!marker)
72
+ return text;
73
+ return text.slice(0, marker.start) + text.slice(marker.end);
74
+ }
75
+ export function blockMarkerVisualWidth(displayNumber) {
76
+ return getBlockPlaceholderText('i', displayNumber).length;
77
+ }
78
+ export function getBlockPlaceholderText(kind, displayNumber) {
79
+ if (kind === 'i') {
80
+ return `[Pasted Image #${displayNumber}]`;
81
+ }
82
+ return `[Paste text #${displayNumber}]`;
83
+ }
@@ -0,0 +1,39 @@
1
+ import type { BlockEntry, BlockState } from './BlockTypes.js';
2
+ import { type BlockMarkerKind } from './BlockMarker.js';
3
+ import type { ImageRef } from './ImageTypes.js';
4
+ export declare function createBlockState(): BlockState;
5
+ export declare function createPasteBlockEntry(state: BlockState, originalText: string, displayText: string): {
6
+ id: string;
7
+ marker: string;
8
+ state: BlockState;
9
+ };
10
+ export declare function createImageBlockEntry(state: BlockState, imageRef: ImageRef, id?: string): {
11
+ id: string;
12
+ marker: string;
13
+ state: BlockState;
14
+ };
15
+ export declare function removeBlock(state: BlockState, id: string): BlockState;
16
+ export declare function getBlock(state: BlockState, id: string): BlockEntry | undefined;
17
+ export declare function getDisplayLine(line: string, entries: Map<string, BlockEntry>): string;
18
+ export declare function getValue(lines: string[], entries: Map<string, BlockEntry>): string;
19
+ export declare function bufferColToDisplayCol(line: string, column: number, entries: Map<string, BlockEntry>): number;
20
+ export declare function displayColToBufferCol(line: string, displayColumn: number, entries: Map<string, BlockEntry>): number;
21
+ export declare function getExpandedLineLength(line: string, entries: Map<string, BlockEntry>): number;
22
+ export declare function getValueCursorOffset(lines: string[], cursor: {
23
+ line: number;
24
+ column: number;
25
+ }, entries: Map<string, BlockEntry>): number;
26
+ export declare function getCursorFromValueOffset(lines: string[], offset: number, entries: Map<string, BlockEntry>): {
27
+ line: number;
28
+ column: number;
29
+ };
30
+ export declare function getDisplayWidthForMarker(m: {
31
+ kind: BlockMarkerKind;
32
+ id: string;
33
+ displayNumber: number;
34
+ }, entries: Map<string, BlockEntry>): number;
35
+ export declare function getDisplayTextForMarker(m: {
36
+ kind: BlockMarkerKind;
37
+ id: string;
38
+ displayNumber: number;
39
+ }, entries: Map<string, BlockEntry>): string;
@@ -0,0 +1,236 @@
1
+ import { createBlockMarker, generateBlockId, parseBlockMarkers, } from './BlockMarker.js';
2
+ export function createBlockState() {
3
+ return { entries: new Map(), nextPasteNumber: 1, nextImageNumber: 1 };
4
+ }
5
+ export function createPasteBlockEntry(state, originalText, displayText) {
6
+ const id = generateBlockId();
7
+ const displayNumber = state.nextPasteNumber;
8
+ const marker = createBlockMarker('p', id, displayNumber);
9
+ const newEntries = new Map(state.entries);
10
+ newEntries.set(id, { kind: 'paste', id, displayNumber, originalText, displayText });
11
+ return {
12
+ id,
13
+ marker,
14
+ state: { entries: newEntries, nextPasteNumber: displayNumber + 1, nextImageNumber: state.nextImageNumber },
15
+ };
16
+ }
17
+ export function createImageBlockEntry(state, imageRef, id) {
18
+ const blockId = id || imageRef.id;
19
+ const displayNumber = imageRef.displayNumber;
20
+ const marker = createBlockMarker('i', blockId, displayNumber);
21
+ const newEntries = new Map(state.entries);
22
+ newEntries.set(blockId, {
23
+ kind: 'image',
24
+ id: blockId,
25
+ displayNumber,
26
+ data: imageRef.data,
27
+ mimeType: imageRef.mimeType,
28
+ byteSize: imageRef.byteSize,
29
+ });
30
+ return {
31
+ id: blockId,
32
+ marker,
33
+ state: {
34
+ entries: newEntries,
35
+ nextPasteNumber: state.nextPasteNumber,
36
+ nextImageNumber: Math.max(state.nextImageNumber, displayNumber + 1),
37
+ },
38
+ };
39
+ }
40
+ export function removeBlock(state, id) {
41
+ const newEntries = new Map(state.entries);
42
+ newEntries.delete(id);
43
+ return { ...state, entries: newEntries };
44
+ }
45
+ export function getBlock(state, id) {
46
+ return state.entries.get(id);
47
+ }
48
+ export function getDisplayLine(line, entries) {
49
+ const markers = parseBlockMarkers(line);
50
+ if (markers.length === 0)
51
+ return line;
52
+ let result = '';
53
+ let lastEnd = 0;
54
+ for (const m of markers) {
55
+ result += line.slice(lastEnd, m.start);
56
+ const entry = entries.get(m.id);
57
+ if (entry && entry.kind === 'paste') {
58
+ result += entry.displayText;
59
+ }
60
+ else {
61
+ result += m.kind === 'i' ? `[Pasted Image #${m.displayNumber}]` : `[Paste text #${m.displayNumber}]`;
62
+ }
63
+ lastEnd = m.end;
64
+ }
65
+ result += line.slice(lastEnd);
66
+ return result;
67
+ }
68
+ export function getValue(lines, entries) {
69
+ return lines.map((line) => {
70
+ const markers = parseBlockMarkers(line);
71
+ if (markers.length === 0)
72
+ return line;
73
+ let result = '';
74
+ let lastEnd = 0;
75
+ for (const m of markers) {
76
+ result += line.slice(lastEnd, m.start);
77
+ const entry = entries.get(m.id);
78
+ if (entry && entry.kind === 'paste') {
79
+ result += entry.originalText;
80
+ }
81
+ else {
82
+ result += line.slice(m.start, m.end);
83
+ }
84
+ lastEnd = m.end;
85
+ }
86
+ result += line.slice(lastEnd);
87
+ return result;
88
+ }).join('\n');
89
+ }
90
+ export function bufferColToDisplayCol(line, column, entries) {
91
+ if (entries.size === 0)
92
+ return column;
93
+ let displayCol = 0;
94
+ let lastEnd = 0;
95
+ const markers = parseBlockMarkers(line);
96
+ for (const m of markers) {
97
+ if (column <= m.start) {
98
+ return displayCol + (column - lastEnd);
99
+ }
100
+ displayCol += m.start - lastEnd;
101
+ const entry = entries.get(m.id);
102
+ const displayLen = entry && entry.kind === 'paste' ? entry.displayText.length : `[Pasted Image #${m.displayNumber}]`.length;
103
+ if (column <= m.end) {
104
+ return displayCol + displayLen;
105
+ }
106
+ displayCol += displayLen;
107
+ lastEnd = m.end;
108
+ }
109
+ return displayCol + (column - lastEnd);
110
+ }
111
+ export function displayColToBufferCol(line, displayColumn, entries) {
112
+ if (entries.size === 0)
113
+ return displayColumn;
114
+ let bufPos = 0;
115
+ let dispPos = 0;
116
+ const markers = parseBlockMarkers(line);
117
+ for (const m of markers) {
118
+ const textLen = m.start - bufPos;
119
+ if (displayColumn <= dispPos + textLen) {
120
+ return bufPos + (displayColumn - dispPos);
121
+ }
122
+ dispPos += textLen;
123
+ bufPos = m.start;
124
+ const entry = entries.get(m.id);
125
+ const displayLen = entry && entry.kind === 'paste' ? entry.displayText.length : `[Pasted Image #${m.displayNumber}]`.length;
126
+ if (displayColumn <= dispPos + displayLen) {
127
+ return m.end;
128
+ }
129
+ dispPos += displayLen;
130
+ bufPos = m.end;
131
+ }
132
+ return bufPos + (displayColumn - dispPos);
133
+ }
134
+ export function getExpandedLineLength(line, entries) {
135
+ let len = 0;
136
+ let lastEnd = 0;
137
+ const markers = parseBlockMarkers(line);
138
+ for (const m of markers) {
139
+ len += m.start - lastEnd;
140
+ const entry = entries.get(m.id);
141
+ if (entry && entry.kind === 'paste') {
142
+ len += entry.originalText.length;
143
+ }
144
+ else {
145
+ len += m.end - m.start;
146
+ }
147
+ lastEnd = m.end;
148
+ }
149
+ len += line.length - lastEnd;
150
+ return len;
151
+ }
152
+ export function getValueCursorOffset(lines, cursor, entries) {
153
+ let offset = 0;
154
+ for (let i = 0; i < cursor.line; i++) {
155
+ offset += getExpandedLineLength(lines[i], entries) + 1;
156
+ }
157
+ const line = lines[cursor.line];
158
+ let bufPos = 0;
159
+ const markers = parseBlockMarkers(line);
160
+ for (const m of markers) {
161
+ if (cursor.column <= m.start) {
162
+ offset += cursor.column - bufPos;
163
+ return offset;
164
+ }
165
+ offset += m.start - bufPos;
166
+ const entry = entries.get(m.id);
167
+ if (entry && entry.kind === 'paste') {
168
+ offset += entry.originalText.length;
169
+ }
170
+ else {
171
+ offset += m.end - m.start;
172
+ }
173
+ bufPos = m.end;
174
+ if (cursor.column <= m.end) {
175
+ return offset;
176
+ }
177
+ }
178
+ offset += cursor.column - bufPos;
179
+ return offset;
180
+ }
181
+ export function getCursorFromValueOffset(lines, offset, entries) {
182
+ let currentOffset = 0;
183
+ const lineCount = lines.length;
184
+ for (let i = 0; i < lineCount; i++) {
185
+ const lineLen = getExpandedLineLength(lines[i], entries);
186
+ if (i === lineCount - 1) {
187
+ if (offset <= currentOffset + lineLen) {
188
+ const colInExpanded = offset - currentOffset;
189
+ return { line: i, column: valueOffsetToBufferColumn(lines[i], colInExpanded, entries) };
190
+ }
191
+ return { line: i, column: lines[i].length };
192
+ }
193
+ if (offset <= currentOffset + lineLen) {
194
+ const colInExpanded = offset - currentOffset;
195
+ return { line: i, column: valueOffsetToBufferColumn(lines[i], colInExpanded, entries) };
196
+ }
197
+ currentOffset += lineLen + 1;
198
+ }
199
+ const lastIdx = lines.length - 1;
200
+ return { line: lastIdx, column: lines[lastIdx].length };
201
+ }
202
+ function valueOffsetToBufferColumn(line, offsetInExpanded, entries) {
203
+ let bufPos = 0;
204
+ let expandedPos = 0;
205
+ const markers = parseBlockMarkers(line);
206
+ for (const m of markers) {
207
+ const textLen = m.start - bufPos;
208
+ if (offsetInExpanded <= expandedPos + textLen) {
209
+ return bufPos + (offsetInExpanded - expandedPos);
210
+ }
211
+ expandedPos += textLen;
212
+ bufPos = m.start;
213
+ const entry = entries.get(m.id);
214
+ const originalLen = entry && entry.kind === 'paste' ? entry.originalText.length : (m.end - m.start);
215
+ if (offsetInExpanded <= expandedPos + originalLen) {
216
+ return m.end;
217
+ }
218
+ expandedPos += originalLen;
219
+ bufPos = m.end;
220
+ }
221
+ return bufPos + (offsetInExpanded - expandedPos);
222
+ }
223
+ export function getDisplayWidthForMarker(m, entries) {
224
+ const entry = entries.get(m.id);
225
+ if (entry && entry.kind === 'paste') {
226
+ return entry.displayText.length;
227
+ }
228
+ return `[Pasted Image #${m.displayNumber}]`.length;
229
+ }
230
+ export function getDisplayTextForMarker(m, entries) {
231
+ const entry = entries.get(m.id);
232
+ if (entry && entry.kind === 'paste') {
233
+ return entry.displayText;
234
+ }
235
+ return `[Pasted Image #${m.displayNumber}]`;
236
+ }
@@ -0,0 +1,22 @@
1
+ export type BlockKind = 'paste' | 'image';
2
+ export interface PasteBlockEntry {
3
+ kind: 'paste';
4
+ id: string;
5
+ displayNumber: number;
6
+ originalText: string;
7
+ displayText: string;
8
+ }
9
+ export interface ImageBlockEntry {
10
+ kind: 'image';
11
+ id: string;
12
+ displayNumber: number;
13
+ data: string;
14
+ mimeType: string;
15
+ byteSize: number;
16
+ }
17
+ export type BlockEntry = PasteBlockEntry | ImageBlockEntry;
18
+ export interface BlockState {
19
+ entries: Map<string, BlockEntry>;
20
+ nextPasteNumber: number;
21
+ nextImageNumber: number;
22
+ }
@@ -6,5 +6,3 @@ export interface ImageRef {
6
6
  displayNumber: number;
7
7
  }
8
8
  export type PasteErrorReason = 'clipboard-timeout' | 'clipboard-read-error' | 'clipboard-unsupported-type' | 'image-too-large' | 'too-many-images' | 'clipboard-empty';
9
- export declare const SENTINEL_OPEN = "\uE000";
10
- export declare const SENTINEL_CLOSE = "\uE001";
@@ -1,2 +1 @@
1
- export const SENTINEL_OPEN = '\uE000';
2
- export const SENTINEL_CLOSE = '\uE001';
1
+ export {};
@@ -1,4 +1,4 @@
1
- import { generateImageId } from './ImageSentinel.js';
1
+ import { generateBlockId as generateImageId } from './BlockMarker.js';
2
2
  const MAGIC_BYTES = [
3
3
  { mimeType: 'image/png', magic: [0x89, 0x50, 0x4e, 0x47] },
4
4
  { mimeType: 'image/jpeg', magic: [0xff, 0xd8, 0xff] },
@@ -1,6 +1,6 @@
1
1
  import { type Key, type Buffer, type Cursor } from './types.js';
2
2
  import { type UseTextInputResult } from './useTextInput.js';
3
- export interface KeyHandlerActions extends Omit<UseTextInputResult, 'value' | 'cursor' | 'cursorOffset' | 'setCursorOffset' | 'buffer' | 'placeholderState' | 'insertImage' | 'images' | 'getImages' | 'setImages'> {
3
+ export interface KeyHandlerActions extends Omit<UseTextInputResult, 'value' | 'cursor' | 'cursorOffset' | 'setCursorOffset' | 'buffer' | 'blockState' | 'insertImage' | 'images' | 'getImages' | 'setImages'> {
4
4
  submit: () => void;
5
5
  onBoundaryArrow?: (direction: 'up' | 'down' | 'left' | 'right') => void;
6
6
  paste?: () => void;
@@ -125,6 +125,15 @@ const BACKSPACE_SEQUENCES = ['\u0008', '\u007f'];
125
125
  function isBackspaceSequence(seq) {
126
126
  return !!seq && BACKSPACE_SEQUENCES.includes(seq);
127
127
  }
128
+ /**
129
+ * Raw sequences that represent Shift+Enter across terminal emulators.
130
+ * - `\x1b\r` / `\x1b\n`: ESC + CR/LF emitted by terminals like iTerm2 / WezTerm when configured.
131
+ * - `\x1b[13;2u`: kitty keyboard protocol encoding for Shift+Enter.
132
+ */
133
+ const SHIFT_ENTER_SEQUENCES = ['\x1b\r', '\x1b\n', '\x1b[13;2u'];
134
+ function isShiftEnterSequence(seq) {
135
+ return !!seq && SHIFT_ENTER_SEQUENCES.includes(seq);
136
+ }
128
137
  /**
129
138
  * Handles keyboard input and maps it to text input actions.
130
139
  *
@@ -223,6 +232,15 @@ export function handleKey(key, input, buffer, actions, cursor, rawInput, width)
223
232
  actions.deleteForward();
224
233
  return;
225
234
  }
235
+ // Shift+Enter inserts a newline regardless of buffer state.
236
+ // Detected via Ink's shift+return flags or raw escape sequences emitted by
237
+ // terminals that distinguish Shift+Enter from Enter.
238
+ if ((key.shift && key.return) ||
239
+ (key.meta && key.return) ||
240
+ isShiftEnterSequence(rawInput)) {
241
+ actions.newLine();
242
+ return;
243
+ }
226
244
  // Submission / New Line
227
245
  if (key.return) {
228
246
  log(`[KEYHANDLER] return key, cursor=${JSON.stringify(cursor)}, currentLine="${(cursor ? buffer.lines[cursor.line || 0] : 'no cursor').replace(/[\x00-\x1F\x7F-\uFFFF]/g, c => `\\x${c.charCodeAt(0).toString(16)}`)}" endsWithBackslash=${cursor ? buffer.lines[cursor.line || 0].endsWith('\\') : false}`);