aiexecode 1.0.66 → 1.0.68
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.
Potentially problematic release.
This version of aiexecode might be problematic. Click here for more details.
- package/config_template/settings.json +1 -3
- package/index.js +46 -71
- package/package.json +1 -12
- package/payload_viewer/out/404/index.html +1 -1
- package/payload_viewer/out/404.html +1 -1
- package/payload_viewer/out/index.html +1 -1
- package/payload_viewer/out/index.txt +1 -1
- package/payload_viewer/web_server.js +0 -163
- package/src/ai_based/completion_judge.js +96 -5
- package/src/ai_based/orchestrator.js +71 -3
- package/src/ai_based/pip_package_installer.js +14 -12
- package/src/ai_based/pip_package_lookup.js +13 -10
- package/src/commands/apikey.js +8 -34
- package/src/commands/help.js +3 -4
- package/src/commands/model.js +17 -74
- package/src/commands/reasoning_effort.js +1 -1
- package/src/config/feature_flags.js +0 -12
- package/src/{ui → frontend}/App.js +23 -25
- package/src/frontend/README.md +81 -0
- package/src/{ui/components/SuggestionsDisplay.js → frontend/components/AutocompleteMenu.js} +3 -3
- package/src/{ui/components/HistoryItemDisplay.js → frontend/components/ConversationItem.js} +37 -89
- package/src/{ui → frontend}/components/CurrentModelView.js +3 -5
- package/src/{ui → frontend}/components/Footer.js +4 -6
- package/src/{ui → frontend}/components/Header.js +2 -5
- package/src/{ui/components/InputPrompt.js → frontend/components/Input.js} +16 -54
- package/src/frontend/components/ModelListView.js +106 -0
- package/src/{ui → frontend}/components/ModelUpdatedView.js +3 -5
- package/src/{ui → frontend}/components/SessionSpinner.js +3 -3
- package/src/{ui → frontend}/components/SetupWizard.js +8 -101
- package/src/{ui → frontend}/components/ToolApprovalPrompt.js +16 -14
- package/src/frontend/design/themeColors.js +42 -0
- package/src/{ui → frontend}/index.js +7 -7
- package/src/frontend/utils/inputBuffer.js +441 -0
- package/src/{ui/utils/markdownRenderer.js → frontend/utils/markdownParser.js} +3 -3
- package/src/{ui/utils/ConsolePatcher.js → frontend/utils/outputRedirector.js} +9 -9
- package/src/{ui/utils/codeColorizer.js → frontend/utils/syntaxHighlighter.js} +2 -3
- package/src/system/ai_request.js +145 -595
- package/src/system/code_executer.js +111 -16
- package/src/system/file_integrity.js +5 -7
- package/src/system/log.js +3 -3
- package/src/system/mcp_integration.js +15 -13
- package/src/system/output_helper.js +0 -20
- package/src/system/session.js +97 -23
- package/src/system/session_memory.js +2 -82
- package/src/system/system_info.js +1 -1
- package/src/system/ui_events.js +0 -43
- package/src/tools/code_editor.js +17 -2
- package/src/tools/file_reader.js +17 -2
- package/src/tools/glob.js +9 -1
- package/src/tools/response_message.js +0 -2
- package/src/tools/ripgrep.js +9 -1
- package/src/tools/web_downloader.js +9 -1
- package/src/util/config.js +3 -8
- package/src/util/debug_log.js +4 -11
- package/src/util/mcp_config_manager.js +3 -5
- package/src/util/output_formatter.js +0 -47
- package/src/util/prompt_loader.js +3 -4
- package/src/util/safe_fs.js +60 -0
- package/src/util/setup_wizard.js +1 -3
- package/src/util/text_formatter.js +0 -86
- package/src/config/claude_models.js +0 -195
- package/src/ui/README.md +0 -208
- package/src/ui/api.js +0 -167
- package/src/ui/components/AgenticProgressDisplay.js +0 -126
- package/src/ui/components/Composer.js +0 -55
- package/src/ui/components/LoadingIndicator.js +0 -54
- package/src/ui/components/ModelListView.js +0 -214
- package/src/ui/components/Notifications.js +0 -55
- package/src/ui/components/StreamingIndicator.js +0 -36
- package/src/ui/contexts/AppContext.js +0 -25
- package/src/ui/contexts/StreamingContext.js +0 -20
- package/src/ui/contexts/UIStateContext.js +0 -117
- package/src/ui/example-usage.js +0 -180
- package/src/ui/hooks/useTerminalResize.js +0 -39
- package/src/ui/themes/semantic-tokens.js +0 -73
- package/src/ui/utils/text-buffer.js +0 -975
- /package/payload_viewer/out/_next/static/{t0WTsjXST7ISD1Boa6ifx → Z3AZSKhutj-kS4L8VpcOl}/_buildManifest.js +0 -0
- /package/payload_viewer/out/_next/static/{t0WTsjXST7ISD1Boa6ifx → Z3AZSKhutj-kS4L8VpcOl}/_clientMiddlewareManifest.json +0 -0
- /package/payload_viewer/out/_next/static/{t0WTsjXST7ISD1Boa6ifx → Z3AZSKhutj-kS4L8VpcOl}/_ssgManifest.js +0 -0
- /package/src/{ui → frontend}/components/BlankLine.js +0 -0
- /package/src/{ui → frontend}/components/FileDiffViewer.js +0 -0
- /package/src/{ui → frontend}/components/HelpView.js +0 -0
- /package/src/{ui → frontend}/hooks/useCompletion.js +0 -0
- /package/src/{ui → frontend}/hooks/useKeypress.js +0 -0
- /package/src/{ui → frontend}/utils/diffUtils.js +0 -0
- /package/src/{ui → frontend}/utils/renderInkComponent.js +0 -0
|
@@ -1,975 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Advanced Text Buffer for multi-line input with cursor management
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import stringWidth from 'string-width';
|
|
6
|
-
|
|
7
|
-
function consolelog() { }
|
|
8
|
-
/**
|
|
9
|
-
* Convert string to code points array
|
|
10
|
-
*/
|
|
11
|
-
export function toCodePoints(str) {
|
|
12
|
-
return Array.from(str);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Get length in code points
|
|
17
|
-
*/
|
|
18
|
-
export function cpLen(str) {
|
|
19
|
-
return toCodePoints(str).length;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Slice string by code points
|
|
24
|
-
*/
|
|
25
|
-
export function cpSlice(str, start, end) {
|
|
26
|
-
const cps = toCodePoints(str);
|
|
27
|
-
return cps.slice(start, end).join('');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Convert logical position (row, col) to offset
|
|
32
|
-
*/
|
|
33
|
-
export function logicalPosToOffset(lines, row, col) {
|
|
34
|
-
let offset = 0;
|
|
35
|
-
for (let i = 0; i < row && i < lines.length; i++) {
|
|
36
|
-
offset += cpLen(lines[i]) + 1; // +1 for newline
|
|
37
|
-
}
|
|
38
|
-
offset += Math.min(col, cpLen(lines[row] || ''));
|
|
39
|
-
return offset;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Convert offset to logical position
|
|
44
|
-
*/
|
|
45
|
-
export function offsetToLogicalPos(lines, offset) {
|
|
46
|
-
let currentOffset = 0;
|
|
47
|
-
for (let row = 0; row < lines.length; row++) {
|
|
48
|
-
const lineLen = cpLen(lines[row]);
|
|
49
|
-
if (currentOffset + lineLen >= offset) {
|
|
50
|
-
return [row, offset - currentOffset];
|
|
51
|
-
}
|
|
52
|
-
currentOffset += lineLen + 1; // +1 for newline
|
|
53
|
-
}
|
|
54
|
-
const lastRow = Math.max(0, lines.length - 1);
|
|
55
|
-
return [lastRow, cpLen(lines[lastRow] || '')];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* TextBuffer class for managing multi-line text input
|
|
60
|
-
*/
|
|
61
|
-
export class TextBuffer {
|
|
62
|
-
constructor(options = {}) {
|
|
63
|
-
const {
|
|
64
|
-
initialText = '',
|
|
65
|
-
viewport = { width: 80, height: 10 },
|
|
66
|
-
stdin = process.stdin,
|
|
67
|
-
setRawMode = null,
|
|
68
|
-
onChange = null,
|
|
69
|
-
} = options;
|
|
70
|
-
|
|
71
|
-
this.lines = initialText ? initialText.split('\n') : [''];
|
|
72
|
-
this.cursor = [0, 0]; // [row, col] in logical coordinates
|
|
73
|
-
this.viewport = viewport;
|
|
74
|
-
this.stdin = stdin;
|
|
75
|
-
this.setRawMode = setRawMode;
|
|
76
|
-
this.visualScrollRow = 0;
|
|
77
|
-
this.onChange = onChange;
|
|
78
|
-
|
|
79
|
-
// Visual line mapping
|
|
80
|
-
this.visualToLogicalMap = [];
|
|
81
|
-
this.allVisualLines = [];
|
|
82
|
-
|
|
83
|
-
// Cache for visual cursor to avoid creating new arrays
|
|
84
|
-
this._cachedVisualCursor = [0, 0];
|
|
85
|
-
this._cachedCursorKey = '0,0';
|
|
86
|
-
|
|
87
|
-
// Cache for viewport visual lines
|
|
88
|
-
this._cachedViewportLines = [];
|
|
89
|
-
this._cachedViewportKey = '';
|
|
90
|
-
|
|
91
|
-
// Track previous text for change detection
|
|
92
|
-
this._previousText = this.text;
|
|
93
|
-
|
|
94
|
-
this.updateVisualLines();
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
notifyChange() {
|
|
98
|
-
if (this.onChange) {
|
|
99
|
-
// Only notify if text actually changed, not just cursor position
|
|
100
|
-
const currentText = this.text;
|
|
101
|
-
if (currentText !== this._previousText) {
|
|
102
|
-
this._previousText = currentText;
|
|
103
|
-
this.onChange();
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
get text() {
|
|
109
|
-
return this.lines.join('\n');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
get viewportVisualLines() {
|
|
113
|
-
const start = this.visualScrollRow;
|
|
114
|
-
const end = start + this.viewport.height;
|
|
115
|
-
const viewportKey = `${start},${end},${this.allVisualLines.length}`;
|
|
116
|
-
|
|
117
|
-
if (viewportKey !== this._cachedViewportKey) {
|
|
118
|
-
this._cachedViewportLines = this.allVisualLines.slice(start, end);
|
|
119
|
-
this._cachedViewportKey = viewportKey;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return this._cachedViewportLines;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
get visualCursor() {
|
|
126
|
-
const cursorKey = `${this.cursor[0]},${this.cursor[1]}`;
|
|
127
|
-
if (cursorKey !== this._cachedCursorKey) {
|
|
128
|
-
this._cachedVisualCursor = this.logicalToVisual(this.cursor);
|
|
129
|
-
this._cachedCursorKey = cursorKey;
|
|
130
|
-
}
|
|
131
|
-
return this._cachedVisualCursor;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Update visual line wrapping
|
|
136
|
-
*/
|
|
137
|
-
updateVisualLines() {
|
|
138
|
-
const newVisualLines = [];
|
|
139
|
-
const newVisualToLogicalMap = [];
|
|
140
|
-
|
|
141
|
-
for (let logicalRow = 0; logicalRow < this.lines.length; logicalRow++) {
|
|
142
|
-
const line = this.lines[logicalRow];
|
|
143
|
-
const wrapped = this.wrapLine(line);
|
|
144
|
-
|
|
145
|
-
for (let i = 0; i < wrapped.length; i++) {
|
|
146
|
-
newVisualLines.push(wrapped[i]);
|
|
147
|
-
newVisualToLogicalMap.push([logicalRow, i * this.viewport.width]);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Only update if something changed
|
|
152
|
-
let changed = false;
|
|
153
|
-
if (newVisualLines.length !== this.allVisualLines.length) {
|
|
154
|
-
changed = true;
|
|
155
|
-
} else {
|
|
156
|
-
for (let i = 0; i < newVisualLines.length; i++) {
|
|
157
|
-
if (newVisualLines[i] !== this.allVisualLines[i]) {
|
|
158
|
-
changed = true;
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (changed) {
|
|
165
|
-
this.allVisualLines = newVisualLines;
|
|
166
|
-
this.visualToLogicalMap = newVisualToLogicalMap;
|
|
167
|
-
// Invalidate viewport cache when visual lines change
|
|
168
|
-
this._cachedViewportKey = '';
|
|
169
|
-
this._cachedCursorKey = '';
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Ensure cursor is visible
|
|
173
|
-
const [visualRow] = this.visualCursor;
|
|
174
|
-
if (visualRow < this.visualScrollRow) {
|
|
175
|
-
this.visualScrollRow = visualRow;
|
|
176
|
-
} else if (visualRow >= this.visualScrollRow + this.viewport.height) {
|
|
177
|
-
this.visualScrollRow = visualRow - this.viewport.height + 1;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Wrap a line to viewport width
|
|
184
|
-
*/
|
|
185
|
-
wrapLine(line) {
|
|
186
|
-
if (!line) return [''];
|
|
187
|
-
|
|
188
|
-
const result = [];
|
|
189
|
-
const cps = toCodePoints(line);
|
|
190
|
-
let currentLine = '';
|
|
191
|
-
let currentWidth = 0;
|
|
192
|
-
|
|
193
|
-
for (const cp of cps) {
|
|
194
|
-
const cpWidth = stringWidth(cp);
|
|
195
|
-
if (currentWidth + cpWidth > this.viewport.width) {
|
|
196
|
-
result.push(currentLine);
|
|
197
|
-
currentLine = cp;
|
|
198
|
-
currentWidth = cpWidth;
|
|
199
|
-
} else {
|
|
200
|
-
currentLine += cp;
|
|
201
|
-
currentWidth += cpWidth;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
result.push(currentLine);
|
|
206
|
-
return result.length > 0 ? result : [''];
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Convert logical cursor to visual cursor
|
|
211
|
-
*/
|
|
212
|
-
logicalToVisual(logicalCursor) {
|
|
213
|
-
const [row, col] = logicalCursor;
|
|
214
|
-
const line = this.lines[row] || '';
|
|
215
|
-
const wrappedLines = this.wrapLine(line);
|
|
216
|
-
|
|
217
|
-
let remainingCol = col;
|
|
218
|
-
let visualRow = 0;
|
|
219
|
-
|
|
220
|
-
// Count visual rows before this logical row
|
|
221
|
-
for (let i = 0; i < row; i++) {
|
|
222
|
-
visualRow += this.wrapLine(this.lines[i]).length;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Find position within wrapped lines
|
|
226
|
-
for (let i = 0; i < wrappedLines.length; i++) {
|
|
227
|
-
const lineLen = cpLen(wrappedLines[i]);
|
|
228
|
-
if (remainingCol <= lineLen) {
|
|
229
|
-
return [visualRow + i, remainingCol];
|
|
230
|
-
}
|
|
231
|
-
remainingCol -= lineLen;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return [visualRow + wrappedLines.length - 1, cpLen(wrappedLines[wrappedLines.length - 1])];
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Set text content
|
|
239
|
-
*/
|
|
240
|
-
setText(newText) {
|
|
241
|
-
this.lines = newText ? newText.split('\n') : [''];
|
|
242
|
-
this.cursor = [0, 0];
|
|
243
|
-
this.updateVisualLines();
|
|
244
|
-
this.notifyChange();
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Handle keyboard input
|
|
249
|
-
* Only handles regular character input, not special keys
|
|
250
|
-
*/
|
|
251
|
-
handleInput(key) {
|
|
252
|
-
// Only handle regular text input (not control characters)
|
|
253
|
-
if (key.sequence && key.sequence.length > 0 && !key.ctrl && !key.meta) {
|
|
254
|
-
// Handle paste with newlines (check for \n, \r\n, or \r)
|
|
255
|
-
if (key.sequence.includes('\n') || key.sequence.includes('\r')) {
|
|
256
|
-
// Normalize line endings to \n
|
|
257
|
-
const normalized = key.sequence.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
258
|
-
this.insertMultilineText(normalized);
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Filter out control characters
|
|
263
|
-
const isPrintable = key.sequence.split('').every(char => {
|
|
264
|
-
const code = char.charCodeAt(0);
|
|
265
|
-
// Allow printable ASCII (32-126) and extended characters (>= 128)
|
|
266
|
-
return (code >= 32 && code <= 126) || code >= 128;
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
if (isPrintable) {
|
|
270
|
-
this.insertText(key.sequence);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Insert text at cursor position
|
|
277
|
-
*/
|
|
278
|
-
insertText(text) {
|
|
279
|
-
const [row, col] = this.cursor;
|
|
280
|
-
const line = this.lines[row] || '';
|
|
281
|
-
const before = cpSlice(line, 0, col);
|
|
282
|
-
const after = cpSlice(line, col);
|
|
283
|
-
|
|
284
|
-
this.lines[row] = before + text + after;
|
|
285
|
-
this.cursor = [row, col + cpLen(text)];
|
|
286
|
-
this.updateVisualLines();
|
|
287
|
-
this.notifyChange();
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Insert multiline text at cursor position
|
|
292
|
-
*/
|
|
293
|
-
insertMultilineText(text) {
|
|
294
|
-
const [row, col] = this.cursor;
|
|
295
|
-
const currentLine = this.lines[row] || '';
|
|
296
|
-
const before = cpSlice(currentLine, 0, col);
|
|
297
|
-
const after = cpSlice(currentLine, col);
|
|
298
|
-
|
|
299
|
-
const newLines = text.split('\n');
|
|
300
|
-
|
|
301
|
-
if (newLines.length === 1) {
|
|
302
|
-
// Single line, use regular insert
|
|
303
|
-
this.insertText(text);
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// First line: append to current line before cursor
|
|
308
|
-
this.lines[row] = before + newLines[0];
|
|
309
|
-
|
|
310
|
-
// Middle lines: insert as new lines
|
|
311
|
-
for (let i = 1; i < newLines.length - 1; i++) {
|
|
312
|
-
this.lines.splice(row + i, 0, newLines[i]);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Last line: prepend to text after cursor
|
|
316
|
-
const lastLine = newLines[newLines.length - 1];
|
|
317
|
-
this.lines.splice(row + newLines.length - 1, 0, lastLine + after);
|
|
318
|
-
|
|
319
|
-
// Update cursor to end of pasted text
|
|
320
|
-
this.cursor = [row + newLines.length - 1, cpLen(lastLine)];
|
|
321
|
-
this.updateVisualLines();
|
|
322
|
-
this.notifyChange();
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Insert newline
|
|
327
|
-
*/
|
|
328
|
-
newline() {
|
|
329
|
-
const [row, col] = this.cursor;
|
|
330
|
-
const line = this.lines[row] || '';
|
|
331
|
-
const before = cpSlice(line, 0, col);
|
|
332
|
-
const after = cpSlice(line, col);
|
|
333
|
-
|
|
334
|
-
this.lines[row] = before;
|
|
335
|
-
this.lines.splice(row + 1, 0, after);
|
|
336
|
-
this.cursor = [row + 1, 0];
|
|
337
|
-
this.updateVisualLines();
|
|
338
|
-
this.notifyChange();
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Backspace
|
|
343
|
-
* Safely handles edge cases and ensures proper cursor positioning
|
|
344
|
-
*/
|
|
345
|
-
backspace() {
|
|
346
|
-
const [row, col] = this.cursor;
|
|
347
|
-
|
|
348
|
-
// Ensure we have valid lines array
|
|
349
|
-
if (!this.lines || this.lines.length === 0) {
|
|
350
|
-
this.lines = [''];
|
|
351
|
-
this.cursor = [0, 0];
|
|
352
|
-
this.updateVisualLines();
|
|
353
|
-
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Case 1: Delete character within line
|
|
358
|
-
if (col > 0) {
|
|
359
|
-
const line = this.lines[row] || '';
|
|
360
|
-
const before = cpSlice(line, 0, col - 1);
|
|
361
|
-
const after = cpSlice(line, col);
|
|
362
|
-
this.lines[row] = before + after;
|
|
363
|
-
this.cursor = [row, col - 1];
|
|
364
|
-
}
|
|
365
|
-
// Case 2: Merge with previous line
|
|
366
|
-
else if (row > 0) {
|
|
367
|
-
const currentLine = this.lines[row] || '';
|
|
368
|
-
const prevLine = this.lines[row - 1] || '';
|
|
369
|
-
const prevLineLen = cpLen(prevLine);
|
|
370
|
-
|
|
371
|
-
// Merge lines
|
|
372
|
-
this.lines[row - 1] = prevLine + currentLine;
|
|
373
|
-
this.lines.splice(row, 1);
|
|
374
|
-
|
|
375
|
-
// Move cursor to end of previous line
|
|
376
|
-
this.cursor = [row - 1, prevLineLen];
|
|
377
|
-
}
|
|
378
|
-
// Case 3: Already at start of first line - do nothing
|
|
379
|
-
else {
|
|
380
|
-
// At position [0, 0], cannot backspace further
|
|
381
|
-
this.updateVisualLines();
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
this.updateVisualLines();
|
|
386
|
-
this.notifyChange();
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Delete character at cursor
|
|
391
|
-
* Safely handles edge cases
|
|
392
|
-
*/
|
|
393
|
-
delete() {
|
|
394
|
-
const [row, col] = this.cursor;
|
|
395
|
-
|
|
396
|
-
// Ensure we have valid lines array
|
|
397
|
-
if (!this.lines || this.lines.length === 0) {
|
|
398
|
-
this.lines = [''];
|
|
399
|
-
this.cursor = [0, 0];
|
|
400
|
-
this.updateVisualLines();
|
|
401
|
-
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const line = this.lines[row] || '';
|
|
406
|
-
const lineLen = cpLen(line);
|
|
407
|
-
|
|
408
|
-
// Case 1: Delete character within line
|
|
409
|
-
if (col < lineLen) {
|
|
410
|
-
const before = cpSlice(line, 0, col);
|
|
411
|
-
const after = cpSlice(line, col + 1);
|
|
412
|
-
this.lines[row] = before + after;
|
|
413
|
-
}
|
|
414
|
-
// Case 2: Merge with next line
|
|
415
|
-
else if (row < this.lines.length - 1) {
|
|
416
|
-
const nextLine = this.lines[row + 1] || '';
|
|
417
|
-
this.lines[row] = line + nextLine;
|
|
418
|
-
this.lines.splice(row + 1, 1);
|
|
419
|
-
}
|
|
420
|
-
// Case 3: At end of last line - do nothing
|
|
421
|
-
else {
|
|
422
|
-
// Already at end, cannot delete further
|
|
423
|
-
this.updateVisualLines();
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
this.updateVisualLines();
|
|
428
|
-
this.notifyChange();
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Move cursor
|
|
433
|
-
*/
|
|
434
|
-
move(direction) {
|
|
435
|
-
const [row, col] = this.cursor;
|
|
436
|
-
const line = this.lines[row] || '';
|
|
437
|
-
const oldCursor = [...this.cursor];
|
|
438
|
-
|
|
439
|
-
switch (direction) {
|
|
440
|
-
case 'left':
|
|
441
|
-
if (col > 0) {
|
|
442
|
-
this.cursor = [row, col - 1];
|
|
443
|
-
} else if (row > 0) {
|
|
444
|
-
this.cursor = [row - 1, cpLen(this.lines[row - 1] || '')];
|
|
445
|
-
}
|
|
446
|
-
break;
|
|
447
|
-
|
|
448
|
-
case 'right':
|
|
449
|
-
if (col < cpLen(line)) {
|
|
450
|
-
this.cursor = [row, col + 1];
|
|
451
|
-
} else if (row < this.lines.length - 1) {
|
|
452
|
-
this.cursor = [row + 1, 0];
|
|
453
|
-
}
|
|
454
|
-
break;
|
|
455
|
-
|
|
456
|
-
case 'up':
|
|
457
|
-
if (row > 0) {
|
|
458
|
-
const prevLineLen = cpLen(this.lines[row - 1] || '');
|
|
459
|
-
this.cursor = [row - 1, Math.min(col, prevLineLen)];
|
|
460
|
-
}
|
|
461
|
-
break;
|
|
462
|
-
|
|
463
|
-
case 'down':
|
|
464
|
-
if (row < this.lines.length - 1) {
|
|
465
|
-
const nextLineLen = cpLen(this.lines[row + 1] || '');
|
|
466
|
-
this.cursor = [row + 1, Math.min(col, nextLineLen)];
|
|
467
|
-
}
|
|
468
|
-
break;
|
|
469
|
-
|
|
470
|
-
case 'home':
|
|
471
|
-
this.cursor = [row, 0];
|
|
472
|
-
break;
|
|
473
|
-
|
|
474
|
-
case 'end':
|
|
475
|
-
this.cursor = [row, cpLen(line)];
|
|
476
|
-
break;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
const cursorChanged = oldCursor[0] !== this.cursor[0] || oldCursor[1] !== this.cursor[1];
|
|
480
|
-
|
|
481
|
-
if (cursorChanged) {
|
|
482
|
-
this.updateVisualLines();
|
|
483
|
-
// Always notify for cursor changes - needed for display update
|
|
484
|
-
this.notifyChange();
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Move to offset position
|
|
490
|
-
*/
|
|
491
|
-
moveToOffset(offset) {
|
|
492
|
-
this.cursor = offsetToLogicalPos(this.lines, offset);
|
|
493
|
-
this.updateVisualLines();
|
|
494
|
-
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* Replace range by offset
|
|
499
|
-
*/
|
|
500
|
-
replaceRangeByOffset(startOffset, endOffset, replacement) {
|
|
501
|
-
const fullText = this.text;
|
|
502
|
-
const cps = toCodePoints(fullText);
|
|
503
|
-
const before = cps.slice(0, startOffset).join('');
|
|
504
|
-
const after = cps.slice(endOffset).join('');
|
|
505
|
-
|
|
506
|
-
this.setText(before + replacement + after);
|
|
507
|
-
this.moveToOffset(startOffset + cpLen(replacement));
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Kill line right (Ctrl+K)
|
|
512
|
-
*/
|
|
513
|
-
killLineRight() {
|
|
514
|
-
const [row, col] = this.cursor;
|
|
515
|
-
const line = this.lines[row] || '';
|
|
516
|
-
this.lines[row] = cpSlice(line, 0, col);
|
|
517
|
-
this.updateVisualLines();
|
|
518
|
-
this.notifyChange();
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/**
|
|
522
|
-
* Kill line left (Ctrl+U)
|
|
523
|
-
*/
|
|
524
|
-
killLineLeft() {
|
|
525
|
-
const [row, col] = this.cursor;
|
|
526
|
-
const line = this.lines[row] || '';
|
|
527
|
-
this.lines[row] = cpSlice(line, col);
|
|
528
|
-
this.cursor = [row, 0];
|
|
529
|
-
this.updateVisualLines();
|
|
530
|
-
this.notifyChange();
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/**
|
|
534
|
-
* Delete word left (Ctrl+W)
|
|
535
|
-
*/
|
|
536
|
-
deleteWordLeft() {
|
|
537
|
-
const [row, col] = this.cursor;
|
|
538
|
-
if (col === 0) return;
|
|
539
|
-
|
|
540
|
-
const line = this.lines[row] || '';
|
|
541
|
-
const before = cpSlice(line, 0, col);
|
|
542
|
-
const after = cpSlice(line, col);
|
|
543
|
-
|
|
544
|
-
// Find word boundary
|
|
545
|
-
const trimmed = before.trimEnd();
|
|
546
|
-
const lastSpace = trimmed.lastIndexOf(' ');
|
|
547
|
-
const newCol = lastSpace >= 0 ? lastSpace + 1 : 0;
|
|
548
|
-
|
|
549
|
-
this.lines[row] = cpSlice(line, 0, newCol) + after;
|
|
550
|
-
this.cursor = [row, newCol];
|
|
551
|
-
this.updateVisualLines();
|
|
552
|
-
this.notifyChange();
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
/**
|
|
556
|
-
* Open in external editor (stub for now)
|
|
557
|
-
*/
|
|
558
|
-
openInExternalEditor() {
|
|
559
|
-
consolelog('External editor not implemented yet');
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
/**
|
|
564
|
-
* Hook to create and manage text buffer using useReducer
|
|
565
|
-
*/
|
|
566
|
-
import { useReducer, useMemo, useCallback, useState, useEffect, useRef } from 'react';
|
|
567
|
-
|
|
568
|
-
function textBufferReducer(state, action) {
|
|
569
|
-
switch (action.type) {
|
|
570
|
-
case 'set_text': {
|
|
571
|
-
const newLines = action.payload ? action.payload.split('\n') : [''];
|
|
572
|
-
return {
|
|
573
|
-
...state,
|
|
574
|
-
lines: newLines,
|
|
575
|
-
cursor: [0, 0]
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
case 'insert_text': {
|
|
579
|
-
const { text } = action.payload;
|
|
580
|
-
const [row, col] = state.cursor;
|
|
581
|
-
const newLines = [...state.lines];
|
|
582
|
-
const line = newLines[row] || '';
|
|
583
|
-
const before = cpSlice(line, 0, col);
|
|
584
|
-
const after = cpSlice(line, col);
|
|
585
|
-
newLines[row] = before + text + after;
|
|
586
|
-
return {
|
|
587
|
-
...state,
|
|
588
|
-
lines: newLines,
|
|
589
|
-
cursor: [row, col + cpLen(text)]
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
case 'move_cursor': {
|
|
593
|
-
const { direction } = action.payload;
|
|
594
|
-
const [row, col] = state.cursor;
|
|
595
|
-
const line = state.lines[row] || '';
|
|
596
|
-
let newCursor = [row, col];
|
|
597
|
-
|
|
598
|
-
switch (direction) {
|
|
599
|
-
case 'left':
|
|
600
|
-
if (col > 0) {
|
|
601
|
-
newCursor = [row, col - 1];
|
|
602
|
-
} else if (row > 0) {
|
|
603
|
-
newCursor = [row - 1, cpLen(state.lines[row - 1] || '')];
|
|
604
|
-
}
|
|
605
|
-
break;
|
|
606
|
-
case 'right':
|
|
607
|
-
if (col < cpLen(line)) {
|
|
608
|
-
newCursor = [row, col + 1];
|
|
609
|
-
} else if (row < state.lines.length - 1) {
|
|
610
|
-
newCursor = [row + 1, 0];
|
|
611
|
-
}
|
|
612
|
-
break;
|
|
613
|
-
case 'up':
|
|
614
|
-
if (row > 0) {
|
|
615
|
-
const prevLineLen = cpLen(state.lines[row - 1] || '');
|
|
616
|
-
newCursor = [row - 1, Math.min(col, prevLineLen)];
|
|
617
|
-
}
|
|
618
|
-
break;
|
|
619
|
-
case 'down':
|
|
620
|
-
if (row < state.lines.length - 1) {
|
|
621
|
-
const nextLineLen = cpLen(state.lines[row + 1] || '');
|
|
622
|
-
newCursor = [row + 1, Math.min(col, nextLineLen)];
|
|
623
|
-
}
|
|
624
|
-
break;
|
|
625
|
-
case 'home':
|
|
626
|
-
newCursor = [row, 0];
|
|
627
|
-
break;
|
|
628
|
-
case 'end':
|
|
629
|
-
newCursor = [row, cpLen(line)];
|
|
630
|
-
break;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
return {
|
|
634
|
-
...state,
|
|
635
|
-
cursor: newCursor
|
|
636
|
-
};
|
|
637
|
-
}
|
|
638
|
-
case 'backspace': {
|
|
639
|
-
const [row, col] = state.cursor;
|
|
640
|
-
if (col === 0 && row === 0) return state;
|
|
641
|
-
|
|
642
|
-
const newLines = [...state.lines];
|
|
643
|
-
let newCursor = [row, col];
|
|
644
|
-
|
|
645
|
-
if (col > 0) {
|
|
646
|
-
const line = newLines[row] || '';
|
|
647
|
-
newLines[row] = cpSlice(line, 0, col - 1) + cpSlice(line, col);
|
|
648
|
-
newCursor = [row, col - 1];
|
|
649
|
-
} else if (row > 0) {
|
|
650
|
-
const currentLine = newLines[row] || '';
|
|
651
|
-
const prevLine = newLines[row - 1] || '';
|
|
652
|
-
const prevLineLen = cpLen(prevLine);
|
|
653
|
-
newLines[row - 1] = prevLine + currentLine;
|
|
654
|
-
newLines.splice(row, 1);
|
|
655
|
-
newCursor = [row - 1, prevLineLen];
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
return {
|
|
659
|
-
...state,
|
|
660
|
-
lines: newLines,
|
|
661
|
-
cursor: newCursor
|
|
662
|
-
};
|
|
663
|
-
}
|
|
664
|
-
case 'delete': {
|
|
665
|
-
const [row, col] = state.cursor;
|
|
666
|
-
const newLines = [...state.lines];
|
|
667
|
-
const line = newLines[row] || '';
|
|
668
|
-
const lineLen = cpLen(line);
|
|
669
|
-
|
|
670
|
-
if (col < lineLen) {
|
|
671
|
-
newLines[row] = cpSlice(line, 0, col) + cpSlice(line, col + 1);
|
|
672
|
-
} else if (row < state.lines.length - 1) {
|
|
673
|
-
const nextLine = newLines[row + 1] || '';
|
|
674
|
-
newLines[row] = line + nextLine;
|
|
675
|
-
newLines.splice(row + 1, 1);
|
|
676
|
-
} else {
|
|
677
|
-
return state;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
return {
|
|
681
|
-
...state,
|
|
682
|
-
lines: newLines
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
case 'newline': {
|
|
686
|
-
const [row, col] = state.cursor;
|
|
687
|
-
const newLines = [...state.lines];
|
|
688
|
-
const line = newLines[row] || '';
|
|
689
|
-
const before = cpSlice(line, 0, col);
|
|
690
|
-
const after = cpSlice(line, col);
|
|
691
|
-
newLines[row] = before;
|
|
692
|
-
newLines.splice(row + 1, 0, after);
|
|
693
|
-
|
|
694
|
-
return {
|
|
695
|
-
...state,
|
|
696
|
-
lines: newLines,
|
|
697
|
-
cursor: [row + 1, 0]
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
|
-
case 'kill_line_right': {
|
|
701
|
-
const [row, col] = state.cursor;
|
|
702
|
-
const newLines = [...state.lines];
|
|
703
|
-
const line = newLines[row] || '';
|
|
704
|
-
newLines[row] = cpSlice(line, 0, col);
|
|
705
|
-
return {
|
|
706
|
-
...state,
|
|
707
|
-
lines: newLines
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
|
-
case 'kill_line_left': {
|
|
711
|
-
const [row, col] = state.cursor;
|
|
712
|
-
const newLines = [...state.lines];
|
|
713
|
-
const line = newLines[row] || '';
|
|
714
|
-
newLines[row] = cpSlice(line, col);
|
|
715
|
-
return {
|
|
716
|
-
...state,
|
|
717
|
-
lines: newLines,
|
|
718
|
-
cursor: [row, 0]
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
case 'delete_word_left': {
|
|
722
|
-
const [row, col] = state.cursor;
|
|
723
|
-
if (col === 0) return state;
|
|
724
|
-
|
|
725
|
-
const newLines = [...state.lines];
|
|
726
|
-
const line = newLines[row] || '';
|
|
727
|
-
const before = cpSlice(line, 0, col);
|
|
728
|
-
const after = cpSlice(line, col);
|
|
729
|
-
|
|
730
|
-
const trimmed = before.trimEnd();
|
|
731
|
-
const lastSpace = trimmed.lastIndexOf(' ');
|
|
732
|
-
const newCol = lastSpace >= 0 ? lastSpace + 1 : 0;
|
|
733
|
-
|
|
734
|
-
newLines[row] = cpSlice(line, 0, newCol) + after;
|
|
735
|
-
|
|
736
|
-
return {
|
|
737
|
-
...state,
|
|
738
|
-
lines: newLines,
|
|
739
|
-
cursor: [row, newCol]
|
|
740
|
-
};
|
|
741
|
-
}
|
|
742
|
-
case 'set_viewport': {
|
|
743
|
-
return {
|
|
744
|
-
...state,
|
|
745
|
-
viewport: action.payload
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
default:
|
|
749
|
-
return state;
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
export function useTextBuffer(options) {
|
|
754
|
-
const { initialText = '', viewport } = options;
|
|
755
|
-
|
|
756
|
-
const initialState = useMemo(() => ({
|
|
757
|
-
lines: initialText ? initialText.split('\n') : [''],
|
|
758
|
-
cursor: [0, 0],
|
|
759
|
-
viewport: viewport || { width: 80, height: 10 }
|
|
760
|
-
}), [initialText, viewport]);
|
|
761
|
-
|
|
762
|
-
const [state, dispatch] = useReducer(textBufferReducer, initialState);
|
|
763
|
-
|
|
764
|
-
// Update viewport when it changes
|
|
765
|
-
useEffect(() => {
|
|
766
|
-
if (viewport && (viewport.width !== state.viewport.width || viewport.height !== state.viewport.height)) {
|
|
767
|
-
dispatch({ type: 'set_viewport', payload: viewport });
|
|
768
|
-
}
|
|
769
|
-
}, [viewport, state.viewport]);
|
|
770
|
-
|
|
771
|
-
// Calculate visual layout
|
|
772
|
-
const visualLayout = useMemo(() => {
|
|
773
|
-
const allVisualLines = [];
|
|
774
|
-
const visualToLogicalMap = [];
|
|
775
|
-
const viewportWidth = state.viewport.width;
|
|
776
|
-
|
|
777
|
-
state.lines.forEach((line, logicalRow) => {
|
|
778
|
-
if (!line) {
|
|
779
|
-
allVisualLines.push('');
|
|
780
|
-
visualToLogicalMap.push([logicalRow, 0]);
|
|
781
|
-
} else {
|
|
782
|
-
const wrapped = wrapLine(line, viewportWidth);
|
|
783
|
-
wrapped.forEach((segment, i) => {
|
|
784
|
-
allVisualLines.push(segment);
|
|
785
|
-
visualToLogicalMap.push([logicalRow, i * viewportWidth]);
|
|
786
|
-
});
|
|
787
|
-
}
|
|
788
|
-
});
|
|
789
|
-
|
|
790
|
-
return { allVisualLines, visualToLogicalMap };
|
|
791
|
-
}, [state.lines, state.viewport.width]);
|
|
792
|
-
|
|
793
|
-
// Calculate visual cursor
|
|
794
|
-
const visualCursor = useMemo(() => {
|
|
795
|
-
const [row, col] = state.cursor;
|
|
796
|
-
const line = state.lines[row] || '';
|
|
797
|
-
const wrappedLines = wrapLine(line, state.viewport.width);
|
|
798
|
-
|
|
799
|
-
let remainingCol = col;
|
|
800
|
-
let visualRow = 0;
|
|
801
|
-
|
|
802
|
-
for (let i = 0; i < row; i++) {
|
|
803
|
-
visualRow += wrapLine(state.lines[i], state.viewport.width).length;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
for (let i = 0; i < wrappedLines.length; i++) {
|
|
807
|
-
const lineLen = cpLen(wrappedLines[i]);
|
|
808
|
-
if (remainingCol <= lineLen) {
|
|
809
|
-
return [visualRow + i, remainingCol];
|
|
810
|
-
}
|
|
811
|
-
remainingCol -= lineLen;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
return [visualRow + wrappedLines.length - 1, cpLen(wrappedLines[wrappedLines.length - 1])];
|
|
815
|
-
}, [state.cursor, state.lines, state.viewport.width]);
|
|
816
|
-
|
|
817
|
-
const [visualScrollRow, setVisualScrollRow] = useState(0);
|
|
818
|
-
|
|
819
|
-
// Update scroll position - memoize the calculation to avoid unnecessary state updates
|
|
820
|
-
const computedScrollRow = useMemo(() => {
|
|
821
|
-
const { height } = state.viewport;
|
|
822
|
-
const totalVisualLines = visualLayout.allVisualLines.length;
|
|
823
|
-
const maxScrollStart = Math.max(0, totalVisualLines - height);
|
|
824
|
-
let newVisualScrollRow = visualScrollRow;
|
|
825
|
-
|
|
826
|
-
// Only adjust scroll if cursor is out of viewport
|
|
827
|
-
if (visualCursor[0] < visualScrollRow) {
|
|
828
|
-
newVisualScrollRow = visualCursor[0];
|
|
829
|
-
} else if (visualCursor[0] >= visualScrollRow + height) {
|
|
830
|
-
newVisualScrollRow = visualCursor[0] - height + 1;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
newVisualScrollRow = Math.min(Math.max(newVisualScrollRow, 0), maxScrollStart);
|
|
834
|
-
return newVisualScrollRow;
|
|
835
|
-
}, [visualCursor, visualScrollRow, state.viewport.height, visualLayout.allVisualLines.length]);
|
|
836
|
-
|
|
837
|
-
// Only update state when scroll actually changes
|
|
838
|
-
useEffect(() => {
|
|
839
|
-
if (computedScrollRow !== visualScrollRow) {
|
|
840
|
-
setVisualScrollRow(computedScrollRow);
|
|
841
|
-
}
|
|
842
|
-
}, [computedScrollRow, visualScrollRow]);
|
|
843
|
-
|
|
844
|
-
// Create stable callbacks with useCallback
|
|
845
|
-
const setText = useCallback((text) => {
|
|
846
|
-
dispatch({ type: 'set_text', payload: text });
|
|
847
|
-
}, []);
|
|
848
|
-
|
|
849
|
-
const insertText = useCallback((text) => {
|
|
850
|
-
dispatch({ type: 'insert_text', payload: { text } });
|
|
851
|
-
}, []);
|
|
852
|
-
|
|
853
|
-
const move = useCallback((direction) => {
|
|
854
|
-
dispatch({ type: 'move_cursor', payload: { direction } });
|
|
855
|
-
}, []);
|
|
856
|
-
|
|
857
|
-
const backspace = useCallback(() => {
|
|
858
|
-
dispatch({ type: 'backspace' });
|
|
859
|
-
}, []);
|
|
860
|
-
|
|
861
|
-
const deleteChar = useCallback(() => {
|
|
862
|
-
dispatch({ type: 'delete' });
|
|
863
|
-
}, []);
|
|
864
|
-
|
|
865
|
-
const newline = useCallback(() => {
|
|
866
|
-
dispatch({ type: 'newline' });
|
|
867
|
-
}, []);
|
|
868
|
-
|
|
869
|
-
const killLineRight = useCallback(() => {
|
|
870
|
-
dispatch({ type: 'kill_line_right' });
|
|
871
|
-
}, []);
|
|
872
|
-
|
|
873
|
-
const killLineLeft = useCallback(() => {
|
|
874
|
-
dispatch({ type: 'kill_line_left' });
|
|
875
|
-
}, []);
|
|
876
|
-
|
|
877
|
-
const deleteWordLeft = useCallback(() => {
|
|
878
|
-
dispatch({ type: 'delete_word_left' });
|
|
879
|
-
}, []);
|
|
880
|
-
|
|
881
|
-
const handleInput = useCallback((key) => {
|
|
882
|
-
if (key.sequence && key.sequence.length > 0 && !key.ctrl && !key.meta) {
|
|
883
|
-
if (key.sequence.includes('\n') || key.sequence.includes('\r')) {
|
|
884
|
-
const normalized = key.sequence.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
885
|
-
const parts = normalized.split('\n');
|
|
886
|
-
parts.forEach((part, i) => {
|
|
887
|
-
if (part) dispatch({ type: 'insert_text', payload: { text: part } });
|
|
888
|
-
if (i < parts.length - 1) dispatch({ type: 'newline' });
|
|
889
|
-
});
|
|
890
|
-
return;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
const isPrintable = key.sequence.split('').every(char => {
|
|
894
|
-
const code = char.charCodeAt(0);
|
|
895
|
-
return (code >= 32 && code <= 126) || code >= 128;
|
|
896
|
-
});
|
|
897
|
-
|
|
898
|
-
if (isPrintable) {
|
|
899
|
-
dispatch({ type: 'insert_text', payload: { text: key.sequence } });
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
}, []);
|
|
903
|
-
|
|
904
|
-
const text = useMemo(() => state.lines.join('\n'), [state.lines]);
|
|
905
|
-
|
|
906
|
-
const viewportVisualLines = useMemo(() => {
|
|
907
|
-
return visualLayout.allVisualLines.slice(visualScrollRow, visualScrollRow + state.viewport.height);
|
|
908
|
-
}, [visualLayout.allVisualLines, visualScrollRow, state.viewport.height]);
|
|
909
|
-
|
|
910
|
-
// Return stable buffer object using useRef to maintain reference identity
|
|
911
|
-
const bufferRef = useRef(null);
|
|
912
|
-
|
|
913
|
-
if (!bufferRef.current) {
|
|
914
|
-
bufferRef.current = {
|
|
915
|
-
get lines() { return state.lines; },
|
|
916
|
-
get cursor() { return state.cursor; },
|
|
917
|
-
get text() { return text; },
|
|
918
|
-
get allVisualLines() { return visualLayout.allVisualLines; },
|
|
919
|
-
get viewportVisualLines() { return viewportVisualLines; },
|
|
920
|
-
get visualCursor() { return visualCursor; },
|
|
921
|
-
get visualScrollRow() { return visualScrollRow; },
|
|
922
|
-
get visualToLogicalMap() { return visualLayout.visualToLogicalMap; },
|
|
923
|
-
get viewport() { return state.viewport; },
|
|
924
|
-
setText,
|
|
925
|
-
insertText,
|
|
926
|
-
move,
|
|
927
|
-
backspace,
|
|
928
|
-
delete: deleteChar,
|
|
929
|
-
newline,
|
|
930
|
-
killLineRight,
|
|
931
|
-
killLineLeft,
|
|
932
|
-
deleteWordLeft,
|
|
933
|
-
handleInput
|
|
934
|
-
};
|
|
935
|
-
} else {
|
|
936
|
-
// Update getters to return current values without changing object reference
|
|
937
|
-
Object.defineProperties(bufferRef.current, {
|
|
938
|
-
lines: { get: () => state.lines, enumerable: true, configurable: true },
|
|
939
|
-
cursor: { get: () => state.cursor, enumerable: true, configurable: true },
|
|
940
|
-
text: { get: () => text, enumerable: true, configurable: true },
|
|
941
|
-
allVisualLines: { get: () => visualLayout.allVisualLines, enumerable: true, configurable: true },
|
|
942
|
-
viewportVisualLines: { get: () => viewportVisualLines, enumerable: true, configurable: true },
|
|
943
|
-
visualCursor: { get: () => visualCursor, enumerable: true, configurable: true },
|
|
944
|
-
visualScrollRow: { get: () => visualScrollRow, enumerable: true, configurable: true },
|
|
945
|
-
visualToLogicalMap: { get: () => visualLayout.visualToLogicalMap, enumerable: true, configurable: true },
|
|
946
|
-
viewport: { get: () => state.viewport, enumerable: true, configurable: true }
|
|
947
|
-
});
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
return bufferRef.current;
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
function wrapLine(line, viewportWidth) {
|
|
954
|
-
if (!line) return [''];
|
|
955
|
-
|
|
956
|
-
const result = [];
|
|
957
|
-
const cps = toCodePoints(line);
|
|
958
|
-
let currentLine = '';
|
|
959
|
-
let currentWidth = 0;
|
|
960
|
-
|
|
961
|
-
for (const cp of cps) {
|
|
962
|
-
const cpWidth = stringWidth(cp);
|
|
963
|
-
if (currentWidth + cpWidth > viewportWidth) {
|
|
964
|
-
result.push(currentLine);
|
|
965
|
-
currentLine = cp;
|
|
966
|
-
currentWidth = cpWidth;
|
|
967
|
-
} else {
|
|
968
|
-
currentLine += cp;
|
|
969
|
-
currentWidth += cpWidth;
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
result.push(currentLine);
|
|
974
|
-
return result.length > 0 ? result : [''];
|
|
975
|
-
}
|