ink-prompt 0.1.5 → 0.1.6
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/KeyHandler.d.ts +3 -1
- package/dist/components/MultilineInput/KeyHandler.js +120 -2
- package/dist/components/MultilineInput/__tests__/KeyHandler.test.js +173 -0
- package/dist/components/MultilineInput/types.d.ts +4 -0
- package/dist/index.d.ts +1 -0
- package/package.json +1 -1
- package/dist/examples/examples/basic.d.ts +0 -1
- package/dist/examples/examples/basic.js +0 -9
- package/dist/examples/src/components/MultilineInput/KeyHandler.d.ts +0 -15
- package/dist/examples/src/components/MultilineInput/KeyHandler.js +0 -97
- package/dist/examples/src/components/MultilineInput/TextBuffer.d.ts +0 -34
- package/dist/examples/src/components/MultilineInput/TextBuffer.js +0 -127
- package/dist/examples/src/components/MultilineInput/TextRenderer.d.ts +0 -24
- package/dist/examples/src/components/MultilineInput/TextRenderer.js +0 -72
- package/dist/examples/src/components/MultilineInput/__tests__/KeyHandler.test.d.ts +0 -1
- package/dist/examples/src/components/MultilineInput/__tests__/KeyHandler.test.js +0 -115
- package/dist/examples/src/components/MultilineInput/__tests__/TextBuffer.test.d.ts +0 -1
- package/dist/examples/src/components/MultilineInput/__tests__/TextBuffer.test.js +0 -254
- package/dist/examples/src/components/MultilineInput/__tests__/TextRenderer.test.d.ts +0 -1
- package/dist/examples/src/components/MultilineInput/__tests__/TextRenderer.test.js +0 -176
- package/dist/examples/src/components/MultilineInput/__tests__/integration.test.d.ts +0 -1
- package/dist/examples/src/components/MultilineInput/__tests__/integration.test.js +0 -71
- package/dist/examples/src/components/MultilineInput/__tests__/useTextInput.test.d.ts +0 -1
- package/dist/examples/src/components/MultilineInput/__tests__/useTextInput.test.js +0 -65
- package/dist/examples/src/components/MultilineInput/index.d.ts +0 -39
- package/dist/examples/src/components/MultilineInput/index.js +0 -82
- package/dist/examples/src/components/MultilineInput/types.d.ts +0 -55
- package/dist/examples/src/components/MultilineInput/types.js +0 -1
- package/dist/examples/src/components/MultilineInput/useTextInput.d.ts +0 -16
- package/dist/examples/src/components/MultilineInput/useTextInput.js +0 -82
- package/dist/examples/src/hello.test.d.ts +0 -1
- package/dist/examples/src/hello.test.js +0 -13
- package/dist/examples/src/index.d.ts +0 -2
- package/dist/examples/src/index.js +0 -2
|
@@ -2,6 +2,7 @@ import { type Key, type Buffer, type Cursor } from './types.js';
|
|
|
2
2
|
import { type UseTextInputResult } from './useTextInput.js';
|
|
3
3
|
export interface KeyHandlerActions extends Omit<UseTextInputResult, 'value' | 'cursor' | 'cursorOffset' | 'setCursorOffset'> {
|
|
4
4
|
submit: () => void;
|
|
5
|
+
onBoundaryArrow?: (direction: 'up' | 'down' | 'left' | 'right') => void;
|
|
5
6
|
}
|
|
6
7
|
/**
|
|
7
8
|
* Handles keyboard input and maps it to text input actions.
|
|
@@ -12,5 +13,6 @@ export interface KeyHandlerActions extends Omit<UseTextInputResult, 'value' | 'c
|
|
|
12
13
|
* @param actions - The actions available to modify the state
|
|
13
14
|
* @param cursor - The current cursor position (optional, but required for some logic like backslash check)
|
|
14
15
|
* @param rawInput - The raw input sequence (optional, used for detecting Home/End keys)
|
|
16
|
+
* @param width - Terminal width for visual-aware boundary detection (optional)
|
|
15
17
|
*/
|
|
16
|
-
export declare function handleKey(key: Partial<Key>, input: string, buffer: Buffer, actions: KeyHandlerActions, cursor?: Cursor, rawInput?: string): void;
|
|
18
|
+
export declare function handleKey(key: Partial<Key>, input: string, buffer: Buffer, actions: KeyHandlerActions, cursor?: Cursor, rawInput?: string, width?: number): void;
|
|
@@ -1,4 +1,105 @@
|
|
|
1
|
+
import { getVisualRows } from './TextBuffer.js';
|
|
1
2
|
import { log } from '../../utils/logger.js';
|
|
3
|
+
/**
|
|
4
|
+
* Check if cursor is at the left boundary (start of text).
|
|
5
|
+
*/
|
|
6
|
+
function isAtLeftBoundary(cursor) {
|
|
7
|
+
return cursor.line === 0 && cursor.column === 0;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Check if cursor is at the right boundary (end of text).
|
|
11
|
+
*/
|
|
12
|
+
function isAtRightBoundary(buffer, cursor) {
|
|
13
|
+
const lastLineIndex = buffer.lines.length - 1;
|
|
14
|
+
const lastLine = buffer.lines[lastLineIndex];
|
|
15
|
+
return cursor.line === lastLineIndex && cursor.column >= lastLine.length;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Check if cursor is at the top boundary (cannot move up).
|
|
19
|
+
* When width is provided, this considers visual line wrapping.
|
|
20
|
+
*/
|
|
21
|
+
function isAtTopBoundary(buffer, cursor, width) {
|
|
22
|
+
if (cursor.line > 0) {
|
|
23
|
+
// Not on first buffer line, check visual wrapping
|
|
24
|
+
if (width !== undefined) {
|
|
25
|
+
const currentLine = buffer.lines[cursor.line];
|
|
26
|
+
const rows = getVisualRows(currentLine, width);
|
|
27
|
+
// Find which visual row the cursor is on
|
|
28
|
+
let visualRow = 0;
|
|
29
|
+
for (let i = 0; i < rows.length; i++) {
|
|
30
|
+
const row = rows[i];
|
|
31
|
+
const rowEnd = row.start + row.length;
|
|
32
|
+
if (cursor.column >= row.start && cursor.column <= rowEnd) {
|
|
33
|
+
visualRow = i;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// If cursor is not on the first visual row of this line, it can move up
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
// Cursor is on first buffer line
|
|
43
|
+
if (width !== undefined) {
|
|
44
|
+
const currentLine = buffer.lines[0];
|
|
45
|
+
const rows = getVisualRows(currentLine, width);
|
|
46
|
+
// Find which visual row the cursor is on
|
|
47
|
+
for (let i = 0; i < rows.length; i++) {
|
|
48
|
+
const row = rows[i];
|
|
49
|
+
const rowEnd = row.start + row.length;
|
|
50
|
+
if (cursor.column >= row.start && cursor.column <= rowEnd) {
|
|
51
|
+
// At top boundary only if on first visual row
|
|
52
|
+
return i === 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// No width - buffer line-based: first line means at top
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Check if cursor is at the bottom boundary (cannot move down).
|
|
61
|
+
* When width is provided, this considers visual line wrapping.
|
|
62
|
+
*/
|
|
63
|
+
function isAtBottomBoundary(buffer, cursor, width) {
|
|
64
|
+
const lastLineIndex = buffer.lines.length - 1;
|
|
65
|
+
if (cursor.line < lastLineIndex) {
|
|
66
|
+
// Not on last buffer line, check visual wrapping
|
|
67
|
+
if (width !== undefined) {
|
|
68
|
+
const currentLine = buffer.lines[cursor.line];
|
|
69
|
+
const rows = getVisualRows(currentLine, width);
|
|
70
|
+
// Find which visual row the cursor is on
|
|
71
|
+
let visualRow = 0;
|
|
72
|
+
for (let i = 0; i < rows.length; i++) {
|
|
73
|
+
const row = rows[i];
|
|
74
|
+
const rowEnd = row.start + row.length;
|
|
75
|
+
if (cursor.column >= row.start && cursor.column <= rowEnd) {
|
|
76
|
+
visualRow = i;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// If cursor is not on the last visual row of this line, it can move down
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
// Cursor is on last buffer line
|
|
86
|
+
if (width !== undefined) {
|
|
87
|
+
const currentLine = buffer.lines[lastLineIndex];
|
|
88
|
+
const rows = getVisualRows(currentLine, width);
|
|
89
|
+
const lastVisualRow = rows.length - 1;
|
|
90
|
+
// Find which visual row the cursor is on
|
|
91
|
+
for (let i = 0; i < rows.length; i++) {
|
|
92
|
+
const row = rows[i];
|
|
93
|
+
const rowEnd = row.start + row.length;
|
|
94
|
+
if (cursor.column >= row.start && cursor.column <= rowEnd) {
|
|
95
|
+
// At bottom boundary only if on last visual row
|
|
96
|
+
return i === lastVisualRow;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// No width - buffer line-based: last line means at bottom
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
2
103
|
/**
|
|
3
104
|
* Escape sequences for Home key (various terminal emulators)
|
|
4
105
|
*/
|
|
@@ -33,22 +134,39 @@ function isBackspaceSequence(seq) {
|
|
|
33
134
|
* @param actions - The actions available to modify the state
|
|
34
135
|
* @param cursor - The current cursor position (optional, but required for some logic like backslash check)
|
|
35
136
|
* @param rawInput - The raw input sequence (optional, used for detecting Home/End keys)
|
|
137
|
+
* @param width - Terminal width for visual-aware boundary detection (optional)
|
|
36
138
|
*/
|
|
37
|
-
export function handleKey(key, input, buffer, actions, cursor, rawInput) {
|
|
38
|
-
// Navigation
|
|
139
|
+
export function handleKey(key, input, buffer, actions, cursor, rawInput, width) {
|
|
140
|
+
// Navigation with boundary detection
|
|
39
141
|
if (key.upArrow) {
|
|
142
|
+
if (cursor && actions.onBoundaryArrow && isAtTopBoundary(buffer, cursor, width)) {
|
|
143
|
+
actions.onBoundaryArrow('up');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
40
146
|
actions.moveCursor('up');
|
|
41
147
|
return;
|
|
42
148
|
}
|
|
43
149
|
if (key.downArrow) {
|
|
150
|
+
if (cursor && actions.onBoundaryArrow && isAtBottomBoundary(buffer, cursor, width)) {
|
|
151
|
+
actions.onBoundaryArrow('down');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
44
154
|
actions.moveCursor('down');
|
|
45
155
|
return;
|
|
46
156
|
}
|
|
47
157
|
if (key.leftArrow) {
|
|
158
|
+
if (cursor && actions.onBoundaryArrow && isAtLeftBoundary(cursor)) {
|
|
159
|
+
actions.onBoundaryArrow('left');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
48
162
|
actions.moveCursor('left');
|
|
49
163
|
return;
|
|
50
164
|
}
|
|
51
165
|
if (key.rightArrow) {
|
|
166
|
+
if (cursor && actions.onBoundaryArrow && isAtRightBoundary(buffer, cursor)) {
|
|
167
|
+
actions.onBoundaryArrow('right');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
52
170
|
actions.moveCursor('right');
|
|
53
171
|
return;
|
|
54
172
|
}
|
|
@@ -62,6 +62,179 @@ describe('KeyHandler', () => {
|
|
|
62
62
|
expect(actions.moveCursor).toHaveBeenCalledWith('lineEnd');
|
|
63
63
|
});
|
|
64
64
|
});
|
|
65
|
+
describe('Boundary Arrow', () => {
|
|
66
|
+
describe('Left boundary', () => {
|
|
67
|
+
it('calls onBoundaryArrow("left") when cursor is at position 0', () => {
|
|
68
|
+
buffer = { lines: ['hello'] };
|
|
69
|
+
const cursor = { line: 0, column: 0 };
|
|
70
|
+
const onBoundaryArrow = vi.fn();
|
|
71
|
+
actions.onBoundaryArrow = onBoundaryArrow;
|
|
72
|
+
handleKey({ leftArrow: true }, '', buffer, actions, cursor);
|
|
73
|
+
expect(onBoundaryArrow).toHaveBeenCalledWith('left');
|
|
74
|
+
expect(actions.moveCursor).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
it('does not call onBoundaryArrow when cursor can move left', () => {
|
|
77
|
+
buffer = { lines: ['hello'] };
|
|
78
|
+
const cursor = { line: 0, column: 3 };
|
|
79
|
+
const onBoundaryArrow = vi.fn();
|
|
80
|
+
actions.onBoundaryArrow = onBoundaryArrow;
|
|
81
|
+
handleKey({ leftArrow: true }, '', buffer, actions, cursor);
|
|
82
|
+
expect(onBoundaryArrow).not.toHaveBeenCalled();
|
|
83
|
+
expect(actions.moveCursor).toHaveBeenCalledWith('left');
|
|
84
|
+
});
|
|
85
|
+
it('does not call onBoundaryArrow when at start of line but previous line exists', () => {
|
|
86
|
+
buffer = { lines: ['first', 'second'] };
|
|
87
|
+
const cursor = { line: 1, column: 0 };
|
|
88
|
+
const onBoundaryArrow = vi.fn();
|
|
89
|
+
actions.onBoundaryArrow = onBoundaryArrow;
|
|
90
|
+
handleKey({ leftArrow: true }, '', buffer, actions, cursor);
|
|
91
|
+
expect(onBoundaryArrow).not.toHaveBeenCalled();
|
|
92
|
+
expect(actions.moveCursor).toHaveBeenCalledWith('left');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe('Right boundary', () => {
|
|
96
|
+
it('calls onBoundaryArrow("right") when cursor is at end of text', () => {
|
|
97
|
+
buffer = { lines: ['hello'] };
|
|
98
|
+
const cursor = { line: 0, column: 5 };
|
|
99
|
+
const onBoundaryArrow = vi.fn();
|
|
100
|
+
actions.onBoundaryArrow = onBoundaryArrow;
|
|
101
|
+
handleKey({ rightArrow: true }, '', buffer, actions, cursor);
|
|
102
|
+
expect(onBoundaryArrow).toHaveBeenCalledWith('right');
|
|
103
|
+
expect(actions.moveCursor).not.toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
it('does not call onBoundaryArrow when cursor can move right', () => {
|
|
106
|
+
buffer = { lines: ['hello'] };
|
|
107
|
+
const cursor = { line: 0, column: 2 };
|
|
108
|
+
const onBoundaryArrow = vi.fn();
|
|
109
|
+
actions.onBoundaryArrow = onBoundaryArrow;
|
|
110
|
+
handleKey({ rightArrow: true }, '', buffer, actions, cursor);
|
|
111
|
+
expect(onBoundaryArrow).not.toHaveBeenCalled();
|
|
112
|
+
expect(actions.moveCursor).toHaveBeenCalledWith('right');
|
|
113
|
+
});
|
|
114
|
+
it('does not call onBoundaryArrow when at end of line but next line exists', () => {
|
|
115
|
+
buffer = { lines: ['first', 'second'] };
|
|
116
|
+
const cursor = { line: 0, column: 5 };
|
|
117
|
+
const onBoundaryArrow = vi.fn();
|
|
118
|
+
actions.onBoundaryArrow = onBoundaryArrow;
|
|
119
|
+
handleKey({ rightArrow: true }, '', buffer, actions, cursor);
|
|
120
|
+
expect(onBoundaryArrow).not.toHaveBeenCalled();
|
|
121
|
+
expect(actions.moveCursor).toHaveBeenCalledWith('right');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
describe('Up boundary', () => {
|
|
125
|
+
it('calls onBoundaryArrow("up") when cursor is on first line', () => {
|
|
126
|
+
buffer = { lines: ['hello'] };
|
|
127
|
+
const cursor = { line: 0, column: 2 };
|
|
128
|
+
const onBoundaryArrow = vi.fn();
|
|
129
|
+
actions.onBoundaryArrow = onBoundaryArrow;
|
|
130
|
+
handleKey({ upArrow: true }, '', buffer, actions, cursor);
|
|
131
|
+
expect(onBoundaryArrow).toHaveBeenCalledWith('up');
|
|
132
|
+
expect(actions.moveCursor).not.toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
it('does not call onBoundaryArrow when previous line exists', () => {
|
|
135
|
+
buffer = { lines: ['first', 'second'] };
|
|
136
|
+
const cursor = { line: 1, column: 2 };
|
|
137
|
+
const onBoundaryArrow = vi.fn();
|
|
138
|
+
actions.onBoundaryArrow = onBoundaryArrow;
|
|
139
|
+
handleKey({ upArrow: true }, '', buffer, actions, cursor);
|
|
140
|
+
expect(onBoundaryArrow).not.toHaveBeenCalled();
|
|
141
|
+
expect(actions.moveCursor).toHaveBeenCalledWith('up');
|
|
142
|
+
});
|
|
143
|
+
it('considers visual rows when width is provided - cursor on first visual row', () => {
|
|
144
|
+
// "hello world" with width 5 wraps to:
|
|
145
|
+
// "hello" (visual row 0)
|
|
146
|
+
// " worl" (visual row 1)
|
|
147
|
+
// "d" (visual row 2)
|
|
148
|
+
buffer = { lines: ['hello world'] };
|
|
149
|
+
const cursor = { line: 0, column: 2 }; // On first visual row
|
|
150
|
+
const onBoundaryArrow = vi.fn();
|
|
151
|
+
actions.onBoundaryArrow = onBoundaryArrow;
|
|
152
|
+
handleKey({ upArrow: true }, '', buffer, actions, cursor, undefined, 5);
|
|
153
|
+
expect(onBoundaryArrow).toHaveBeenCalledWith('up');
|
|
154
|
+
expect(actions.moveCursor).not.toHaveBeenCalled();
|
|
155
|
+
});
|
|
156
|
+
it('considers visual rows when width is provided - cursor on second visual row', () => {
|
|
157
|
+
// "hello world" with width 5 wraps to visual rows
|
|
158
|
+
buffer = { lines: ['hello world'] };
|
|
159
|
+
const cursor = { line: 0, column: 7 }; // On second visual row (after first wrap)
|
|
160
|
+
const onBoundaryArrow = vi.fn();
|
|
161
|
+
actions.onBoundaryArrow = onBoundaryArrow;
|
|
162
|
+
handleKey({ upArrow: true }, '', buffer, actions, cursor, undefined, 5);
|
|
163
|
+
// Can move up within the same buffer line (to previous visual row)
|
|
164
|
+
expect(onBoundaryArrow).not.toHaveBeenCalled();
|
|
165
|
+
expect(actions.moveCursor).toHaveBeenCalledWith('up');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
describe('Down boundary', () => {
|
|
169
|
+
it('calls onBoundaryArrow("down") when cursor is on last line', () => {
|
|
170
|
+
buffer = { lines: ['hello'] };
|
|
171
|
+
const cursor = { line: 0, column: 2 };
|
|
172
|
+
const onBoundaryArrow = vi.fn();
|
|
173
|
+
actions.onBoundaryArrow = onBoundaryArrow;
|
|
174
|
+
handleKey({ downArrow: true }, '', buffer, actions, cursor);
|
|
175
|
+
expect(onBoundaryArrow).toHaveBeenCalledWith('down');
|
|
176
|
+
expect(actions.moveCursor).not.toHaveBeenCalled();
|
|
177
|
+
});
|
|
178
|
+
it('does not call onBoundaryArrow when next line exists', () => {
|
|
179
|
+
buffer = { lines: ['first', 'second'] };
|
|
180
|
+
const cursor = { line: 0, column: 2 };
|
|
181
|
+
const onBoundaryArrow = vi.fn();
|
|
182
|
+
actions.onBoundaryArrow = onBoundaryArrow;
|
|
183
|
+
handleKey({ downArrow: true }, '', buffer, actions, cursor);
|
|
184
|
+
expect(onBoundaryArrow).not.toHaveBeenCalled();
|
|
185
|
+
expect(actions.moveCursor).toHaveBeenCalledWith('down');
|
|
186
|
+
});
|
|
187
|
+
it('considers visual rows when width is provided - cursor on last visual row', () => {
|
|
188
|
+
// "hello world" with width 5 wraps to visual rows
|
|
189
|
+
buffer = { lines: ['hello world'] };
|
|
190
|
+
const cursor = { line: 0, column: 10 }; // On last visual row (at 'd')
|
|
191
|
+
const onBoundaryArrow = vi.fn();
|
|
192
|
+
actions.onBoundaryArrow = onBoundaryArrow;
|
|
193
|
+
handleKey({ downArrow: true }, '', buffer, actions, cursor, undefined, 5);
|
|
194
|
+
expect(onBoundaryArrow).toHaveBeenCalledWith('down');
|
|
195
|
+
expect(actions.moveCursor).not.toHaveBeenCalled();
|
|
196
|
+
});
|
|
197
|
+
it('considers visual rows when width is provided - cursor on first visual row', () => {
|
|
198
|
+
// "hello world" with width 5 wraps to visual rows
|
|
199
|
+
buffer = { lines: ['hello world'] };
|
|
200
|
+
const cursor = { line: 0, column: 2 }; // On first visual row
|
|
201
|
+
const onBoundaryArrow = vi.fn();
|
|
202
|
+
actions.onBoundaryArrow = onBoundaryArrow;
|
|
203
|
+
handleKey({ downArrow: true }, '', buffer, actions, cursor, undefined, 5);
|
|
204
|
+
// Can move down within the same buffer line (to next visual row)
|
|
205
|
+
expect(onBoundaryArrow).not.toHaveBeenCalled();
|
|
206
|
+
expect(actions.moveCursor).toHaveBeenCalledWith('down');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
describe('No callback provided', () => {
|
|
210
|
+
it('moves cursor normally when onBoundaryArrow is not set', () => {
|
|
211
|
+
buffer = { lines: ['hello'] };
|
|
212
|
+
const cursor = { line: 0, column: 0 };
|
|
213
|
+
// actions.onBoundaryArrow is not set
|
|
214
|
+
handleKey({ leftArrow: true }, '', buffer, actions, cursor);
|
|
215
|
+
expect(actions.moveCursor).toHaveBeenCalledWith('left');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
describe('Empty buffer', () => {
|
|
219
|
+
it('calls onBoundaryArrow for all directions in empty buffer', () => {
|
|
220
|
+
buffer = { lines: [''] };
|
|
221
|
+
const cursor = { line: 0, column: 0 };
|
|
222
|
+
const onBoundaryArrow = vi.fn();
|
|
223
|
+
actions.onBoundaryArrow = onBoundaryArrow;
|
|
224
|
+
handleKey({ upArrow: true }, '', buffer, actions, cursor);
|
|
225
|
+
expect(onBoundaryArrow).toHaveBeenCalledWith('up');
|
|
226
|
+
onBoundaryArrow.mockClear();
|
|
227
|
+
handleKey({ downArrow: true }, '', buffer, actions, cursor);
|
|
228
|
+
expect(onBoundaryArrow).toHaveBeenCalledWith('down');
|
|
229
|
+
onBoundaryArrow.mockClear();
|
|
230
|
+
handleKey({ leftArrow: true }, '', buffer, actions, cursor);
|
|
231
|
+
expect(onBoundaryArrow).toHaveBeenCalledWith('left');
|
|
232
|
+
onBoundaryArrow.mockClear();
|
|
233
|
+
handleKey({ rightArrow: true }, '', buffer, actions, cursor);
|
|
234
|
+
expect(onBoundaryArrow).toHaveBeenCalledWith('right');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
});
|
|
65
238
|
describe('Editing', () => {
|
|
66
239
|
it('handles Backspace', () => {
|
|
67
240
|
handleKey({ backspace: true }, 'backspace', buffer, actions);
|
|
@@ -18,6 +18,10 @@ export interface Buffer {
|
|
|
18
18
|
* Cursor movement directions
|
|
19
19
|
*/
|
|
20
20
|
export type Direction = 'up' | 'down' | 'left' | 'right' | 'lineStart' | 'lineEnd';
|
|
21
|
+
/**
|
|
22
|
+
* Boundary arrow directions (subset of Direction used for boundary detection)
|
|
23
|
+
*/
|
|
24
|
+
export type BoundaryDirection = 'up' | 'down' | 'left' | 'right';
|
|
21
25
|
/**
|
|
22
26
|
* Result of wrapping buffer lines for visual display
|
|
23
27
|
*/
|
package/dist/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { type Key, type Buffer, type Cursor } from './types';
|
|
2
|
-
import { type UseTextInputResult } from './useTextInput';
|
|
3
|
-
export interface KeyHandlerActions extends Omit<UseTextInputResult, 'value' | 'cursor'> {
|
|
4
|
-
submit: () => void;
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Handles keyboard input and maps it to text input actions.
|
|
8
|
-
*
|
|
9
|
-
* @param key - The Ink key object
|
|
10
|
-
* @param input - The input string (if any)
|
|
11
|
-
* @param buffer - The current text buffer
|
|
12
|
-
* @param actions - The actions available to modify the state
|
|
13
|
-
* @param cursor - The current cursor position (optional, but required for some logic like backslash check)
|
|
14
|
-
*/
|
|
15
|
-
export declare function handleKey(key: Partial<Key>, input: string, buffer: Buffer, actions: KeyHandlerActions, cursor?: Cursor): void;
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Handles keyboard input and maps it to text input actions.
|
|
3
|
-
*
|
|
4
|
-
* @param key - The Ink key object
|
|
5
|
-
* @param input - The input string (if any)
|
|
6
|
-
* @param buffer - The current text buffer
|
|
7
|
-
* @param actions - The actions available to modify the state
|
|
8
|
-
* @param cursor - The current cursor position (optional, but required for some logic like backslash check)
|
|
9
|
-
*/
|
|
10
|
-
export function handleKey(key, input, buffer, actions, cursor) {
|
|
11
|
-
// Navigation
|
|
12
|
-
if (key.upArrow) {
|
|
13
|
-
actions.moveCursor('up');
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
if (key.downArrow) {
|
|
17
|
-
actions.moveCursor('down');
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
if (key.leftArrow) {
|
|
21
|
-
actions.moveCursor('left');
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
if (key.rightArrow) {
|
|
25
|
-
actions.moveCursor('right');
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
// Home/End (Ink might not provide these directly in all environments, but if it does)
|
|
29
|
-
// We check for 'home' and 'end' properties if they exist on the key object,
|
|
30
|
-
// or specific sequences if we were parsing raw input, but here we assume Ink's Key object.
|
|
31
|
-
// Note: Ink's Key interface might not have home/end in all versions, but we'll assume it does or we extend it.
|
|
32
|
-
// If not, we might need to check specific input sequences, but for now let's trust the test/types.
|
|
33
|
-
if (key.home) {
|
|
34
|
-
actions.moveCursor('lineStart');
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
if (key.end) {
|
|
38
|
-
actions.moveCursor('lineEnd');
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
// History
|
|
42
|
-
if (key.ctrl) {
|
|
43
|
-
if (input === 'z') {
|
|
44
|
-
actions.undo();
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
if (input === 'y') {
|
|
48
|
-
actions.redo();
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
if (input === 'j') {
|
|
52
|
-
actions.newLine();
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
// Editing
|
|
57
|
-
if (key.backspace) {
|
|
58
|
-
actions.delete();
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
if (key.delete) {
|
|
62
|
-
// Currently mapped to delete (backspace behavior) as per requirements/tests,
|
|
63
|
-
// but usually delete is forward.
|
|
64
|
-
// The plan said "Delete (delete at cursor)", which usually means forward.
|
|
65
|
-
// But our useTextInput only has `delete` (which is backspace).
|
|
66
|
-
// For now, we map it to `delete` as per the test "handles Delete".
|
|
67
|
-
actions.delete();
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
// Submission / New Line
|
|
71
|
-
if (key.return) {
|
|
72
|
-
if (cursor) {
|
|
73
|
-
const currentLine = buffer.lines[cursor.line];
|
|
74
|
-
// Check if line ends with backslash AND cursor is at the end (or we just check the line content?)
|
|
75
|
-
// Requirement: "Line ending with \ + Enter continues to next line"
|
|
76
|
-
// Usually this implies the user typed '\' then Enter.
|
|
77
|
-
// We should probably check if the character *before* the cursor is '\' if we want to be precise,
|
|
78
|
-
// or just if the line ends with '\'.
|
|
79
|
-
// Let's assume "line ends with \" means the last char of the line is '\'.
|
|
80
|
-
if (currentLine.endsWith('\\')) {
|
|
81
|
-
actions.delete(); // Remove the backslash
|
|
82
|
-
actions.newLine(); // Insert newline
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
actions.submit();
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
// Text Insertion
|
|
90
|
-
// Ignore control keys if they don't have a specific handler above
|
|
91
|
-
if (key.ctrl || key.meta) {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
if (input) {
|
|
95
|
-
actions.insert(input);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { Buffer, Cursor, Direction } from './types';
|
|
2
|
-
/**
|
|
3
|
-
* Create a new buffer from optional initial text
|
|
4
|
-
*/
|
|
5
|
-
export declare function createBuffer(text?: string): Buffer;
|
|
6
|
-
/**
|
|
7
|
-
* Insert a character at the cursor position
|
|
8
|
-
*/
|
|
9
|
-
export declare function insertChar(buffer: Buffer, cursor: Cursor, char: string): {
|
|
10
|
-
buffer: Buffer;
|
|
11
|
-
cursor: Cursor;
|
|
12
|
-
};
|
|
13
|
-
/**
|
|
14
|
-
* Delete character before cursor (backspace)
|
|
15
|
-
*/
|
|
16
|
-
export declare function deleteChar(buffer: Buffer, cursor: Cursor): {
|
|
17
|
-
buffer: Buffer;
|
|
18
|
-
cursor: Cursor;
|
|
19
|
-
};
|
|
20
|
-
/**
|
|
21
|
-
* Insert a new line at cursor position (splits current line)
|
|
22
|
-
*/
|
|
23
|
-
export declare function insertNewLine(buffer: Buffer, cursor: Cursor): {
|
|
24
|
-
buffer: Buffer;
|
|
25
|
-
cursor: Cursor;
|
|
26
|
-
};
|
|
27
|
-
/**
|
|
28
|
-
* Move cursor in specified direction with bounds checking
|
|
29
|
-
*/
|
|
30
|
-
export declare function moveCursor(buffer: Buffer, cursor: Cursor, direction: Direction): Cursor;
|
|
31
|
-
/**
|
|
32
|
-
* Get the full text content from buffer (lines joined with newlines)
|
|
33
|
-
*/
|
|
34
|
-
export declare function getTextContent(buffer: Buffer): string;
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Create a new buffer from optional initial text
|
|
3
|
-
*/
|
|
4
|
-
export function createBuffer(text) {
|
|
5
|
-
if (!text) {
|
|
6
|
-
return { lines: [''] };
|
|
7
|
-
}
|
|
8
|
-
return { lines: text.split('\n') };
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Insert a character at the cursor position
|
|
12
|
-
*/
|
|
13
|
-
export function insertChar(buffer, cursor, char) {
|
|
14
|
-
const { line, column } = cursor;
|
|
15
|
-
const currentLine = buffer.lines[line];
|
|
16
|
-
const newLine = currentLine.slice(0, column) + char + currentLine.slice(column);
|
|
17
|
-
const newLines = [...buffer.lines];
|
|
18
|
-
newLines[line] = newLine;
|
|
19
|
-
return {
|
|
20
|
-
buffer: { lines: newLines },
|
|
21
|
-
cursor: { line, column: column + 1 },
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Delete character before cursor (backspace)
|
|
26
|
-
*/
|
|
27
|
-
export function deleteChar(buffer, cursor) {
|
|
28
|
-
const { line, column } = cursor;
|
|
29
|
-
// At the very start of the buffer - nothing to delete
|
|
30
|
-
if (line === 0 && column === 0) {
|
|
31
|
-
return { buffer, cursor };
|
|
32
|
-
}
|
|
33
|
-
// At the start of a line - merge with previous line
|
|
34
|
-
if (column === 0) {
|
|
35
|
-
const previousLine = buffer.lines[line - 1];
|
|
36
|
-
const currentLine = buffer.lines[line];
|
|
37
|
-
const mergedLine = previousLine + currentLine;
|
|
38
|
-
const newLines = [...buffer.lines];
|
|
39
|
-
newLines[line - 1] = mergedLine;
|
|
40
|
-
newLines.splice(line, 1);
|
|
41
|
-
return {
|
|
42
|
-
buffer: { lines: newLines },
|
|
43
|
-
cursor: { line: line - 1, column: previousLine.length },
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
// Delete character within the line
|
|
47
|
-
const currentLine = buffer.lines[line];
|
|
48
|
-
const newLine = currentLine.slice(0, column - 1) + currentLine.slice(column);
|
|
49
|
-
const newLines = [...buffer.lines];
|
|
50
|
-
newLines[line] = newLine;
|
|
51
|
-
return {
|
|
52
|
-
buffer: { lines: newLines },
|
|
53
|
-
cursor: { line, column: column - 1 },
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Insert a new line at cursor position (splits current line)
|
|
58
|
-
*/
|
|
59
|
-
export function insertNewLine(buffer, cursor) {
|
|
60
|
-
const { line, column } = cursor;
|
|
61
|
-
const currentLine = buffer.lines[line];
|
|
62
|
-
const beforeCursor = currentLine.slice(0, column);
|
|
63
|
-
const afterCursor = currentLine.slice(column);
|
|
64
|
-
const newLines = [...buffer.lines];
|
|
65
|
-
newLines[line] = beforeCursor;
|
|
66
|
-
newLines.splice(line + 1, 0, afterCursor);
|
|
67
|
-
return {
|
|
68
|
-
buffer: { lines: newLines },
|
|
69
|
-
cursor: { line: line + 1, column: 0 },
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Move cursor in specified direction with bounds checking
|
|
74
|
-
*/
|
|
75
|
-
export function moveCursor(buffer, cursor, direction) {
|
|
76
|
-
const { line, column } = cursor;
|
|
77
|
-
const currentLine = buffer.lines[line];
|
|
78
|
-
const lineCount = buffer.lines.length;
|
|
79
|
-
switch (direction) {
|
|
80
|
-
case 'left':
|
|
81
|
-
if (column > 0) {
|
|
82
|
-
return { line, column: column - 1 };
|
|
83
|
-
}
|
|
84
|
-
// Wrap to end of previous line
|
|
85
|
-
if (line > 0) {
|
|
86
|
-
return { line: line - 1, column: buffer.lines[line - 1].length };
|
|
87
|
-
}
|
|
88
|
-
return cursor;
|
|
89
|
-
case 'right':
|
|
90
|
-
if (column < currentLine.length) {
|
|
91
|
-
return { line, column: column + 1 };
|
|
92
|
-
}
|
|
93
|
-
// Wrap to start of next line
|
|
94
|
-
if (line < lineCount - 1) {
|
|
95
|
-
return { line: line + 1, column: 0 };
|
|
96
|
-
}
|
|
97
|
-
return cursor;
|
|
98
|
-
case 'up':
|
|
99
|
-
if (line > 0) {
|
|
100
|
-
const targetLine = buffer.lines[line - 1];
|
|
101
|
-
return { line: line - 1, column: Math.min(column, targetLine.length) };
|
|
102
|
-
}
|
|
103
|
-
return cursor;
|
|
104
|
-
case 'down':
|
|
105
|
-
if (line < lineCount - 1) {
|
|
106
|
-
const targetLine = buffer.lines[line + 1];
|
|
107
|
-
return { line: line + 1, column: Math.min(column, targetLine.length) };
|
|
108
|
-
}
|
|
109
|
-
return cursor;
|
|
110
|
-
case 'lineStart':
|
|
111
|
-
return { line, column: 0 };
|
|
112
|
-
case 'lineEnd':
|
|
113
|
-
return { line, column: currentLine.length };
|
|
114
|
-
default:
|
|
115
|
-
return cursor;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Get the full text content from buffer (lines joined with newlines)
|
|
120
|
-
*/
|
|
121
|
-
export function getTextContent(buffer) {
|
|
122
|
-
// Single empty line is considered empty buffer
|
|
123
|
-
if (buffer.lines.length === 1 && buffer.lines[0] === '') {
|
|
124
|
-
return '';
|
|
125
|
-
}
|
|
126
|
-
return buffer.lines.join('\n');
|
|
127
|
-
}
|