@mariozechner/pi-tui 0.5.0

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.
@@ -0,0 +1,670 @@
1
+ import chalk from "chalk";
2
+ import { logger } from "./logger.js";
3
+ import { SelectList } from "./select-list.js";
4
+ export class TextEditor {
5
+ state = {
6
+ lines: [""],
7
+ cursorLine: 0,
8
+ cursorCol: 0,
9
+ };
10
+ config = {};
11
+ // Autocomplete support
12
+ autocompleteProvider;
13
+ autocompleteList;
14
+ isAutocompleting = false;
15
+ autocompletePrefix = "";
16
+ onSubmit;
17
+ onChange;
18
+ disableSubmit = false;
19
+ constructor(config) {
20
+ if (config) {
21
+ this.config = { ...this.config, ...config };
22
+ }
23
+ logger.componentLifecycle("TextEditor", "created", { config: this.config });
24
+ }
25
+ configure(config) {
26
+ this.config = { ...this.config, ...config };
27
+ logger.info("TextEditor", "Configuration updated", { config: this.config });
28
+ }
29
+ setAutocompleteProvider(provider) {
30
+ this.autocompleteProvider = provider;
31
+ }
32
+ render(width) {
33
+ // Box drawing characters
34
+ const topLeft = chalk.gray("╭");
35
+ const topRight = chalk.gray("╮");
36
+ const bottomLeft = chalk.gray("╰");
37
+ const bottomRight = chalk.gray("╯");
38
+ const horizontal = chalk.gray("─");
39
+ const vertical = chalk.gray("│");
40
+ // Calculate box width (leave some margin)
41
+ const boxWidth = width - 1;
42
+ const contentWidth = boxWidth - 4; // Account for "│ " and " │"
43
+ // Layout the text
44
+ const layoutLines = this.layoutText(contentWidth);
45
+ const result = [];
46
+ // Render top border
47
+ result.push(topLeft + horizontal.repeat(boxWidth - 2) + topRight);
48
+ // Render each layout line
49
+ for (const layoutLine of layoutLines) {
50
+ let displayText = layoutLine.text;
51
+ let visibleLength = layoutLine.text.length;
52
+ // Add cursor if this line has it
53
+ if (layoutLine.hasCursor && layoutLine.cursorPos !== undefined) {
54
+ const before = displayText.slice(0, layoutLine.cursorPos);
55
+ const after = displayText.slice(layoutLine.cursorPos);
56
+ if (after.length > 0) {
57
+ // Cursor is on a character - replace it with highlighted version
58
+ const cursor = `\x1b[7m${after[0]}\x1b[0m`;
59
+ const restAfter = after.slice(1);
60
+ displayText = before + cursor + restAfter;
61
+ // visibleLength stays the same - we're replacing, not adding
62
+ }
63
+ else {
64
+ // Cursor is at the end - add highlighted space
65
+ const cursor = "\x1b[7m \x1b[0m";
66
+ displayText = before + cursor;
67
+ // visibleLength increases by 1 - we're adding a space
68
+ visibleLength = layoutLine.text.length + 1;
69
+ }
70
+ }
71
+ // Calculate padding based on actual visible length
72
+ const padding = " ".repeat(Math.max(0, contentWidth - visibleLength));
73
+ // Render the line
74
+ result.push(`${vertical} ${displayText}${padding} ${vertical}`);
75
+ }
76
+ // Render bottom border
77
+ result.push(bottomLeft + horizontal.repeat(boxWidth - 2) + bottomRight);
78
+ // Add autocomplete list if active
79
+ if (this.isAutocompleting && this.autocompleteList) {
80
+ const autocompleteResult = this.autocompleteList.render(width);
81
+ result.push(...autocompleteResult.lines);
82
+ }
83
+ // For interactive components like text editors, always assume changed
84
+ // This ensures cursor position updates are always reflected
85
+ return {
86
+ lines: result,
87
+ changed: true,
88
+ };
89
+ }
90
+ handleInput(data) {
91
+ logger.keyInput("TextEditor", data);
92
+ logger.debug("TextEditor", "Current state before input", {
93
+ lines: this.state.lines,
94
+ cursorLine: this.state.cursorLine,
95
+ cursorCol: this.state.cursorCol,
96
+ });
97
+ // Handle special key combinations first
98
+ // Ctrl+C - Exit (let parent handle this)
99
+ if (data.charCodeAt(0) === 3) {
100
+ logger.debug("TextEditor", "Ctrl+C received, returning to parent");
101
+ return;
102
+ }
103
+ // Handle paste - detect when we get a lot of text at once
104
+ const isPaste = data.length > 10 || (data.length > 2 && data.includes("\n"));
105
+ logger.debug("TextEditor", "Paste detection", {
106
+ dataLength: data.length,
107
+ includesNewline: data.includes("\n"),
108
+ includesTabs: data.includes("\t"),
109
+ tabCount: (data.match(/\t/g) || []).length,
110
+ isPaste,
111
+ data: JSON.stringify(data),
112
+ charCodes: Array.from(data).map((c) => c.charCodeAt(0)),
113
+ });
114
+ if (isPaste) {
115
+ logger.info("TextEditor", "Handling as paste");
116
+ this.handlePaste(data);
117
+ return;
118
+ }
119
+ // Handle autocomplete special keys first (but don't block other input)
120
+ if (this.isAutocompleting && this.autocompleteList) {
121
+ logger.debug("TextEditor", "Autocomplete active, handling input", {
122
+ data,
123
+ charCode: data.charCodeAt(0),
124
+ isEscape: data === "\x1b",
125
+ isArrowOrEnter: data === "\x1b[A" || data === "\x1b[B" || data === "\r",
126
+ });
127
+ // Escape - cancel autocomplete
128
+ if (data === "\x1b") {
129
+ this.cancelAutocomplete();
130
+ return;
131
+ }
132
+ // Let the autocomplete list handle navigation and selection
133
+ else if (data === "\x1b[A" || data === "\x1b[B" || data === "\r" || data === "\t") {
134
+ // Only pass arrow keys to the list, not Enter/Tab (we handle those directly)
135
+ if (data === "\x1b[A" || data === "\x1b[B") {
136
+ this.autocompleteList.handleInput(data);
137
+ }
138
+ // If Tab was pressed, apply the selection
139
+ if (data === "\t") {
140
+ const selected = this.autocompleteList.getSelectedItem();
141
+ if (selected && this.autocompleteProvider) {
142
+ const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, selected, this.autocompletePrefix);
143
+ this.state.lines = result.lines;
144
+ this.state.cursorLine = result.cursorLine;
145
+ this.state.cursorCol = result.cursorCol;
146
+ this.cancelAutocomplete();
147
+ if (this.onChange) {
148
+ this.onChange(this.getText());
149
+ }
150
+ }
151
+ return;
152
+ }
153
+ // If Enter was pressed, cancel autocomplete and let it fall through to submission
154
+ else if (data === "\r") {
155
+ this.cancelAutocomplete();
156
+ // Don't return here - let Enter fall through to normal submission handling
157
+ }
158
+ else {
159
+ // For other keys, handle normally within autocomplete
160
+ return;
161
+ }
162
+ }
163
+ // For other keys (like regular typing), DON'T return here
164
+ // Let them fall through to normal character handling
165
+ logger.debug("TextEditor", "Autocomplete active but falling through to normal handling");
166
+ }
167
+ // Tab key - context-aware completion (but not when already autocompleting)
168
+ if (data === "\t" && !this.isAutocompleting) {
169
+ logger.debug("TextEditor", "Tab key pressed, determining context", {
170
+ isAutocompleting: this.isAutocompleting,
171
+ hasProvider: !!this.autocompleteProvider,
172
+ });
173
+ this.handleTabCompletion();
174
+ return;
175
+ }
176
+ // Continue with rest of input handling
177
+ // Ctrl+K - Delete current line
178
+ if (data.charCodeAt(0) === 11) {
179
+ this.deleteCurrentLine();
180
+ }
181
+ // Ctrl+A - Move to start of line
182
+ else if (data.charCodeAt(0) === 1) {
183
+ this.moveToLineStart();
184
+ }
185
+ // Ctrl+E - Move to end of line
186
+ else if (data.charCodeAt(0) === 5) {
187
+ this.moveToLineEnd();
188
+ }
189
+ // New line shortcuts (but not plain LF/CR which should be submit)
190
+ else if ((data.charCodeAt(0) === 10 && data.length > 1) || // Ctrl+Enter with modifiers
191
+ data === "\x1b\r" || // Option+Enter in some terminals
192
+ data === "\x1b[13;2~" || // Shift+Enter in some terminals
193
+ (data.length > 1 && data.includes("\x1b") && data.includes("\r")) ||
194
+ (data === "\n" && data.length === 1) || // Shift+Enter from iTerm2 mapping
195
+ data === "\\\r" // Shift+Enter in VS Code terminal
196
+ ) {
197
+ // Modifier + Enter = new line
198
+ this.addNewLine();
199
+ }
200
+ // Plain Enter (char code 13 for CR) - only CR submits, LF adds new line
201
+ else if (data.charCodeAt(0) === 13 && data.length === 1) {
202
+ // If submit is disabled, do nothing
203
+ if (this.disableSubmit) {
204
+ return;
205
+ }
206
+ // Plain Enter = submit
207
+ const result = this.state.lines.join("\n").trim();
208
+ logger.info("TextEditor", "Submit triggered", {
209
+ result,
210
+ rawResult: JSON.stringify(this.state.lines.join("\n")),
211
+ lines: this.state.lines,
212
+ resultLines: result.split("\n"),
213
+ });
214
+ // Reset editor
215
+ this.state = {
216
+ lines: [""],
217
+ cursorLine: 0,
218
+ cursorCol: 0,
219
+ };
220
+ // Notify that editor is now empty
221
+ if (this.onChange) {
222
+ this.onChange("");
223
+ }
224
+ if (this.onSubmit) {
225
+ logger.info("TextEditor", "Calling onSubmit callback", { result });
226
+ this.onSubmit(result);
227
+ }
228
+ else {
229
+ logger.warn("TextEditor", "No onSubmit callback set");
230
+ }
231
+ }
232
+ // Backspace
233
+ else if (data.charCodeAt(0) === 127 || data.charCodeAt(0) === 8) {
234
+ this.handleBackspace();
235
+ }
236
+ // Line navigation shortcuts (Home/End keys)
237
+ else if (data === "\x1b[H" || data === "\x1b[1~" || data === "\x1b[7~") {
238
+ // Home key
239
+ this.moveToLineStart();
240
+ }
241
+ else if (data === "\x1b[F" || data === "\x1b[4~" || data === "\x1b[8~") {
242
+ // End key
243
+ this.moveToLineEnd();
244
+ }
245
+ // Forward delete (Fn+Backspace or Delete key)
246
+ else if (data === "\x1b[3~") {
247
+ // Delete key
248
+ this.handleForwardDelete();
249
+ }
250
+ // Arrow keys
251
+ else if (data === "\x1b[A") {
252
+ // Up
253
+ this.moveCursor(-1, 0);
254
+ }
255
+ else if (data === "\x1b[B") {
256
+ // Down
257
+ this.moveCursor(1, 0);
258
+ }
259
+ else if (data === "\x1b[C") {
260
+ // Right
261
+ this.moveCursor(0, 1);
262
+ }
263
+ else if (data === "\x1b[D") {
264
+ // Left
265
+ this.moveCursor(0, -1);
266
+ }
267
+ // Regular characters (printable ASCII)
268
+ else if (data.charCodeAt(0) >= 32 && data.charCodeAt(0) <= 126) {
269
+ logger.debug("TextEditor", "Inserting character", { char: data, charCode: data.charCodeAt(0) });
270
+ this.insertCharacter(data);
271
+ }
272
+ else {
273
+ logger.warn("TextEditor", "Unhandled input", {
274
+ data,
275
+ charCodes: Array.from(data).map((c) => c.charCodeAt(0)),
276
+ });
277
+ }
278
+ }
279
+ layoutText(contentWidth) {
280
+ const layoutLines = [];
281
+ if (this.state.lines.length === 0 || (this.state.lines.length === 1 && this.state.lines[0] === "")) {
282
+ // Empty editor
283
+ layoutLines.push({
284
+ text: "> ",
285
+ hasCursor: true,
286
+ cursorPos: 2,
287
+ });
288
+ return layoutLines;
289
+ }
290
+ // Process each logical line
291
+ for (let i = 0; i < this.state.lines.length; i++) {
292
+ const line = this.state.lines[i] || "";
293
+ const isCurrentLine = i === this.state.cursorLine;
294
+ const prefix = i === 0 ? "> " : " ";
295
+ const prefixedLine = prefix + line;
296
+ const maxLineLength = contentWidth;
297
+ if (prefixedLine.length <= maxLineLength) {
298
+ // Line fits in one layout line
299
+ if (isCurrentLine) {
300
+ layoutLines.push({
301
+ text: prefixedLine,
302
+ hasCursor: true,
303
+ cursorPos: prefix.length + this.state.cursorCol,
304
+ });
305
+ }
306
+ else {
307
+ layoutLines.push({
308
+ text: prefixedLine,
309
+ hasCursor: false,
310
+ });
311
+ }
312
+ }
313
+ else {
314
+ // Line needs wrapping
315
+ const chunks = [];
316
+ for (let pos = 0; pos < prefixedLine.length; pos += maxLineLength) {
317
+ chunks.push(prefixedLine.slice(pos, pos + maxLineLength));
318
+ }
319
+ for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
320
+ const chunk = chunks[chunkIndex];
321
+ if (!chunk)
322
+ continue;
323
+ const chunkStart = chunkIndex * maxLineLength;
324
+ const chunkEnd = chunkStart + chunk.length;
325
+ const cursorPos = prefix.length + this.state.cursorCol;
326
+ const hasCursorInChunk = isCurrentLine && cursorPos >= chunkStart && cursorPos < chunkEnd;
327
+ if (hasCursorInChunk) {
328
+ layoutLines.push({
329
+ text: chunk,
330
+ hasCursor: true,
331
+ cursorPos: cursorPos - chunkStart,
332
+ });
333
+ }
334
+ else {
335
+ layoutLines.push({
336
+ text: chunk,
337
+ hasCursor: false,
338
+ });
339
+ }
340
+ }
341
+ }
342
+ }
343
+ return layoutLines;
344
+ }
345
+ getText() {
346
+ return this.state.lines.join("\n");
347
+ }
348
+ setText(text) {
349
+ // Split text into lines, handling different line endings
350
+ const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
351
+ // Ensure at least one empty line
352
+ this.state.lines = lines.length === 0 ? [""] : lines;
353
+ // Reset cursor to end of text
354
+ this.state.cursorLine = this.state.lines.length - 1;
355
+ this.state.cursorCol = this.state.lines[this.state.cursorLine]?.length || 0;
356
+ // Notify of change
357
+ if (this.onChange) {
358
+ this.onChange(this.getText());
359
+ }
360
+ }
361
+ // All the editor methods from before...
362
+ insertCharacter(char) {
363
+ const line = this.state.lines[this.state.cursorLine] || "";
364
+ const before = line.slice(0, this.state.cursorCol);
365
+ const after = line.slice(this.state.cursorCol);
366
+ this.state.lines[this.state.cursorLine] = before + char + after;
367
+ this.state.cursorCol += char.length; // Fix: increment by the length of the inserted string
368
+ if (this.onChange) {
369
+ this.onChange(this.getText());
370
+ }
371
+ // Check if we should trigger or update autocomplete
372
+ if (!this.isAutocompleting) {
373
+ // Auto-trigger for "/" at the start of a line (slash commands)
374
+ if (char === "/" && this.isAtStartOfMessage()) {
375
+ this.tryTriggerAutocomplete();
376
+ }
377
+ // Also auto-trigger when typing letters in a slash command context
378
+ else if (/[a-zA-Z0-9]/.test(char)) {
379
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
380
+ const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
381
+ // Check if we're in a slash command with a space (i.e., typing arguments)
382
+ if (textBeforeCursor.startsWith("/") && textBeforeCursor.includes(" ")) {
383
+ this.tryTriggerAutocomplete();
384
+ }
385
+ }
386
+ }
387
+ else {
388
+ this.updateAutocomplete();
389
+ }
390
+ }
391
+ handlePaste(pastedText) {
392
+ logger.debug("TextEditor", "Processing paste", {
393
+ pastedText: JSON.stringify(pastedText),
394
+ hasTab: pastedText.includes("\t"),
395
+ tabCount: (pastedText.match(/\t/g) || []).length,
396
+ });
397
+ // Clean the pasted text
398
+ const cleanText = pastedText.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
399
+ // Convert tabs to spaces (4 spaces per tab)
400
+ const tabExpandedText = cleanText.replace(/\t/g, " ");
401
+ // Filter out non-printable characters except newlines
402
+ const filteredText = tabExpandedText
403
+ .split("")
404
+ .filter((char) => char === "\n" || (char >= " " && char <= "~"))
405
+ .join("");
406
+ // Split into lines
407
+ const pastedLines = filteredText.split("\n");
408
+ if (pastedLines.length === 1) {
409
+ // Single line - just insert each character
410
+ const text = pastedLines[0] || "";
411
+ for (const char of text) {
412
+ this.insertCharacter(char);
413
+ }
414
+ return;
415
+ }
416
+ // Multi-line paste - be very careful with array manipulation
417
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
418
+ const beforeCursor = currentLine.slice(0, this.state.cursorCol);
419
+ const afterCursor = currentLine.slice(this.state.cursorCol);
420
+ // Build the new lines array step by step
421
+ const newLines = [];
422
+ // Add all lines before current line
423
+ for (let i = 0; i < this.state.cursorLine; i++) {
424
+ newLines.push(this.state.lines[i] || "");
425
+ }
426
+ // Add the first pasted line merged with before cursor text
427
+ newLines.push(beforeCursor + (pastedLines[0] || ""));
428
+ // Add all middle pasted lines
429
+ for (let i = 1; i < pastedLines.length - 1; i++) {
430
+ newLines.push(pastedLines[i] || "");
431
+ }
432
+ // Add the last pasted line with after cursor text
433
+ newLines.push((pastedLines[pastedLines.length - 1] || "") + afterCursor);
434
+ // Add all lines after current line
435
+ for (let i = this.state.cursorLine + 1; i < this.state.lines.length; i++) {
436
+ newLines.push(this.state.lines[i] || "");
437
+ }
438
+ // Replace the entire lines array
439
+ this.state.lines = newLines;
440
+ // Update cursor position to end of pasted content
441
+ this.state.cursorLine += pastedLines.length - 1;
442
+ this.state.cursorCol = (pastedLines[pastedLines.length - 1] || "").length;
443
+ // Notify of change
444
+ if (this.onChange) {
445
+ this.onChange(this.getText());
446
+ }
447
+ }
448
+ addNewLine() {
449
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
450
+ const before = currentLine.slice(0, this.state.cursorCol);
451
+ const after = currentLine.slice(this.state.cursorCol);
452
+ // Split current line
453
+ this.state.lines[this.state.cursorLine] = before;
454
+ this.state.lines.splice(this.state.cursorLine + 1, 0, after);
455
+ // Move cursor to start of new line
456
+ this.state.cursorLine++;
457
+ this.state.cursorCol = 0;
458
+ if (this.onChange) {
459
+ this.onChange(this.getText());
460
+ }
461
+ }
462
+ handleBackspace() {
463
+ if (this.state.cursorCol > 0) {
464
+ // Delete character in current line
465
+ const line = this.state.lines[this.state.cursorLine] || "";
466
+ const before = line.slice(0, this.state.cursorCol - 1);
467
+ const after = line.slice(this.state.cursorCol);
468
+ this.state.lines[this.state.cursorLine] = before + after;
469
+ this.state.cursorCol--;
470
+ }
471
+ else if (this.state.cursorLine > 0) {
472
+ // Merge with previous line
473
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
474
+ const previousLine = this.state.lines[this.state.cursorLine - 1] || "";
475
+ this.state.lines[this.state.cursorLine - 1] = previousLine + currentLine;
476
+ this.state.lines.splice(this.state.cursorLine, 1);
477
+ this.state.cursorLine--;
478
+ this.state.cursorCol = previousLine.length;
479
+ }
480
+ if (this.onChange) {
481
+ this.onChange(this.getText());
482
+ }
483
+ // Update autocomplete after backspace
484
+ if (this.isAutocompleting) {
485
+ this.updateAutocomplete();
486
+ }
487
+ }
488
+ moveToLineStart() {
489
+ this.state.cursorCol = 0;
490
+ }
491
+ moveToLineEnd() {
492
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
493
+ this.state.cursorCol = currentLine.length;
494
+ }
495
+ handleForwardDelete() {
496
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
497
+ if (this.state.cursorCol < currentLine.length) {
498
+ // Delete character at cursor position (forward delete)
499
+ const before = currentLine.slice(0, this.state.cursorCol);
500
+ const after = currentLine.slice(this.state.cursorCol + 1);
501
+ this.state.lines[this.state.cursorLine] = before + after;
502
+ }
503
+ else if (this.state.cursorLine < this.state.lines.length - 1) {
504
+ // At end of line - merge with next line
505
+ const nextLine = this.state.lines[this.state.cursorLine + 1] || "";
506
+ this.state.lines[this.state.cursorLine] = currentLine + nextLine;
507
+ this.state.lines.splice(this.state.cursorLine + 1, 1);
508
+ }
509
+ if (this.onChange) {
510
+ this.onChange(this.getText());
511
+ }
512
+ }
513
+ deleteCurrentLine() {
514
+ if (this.state.lines.length === 1) {
515
+ // Only one line - just clear it
516
+ this.state.lines[0] = "";
517
+ this.state.cursorCol = 0;
518
+ }
519
+ else {
520
+ // Multiple lines - remove current line
521
+ this.state.lines.splice(this.state.cursorLine, 1);
522
+ // Adjust cursor position
523
+ if (this.state.cursorLine >= this.state.lines.length) {
524
+ // Was on last line, move to new last line
525
+ this.state.cursorLine = this.state.lines.length - 1;
526
+ }
527
+ // Clamp cursor column to new line length
528
+ const newLine = this.state.lines[this.state.cursorLine] || "";
529
+ this.state.cursorCol = Math.min(this.state.cursorCol, newLine.length);
530
+ }
531
+ if (this.onChange) {
532
+ this.onChange(this.getText());
533
+ }
534
+ }
535
+ moveCursor(deltaLine, deltaCol) {
536
+ if (deltaLine !== 0) {
537
+ const newLine = this.state.cursorLine + deltaLine;
538
+ if (newLine >= 0 && newLine < this.state.lines.length) {
539
+ this.state.cursorLine = newLine;
540
+ // Clamp cursor column to new line length
541
+ const line = this.state.lines[this.state.cursorLine] || "";
542
+ this.state.cursorCol = Math.min(this.state.cursorCol, line.length);
543
+ }
544
+ }
545
+ if (deltaCol !== 0) {
546
+ // Move column
547
+ const newCol = this.state.cursorCol + deltaCol;
548
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
549
+ const maxCol = currentLine.length;
550
+ this.state.cursorCol = Math.max(0, Math.min(maxCol, newCol));
551
+ }
552
+ }
553
+ // Helper method to check if cursor is at start of message (for slash command detection)
554
+ isAtStartOfMessage() {
555
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
556
+ const beforeCursor = currentLine.slice(0, this.state.cursorCol);
557
+ // At start if line is empty, only contains whitespace, or is just "/"
558
+ return beforeCursor.trim() === "" || beforeCursor.trim() === "/";
559
+ }
560
+ // Autocomplete methods
561
+ tryTriggerAutocomplete(explicitTab = false) {
562
+ logger.debug("TextEditor", "tryTriggerAutocomplete called", {
563
+ explicitTab,
564
+ hasProvider: !!this.autocompleteProvider,
565
+ });
566
+ if (!this.autocompleteProvider)
567
+ return;
568
+ // Check if we should trigger file completion on Tab
569
+ if (explicitTab) {
570
+ const provider = this.autocompleteProvider;
571
+ const shouldTrigger = !provider.shouldTriggerFileCompletion ||
572
+ provider.shouldTriggerFileCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol);
573
+ logger.debug("TextEditor", "Tab file completion check", {
574
+ hasShouldTriggerMethod: !!provider.shouldTriggerFileCompletion,
575
+ shouldTrigger,
576
+ lines: this.state.lines,
577
+ cursorLine: this.state.cursorLine,
578
+ cursorCol: this.state.cursorCol,
579
+ });
580
+ if (!shouldTrigger) {
581
+ return;
582
+ }
583
+ }
584
+ const suggestions = this.autocompleteProvider.getSuggestions(this.state.lines, this.state.cursorLine, this.state.cursorCol);
585
+ logger.debug("TextEditor", "Autocomplete suggestions", {
586
+ hasSuggestions: !!suggestions,
587
+ itemCount: suggestions?.items.length || 0,
588
+ prefix: suggestions?.prefix,
589
+ });
590
+ if (suggestions && suggestions.items.length > 0) {
591
+ this.autocompletePrefix = suggestions.prefix;
592
+ this.autocompleteList = new SelectList(suggestions.items, 5);
593
+ this.isAutocompleting = true;
594
+ }
595
+ else {
596
+ this.cancelAutocomplete();
597
+ }
598
+ }
599
+ handleTabCompletion() {
600
+ if (!this.autocompleteProvider)
601
+ return;
602
+ const currentLine = this.state.lines[this.state.cursorLine] || "";
603
+ const beforeCursor = currentLine.slice(0, this.state.cursorCol);
604
+ // Check if we're in a slash command context
605
+ if (beforeCursor.trimStart().startsWith("/")) {
606
+ logger.debug("TextEditor", "Tab in slash command context", { beforeCursor });
607
+ this.handleSlashCommandCompletion();
608
+ }
609
+ else {
610
+ logger.debug("TextEditor", "Tab in file completion context", { beforeCursor });
611
+ this.forceFileAutocomplete();
612
+ }
613
+ }
614
+ handleSlashCommandCompletion() {
615
+ // For now, fall back to regular autocomplete (slash commands)
616
+ // This can be extended later to handle command-specific argument completion
617
+ logger.debug("TextEditor", "Handling slash command completion");
618
+ this.tryTriggerAutocomplete(true);
619
+ }
620
+ forceFileAutocomplete() {
621
+ logger.debug("TextEditor", "forceFileAutocomplete called", {
622
+ hasProvider: !!this.autocompleteProvider,
623
+ });
624
+ if (!this.autocompleteProvider)
625
+ return;
626
+ // Check if provider has the force method
627
+ const provider = this.autocompleteProvider;
628
+ if (!provider.getForceFileSuggestions) {
629
+ logger.debug("TextEditor", "Provider doesn't support forced file completion, falling back to regular");
630
+ this.tryTriggerAutocomplete(true);
631
+ return;
632
+ }
633
+ const suggestions = provider.getForceFileSuggestions(this.state.lines, this.state.cursorLine, this.state.cursorCol);
634
+ logger.debug("TextEditor", "Forced file autocomplete suggestions", {
635
+ hasSuggestions: !!suggestions,
636
+ itemCount: suggestions?.items.length || 0,
637
+ prefix: suggestions?.prefix,
638
+ });
639
+ if (suggestions && suggestions.items.length > 0) {
640
+ this.autocompletePrefix = suggestions.prefix;
641
+ this.autocompleteList = new SelectList(suggestions.items, 5);
642
+ this.isAutocompleting = true;
643
+ }
644
+ else {
645
+ this.cancelAutocomplete();
646
+ }
647
+ }
648
+ cancelAutocomplete() {
649
+ this.isAutocompleting = false;
650
+ this.autocompleteList = undefined;
651
+ this.autocompletePrefix = "";
652
+ }
653
+ updateAutocomplete() {
654
+ if (!this.isAutocompleting || !this.autocompleteProvider)
655
+ return;
656
+ const suggestions = this.autocompleteProvider.getSuggestions(this.state.lines, this.state.cursorLine, this.state.cursorCol);
657
+ if (suggestions && suggestions.items.length > 0) {
658
+ this.autocompletePrefix = suggestions.prefix;
659
+ if (this.autocompleteList) {
660
+ // Update the existing list with new items
661
+ this.autocompleteList = new SelectList(suggestions.items, 5);
662
+ }
663
+ }
664
+ else {
665
+ // No more matches, cancel autocomplete
666
+ this.cancelAutocomplete();
667
+ }
668
+ }
669
+ }
670
+ //# sourceMappingURL=text-editor.js.map