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
@@ -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
+ });
@@ -0,0 +1,225 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createBlockState, createPasteBlockEntry, createImageBlockEntry, removeBlock, getBlock, getDisplayLine, getValue, bufferColToDisplayCol, displayColToBufferCol, getValueCursorOffset, getCursorFromValueOffset, } from '../BlockRegistry.js';
3
+ import { parseBlockMarkers } from '../BlockMarker.js';
4
+ function makeImageRef(overrides = {}) {
5
+ return {
6
+ id: 'img1',
7
+ data: '',
8
+ mimeType: 'image/png',
9
+ byteSize: 100,
10
+ displayNumber: 1,
11
+ ...overrides,
12
+ };
13
+ }
14
+ describe('createBlockState', () => {
15
+ it('creates empty state with counters at 1', () => {
16
+ const state = createBlockState();
17
+ expect(state.entries.size).toBe(0);
18
+ expect(state.nextPasteNumber).toBe(1);
19
+ expect(state.nextImageNumber).toBe(1);
20
+ });
21
+ });
22
+ describe('createPasteBlockEntry', () => {
23
+ it('adds paste block and returns marker', () => {
24
+ const state = createBlockState();
25
+ const { id, marker, state: newState } = createPasteBlockEntry(state, 'original text', '[Paste text #1]');
26
+ expect(id).toBeTruthy();
27
+ expect(marker).toContain('p:');
28
+ expect(marker).toContain(':1');
29
+ expect(newState.entries.size).toBe(1);
30
+ expect(newState.nextPasteNumber).toBe(2);
31
+ expect(newState.nextImageNumber).toBe(1);
32
+ const entry = newState.entries.get(id);
33
+ expect(entry.kind).toBe('paste');
34
+ if (entry.kind === 'paste') {
35
+ expect(entry.originalText).toBe('original text');
36
+ expect(entry.displayText).toBe('[Paste text #1]');
37
+ expect(entry.displayNumber).toBe(1);
38
+ }
39
+ });
40
+ it('increments paste counter', () => {
41
+ const state = createBlockState();
42
+ const { state: s1 } = createPasteBlockEntry(state, 'a', '[#1]');
43
+ const { state: s2 } = createPasteBlockEntry(s1, 'b', '[#2]');
44
+ expect(s2.nextPasteNumber).toBe(3);
45
+ expect(s2.nextImageNumber).toBe(1);
46
+ expect(s2.entries.size).toBe(2);
47
+ });
48
+ });
49
+ describe('createImageBlockEntry', () => {
50
+ it('adds image block and returns marker', () => {
51
+ const state = createBlockState();
52
+ const imgRef = makeImageRef();
53
+ const { id, marker, state: newState } = createImageBlockEntry(state, imgRef);
54
+ expect(id).toBe('img1');
55
+ expect(marker).toContain('i:');
56
+ expect(marker).toContain(':1');
57
+ expect(newState.entries.size).toBe(1);
58
+ expect(newState.nextImageNumber).toBe(2);
59
+ expect(newState.nextPasteNumber).toBe(1);
60
+ const entry = newState.entries.get(id);
61
+ expect(entry.kind).toBe('image');
62
+ if (entry.kind === 'image') {
63
+ expect(entry.data).toBe('');
64
+ expect(entry.mimeType).toBe('image/png');
65
+ expect(entry.byteSize).toBe(100);
66
+ expect(entry.displayNumber).toBe(1);
67
+ }
68
+ });
69
+ it('increments image counter', () => {
70
+ const state = createBlockState();
71
+ const { state: s1 } = createImageBlockEntry(state, makeImageRef({ id: 'a' }));
72
+ const { state: s2 } = createImageBlockEntry(s1, makeImageRef({ id: 'b', displayNumber: 2 }));
73
+ expect(s2.nextImageNumber).toBe(3);
74
+ expect(s2.nextPasteNumber).toBe(1);
75
+ expect(s2.entries.size).toBe(2);
76
+ });
77
+ it('uses image ref displayNumber for marker and counter', () => {
78
+ const state = createBlockState();
79
+ const { marker, state: newState } = createImageBlockEntry(state, makeImageRef({ id: 'img5', displayNumber: 5 }));
80
+ expect(marker).toContain(':5');
81
+ expect(newState.nextImageNumber).toBe(6);
82
+ const entry = newState.entries.get('img5');
83
+ expect(entry.displayNumber).toBe(5);
84
+ });
85
+ it('paste and image counters are independent', () => {
86
+ const state = createBlockState();
87
+ const { state: s1 } = createPasteBlockEntry(state, 'text', '[Paste text #1]');
88
+ const { state: s2 } = createImageBlockEntry(s1, makeImageRef());
89
+ expect(s2.nextPasteNumber).toBe(2);
90
+ expect(s2.nextImageNumber).toBe(2);
91
+ });
92
+ });
93
+ describe('removeBlock', () => {
94
+ it('removes block by id', () => {
95
+ const state = createBlockState();
96
+ const { id, state: s1 } = createPasteBlockEntry(state, 'a', '[#1]');
97
+ expect(s1.entries.size).toBe(1);
98
+ const s2 = removeBlock(s1, id);
99
+ expect(s2.entries.size).toBe(0);
100
+ });
101
+ it('does nothing for non-existent id', () => {
102
+ const state = createBlockState();
103
+ const s1 = removeBlock(state, 'nonexistent');
104
+ expect(s1.entries.size).toBe(0);
105
+ });
106
+ });
107
+ describe('getBlock', () => {
108
+ it('returns entry by id', () => {
109
+ const state = createBlockState();
110
+ const { id, state: s1 } = createPasteBlockEntry(state, 'a', '[#1]');
111
+ const entry = getBlock(s1, id);
112
+ expect(entry).toBeDefined();
113
+ expect(entry.kind).toBe('paste');
114
+ });
115
+ it('returns undefined for non-existent id', () => {
116
+ const state = createBlockState();
117
+ expect(getBlock(state, 'nope')).toBeUndefined();
118
+ });
119
+ });
120
+ describe('getDisplayLine', () => {
121
+ it('expands paste marker to display text', () => {
122
+ const state = createBlockState();
123
+ const { marker, state: s1 } = createPasteBlockEntry(state, 'original', '[Custom #1]');
124
+ const line = `Hello ${marker} world`;
125
+ expect(getDisplayLine(line, s1.entries)).toBe('Hello [Custom #1] world');
126
+ });
127
+ it('expands image marker to display text', () => {
128
+ const state = createBlockState();
129
+ const { marker, state: s1 } = createImageBlockEntry(state, makeImageRef());
130
+ const line = `a${marker}b`;
131
+ expect(getDisplayLine(line, s1.entries)).toBe('a[Pasted Image #1]b');
132
+ });
133
+ it('returns line unchanged if no markers', () => {
134
+ const state = createBlockState();
135
+ expect(getDisplayLine('plain text', state.entries)).toBe('plain text');
136
+ });
137
+ });
138
+ describe('getValue', () => {
139
+ it('expands paste markers to original text', () => {
140
+ const state = createBlockState();
141
+ const { marker, state: s1 } = createPasteBlockEntry(state, 'long original text', '[#1]');
142
+ const lines = [`Hello ${marker} world`];
143
+ expect(getValue(lines, s1.entries)).toBe('Hello long original text world');
144
+ });
145
+ it('leaves image markers as raw markers', () => {
146
+ const state = createBlockState();
147
+ const { marker, state: s1 } = createImageBlockEntry(state, makeImageRef());
148
+ const lines = [`a${marker}b`];
149
+ expect(getValue(lines, s1.entries)).toBe(`a${marker}b`);
150
+ });
151
+ it('handles multiple lines', () => {
152
+ const state = createBlockState();
153
+ const { marker, state: s1 } = createPasteBlockEntry(state, 'long', '[#1]');
154
+ const lines = [`line1 ${marker}`, 'line2'];
155
+ expect(getValue(lines, s1.entries)).toBe('line1 long\nline2');
156
+ });
157
+ it('returns original lines if no entries', () => {
158
+ const state = createBlockState();
159
+ expect(getValue(['hello'], state.entries)).toBe('hello');
160
+ });
161
+ });
162
+ describe('bufferColToDisplayCol', () => {
163
+ it('handles plain text without markers', () => {
164
+ const state = createBlockState();
165
+ expect(bufferColToDisplayCol('hello', 3, state.entries)).toBe(3);
166
+ });
167
+ it('maps column before marker', () => {
168
+ const state = createBlockState();
169
+ const { marker, state: s1 } = createPasteBlockEntry(state, 'x', '[Display]');
170
+ const line = `ab${marker}cd`;
171
+ expect(bufferColToDisplayCol(line, 0, s1.entries)).toBe(0);
172
+ expect(bufferColToDisplayCol(line, 1, s1.entries)).toBe(1);
173
+ expect(bufferColToDisplayCol(line, 2, s1.entries)).toBe(2);
174
+ });
175
+ it('maps column within paste marker to display length', () => {
176
+ const state = createBlockState();
177
+ const { marker, state: s1 } = createPasteBlockEntry(state, 'x', '[Display]');
178
+ const line = `ab${marker}cd`;
179
+ // marker is at buffer positions 2-~15
180
+ const markers = parseBlockMarkers(line);
181
+ const m = markers[0];
182
+ // Any column within the marker should map to display col = 2 + 9 = 11
183
+ expect(bufferColToDisplayCol(line, m.start + 1, s1.entries)).toBe(11);
184
+ expect(bufferColToDisplayCol(line, m.end - 1, s1.entries)).toBe(11);
185
+ });
186
+ });
187
+ describe('getValueCursorOffset', () => {
188
+ it('computes offset in expanded value', () => {
189
+ const state = createBlockState();
190
+ const { marker, state: s1 } = createPasteBlockEntry(state, 'hello', '[#1]');
191
+ const lines = [`${marker} world`];
192
+ // marker expands to 5 chars, so cursor at buffer position 0 maps to value position 0
193
+ expect(getValueCursorOffset(lines, { line: 0, column: 0 }, s1.entries)).toBe(0);
194
+ // Cursor after marker in buffer maps to value position after original text
195
+ const markers = parseBlockMarkers(marker);
196
+ expect(getValueCursorOffset(lines, { line: 0, column: markers[0].end }, s1.entries)).toBe(5);
197
+ expect(getValueCursorOffset(lines, { line: 0, column: markers[0].end + 1 }, s1.entries)).toBe(6);
198
+ });
199
+ });
200
+ describe('getCursorFromValueOffset', () => {
201
+ it('converts value offset back to buffer cursor', () => {
202
+ const state = createBlockState();
203
+ const { marker, state: s1 } = createPasteBlockEntry(state, 'hello', '[#1]');
204
+ const lines = [`${marker} world`];
205
+ const markers = parseBlockMarkers(marker);
206
+ const cursor = getCursorFromValueOffset(lines, 5, s1.entries);
207
+ expect(cursor.line).toBe(0);
208
+ expect(cursor.column).toBe(markers[0].end);
209
+ });
210
+ });
211
+ describe('displayColToBufferCol', () => {
212
+ it('handles plain text without markers', () => {
213
+ const state = createBlockState();
214
+ expect(displayColToBufferCol('hello', 3, state.entries)).toBe(3);
215
+ });
216
+ it('maps display column past marker to buffer column past marker', () => {
217
+ const state = createBlockState();
218
+ const { marker, state: s1 } = createPasteBlockEntry(state, 'x', '[Display]');
219
+ const line = `ab${marker}cd`;
220
+ const markers = parseBlockMarkers(line);
221
+ const displayLen = '[Display]'.length; // 9
222
+ // display position after marker: 2 + 9 = 11 → buffer after marker
223
+ expect(displayColToBufferCol(line, 2 + displayLen, s1.entries)).toBe(markers[0].end);
224
+ });
225
+ });
@@ -289,6 +289,24 @@ describe('KeyHandler', () => {
289
289
  expect(actions.newLine).not.toHaveBeenCalled();
290
290
  expect(actions.submit).not.toHaveBeenCalled();
291
291
  });
