ink-prompt 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/components/MultilineInput/AtomicBlocks.d.ts +10 -20
- package/dist/components/MultilineInput/AtomicBlocks.js +33 -42
- package/dist/components/MultilineInput/BlockMarker.d.ts +19 -0
- package/dist/components/MultilineInput/BlockMarker.js +83 -0
- package/dist/components/MultilineInput/BlockRegistry.d.ts +39 -0
- package/dist/components/MultilineInput/BlockRegistry.js +236 -0
- package/dist/components/MultilineInput/BlockTypes.d.ts +22 -0
- package/dist/components/MultilineInput/ImageTypes.d.ts +0 -2
- package/dist/components/MultilineInput/ImageTypes.js +1 -2
- package/dist/components/MultilineInput/ImageValidator.js +1 -1
- package/dist/components/MultilineInput/KeyHandler.d.ts +1 -1
- package/dist/components/MultilineInput/TextBuffer.d.ts +5 -31
- package/dist/components/MultilineInput/TextBuffer.js +91 -161
- package/dist/components/MultilineInput/TextRenderer.d.ts +6 -7
- package/dist/components/MultilineInput/TextRenderer.js +7 -7
- package/dist/components/MultilineInput/__tests__/BlockMarker.test.js +130 -0
- package/dist/components/MultilineInput/__tests__/BlockRegistry.test.js +225 -0
- package/dist/components/MultilineInput/__tests__/Placeholder.integration.test.js +44 -65
- package/dist/components/MultilineInput/__tests__/TextBuffer_images.test.js +10 -31
- package/dist/components/MultilineInput/__tests__/TextRenderer_images.test.js +27 -13
- package/dist/components/MultilineInput/__tests__/integration_images.test.js +2 -4
- package/dist/components/MultilineInput/__tests__/useTextInput_images.test.js +30 -29
- package/dist/components/MultilineInput/index.d.ts +6 -6
- package/dist/components/MultilineInput/index.js +2 -11
- package/dist/components/MultilineInput/types.d.ts +0 -20
- package/dist/components/MultilineInput/useTextInput.d.ts +4 -11
- package/dist/components/MultilineInput/useTextInput.js +79 -76
- package/package.json +1 -1
- package/dist/components/MultilineInput/Placeholder.d.ts +0 -30
- package/dist/components/MultilineInput/Placeholder.js +0 -200
- package/dist/components/MultilineInput/__tests__/Placeholder.test.js +0 -235
- 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.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
- /package/dist/components/MultilineInput/{__tests__/Placeholder.test.d.ts → BlockTypes.js} +0 -0
- /package/dist/{examples/examples/basic.d.ts → components/MultilineInput/__tests__/BlockMarker.test.d.ts} +0 -0
- /package/dist/{examples/src/components/MultilineInput/__tests__/KeyHandler.test.d.ts → components/MultilineInput/__tests__/BlockRegistry.test.d.ts} +0 -0
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { describe, it, expect } from 'vitest';
|
|
3
|
-
import { render } from '@testing-library/react';
|
|
4
|
-
import { TextRenderer, wrapLines } from '../TextRenderer';
|
|
5
|
-
describe('wrapLines', () => {
|
|
6
|
-
describe('no wrapping needed', () => {
|
|
7
|
-
it('returns single line unchanged when shorter than width', () => {
|
|
8
|
-
const buffer = { lines: ['hello'] };
|
|
9
|
-
const cursor = { line: 0, column: 5 };
|
|
10
|
-
const result = wrapLines(buffer, cursor, 80);
|
|
11
|
-
expect(result.visualLines).toEqual(['hello']);
|
|
12
|
-
expect(result.cursorVisualRow).toBe(0);
|
|
13
|
-
expect(result.cursorVisualCol).toBe(5);
|
|
14
|
-
});
|
|
15
|
-
it('returns multiple lines unchanged when all shorter than width', () => {
|
|
16
|
-
const buffer = { lines: ['hello', 'world'] };
|
|
17
|
-
const cursor = { line: 1, column: 3 };
|
|
18
|
-
const result = wrapLines(buffer, cursor, 80);
|
|
19
|
-
expect(result.visualLines).toEqual(['hello', 'world']);
|
|
20
|
-
expect(result.cursorVisualRow).toBe(1);
|
|
21
|
-
expect(result.cursorVisualCol).toBe(3);
|
|
22
|
-
});
|
|
23
|
-
it('handles empty buffer', () => {
|
|
24
|
-
const buffer = { lines: [''] };
|
|
25
|
-
const cursor = { line: 0, column: 0 };
|
|
26
|
-
const result = wrapLines(buffer, cursor, 80);
|
|
27
|
-
expect(result.visualLines).toEqual(['']);
|
|
28
|
-
expect(result.cursorVisualRow).toBe(0);
|
|
29
|
-
expect(result.cursorVisualCol).toBe(0);
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
describe('wrapping single line', () => {
|
|
33
|
-
it('wraps line exceeding width', () => {
|
|
34
|
-
const buffer = { lines: ['abcdefghij'] };
|
|
35
|
-
const cursor = { line: 0, column: 0 };
|
|
36
|
-
const result = wrapLines(buffer, cursor, 5);
|
|
37
|
-
expect(result.visualLines).toEqual(['abcde', 'fghij']);
|
|
38
|
-
});
|
|
39
|
-
it('wraps line into multiple visual lines', () => {
|
|
40
|
-
const buffer = { lines: ['abcdefghijklmno'] };
|
|
41
|
-
const cursor = { line: 0, column: 0 };
|
|
42
|
-
const result = wrapLines(buffer, cursor, 5);
|
|
43
|
-
expect(result.visualLines).toEqual(['abcde', 'fghij', 'klmno']);
|
|
44
|
-
});
|
|
45
|
-
it('handles exact width match (no extra empty line)', () => {
|
|
46
|
-
const buffer = { lines: ['abcde'] };
|
|
47
|
-
const cursor = { line: 0, column: 5 };
|
|
48
|
-
const result = wrapLines(buffer, cursor, 5);
|
|
49
|
-
expect(result.visualLines).toEqual(['abcde']);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
describe('cursor position in wrapped lines', () => {
|
|
53
|
-
it('cursor on first visual row', () => {
|
|
54
|
-
const buffer = { lines: ['abcdefghij'] };
|
|
55
|
-
const cursor = { line: 0, column: 3 };
|
|
56
|
-
const result = wrapLines(buffer, cursor, 5);
|
|
57
|
-
expect(result.cursorVisualRow).toBe(0);
|
|
58
|
-
expect(result.cursorVisualCol).toBe(3);
|
|
59
|
-
});
|
|
60
|
-
it('cursor on second visual row', () => {
|
|
61
|
-
const buffer = { lines: ['abcdefghij'] };
|
|
62
|
-
const cursor = { line: 0, column: 7 };
|
|
63
|
-
const result = wrapLines(buffer, cursor, 5);
|
|
64
|
-
expect(result.cursorVisualRow).toBe(1);
|
|
65
|
-
expect(result.cursorVisualCol).toBe(2);
|
|
66
|
-
});
|
|
67
|
-
it('cursor at wrap boundary (end of first visual row)', () => {
|
|
68
|
-
const buffer = { lines: ['abcdefghij'] };
|
|
69
|
-
const cursor = { line: 0, column: 5 };
|
|
70
|
-
const result = wrapLines(buffer, cursor, 5);
|
|
71
|
-
expect(result.cursorVisualRow).toBe(1);
|
|
72
|
-
expect(result.cursorVisualCol).toBe(0);
|
|
73
|
-
});
|
|
74
|
-
it('cursor at end of wrapped line', () => {
|
|
75
|
-
const buffer = { lines: ['abcdefghij'] };
|
|
76
|
-
const cursor = { line: 0, column: 10 };
|
|
77
|
-
const result = wrapLines(buffer, cursor, 5);
|
|
78
|
-
expect(result.cursorVisualRow).toBe(1);
|
|
79
|
-
expect(result.cursorVisualCol).toBe(5);
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
describe('wrapping multiple lines', () => {
|
|
83
|
-
it('wraps only the line that exceeds width', () => {
|
|
84
|
-
const buffer = { lines: ['hi', 'abcdefghij'] };
|
|
85
|
-
const cursor = { line: 0, column: 0 };
|
|
86
|
-
const result = wrapLines(buffer, cursor, 5);
|
|
87
|
-
expect(result.visualLines).toEqual(['hi', 'abcde', 'fghij']);
|
|
88
|
-
});
|
|
89
|
-
it('cursor on second logical line that wraps', () => {
|
|
90
|
-
const buffer = { lines: ['hi', 'abcdefghij'] };
|
|
91
|
-
const cursor = { line: 1, column: 7 };
|
|
92
|
-
const result = wrapLines(buffer, cursor, 5);
|
|
93
|
-
expect(result.visualLines).toEqual(['hi', 'abcde', 'fghij']);
|
|
94
|
-
expect(result.cursorVisualRow).toBe(2);
|
|
95
|
-
expect(result.cursorVisualCol).toBe(2);
|
|
96
|
-
});
|
|
97
|
-
it('handles multiple wrapped lines', () => {
|
|
98
|
-
const buffer = { lines: ['abcdefg', 'hijklmn'] };
|
|
99
|
-
const cursor = { line: 1, column: 5 };
|
|
100
|
-
const result = wrapLines(buffer, cursor, 4);
|
|
101
|
-
expect(result.visualLines).toEqual(['abcd', 'efg', 'hijk', 'lmn']);
|
|
102
|
-
expect(result.cursorVisualRow).toBe(3);
|
|
103
|
-
expect(result.cursorVisualCol).toBe(1);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
describe('TextRenderer', () => {
|
|
108
|
-
describe('basic rendering', () => {
|
|
109
|
-
it('renders single line', () => {
|
|
110
|
-
const buffer = { lines: ['hello'] };
|
|
111
|
-
const cursor = { line: 0, column: 0 };
|
|
112
|
-
const { container } = render(React.createElement(TextRenderer, { buffer: buffer, cursor: cursor }));
|
|
113
|
-
expect(container.textContent).toContain('hello');
|
|
114
|
-
});
|
|
115
|
-
it('renders multiple lines', () => {
|
|
116
|
-
const buffer = { lines: ['hello', 'world'] };
|
|
117
|
-
const cursor = { line: 0, column: 0 };
|
|
118
|
-
const { container } = render(React.createElement(TextRenderer, { buffer: buffer, cursor: cursor }));
|
|
119
|
-
expect(container.textContent).toContain('hello');
|
|
120
|
-
expect(container.textContent).toContain('world');
|
|
121
|
-
});
|
|
122
|
-
it('renders empty buffer', () => {
|
|
123
|
-
const buffer = { lines: [''] };
|
|
124
|
-
const cursor = { line: 0, column: 0 };
|
|
125
|
-
const { container } = render(React.createElement(TextRenderer, { buffer: buffer, cursor: cursor }));
|
|
126
|
-
// Should render cursor character in empty buffer
|
|
127
|
-
expect(container.textContent).toContain('█');
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
describe('cursor display', () => {
|
|
131
|
-
it('shows cursor at start of line', () => {
|
|
132
|
-
const buffer = { lines: ['hello'] };
|
|
133
|
-
const cursor = { line: 0, column: 0 };
|
|
134
|
-
const { container } = render(React.createElement(TextRenderer, { buffer: buffer, cursor: cursor, showCursor: true }));
|
|
135
|
-
// Cursor should be rendered before 'hello'
|
|
136
|
-
expect(container.textContent).toContain('█hello');
|
|
137
|
-
});
|
|
138
|
-
it('shows cursor at end of line', () => {
|
|
139
|
-
const buffer = { lines: ['hello'] };
|
|
140
|
-
const cursor = { line: 0, column: 5 };
|
|
141
|
-
const { container } = render(React.createElement(TextRenderer, { buffer: buffer, cursor: cursor, showCursor: true }));
|
|
142
|
-
expect(container.textContent).toContain('hello█');
|
|
143
|
-
});
|
|
144
|
-
it('shows cursor in middle of line', () => {
|
|
145
|
-
const buffer = { lines: ['hello'] };
|
|
146
|
-
const cursor = { line: 0, column: 2 };
|
|
147
|
-
const { container } = render(React.createElement(TextRenderer, { buffer: buffer, cursor: cursor, showCursor: true }));
|
|
148
|
-
expect(container.textContent).toContain('he█llo');
|
|
149
|
-
});
|
|
150
|
-
it('hides cursor when showCursor is false', () => {
|
|
151
|
-
const buffer = { lines: ['hello'] };
|
|
152
|
-
const cursor = { line: 0, column: 0 };
|
|
153
|
-
const { container } = render(React.createElement(TextRenderer, { buffer: buffer, cursor: cursor, showCursor: false }));
|
|
154
|
-
expect(container.textContent).not.toContain('█');
|
|
155
|
-
expect(container.textContent).toContain('hello');
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
describe('word wrapping in render', () => {
|
|
159
|
-
it('renders wrapped lines', () => {
|
|
160
|
-
const buffer = { lines: ['abcdefghij'] };
|
|
161
|
-
const cursor = { line: 0, column: 0 };
|
|
162
|
-
const { container } = render(React.createElement(TextRenderer, { buffer: buffer, cursor: cursor, width: 5 }));
|
|
163
|
-
// Both visual lines should be present
|
|
164
|
-
expect(container.textContent).toContain('abcde');
|
|
165
|
-
expect(container.textContent).toContain('fghij');
|
|
166
|
-
});
|
|
167
|
-
it('shows cursor correctly on wrapped line', () => {
|
|
168
|
-
const buffer = { lines: ['abcdefghij'] };
|
|
169
|
-
const cursor = { line: 0, column: 7 };
|
|
170
|
-
const { container } = render(React.createElement(TextRenderer, { buffer: buffer, cursor: cursor, width: 5, showCursor: true }));
|
|
171
|
-
// Cursor at position 7 wraps to second visual row, column 2
|
|
172
|
-
// So we should see 'fg█hij' on the second visual row
|
|
173
|
-
expect(container.textContent).toContain('fg█hij');
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
-
import { render } from '@testing-library/react';
|
|
4
|
-
import { MultilineInputCore } from '../index';
|
|
5
|
-
/**
|
|
6
|
-
* Integration tests for MultilineInputCore.
|
|
7
|
-
*
|
|
8
|
-
* These tests validate the component's rendering and props behavior.
|
|
9
|
-
* We test MultilineInputCore instead of MultilineInput to avoid requiring
|
|
10
|
-
* the Ink runtime (useInput, useStdout hooks).
|
|
11
|
-
*
|
|
12
|
-
* Keyboard handling is thoroughly tested via KeyHandler.test.ts.
|
|
13
|
-
* State management is tested via useTextInput.test.ts.
|
|
14
|
-
*/
|
|
15
|
-
describe('MultilineInputCore', () => {
|
|
16
|
-
describe('Rendering', () => {
|
|
17
|
-
it('renders empty input with cursor', () => {
|
|
18
|
-
const { container } = render(React.createElement(MultilineInputCore, null));
|
|
19
|
-
expect(container.textContent).toContain('█');
|
|
20
|
-
});
|
|
21
|
-
it('renders with initial value', () => {
|
|
22
|
-
const { container } = render(React.createElement(MultilineInputCore, { value: "hello" }));
|
|
23
|
-
expect(container.textContent).toContain('hello');
|
|
24
|
-
});
|
|
25
|
-
it('renders multiline value', () => {
|
|
26
|
-
const { container } = render(React.createElement(MultilineInputCore, { value: "line1\\nline2" }));
|
|
27
|
-
expect(container.textContent).toContain('line1');
|
|
28
|
-
expect(container.textContent).toContain('line2');
|
|
29
|
-
});
|
|
30
|
-
it('shows cursor at end of value', () => {
|
|
31
|
-
const { container } = render(React.createElement(MultilineInputCore, { value: "hi" }));
|
|
32
|
-
expect(container.textContent).toContain('hi█');
|
|
33
|
-
});
|
|
34
|
-
it('hides cursor when showCursor is false', () => {
|
|
35
|
-
const { container } = render(React.createElement(MultilineInputCore, { value: "hello", showCursor: false }));
|
|
36
|
-
expect(container.textContent).not.toContain('█');
|
|
37
|
-
expect(container.textContent).toContain('hello');
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
describe('Props', () => {
|
|
41
|
-
it('accepts width prop for word wrapping', () => {
|
|
42
|
-
const { container } = render(React.createElement(MultilineInputCore, { value: "abcdefghij", width: 5 }));
|
|
43
|
-
// Should wrap at width 5
|
|
44
|
-
expect(container.textContent).toContain('abcde');
|
|
45
|
-
expect(container.textContent).toContain('fghij');
|
|
46
|
-
});
|
|
47
|
-
it('calls onChange on initial render with value', () => {
|
|
48
|
-
const onChange = vi.fn();
|
|
49
|
-
render(React.createElement(MultilineInputCore, { value: "test", onChange: onChange }));
|
|
50
|
-
// onChange is called with the initial value
|
|
51
|
-
expect(onChange).toHaveBeenCalledWith('test');
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
describe('Placeholder', () => {
|
|
55
|
-
it('shows placeholder when empty and cursor hidden', () => {
|
|
56
|
-
const { container } = render(React.createElement(MultilineInputCore, { placeholder: "Type here...", showCursor: false }));
|
|
57
|
-
expect(container.textContent).toContain('Type here...');
|
|
58
|
-
});
|
|
59
|
-
it('does not show placeholder when showCursor is true', () => {
|
|
60
|
-
const { container } = render(React.createElement(MultilineInputCore, { placeholder: "Type here...", showCursor: true }));
|
|
61
|
-
// Shows cursor instead, not placeholder
|
|
62
|
-
expect(container.textContent).toContain('█');
|
|
63
|
-
expect(container.textContent).not.toContain('Type here...');
|
|
64
|
-
});
|
|
65
|
-
it('does not show placeholder when has value', () => {
|
|
66
|
-
const { container } = render(React.createElement(MultilineInputCore, { value: "hello", placeholder: "Type here...", showCursor: false }));
|
|
67
|
-
expect(container.textContent).toContain('hello');
|
|
68
|
-
expect(container.textContent).not.toContain('Type here...');
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { renderHook, act } from '@testing-library/react';
|
|
2
|
-
import { describe, it, expect } from 'vitest';
|
|
3
|
-
import { useTextInput } from '../useTextInput';
|
|
4
|
-
describe('useTextInput', () => {
|
|
5
|
-
it('should initialize with empty buffer', () => {
|
|
6
|
-
const { result } = renderHook(() => useTextInput());
|
|
7
|
-
expect(result.current.value).toBe('');
|
|
8
|
-
expect(result.current.cursor).toEqual({ line: 0, column: 0 });
|
|
9
|
-
});
|
|
10
|
-
it('should initialize with initial value', () => {
|
|
11
|
-
const { result } = renderHook(() => useTextInput({ initialValue: 'Hello\nWorld' }));
|
|
12
|
-
expect(result.current.value).toBe('Hello\nWorld');
|
|
13
|
-
expect(result.current.cursor).toEqual({ line: 1, column: 5 }); // Cursor at end
|
|
14
|
-
});
|
|
15
|
-
it('should insert character', () => {
|
|
16
|
-
const { result } = renderHook(() => useTextInput());
|
|
17
|
-
act(() => {
|
|
18
|
-
result.current.insert('a');
|
|
19
|
-
});
|
|
20
|
-
expect(result.current.value).toBe('a');
|
|
21
|
-
expect(result.current.cursor).toEqual({ line: 0, column: 1 });
|
|
22
|
-
});
|
|
23
|
-
it('should delete character', () => {
|
|
24
|
-
const { result } = renderHook(() => useTextInput({ initialValue: 'abc' }));
|
|
25
|
-
act(() => {
|
|
26
|
-
result.current.delete();
|
|
27
|
-
});
|
|
28
|
-
expect(result.current.value).toBe('ab');
|
|
29
|
-
expect(result.current.cursor).toEqual({ line: 0, column: 2 });
|
|
30
|
-
});
|
|
31
|
-
it('should insert new line', () => {
|
|
32
|
-
const { result } = renderHook(() => useTextInput({ initialValue: 'abc' }));
|
|
33
|
-
// Move cursor to middle
|
|
34
|
-
act(() => {
|
|
35
|
-
result.current.moveCursor('left');
|
|
36
|
-
});
|
|
37
|
-
act(() => {
|
|
38
|
-
result.current.newLine();
|
|
39
|
-
});
|
|
40
|
-
expect(result.current.value).toBe('ab\nc');
|
|
41
|
-
expect(result.current.cursor).toEqual({ line: 1, column: 0 });
|
|
42
|
-
});
|
|
43
|
-
it('should move cursor', () => {
|
|
44
|
-
const { result } = renderHook(() => useTextInput({ initialValue: 'abc' }));
|
|
45
|
-
act(() => {
|
|
46
|
-
result.current.moveCursor('left');
|
|
47
|
-
});
|
|
48
|
-
expect(result.current.cursor).toEqual({ line: 0, column: 2 });
|
|
49
|
-
});
|
|
50
|
-
it('should support undo/redo', () => {
|
|
51
|
-
const { result } = renderHook(() => useTextInput());
|
|
52
|
-
act(() => {
|
|
53
|
-
result.current.insert('a');
|
|
54
|
-
});
|
|
55
|
-
expect(result.current.value).toBe('a');
|
|
56
|
-
act(() => {
|
|
57
|
-
result.current.undo();
|
|
58
|
-
});
|
|
59
|
-
expect(result.current.value).toBe('');
|
|
60
|
-
act(() => {
|
|
61
|
-
result.current.redo();
|
|
62
|
-
});
|
|
63
|
-
expect(result.current.value).toBe('a');
|
|
64
|
-
});
|
|
65
|
-
});
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
export interface MultilineInputProps {
|
|
3
|
-
/** Controlled text value */
|
|
4
|
-
value?: string;
|
|
5
|
-
/** Called when text changes */
|
|
6
|
-
onChange?: (value: string) => void;
|
|
7
|
-
/** Called when user submits (Enter without backslash) */
|
|
8
|
-
onSubmit?: (value: string) => void;
|
|
9
|
-
/** Placeholder text when empty */
|
|
10
|
-
placeholder?: string;
|
|
11
|
-
/** Whether to show the cursor (defaults to true) */
|
|
12
|
-
showCursor?: boolean;
|
|
13
|
-
/** Terminal width for word wrapping */
|
|
14
|
-
width?: number;
|
|
15
|
-
/** Whether input is active/focused (defaults to true) */
|
|
16
|
-
isActive?: boolean;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Props for the core component (without Ink-specific hooks)
|
|
20
|
-
* This allows testing the rendering logic separately.
|
|
21
|
-
*/
|
|
22
|
-
export interface MultilineInputCoreProps {
|
|
23
|
-
value?: string;
|
|
24
|
-
onChange?: (value: string) => void;
|
|
25
|
-
onSubmit?: (value: string) => void;
|
|
26
|
-
placeholder?: string;
|
|
27
|
-
showCursor?: boolean;
|
|
28
|
-
width?: number;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Core rendering component that can be tested without Ink runtime.
|
|
32
|
-
* Does not include useInput/useStdout hooks.
|
|
33
|
-
*/
|
|
34
|
-
export declare const MultilineInputCore: React.FC<MultilineInputCoreProps>;
|
|
35
|
-
/**
|
|
36
|
-
* Full MultilineInput with Ink keyboard handling.
|
|
37
|
-
* This component uses Ink-specific hooks and must be rendered in an Ink context.
|
|
38
|
-
*/
|
|
39
|
-
export declare const MultilineInput: React.FC<MultilineInputProps>;
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useCallback } from 'react';
|
|
2
|
-
import { useTextInput } from './useTextInput';
|
|
3
|
-
import { handleKey } from './KeyHandler';
|
|
4
|
-
import { TextRenderer } from './TextRenderer';
|
|
5
|
-
import { createBuffer } from './TextBuffer';
|
|
6
|
-
/**
|
|
7
|
-
* Core rendering component that can be tested without Ink runtime.
|
|
8
|
-
* Does not include useInput/useStdout hooks.
|
|
9
|
-
*/
|
|
10
|
-
export const MultilineInputCore = ({ value, onChange, placeholder, showCursor = true, width = 80, }) => {
|
|
11
|
-
const textInput = useTextInput({ initialValue: value ?? '' });
|
|
12
|
-
// Sync external value changes
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
if (value !== undefined && value !== textInput.value) {
|
|
15
|
-
textInput.setText(value);
|
|
16
|
-
}
|
|
17
|
-
}, [value]);
|
|
18
|
-
// Notify parent of changes
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
onChange?.(textInput.value);
|
|
21
|
-
}, [textInput.value, onChange]);
|
|
22
|
-
// Create buffer for TextRenderer
|
|
23
|
-
const buffer = createBuffer(textInput.value);
|
|
24
|
-
// Show placeholder if empty and no cursor shown
|
|
25
|
-
const isEmpty = textInput.value === '';
|
|
26
|
-
const showPlaceholder = isEmpty && placeholder && !showCursor;
|
|
27
|
-
if (showPlaceholder) {
|
|
28
|
-
return React.createElement("div", { style: { opacity: 0.5 } }, placeholder);
|
|
29
|
-
}
|
|
30
|
-
return (React.createElement(TextRenderer, { buffer: buffer, cursor: textInput.cursor, width: width, showCursor: showCursor }));
|
|
31
|
-
};
|
|
32
|
-
/**
|
|
33
|
-
* Full MultilineInput with Ink keyboard handling.
|
|
34
|
-
* This component uses Ink-specific hooks and must be rendered in an Ink context.
|
|
35
|
-
*/
|
|
36
|
-
export const MultilineInput = ({ value, onChange, onSubmit, placeholder, showCursor = true, width, isActive = true, }) => {
|
|
37
|
-
// Import Ink hooks dynamically to avoid import-time errors in tests
|
|
38
|
-
const { useInput, useStdout, Box, Text } = require('ink');
|
|
39
|
-
// Get terminal width from Ink if not provided
|
|
40
|
-
const { stdout } = useStdout();
|
|
41
|
-
const terminalWidth = width ?? stdout?.columns ?? 80;
|
|
42
|
-
const textInput = useTextInput({ initialValue: value ?? '' });
|
|
43
|
-
// Sync external value changes
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
if (value !== undefined && value !== textInput.value) {
|
|
46
|
-
textInput.setText(value);
|
|
47
|
-
}
|
|
48
|
-
}, [value]);
|
|
49
|
-
// Notify parent of changes
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
onChange?.(textInput.value);
|
|
52
|
-
}, [textInput.value, onChange]);
|
|
53
|
-
// Create buffer for TextRenderer and KeyHandler
|
|
54
|
-
const buffer = createBuffer(textInput.value);
|
|
55
|
-
// Create submit handler
|
|
56
|
-
const handleSubmit = useCallback(() => {
|
|
57
|
-
onSubmit?.(textInput.value);
|
|
58
|
-
}, [onSubmit, textInput.value]);
|
|
59
|
-
// Create actions for KeyHandler
|
|
60
|
-
const actions = {
|
|
61
|
-
insert: textInput.insert,
|
|
62
|
-
delete: textInput.delete,
|
|
63
|
-
newLine: textInput.newLine,
|
|
64
|
-
moveCursor: textInput.moveCursor,
|
|
65
|
-
undo: textInput.undo,
|
|
66
|
-
redo: textInput.redo,
|
|
67
|
-
setText: textInput.setText,
|
|
68
|
-
submit: handleSubmit,
|
|
69
|
-
};
|
|
70
|
-
// Handle keyboard input
|
|
71
|
-
useInput((input, key) => {
|
|
72
|
-
handleKey(key, input, buffer, actions, textInput.cursor);
|
|
73
|
-
}, { isActive });
|
|
74
|
-
// Show placeholder if empty and no cursor shown
|
|
75
|
-
const isEmpty = textInput.value === '';
|
|
76
|
-
const showPlaceholder = isEmpty && placeholder && !showCursor;
|
|
77
|
-
if (showPlaceholder) {
|
|
78
|
-
return (React.createElement(Box, null,
|
|
79
|
-
React.createElement(Text, { dimColor: true }, placeholder)));
|
|
80
|
-
}
|
|
81
|
-
return (React.createElement(TextRenderer, { buffer: buffer, cursor: textInput.cursor, width: terminalWidth, showCursor: showCursor }));
|
|
82
|
-
};
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cursor position in the text buffer
|
|
3
|
-
*/
|
|
4
|
-
export interface Cursor {
|
|
5
|
-
/** 0-indexed line number */
|
|
6
|
-
line: number;
|
|
7
|
-
/** 0-indexed column position */
|
|
8
|
-
column: number;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Text buffer containing multiple lines
|
|
12
|
-
*/
|
|
13
|
-
export interface Buffer {
|
|
14
|
-
/** Array of text lines (without newline characters) */
|
|
15
|
-
lines: string[];
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Cursor movement directions
|
|
19
|
-
*/
|
|
20
|
-
export type Direction = 'up' | 'down' | 'left' | 'right' | 'lineStart' | 'lineEnd';
|
|
21
|
-
/**
|
|
22
|
-
* Result of wrapping buffer lines for visual display
|
|
23
|
-
*/
|
|
24
|
-
export interface WrapResult {
|
|
25
|
-
/** Visual lines after wrapping */
|
|
26
|
-
visualLines: string[];
|
|
27
|
-
/** Row in visual lines where cursor appears */
|
|
28
|
-
cursorVisualRow: number;
|
|
29
|
-
/** Column in that visual row where cursor appears */
|
|
30
|
-
cursorVisualCol: number;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Keyboard key state (mirrors Ink's Key interface)
|
|
34
|
-
* Defined locally to avoid ESM/CJS import issues with Ink
|
|
35
|
-
*/
|
|
36
|
-
export interface Key {
|
|
37
|
-
upArrow?: boolean;
|
|
38
|
-
downArrow?: boolean;
|
|
39
|
-
leftArrow?: boolean;
|
|
40
|
-
rightArrow?: boolean;
|
|
41
|
-
pageDown?: boolean;
|
|
42
|
-
pageUp?: boolean;
|
|
43
|
-
return?: boolean;
|
|
44
|
-
escape?: boolean;
|
|
45
|
-
ctrl?: boolean;
|
|
46
|
-
shift?: boolean;
|
|
47
|
-
tab?: boolean;
|
|
48
|
-
backspace?: boolean;
|
|
49
|
-
delete?: boolean;
|
|
50
|
-
meta?: boolean;
|
|
51
|
-
/** Home key (may not be available in all terminals) */
|
|
52
|
-
home?: boolean;
|
|
53
|
-
/** End key (may not be available in all terminals) */
|
|
54
|
-
end?: boolean;
|
|
55
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { Cursor, Direction } from './types';
|
|
2
|
-
export interface UseTextInputProps {
|
|
3
|
-
initialValue?: string;
|
|
4
|
-
}
|
|
5
|
-
export interface UseTextInputResult {
|
|
6
|
-
value: string;
|
|
7
|
-
cursor: Cursor;
|
|
8
|
-
insert: (char: string) => void;
|
|
9
|
-
delete: () => void;
|
|
10
|
-
newLine: () => void;
|
|
11
|
-
moveCursor: (direction: Direction) => void;
|
|
12
|
-
undo: () => void;
|
|
13
|
-
redo: () => void;
|
|
14
|
-
setText: (text: string) => void;
|
|
15
|
-
}
|
|
16
|
-
export declare function useTextInput({ initialValue }?: UseTextInputProps): UseTextInputResult;
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { useState, useCallback } from 'react';
|
|
2
|
-
import { createBuffer, insertChar as bufferInsertChar, deleteChar as bufferDeleteChar, insertNewLine as bufferInsertNewLine, moveCursor as bufferMoveCursor, getTextContent, } from './TextBuffer';
|
|
3
|
-
export function useTextInput({ initialValue = '' } = {}) {
|
|
4
|
-
const [buffer, setBuffer] = useState(() => createBuffer(initialValue));
|
|
5
|
-
const [cursor, setCursor] = useState(() => {
|
|
6
|
-
const lines = initialValue.split('\n');
|
|
7
|
-
return {
|
|
8
|
-
line: lines.length - 1,
|
|
9
|
-
column: lines[lines.length - 1].length,
|
|
10
|
-
};
|
|
11
|
-
});
|
|
12
|
-
const [undoStack, setUndoStack] = useState([]);
|
|
13
|
-
const [redoStack, setRedoStack] = useState([]);
|
|
14
|
-
const pushToHistory = useCallback((currentBuffer, currentCursor) => {
|
|
15
|
-
setUndoStack((prev) => [...prev, { buffer: currentBuffer, cursor: currentCursor }]);
|
|
16
|
-
setRedoStack([]);
|
|
17
|
-
}, []);
|
|
18
|
-
const insert = useCallback((char) => {
|
|
19
|
-
pushToHistory(buffer, cursor);
|
|
20
|
-
const result = bufferInsertChar(buffer, cursor, char);
|
|
21
|
-
setBuffer(result.buffer);
|
|
22
|
-
setCursor(result.cursor);
|
|
23
|
-
}, [buffer, cursor, pushToHistory]);
|
|
24
|
-
const deleteChar = useCallback(() => {
|
|
25
|
-
pushToHistory(buffer, cursor);
|
|
26
|
-
const result = bufferDeleteChar(buffer, cursor);
|
|
27
|
-
setBuffer(result.buffer);
|
|
28
|
-
setCursor(result.cursor);
|
|
29
|
-
}, [buffer, cursor, pushToHistory]);
|
|
30
|
-
const newLine = useCallback(() => {
|
|
31
|
-
pushToHistory(buffer, cursor);
|
|
32
|
-
const result = bufferInsertNewLine(buffer, cursor);
|
|
33
|
-
setBuffer(result.buffer);
|
|
34
|
-
setCursor(result.cursor);
|
|
35
|
-
}, [buffer, cursor, pushToHistory]);
|
|
36
|
-
const moveCursor = useCallback((direction) => {
|
|
37
|
-
const newCursor = bufferMoveCursor(buffer, cursor, direction);
|
|
38
|
-
setCursor(newCursor);
|
|
39
|
-
}, [buffer, cursor]);
|
|
40
|
-
const undo = useCallback(() => {
|
|
41
|
-
if (undoStack.length === 0)
|
|
42
|
-
return;
|
|
43
|
-
const previousState = undoStack[undoStack.length - 1];
|
|
44
|
-
const newUndoStack = undoStack.slice(0, -1);
|
|
45
|
-
setRedoStack((prev) => [...prev, { buffer, cursor }]);
|
|
46
|
-
setBuffer(previousState.buffer);
|
|
47
|
-
setCursor(previousState.cursor);
|
|
48
|
-
setUndoStack(newUndoStack);
|
|
49
|
-
}, [buffer, cursor, undoStack]);
|
|
50
|
-
const redo = useCallback(() => {
|
|
51
|
-
if (redoStack.length === 0)
|
|
52
|
-
return;
|
|
53
|
-
const nextState = redoStack[redoStack.length - 1];
|
|
54
|
-
const newRedoStack = redoStack.slice(0, -1);
|
|
55
|
-
setUndoStack((prev) => [...prev, { buffer, cursor }]);
|
|
56
|
-
setBuffer(nextState.buffer);
|
|
57
|
-
setCursor(nextState.cursor);
|
|
58
|
-
setRedoStack(newRedoStack);
|
|
59
|
-
}, [buffer, cursor, redoStack]);
|
|
60
|
-
const setText = useCallback((text) => {
|
|
61
|
-
pushToHistory(buffer, cursor);
|
|
62
|
-
const newBuffer = createBuffer(text);
|
|
63
|
-
setBuffer(newBuffer);
|
|
64
|
-
// Move cursor to end of new text
|
|
65
|
-
const lines = text.split('\n');
|
|
66
|
-
setCursor({
|
|
67
|
-
line: lines.length - 1,
|
|
68
|
-
column: lines[lines.length - 1].length,
|
|
69
|
-
});
|
|
70
|
-
}, [buffer, cursor, pushToHistory]);
|
|
71
|
-
return {
|
|
72
|
-
value: getTextContent(buffer),
|
|
73
|
-
cursor,
|
|
74
|
-
insert,
|
|
75
|
-
delete: deleteChar,
|
|
76
|
-
newLine,
|
|
77
|
-
moveCursor,
|
|
78
|
-
undo,
|
|
79
|
-
redo,
|
|
80
|
-
setText,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
describe('Hello World Test', () => {
|
|
3
|
-
it('should pass a simple test', () => {
|
|
4
|
-
expect(true).toBe(true);
|
|
5
|
-
});
|
|
6
|
-
it('should perform basic arithmetic', () => {
|
|
7
|
-
expect(2 + 2).toBe(4);
|
|
8
|
-
});
|
|
9
|
-
it('should handle string concatenation', () => {
|
|
10
|
-
const greeting = 'Hello' + ' ' + 'World';
|
|
11
|
-
expect(greeting).toBe('Hello World');
|
|
12
|
-
});
|
|
13
|
-
});
|
|
File without changes
|
|
File without changes
|