ink-prompt 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/MultilineInput/TextBuffer.d.ts +16 -0
- package/dist/components/MultilineInput/TextBuffer.js +90 -35
- package/dist/components/MultilineInput/TextRenderer.d.ts +1 -1
- package/dist/components/MultilineInput/TextRenderer.js +58 -21
- package/dist/components/MultilineInput/__tests__/TextBuffer.test.js +124 -1
- package/dist/components/MultilineInput/__tests__/TextRenderer_enhanced.test.d.ts +1 -0
- package/dist/components/MultilineInput/__tests__/TextRenderer_enhanced.test.js +202 -0
- package/dist/components/MultilineInput/index.js +4 -4
- package/dist/examples/examples/basic.d.ts +1 -0
- package/dist/examples/examples/basic.js +9 -0
- package/dist/examples/src/components/MultilineInput/KeyHandler.d.ts +15 -0
- package/dist/examples/src/components/MultilineInput/KeyHandler.js +97 -0
- package/dist/examples/src/components/MultilineInput/TextBuffer.d.ts +34 -0
- package/dist/examples/src/components/MultilineInput/TextBuffer.js +127 -0
- package/dist/examples/src/components/MultilineInput/TextRenderer.d.ts +24 -0
- package/dist/examples/src/components/MultilineInput/TextRenderer.js +72 -0
- package/dist/examples/src/components/MultilineInput/__tests__/KeyHandler.test.d.ts +1 -0
- package/dist/examples/src/components/MultilineInput/__tests__/KeyHandler.test.js +115 -0
- package/dist/examples/src/components/MultilineInput/__tests__/TextBuffer.test.d.ts +1 -0
- package/dist/examples/src/components/MultilineInput/__tests__/TextBuffer.test.js +254 -0
- package/dist/examples/src/components/MultilineInput/__tests__/TextRenderer.test.d.ts +1 -0
- package/dist/examples/src/components/MultilineInput/__tests__/TextRenderer.test.js +176 -0
- package/dist/examples/src/components/MultilineInput/__tests__/integration.test.d.ts +1 -0
- package/dist/examples/src/components/MultilineInput/__tests__/integration.test.js +71 -0
- package/dist/examples/src/components/MultilineInput/__tests__/useTextInput.test.d.ts +1 -0
- package/dist/examples/src/components/MultilineInput/__tests__/useTextInput.test.js +65 -0
- package/dist/examples/src/components/MultilineInput/index.d.ts +39 -0
- package/dist/examples/src/components/MultilineInput/index.js +82 -0
- package/dist/examples/src/components/MultilineInput/types.d.ts +55 -0
- package/dist/examples/src/components/MultilineInput/types.js +1 -0
- package/dist/examples/src/components/MultilineInput/useTextInput.d.ts +16 -0
- package/dist/examples/src/components/MultilineInput/useTextInput.js +82 -0
- package/dist/examples/src/hello.test.d.ts +1 -0
- package/dist/examples/src/hello.test.js +13 -0
- package/dist/examples/src/index.d.ts +2 -0
- package/dist/examples/src/index.js +2 -0
- package/dist/hooks/__tests__/useTerminalWidth.test.d.ts +1 -0
- package/dist/hooks/__tests__/useTerminalWidth.test.js +60 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useTerminalWidth.d.ts +7 -0
- package/dist/hooks/useTerminalWidth.js +24 -0
- package/package.json +1 -1
|
@@ -32,6 +32,21 @@ export declare function insertNewLine(buffer: Buffer, cursor: Cursor): {
|
|
|
32
32
|
buffer: Buffer;
|
|
33
33
|
cursor: Cursor;
|
|
34
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* Information about a visual row within a wrapped line.
|
|
37
|
+
*/
|
|
38
|
+
interface VisualRowInfo {
|
|
39
|
+
/** Starting offset in the buffer line */
|
|
40
|
+
start: number;
|
|
41
|
+
/** Length of this visual row */
|
|
42
|
+
length: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Break a line into visual rows using word-aware wrapping.
|
|
46
|
+
* Words are kept intact when possible, breaking at spaces.
|
|
47
|
+
* Long words that exceed width are hard-wrapped.
|
|
48
|
+
*/
|
|
49
|
+
export declare function getVisualRows(line: string, width: number): VisualRowInfo[];
|
|
35
50
|
/**
|
|
36
51
|
* Move cursor in specified direction with bounds checking.
|
|
37
52
|
* When width is provided, up/down movement is based on visual lines (accounting for wrapping).
|
|
@@ -50,3 +65,4 @@ export declare function getOffset(buffer: Buffer, cursor: Cursor): number;
|
|
|
50
65
|
* Get the cursor position from a flat offset.
|
|
51
66
|
*/
|
|
52
67
|
export declare function getCursor(buffer: Buffer, offset: number): Cursor;
|
|
68
|
+
export {};
|
|
@@ -119,46 +119,101 @@ export function insertNewLine(buffer, cursor) {
|
|
|
119
119
|
cursor: { line: line + 1, column: 0 },
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Break a line into visual rows using word-aware wrapping.
|
|
124
|
+
* Words are kept intact when possible, breaking at spaces.
|
|
125
|
+
* Long words that exceed width are hard-wrapped.
|
|
126
|
+
*/
|
|
127
|
+
export function getVisualRows(line, width) {
|
|
128
|
+
const safeWidth = Math.max(1, width);
|
|
129
|
+
const rows = [];
|
|
130
|
+
if (line.length === 0) {
|
|
131
|
+
return [{ start: 0, length: 0 }];
|
|
132
|
+
}
|
|
133
|
+
let offset = 0;
|
|
134
|
+
let remaining = line;
|
|
135
|
+
while (remaining.length > 0) {
|
|
136
|
+
let chunkLength = safeWidth;
|
|
137
|
+
if (remaining.length <= safeWidth) {
|
|
138
|
+
chunkLength = remaining.length;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
// Find split point (last space within width)
|
|
142
|
+
let splitIndex = -1;
|
|
143
|
+
for (let i = safeWidth - 1; i >= 0; i--) {
|
|
144
|
+
if (remaining[i] === ' ') {
|
|
145
|
+
splitIndex = i;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (splitIndex !== -1) {
|
|
150
|
+
// Include the space in the chunk
|
|
151
|
+
chunkLength = splitIndex + 1;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
rows.push({ start: offset, length: chunkLength });
|
|
155
|
+
remaining = remaining.slice(chunkLength);
|
|
156
|
+
offset += chunkLength;
|
|
157
|
+
}
|
|
158
|
+
return rows;
|
|
159
|
+
}
|
|
122
160
|
/**
|
|
123
161
|
* Calculate which visual row (within a buffer line) the cursor is on,
|
|
124
162
|
* and the column within that visual row.
|
|
163
|
+
* Uses word-aware wrapping.
|
|
125
164
|
*/
|
|
126
|
-
function getVisualPosition(bufferColumn,
|
|
127
|
-
|
|
128
|
-
|
|
165
|
+
function getVisualPosition(bufferColumn, line, width) {
|
|
166
|
+
const rows = getVisualRows(line, width);
|
|
167
|
+
for (let i = 0; i < rows.length; i++) {
|
|
168
|
+
const row = rows[i];
|
|
169
|
+
const rowEnd = row.start + row.length;
|
|
170
|
+
if (bufferColumn >= row.start && bufferColumn < rowEnd) {
|
|
171
|
+
return { visualRow: i, visualCol: bufferColumn - row.start };
|
|
172
|
+
}
|
|
173
|
+
// Handle cursor at the very end of this row
|
|
174
|
+
if (bufferColumn === rowEnd && i === rows.length - 1) {
|
|
175
|
+
return { visualRow: i, visualCol: row.length };
|
|
176
|
+
}
|
|
129
177
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
178
|
+
// Cursor at wrap point - belongs to the next row
|
|
179
|
+
for (let i = 0; i < rows.length; i++) {
|
|
180
|
+
const row = rows[i];
|
|
181
|
+
if (bufferColumn === row.start + row.length && i < rows.length - 1) {
|
|
182
|
+
return { visualRow: i + 1, visualCol: 0 };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Fallback: cursor at end of line
|
|
186
|
+
const lastRow = rows[rows.length - 1];
|
|
187
|
+
return { visualRow: rows.length - 1, visualCol: lastRow.length };
|
|
133
188
|
}
|
|
134
189
|
/**
|
|
135
190
|
* Calculate how many visual rows a buffer line takes.
|
|
191
|
+
* Uses word-aware wrapping.
|
|
136
192
|
*/
|
|
137
|
-
function getVisualRowCount(
|
|
138
|
-
|
|
139
|
-
return 1;
|
|
140
|
-
return Math.ceil(lineLength / width);
|
|
193
|
+
function getVisualRowCount(line, width) {
|
|
194
|
+
return getVisualRows(line, width).length;
|
|
141
195
|
}
|
|
142
196
|
/**
|
|
143
197
|
* Convert visual position back to buffer column.
|
|
198
|
+
* Uses word-aware wrapping.
|
|
144
199
|
*/
|
|
145
|
-
function visualToBufferColumn(visualRow, visualCol,
|
|
146
|
-
const
|
|
147
|
-
|
|
200
|
+
function visualToBufferColumn(visualRow, visualCol, line, width) {
|
|
201
|
+
const rows = getVisualRows(line, width);
|
|
202
|
+
if (visualRow >= rows.length) {
|
|
203
|
+
return line.length;
|
|
204
|
+
}
|
|
205
|
+
const row = rows[visualRow];
|
|
206
|
+
return Math.min(row.start + visualCol, line.length);
|
|
148
207
|
}
|
|
149
208
|
/**
|
|
150
209
|
* Get the length of a specific visual row within a buffer line.
|
|
210
|
+
* Uses word-aware wrapping.
|
|
151
211
|
*/
|
|
152
|
-
function getVisualRowLength(
|
|
153
|
-
const
|
|
154
|
-
if (visualRow >=
|
|
212
|
+
function getVisualRowLength(line, visualRow, width) {
|
|
213
|
+
const rows = getVisualRows(line, width);
|
|
214
|
+
if (visualRow >= rows.length)
|
|
155
215
|
return 0;
|
|
156
|
-
|
|
157
|
-
// Last visual row - might be shorter
|
|
158
|
-
const remaining = lineLength - visualRow * width;
|
|
159
|
-
return remaining;
|
|
160
|
-
}
|
|
161
|
-
return width;
|
|
216
|
+
return rows[visualRow].length;
|
|
162
217
|
}
|
|
163
218
|
/**
|
|
164
219
|
* Move cursor in specified direction with bounds checking.
|
|
@@ -190,23 +245,23 @@ export function moveCursor(buffer, cursor, direction, width) {
|
|
|
190
245
|
return cursor;
|
|
191
246
|
case 'up':
|
|
192
247
|
if (width !== undefined) {
|
|
193
|
-
// Visual-aware movement
|
|
194
|
-
const { visualRow, visualCol } = getVisualPosition(column, currentLine
|
|
248
|
+
// Visual-aware movement (word-aware wrapping)
|
|
249
|
+
const { visualRow, visualCol } = getVisualPosition(column, currentLine, width);
|
|
195
250
|
if (visualRow > 0) {
|
|
196
251
|
// Move to previous visual row within the same buffer line
|
|
197
252
|
const targetVisualRow = visualRow - 1;
|
|
198
|
-
const targetVisualRowLength = getVisualRowLength(currentLine
|
|
253
|
+
const targetVisualRowLength = getVisualRowLength(currentLine, targetVisualRow, width);
|
|
199
254
|
const targetVisualCol = Math.min(visualCol, targetVisualRowLength);
|
|
200
|
-
return { line, column: visualToBufferColumn(targetVisualRow, targetVisualCol, currentLine
|
|
255
|
+
return { line, column: visualToBufferColumn(targetVisualRow, targetVisualCol, currentLine, width) };
|
|
201
256
|
}
|
|
202
257
|
// At first visual row of current line - move to last visual row of previous buffer line
|
|
203
258
|
if (line > 0) {
|
|
204
259
|
const prevLine = buffer.lines[line - 1];
|
|
205
|
-
const prevLineVisualRows = getVisualRowCount(prevLine
|
|
260
|
+
const prevLineVisualRows = getVisualRowCount(prevLine, width);
|
|
206
261
|
const targetVisualRow = prevLineVisualRows - 1;
|
|
207
|
-
const targetVisualRowLength = getVisualRowLength(prevLine
|
|
262
|
+
const targetVisualRowLength = getVisualRowLength(prevLine, targetVisualRow, width);
|
|
208
263
|
const targetVisualCol = Math.min(visualCol, targetVisualRowLength);
|
|
209
|
-
return { line: line - 1, column: visualToBufferColumn(targetVisualRow, targetVisualCol, prevLine
|
|
264
|
+
return { line: line - 1, column: visualToBufferColumn(targetVisualRow, targetVisualCol, prevLine, width) };
|
|
210
265
|
}
|
|
211
266
|
return cursor;
|
|
212
267
|
}
|
|
@@ -218,20 +273,20 @@ export function moveCursor(buffer, cursor, direction, width) {
|
|
|
218
273
|
return cursor;
|
|
219
274
|
case 'down':
|
|
220
275
|
if (width !== undefined) {
|
|
221
|
-
// Visual-aware movement
|
|
222
|
-
const { visualRow, visualCol } = getVisualPosition(column, currentLine
|
|
223
|
-
const currentLineVisualRows = getVisualRowCount(currentLine
|
|
276
|
+
// Visual-aware movement (word-aware wrapping)
|
|
277
|
+
const { visualRow, visualCol } = getVisualPosition(column, currentLine, width);
|
|
278
|
+
const currentLineVisualRows = getVisualRowCount(currentLine, width);
|
|
224
279
|
if (visualRow < currentLineVisualRows - 1) {
|
|
225
280
|
// Move to next visual row within the same buffer line
|
|
226
281
|
const targetVisualRow = visualRow + 1;
|
|
227
|
-
const targetVisualRowLength = getVisualRowLength(currentLine
|
|
282
|
+
const targetVisualRowLength = getVisualRowLength(currentLine, targetVisualRow, width);
|
|
228
283
|
const targetVisualCol = Math.min(visualCol, targetVisualRowLength);
|
|
229
|
-
return { line, column: visualToBufferColumn(targetVisualRow, targetVisualCol, currentLine
|
|
284
|
+
return { line, column: visualToBufferColumn(targetVisualRow, targetVisualCol, currentLine, width) };
|
|
230
285
|
}
|
|
231
286
|
// At last visual row of current line - move to first visual row of next buffer line
|
|
232
287
|
if (line < lineCount - 1) {
|
|
233
288
|
const nextLine = buffer.lines[line + 1];
|
|
234
|
-
const targetVisualRowLength = getVisualRowLength(nextLine
|
|
289
|
+
const targetVisualRowLength = getVisualRowLength(nextLine, 0, width);
|
|
235
290
|
const targetVisualCol = Math.min(visualCol, targetVisualRowLength);
|
|
236
291
|
return { line: line + 1, column: Math.min(targetVisualCol, nextLine.length) };
|
|
237
292
|
}
|
|
@@ -21,4 +21,4 @@ export declare function wrapLines(buffer: Buffer, cursor: Cursor, width: number)
|
|
|
21
21
|
/**
|
|
22
22
|
* TextRenderer component for displaying buffer content with cursor
|
|
23
23
|
*/
|
|
24
|
-
export declare function TextRenderer({ buffer, cursor, width, showCursor, }: TextRendererProps): React.ReactElement;
|
|
24
|
+
export declare function TextRenderer({ buffer, cursor, width: propWidth, showCursor, }: TextRendererProps): React.ReactElement;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
import { useTerminalWidth } from '../../hooks/useTerminalWidth.js';
|
|
3
4
|
/**
|
|
4
5
|
* Wrap buffer lines to fit within a given width.
|
|
5
6
|
* Returns visual lines and maps cursor position to visual coordinates.
|
|
@@ -8,40 +9,75 @@ export function wrapLines(buffer, cursor, width) {
|
|
|
8
9
|
const visualLines = [];
|
|
9
10
|
let cursorVisualRow = 0;
|
|
10
11
|
let cursorVisualCol = 0;
|
|
12
|
+
// Ensure width is at least 1 to avoid infinite loops
|
|
13
|
+
const safeWidth = Math.max(1, width);
|
|
11
14
|
let visualRowIndex = 0;
|
|
12
15
|
for (let lineIndex = 0; lineIndex < buffer.lines.length; lineIndex++) {
|
|
13
16
|
const line = buffer.lines[lineIndex];
|
|
14
17
|
const isCursorLine = lineIndex === cursor.line;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
visualLines.push(
|
|
18
|
+
// Handle empty line case
|
|
19
|
+
if (line.length === 0) {
|
|
20
|
+
visualLines.push('');
|
|
18
21
|
if (isCursorLine) {
|
|
19
22
|
cursorVisualRow = visualRowIndex;
|
|
20
|
-
cursorVisualCol =
|
|
23
|
+
cursorVisualCol = 0;
|
|
21
24
|
}
|
|
22
25
|
visualRowIndex++;
|
|
26
|
+
continue;
|
|
23
27
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
let remaining = line;
|
|
29
|
+
let offset = 0;
|
|
30
|
+
while (remaining.length > 0) {
|
|
31
|
+
let chunkLength = safeWidth;
|
|
32
|
+
if (remaining.length <= safeWidth) {
|
|
33
|
+
chunkLength = remaining.length;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
// Find split point (last space within width)
|
|
37
|
+
let splitIndex = -1;
|
|
38
|
+
for (let i = safeWidth - 1; i >= 0; i--) {
|
|
39
|
+
if (remaining[i] === ' ') {
|
|
40
|
+
splitIndex = i;
|
|
41
|
+
break;
|
|
35
42
|
}
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
}
|
|
44
|
+
if (splitIndex !== -1) {
|
|
45
|
+
// Include the space in the chunk
|
|
46
|
+
chunkLength = splitIndex + 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const chunk = remaining.slice(0, chunkLength);
|
|
50
|
+
visualLines.push(chunk);
|
|
51
|
+
if (isCursorLine) {
|
|
52
|
+
// Check if cursor falls within this chunk
|
|
53
|
+
// Cursor is at `cursor.column` (relative to line start)
|
|
54
|
+
// Current chunk covers `offset` to `offset + chunkLength`
|
|
55
|
+
if (cursor.column >= offset && cursor.column < offset + chunkLength) {
|
|
56
|
+
cursorVisualRow = visualRowIndex;
|
|
57
|
+
cursorVisualCol = cursor.column - offset;
|
|
58
|
+
}
|
|
59
|
+
else if (cursor.column === offset + chunkLength) {
|
|
60
|
+
// Cursor is at the end of this chunk
|
|
61
|
+
if (offset + chunkLength === line.length) {
|
|
62
|
+
// End of line
|
|
38
63
|
cursorVisualRow = visualRowIndex;
|
|
39
|
-
cursorVisualCol =
|
|
64
|
+
cursorVisualCol = chunkLength;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// Wrap point - cursor should be at start of next line
|
|
68
|
+
// We'll let the next iteration handle it (it will be index 0 of next chunk)
|
|
69
|
+
// But wait, if we are at the wrap point, the next iteration will see:
|
|
70
|
+
// offset = oldOffset + chunkLength.
|
|
71
|
+
// cursor.column == offset.
|
|
72
|
+
// So it enters the first `if` block: cursor.column >= offset.
|
|
73
|
+
// cursorVisualCol = cursor.column - offset = 0.
|
|
74
|
+
// This is correct.
|
|
40
75
|
}
|
|
41
76
|
}
|
|
42
|
-
offset += width;
|
|
43
|
-
visualRowIndex++;
|
|
44
77
|
}
|
|
78
|
+
remaining = remaining.slice(chunkLength);
|
|
79
|
+
offset += chunkLength;
|
|
80
|
+
visualRowIndex++;
|
|
45
81
|
}
|
|
46
82
|
}
|
|
47
83
|
return { visualLines, cursorVisualRow, cursorVisualCol };
|
|
@@ -69,7 +105,8 @@ function renderLineWithCursor(line, cursorCol, showCursor) {
|
|
|
69
105
|
/**
|
|
70
106
|
* TextRenderer component for displaying buffer content with cursor
|
|
71
107
|
*/
|
|
72
|
-
export function TextRenderer({ buffer, cursor, width
|
|
108
|
+
export function TextRenderer({ buffer, cursor, width: propWidth, showCursor = true, }) {
|
|
109
|
+
const width = useTerminalWidth(propWidth);
|
|
73
110
|
const { visualLines, cursorVisualRow, cursorVisualCol } = wrapLines(buffer, cursor, width);
|
|
74
111
|
return (_jsx(Box, { flexDirection: "column", children: visualLines.map((line, index) => {
|
|
75
112
|
const isCursorRow = index === cursorVisualRow;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { createBuffer, insertText, deleteChar, deleteCharForward, insertNewLine, moveCursor, getTextContent, } from '../TextBuffer.js';
|
|
2
|
+
import { createBuffer, insertText, deleteChar, deleteCharForward, insertNewLine, moveCursor, getTextContent, getVisualRows, } from '../TextBuffer.js';
|
|
3
3
|
describe('TextBuffer', () => {
|
|
4
4
|
describe('createBuffer', () => {
|
|
5
5
|
it('creates empty buffer with single empty line', () => {
|
|
@@ -447,4 +447,127 @@ describe('TextBuffer', () => {
|
|
|
447
447
|
expect(getTextContent(buffer)).toBe('hello\n\nworld');
|
|
448
448
|
});
|
|
449
449
|
});
|
|
450
|
+
describe('getVisualRows (word-aware wrapping)', () => {
|
|
451
|
+
it('returns single row for short line', () => {
|
|
452
|
+
const rows = getVisualRows('hello', 10);
|
|
453
|
+
expect(rows).toEqual([{ start: 0, length: 5 }]);
|
|
454
|
+
});
|
|
455
|
+
it('returns single row for empty line', () => {
|
|
456
|
+
const rows = getVisualRows('', 10);
|
|
457
|
+
expect(rows).toEqual([{ start: 0, length: 0 }]);
|
|
458
|
+
});
|
|
459
|
+
it('wraps at space boundary', () => {
|
|
460
|
+
const rows = getVisualRows('hello world', 7);
|
|
461
|
+
// "hello " (6) fits, "world" (5) on next row
|
|
462
|
+
expect(rows).toEqual([
|
|
463
|
+
{ start: 0, length: 6 }, // "hello "
|
|
464
|
+
{ start: 6, length: 5 }, // "world"
|
|
465
|
+
]);
|
|
466
|
+
});
|
|
467
|
+
it('hard-wraps long words that exceed width', () => {
|
|
468
|
+
const rows = getVisualRows('supercalifragilistic', 5);
|
|
469
|
+
expect(rows).toEqual([
|
|
470
|
+
{ start: 0, length: 5 }, // "super"
|
|
471
|
+
{ start: 5, length: 5 }, // "calif"
|
|
472
|
+
{ start: 10, length: 5 }, // "ragil"
|
|
473
|
+
{ start: 15, length: 5 }, // "istic"
|
|
474
|
+
]);
|
|
475
|
+
});
|
|
476
|
+
it('wraps mixed content correctly', () => {
|
|
477
|
+
const rows = getVisualRows('a very longwordthatwraps', 10);
|
|
478
|
+
expect(rows).toEqual([
|
|
479
|
+
{ start: 0, length: 7 }, // "a very "
|
|
480
|
+
{ start: 7, length: 10 }, // "longwordth"
|
|
481
|
+
{ start: 17, length: 7 }, // "atwraps"
|
|
482
|
+
]);
|
|
483
|
+
});
|
|
484
|
+
it('handles multiple spaces', () => {
|
|
485
|
+
const rows = getVisualRows('hello world', 8);
|
|
486
|
+
// "hello " (8) fits exactly
|
|
487
|
+
expect(rows).toEqual([
|
|
488
|
+
{ start: 0, length: 8 }, // "hello "
|
|
489
|
+
{ start: 8, length: 5 }, // "world"
|
|
490
|
+
]);
|
|
491
|
+
});
|
|
492
|
+
it('handles leading space', () => {
|
|
493
|
+
const rows = getVisualRows(' hello world', 7);
|
|
494
|
+
// " hello " (7) fits exactly
|
|
495
|
+
expect(rows).toEqual([
|
|
496
|
+
{ start: 0, length: 7 }, // " hello "
|
|
497
|
+
{ start: 7, length: 5 }, // "world"
|
|
498
|
+
]);
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
describe('moveCursor with word-aware wrapping', () => {
|
|
502
|
+
describe('up/down in word-wrapped lines', () => {
|
|
503
|
+
it('moves up within word-wrapped line respecting word boundaries', () => {
|
|
504
|
+
// "hello world" with width 7 wraps as ["hello ", "world"]
|
|
505
|
+
const buffer = createBuffer('hello world');
|
|
506
|
+
const cursor = { line: 0, column: 8 }; // at 'r' in 'world'
|
|
507
|
+
const result = moveCursor(buffer, cursor, 'up', 7);
|
|
508
|
+
// Visual row 1, col 2 -> visual row 0
|
|
509
|
+
// Visual row 0 is "hello " (6 chars), col 2 is 'l'
|
|
510
|
+
expect(result).toEqual({ line: 0, column: 2 });
|
|
511
|
+
});
|
|
512
|
+
it('moves down within word-wrapped line respecting word boundaries', () => {
|
|
513
|
+
// "hello world" with width 7 wraps as ["hello ", "world"]
|
|
514
|
+
const buffer = createBuffer('hello world');
|
|
515
|
+
const cursor = { line: 0, column: 2 }; // at first 'l' in 'hello'
|
|
516
|
+
const result = moveCursor(buffer, cursor, 'down', 7);
|
|
517
|
+
// Visual row 0, col 2 -> visual row 1, col 2
|
|
518
|
+
// Visual row 1 starts at offset 6, so target = 6 + 2 = 8
|
|
519
|
+
expect(result).toEqual({ line: 0, column: 8 });
|
|
520
|
+
});
|
|
521
|
+
it('clamps column when moving to shorter word-wrapped row', () => {
|
|
522
|
+
// "hello world" width 7: ["hello ", "world"] (row 0: 6 chars, row 1: 5 chars)
|
|
523
|
+
const buffer = createBuffer('hello world');
|
|
524
|
+
const cursor = { line: 0, column: 5 }; // at the space after 'hello'
|
|
525
|
+
const result = moveCursor(buffer, cursor, 'down', 7);
|
|
526
|
+
// Visual row 0, col 5 -> visual row 1, but row 1 only has 5 chars
|
|
527
|
+
// So col should clamp to 5 (end of "world")
|
|
528
|
+
// Target = 6 + 5 = 11 = line length
|
|
529
|
+
expect(result).toEqual({ line: 0, column: 11 });
|
|
530
|
+
});
|
|
531
|
+
it('moves between buffer lines with word-wrapping', () => {
|
|
532
|
+
// Line 0: "hello world" wraps as ["hello ", "world"]
|
|
533
|
+
// Line 1: "test"
|
|
534
|
+
const buffer = createBuffer('hello world\ntest');
|
|
535
|
+
const cursor = { line: 0, column: 8 }; // 'r' in 'world'
|
|
536
|
+
const result = moveCursor(buffer, cursor, 'down', 7);
|
|
537
|
+
// From visual row 1 (bottom of line 0) to line 1, visual row 0
|
|
538
|
+
// "test" row 0 is 4 chars, col 2 requested, result = col 2
|
|
539
|
+
expect(result).toEqual({ line: 1, column: 2 });
|
|
540
|
+
});
|
|
541
|
+
it('handles cursor at word wrap boundary going up', () => {
|
|
542
|
+
// "hello world" width 7: ["hello ", "world"]
|
|
543
|
+
// Cursor at position 6 (start of "world")
|
|
544
|
+
const buffer = createBuffer('hello world');
|
|
545
|
+
const cursor = { line: 0, column: 6 };
|
|
546
|
+
const result = moveCursor(buffer, cursor, 'up', 7);
|
|
547
|
+
// Should go from visual row 1, col 0 to visual row 0, col 0
|
|
548
|
+
expect(result).toEqual({ line: 0, column: 0 });
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
describe('cursor position calculation at word boundaries', () => {
|
|
552
|
+
it('cursor in middle of first word-wrapped row', () => {
|
|
553
|
+
// "a very longword" width 10: ["a very ", "longword"]
|
|
554
|
+
const buffer = createBuffer('a very longword');
|
|
555
|
+
const cursor = { line: 0, column: 3 }; // at 'e' in 'very'
|
|
556
|
+
const result = moveCursor(buffer, cursor, 'down', 10);
|
|
557
|
+
// From row 0, col 3 to row 1, col 3 -> offset 7 + 3 = 10
|
|
558
|
+
expect(result).toEqual({ line: 0, column: 10 });
|
|
559
|
+
});
|
|
560
|
+
it('preserves column when navigating up then down', () => {
|
|
561
|
+
// "hello world test" width 7: ["hello ", "world ", "test"]
|
|
562
|
+
const buffer = createBuffer('hello world test');
|
|
563
|
+
const cursor = { line: 0, column: 9 }; // 'r' in 'world'
|
|
564
|
+
// Move up
|
|
565
|
+
const afterUp = moveCursor(buffer, cursor, 'up', 7);
|
|
566
|
+
expect(afterUp).toEqual({ line: 0, column: 3 });
|
|
567
|
+
// Move back down
|
|
568
|
+
const afterDown = moveCursor(buffer, afterUp, 'down', 7);
|
|
569
|
+
expect(afterDown).toEqual({ line: 0, column: 9 });
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
});
|
|
450
573
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|