292
+ it('handles Shift+Enter as newline', () => {
293
+ buffer = { lines: ['hello'] };
294
+ handleKey({ shift: true, return: true }, '', buffer, actions);
295
+ expect(actions.newLine).toHaveBeenCalledTimes(1);
296
+ expect(actions.submit).not.toHaveBeenCalled();
297
+ });
298
+ it('handles ESC+CR raw sequence (Shift+Enter) as newline', () => {
299
+ buffer = { lines: ['hello'] };
300
+ handleKey({ meta: true, return: true }, '', buffer, actions, undefined, '\x1b\r');
301
+ expect(actions.newLine).toHaveBeenCalledTimes(1);
302
+ expect(actions.submit).not.toHaveBeenCalled();
303
+ });
304
+ it('handles kitty Shift+Enter CSI u sequence as newline', () => {
305
+ buffer = { lines: ['hello'] };
306
+ handleKey({}, '', buffer, actions, undefined, '\x1b[13;2u');
307
+ expect(actions.newLine).toHaveBeenCalledTimes(1);
308
+ expect(actions.submit).not.toHaveBeenCalled();
309
+ });
292
310
  it('handles Enter as newline if line ends with backslash (multiple lines)', () => {
293
311
  const cursor = { line: 1, column: 7 };
294
312
  buffer = { lines: ['first', 'second\\'] };