@travisennis/acai 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/agent/index.d.ts +119 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +406 -0
- package/dist/agent/manual-loop.d.ts +41 -0
- package/dist/agent/manual-loop.d.ts.map +1 -0
- package/dist/agent/manual-loop.js +278 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +27 -33
- package/dist/commands/add-directory-command.d.ts +3 -0
- package/dist/commands/add-directory-command.d.ts.map +1 -0
- package/dist/commands/add-directory-command.js +85 -0
- package/dist/commands/application-log-command.d.ts.map +1 -1
- package/dist/commands/application-log-command.js +34 -0
- package/dist/commands/clear-command.d.ts.map +1 -1
- package/dist/commands/clear-command.js +8 -0
- package/dist/commands/compact-command.d.ts.map +1 -1
- package/dist/commands/compact-command.js +15 -2
- package/dist/commands/context-command.d.ts +3 -0
- package/dist/commands/context-command.d.ts.map +1 -0
- package/dist/commands/context-command.js +183 -0
- package/dist/commands/copy-command.d.ts.map +1 -1
- package/dist/commands/copy-command.js +28 -0
- package/dist/commands/edit-command.d.ts.map +1 -1
- package/dist/commands/edit-command.js +33 -0
- package/dist/commands/edit-prompt-command.d.ts.map +1 -1
- package/dist/commands/edit-prompt-command.js +28 -0
- package/dist/commands/exit-command.d.ts.map +1 -1
- package/dist/commands/exit-command.js +20 -0
- package/dist/commands/files-command.d.ts.map +1 -1
- package/dist/commands/files-command.js +57 -0
- package/dist/commands/generate-rules-command.d.ts.map +1 -1
- package/dist/commands/generate-rules-command.js +311 -1
- package/dist/commands/handoff-command.d.ts +3 -0
- package/dist/commands/handoff-command.d.ts.map +1 -0
- package/dist/commands/handoff-command.js +202 -0
- package/dist/commands/health-command.d.ts.map +1 -1
- package/dist/commands/health-command.js +119 -2
- package/dist/commands/help-command.d.ts.map +1 -1
- package/dist/commands/help-command.js +28 -0
- package/dist/commands/history-command.d.ts +3 -0
- package/dist/commands/history-command.d.ts.map +1 -0
- package/dist/commands/history-command.js +534 -0
- package/dist/commands/init-command.d.ts +1 -1
- package/dist/commands/init-command.d.ts.map +1 -1
- package/dist/commands/init-command.js +55 -18
- package/dist/commands/last-log-command.d.ts.map +1 -1
- package/dist/commands/last-log-command.js +27 -0
- package/dist/commands/list-directories-command.d.ts +3 -0
- package/dist/commands/list-directories-command.d.ts.map +1 -0
- package/dist/commands/list-directories-command.js +48 -0
- package/dist/commands/list-tools-command.d.ts.map +1 -1
- package/dist/commands/list-tools-command.js +66 -3
- package/dist/commands/manager.d.ts +15 -3
- package/dist/commands/manager.d.ts.map +1 -1
- package/dist/commands/manager.js +86 -26
- package/dist/commands/model-command.d.ts +22 -0
- package/dist/commands/model-command.d.ts.map +1 -1
- package/dist/commands/model-command.js +256 -0
- package/dist/commands/paste-command.d.ts.map +1 -1
- package/dist/commands/paste-command.js +92 -0
- package/dist/commands/pickup-command.d.ts +3 -0
- package/dist/commands/pickup-command.d.ts.map +1 -0
- package/dist/commands/pickup-command.js +161 -0
- package/dist/commands/prompt-command.d.ts +1 -1
- package/dist/commands/prompt-command.d.ts.map +1 -1
- package/dist/commands/prompt-command.js +117 -2
- package/dist/commands/remove-directory-command.d.ts +3 -0
- package/dist/commands/remove-directory-command.d.ts.map +1 -0
- package/dist/commands/remove-directory-command.js +87 -0
- package/dist/commands/reset-command.d.ts +1 -1
- package/dist/commands/reset-command.d.ts.map +1 -1
- package/dist/commands/reset-command.js +13 -2
- package/dist/commands/rules-command.d.ts.map +1 -1
- package/dist/commands/rules-command.js +65 -0
- package/dist/commands/save-command.d.ts.map +1 -1
- package/dist/commands/save-command.js +12 -0
- package/dist/commands/shell-command.d.ts.map +1 -1
- package/dist/commands/shell-command.js +68 -0
- package/dist/commands/types.d.ts +9 -4
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/commands/usage-command.d.ts.map +1 -1
- package/dist/commands/usage-command.js +22 -0
- package/dist/config.d.ts +6 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +23 -29
- package/dist/formatting.d.ts +108 -0
- package/dist/formatting.d.ts.map +1 -1
- package/dist/formatting.js +147 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +140 -38
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +47 -18
- package/dist/mentions.d.ts +2 -1
- package/dist/mentions.d.ts.map +1 -1
- package/dist/mentions.js +16 -1
- package/dist/messages.d.ts +8 -0
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +56 -19
- package/dist/middleware/cache.d.ts +3 -0
- package/dist/middleware/cache.d.ts.map +1 -0
- package/dist/middleware/cache.js +53 -0
- package/dist/middleware/index.d.ts +1 -0
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +1 -0
- package/dist/models/ai-config.d.ts +4 -2
- package/dist/models/ai-config.d.ts.map +1 -1
- package/dist/models/ai-config.js +12 -2
- package/dist/models/anthropic-provider.d.ts.map +1 -1
- package/dist/models/anthropic-provider.js +3 -60
- package/dist/models/manager.d.ts +2 -1
- package/dist/models/manager.d.ts.map +1 -1
- package/dist/models/manager.js +26 -2
- package/dist/models/openrouter-provider.d.ts +7 -14
- package/dist/models/openrouter-provider.d.ts.map +1 -1
- package/dist/models/openrouter-provider.js +114 -169
- package/dist/models/providers.d.ts +1 -1
- package/dist/models/providers.d.ts.map +1 -1
- package/dist/prompts.d.ts +1 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +53 -4
- package/dist/repl/display-tool-messages.d.ts +1 -1
- package/dist/repl/display-tool-messages.d.ts.map +1 -1
- package/dist/repl/display-tool-messages.js +47 -44
- package/dist/repl/get-prompt-header.d.ts.map +1 -1
- package/dist/repl/get-prompt-header.js +1 -30
- package/dist/repl/project-status-line.d.ts +2 -0
- package/dist/repl/project-status-line.d.ts.map +1 -0
- package/dist/repl/project-status-line.js +31 -0
- package/dist/repl/prompt.d.ts +21 -0
- package/dist/repl/prompt.d.ts.map +1 -0
- package/dist/{repl-prompt.js → repl/prompt.js} +119 -22
- package/dist/repl/tool-call-repair.d.ts.map +1 -1
- package/dist/repl/tool-call-repair.js +8 -4
- package/dist/repl-new.d.ts +53 -0
- package/dist/repl-new.d.ts.map +1 -0
- package/dist/repl-new.js +374 -0
- package/dist/repl.d.ts +3 -5
- package/dist/repl.d.ts.map +1 -1
- package/dist/repl.js +74 -166
- package/dist/terminal/checkbox-prompt.d.ts.map +1 -1
- package/dist/terminal/checkbox-prompt.js +10 -4
- package/dist/terminal/index.d.ts +7 -0
- package/dist/terminal/index.d.ts.map +1 -1
- package/dist/terminal/index.js +94 -0
- package/dist/terminal/input-prompt.d.ts +2 -1
- package/dist/terminal/input-prompt.d.ts.map +1 -1
- package/dist/terminal/markdown.js +3 -0
- package/dist/terminal/search-prompt.d.ts.map +1 -1
- package/dist/terminal/search-prompt.js +11 -10
- package/dist/terminal/select-prompt.d.ts +2 -2
- package/dist/terminal/select-prompt.d.ts.map +1 -1
- package/dist/terminal/select-prompt.js +47 -39
- package/dist/tokens/threshold.d.ts +35 -0
- package/dist/tokens/threshold.d.ts.map +1 -0
- package/dist/tokens/threshold.js +85 -0
- package/dist/tools/advanced-edit-file.d.ts +69 -0
- package/dist/tools/advanced-edit-file.d.ts.map +1 -0
- package/dist/tools/advanced-edit-file.js +281 -0
- package/dist/tools/agent.d.ts +16 -5
- package/dist/tools/agent.d.ts.map +1 -1
- package/dist/tools/agent.js +71 -58
- package/dist/tools/bash-utils.d.ts +1 -1
- package/dist/tools/bash-utils.d.ts.map +1 -1
- package/dist/tools/bash-utils.js +14 -6
- package/dist/tools/bash.d.ts +21 -12
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +88 -135
- package/dist/tools/code-interpreter.d.ts +21 -9
- package/dist/tools/code-interpreter.d.ts.map +1 -1
- package/dist/tools/code-interpreter.js +138 -137
- package/dist/tools/delete-file.d.ts +17 -10
- package/dist/tools/delete-file.d.ts.map +1 -1
- package/dist/tools/delete-file.js +51 -95
- package/dist/tools/directory-tree.d.ts +17 -6
- package/dist/tools/directory-tree.d.ts.map +1 -1
- package/dist/tools/directory-tree.js +47 -49
- package/dist/tools/dynamic-tool-loader.d.ts +18 -8
- package/dist/tools/dynamic-tool-loader.d.ts.map +1 -1
- package/dist/tools/dynamic-tool-loader.js +121 -129
- package/dist/tools/dynamic-tool-parser.d.ts +1 -0
- package/dist/tools/dynamic-tool-parser.d.ts.map +1 -1
- package/dist/tools/dynamic-tool-parser.js +1 -0
- package/dist/tools/edit-file.d.ts +35 -15
- package/dist/tools/edit-file.d.ts.map +1 -1
- package/dist/tools/edit-file.js +112 -112
- package/dist/tools/filesystem-utils.d.ts +2 -1
- package/dist/tools/filesystem-utils.d.ts.map +1 -1
- package/dist/tools/filesystem-utils.js +31 -17
- package/dist/tools/glob.d.ts +36 -0
- package/dist/tools/glob.d.ts.map +1 -0
- package/dist/tools/glob.js +143 -0
- package/dist/tools/grep.d.ts +73 -12
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +413 -168
- package/dist/tools/index.d.ts +204 -124
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +242 -135
- package/dist/tools/llm-edit-fixer.d.ts +25 -0
- package/dist/tools/llm-edit-fixer.d.ts.map +1 -0
- package/dist/tools/llm-edit-fixer.js +150 -0
- package/dist/tools/move-file.d.ts +19 -7
- package/dist/tools/move-file.d.ts.map +1 -1
- package/dist/tools/move-file.js +40 -33
- package/dist/tools/read-file.d.ts +47 -9
- package/dist/tools/read-file.d.ts.map +1 -1
- package/dist/tools/read-file.js +74 -69
- package/dist/tools/read-multiple-files.d.ts +17 -6
- package/dist/tools/read-multiple-files.d.ts.map +1 -1
- package/dist/tools/read-multiple-files.js +76 -73
- package/dist/tools/save-file.d.ts +45 -12
- package/dist/tools/save-file.d.ts.map +1 -1
- package/dist/tools/save-file.js +58 -101
- package/dist/tools/think.d.ts +15 -7
- package/dist/tools/think.d.ts.map +1 -1
- package/dist/tools/think.js +30 -22
- package/dist/tools/types.d.ts +4 -10
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/types.js +9 -0
- package/dist/tools/utils.d.ts +14 -0
- package/dist/tools/utils.d.ts.map +1 -0
- package/dist/tools/utils.js +16 -0
- package/dist/tools/web-fetch.d.ts +11 -4
- package/dist/tools/web-fetch.d.ts.map +1 -1
- package/dist/tools/web-fetch.js +39 -38
- package/dist/tools/web-search.d.ts +15 -6
- package/dist/tools/web-search.d.ts.map +1 -1
- package/dist/tools/web-search.js +50 -32
- package/dist/tui/autocomplete.d.ts +44 -0
- package/dist/tui/autocomplete.d.ts.map +1 -0
- package/dist/tui/autocomplete.js +466 -0
- package/dist/tui/components/assistant-message.d.ts +18 -0
- package/dist/tui/components/assistant-message.d.ts.map +1 -0
- package/dist/tui/components/assistant-message.js +29 -0
- package/dist/tui/components/editor.d.ts +51 -0
- package/dist/tui/components/editor.d.ts.map +1 -0
- package/dist/tui/components/editor.js +758 -0
- package/dist/tui/components/footer.d.ts +24 -0
- package/dist/tui/components/footer.d.ts.map +1 -0
- package/dist/tui/components/footer.js +197 -0
- package/dist/tui/components/input.d.ts +14 -0
- package/dist/tui/components/input.d.ts.map +1 -0
- package/dist/tui/components/input.js +122 -0
- package/dist/tui/components/loader.d.ts +19 -0
- package/dist/tui/components/loader.d.ts.map +1 -0
- package/dist/tui/components/loader.js +45 -0
- package/dist/tui/components/markdown.d.ts +103 -0
- package/dist/tui/components/markdown.d.ts.map +1 -0
- package/dist/tui/components/markdown.js +533 -0
- package/dist/tui/components/modal.d.ts +40 -0
- package/dist/tui/components/modal.d.ts.map +1 -0
- package/dist/tui/components/modal.js +292 -0
- package/dist/tui/components/prompt-status.d.ts +16 -0
- package/dist/tui/components/prompt-status.d.ts.map +1 -0
- package/dist/tui/components/prompt-status.js +21 -0
- package/dist/tui/components/select-list.d.ts +22 -0
- package/dist/tui/components/select-list.d.ts.map +1 -0
- package/dist/tui/components/select-list.js +143 -0
- package/dist/tui/components/spacer.d.ts +16 -0
- package/dist/tui/components/spacer.d.ts.map +1 -0
- package/dist/tui/components/spacer.js +27 -0
- package/dist/tui/components/text.d.ts +26 -0
- package/dist/tui/components/text.d.ts.map +1 -0
- package/dist/tui/components/text.js +143 -0
- package/dist/tui/components/thinking-block.d.ts +14 -0
- package/dist/tui/components/thinking-block.d.ts.map +1 -0
- package/dist/tui/components/thinking-block.js +30 -0
- package/dist/tui/components/tool-execution.d.ts +17 -0
- package/dist/tui/components/tool-execution.d.ts.map +1 -0
- package/dist/tui/components/tool-execution.js +153 -0
- package/dist/tui/components/user-message.d.ts +9 -0
- package/dist/tui/components/user-message.d.ts.map +1 -0
- package/dist/tui/components/user-message.js +21 -0
- package/dist/tui/components/welcome.d.ts +6 -0
- package/dist/tui/components/welcome.d.ts.map +1 -0
- package/dist/tui/components/welcome.js +30 -0
- package/dist/tui/index.d.ts +14 -0
- package/dist/tui/index.d.ts.map +1 -0
- package/dist/tui/index.js +18 -0
- package/dist/tui/terminal.d.ts +37 -0
- package/dist/tui/terminal.d.ts.map +1 -0
- package/dist/tui/terminal.js +104 -0
- package/dist/tui/tui.d.ts +67 -0
- package/dist/tui/tui.d.ts.map +1 -0
- package/dist/tui/tui.js +184 -0
- package/dist/tui/utils.d.ts +19 -0
- package/dist/tui/utils.d.ts.map +1 -0
- package/dist/tui/utils.js +31 -0
- package/dist/utils/generators.d.ts +3 -0
- package/dist/utils/generators.d.ts.map +1 -0
- package/dist/utils/generators.js +25 -0
- package/dist/utils/iterables.d.ts +2 -0
- package/dist/utils/iterables.d.ts.map +1 -0
- package/dist/utils/iterables.js +6 -0
- package/package.json +16 -16
- package/dist/conversation-analyzer.d.ts +0 -11
- package/dist/conversation-analyzer.d.ts.map +0 -1
- package/dist/conversation-analyzer.js +0 -88
- package/dist/repl-prompt.d.ts +0 -15
- package/dist/repl-prompt.d.ts.map +0 -1
- package/dist/tokens/manage-output.d.ts +0 -34
- package/dist/tokens/manage-output.d.ts.map +0 -1
- package/dist/tokens/manage-output.js +0 -44
- package/dist/tool-executor.d.ts +0 -28
- package/dist/tool-executor.d.ts.map +0 -1
- package/dist/tool-executor.js +0 -74
- package/dist/tools/file-editing-utils.d.ts +0 -2
- package/dist/tools/file-editing-utils.d.ts.map +0 -1
- package/dist/tools/file-editing-utils.js +0 -135
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
import style from "../../terminal/style.js";
|
|
2
|
+
import { SelectList } from "./select-list.js";
|
|
3
|
+
export class Editor {
|
|
4
|
+
state = {
|
|
5
|
+
lines: [""],
|
|
6
|
+
cursorLine: 0,
|
|
7
|
+
cursorCol: 0,
|
|
8
|
+
};
|
|
9
|
+
config = {};
|
|
10
|
+
// Autocomplete support
|
|
11
|
+
autocompleteProvider;
|
|
12
|
+
autocompleteList;
|
|
13
|
+
isAutocompleting = false;
|
|
14
|
+
autocompletePrefix = "";
|
|
15
|
+
autocompleteDebounceTimer;
|
|
16
|
+
// Paste tracking for large pastes
|
|
17
|
+
pastes = new Map();
|
|
18
|
+
pasteCounter = 0;
|
|
19
|
+
// Bracketed paste mode buffering
|
|
20
|
+
pasteBuffer = "";
|
|
21
|
+
isInPaste = false;
|
|
22
|
+
onSubmit;
|
|
23
|
+
onChange;
|
|
24
|
+
disableSubmit = false;
|
|
25
|
+
// Custom key handlers for coding-agent
|
|
26
|
+
onEscape;
|
|
27
|
+
onCtrlC;
|
|
28
|
+
onRenderRequested;
|
|
29
|
+
constructor(config) {
|
|
30
|
+
if (config) {
|
|
31
|
+
this.config = { ...this.config, ...config };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
configure(config) {
|
|
35
|
+
this.config = { ...this.config, ...config };
|
|
36
|
+
}
|
|
37
|
+
setAutocompleteProvider(provider) {
|
|
38
|
+
this.autocompleteProvider = provider;
|
|
39
|
+
}
|
|
40
|
+
render(width) {
|
|
41
|
+
const horizontal = style.gray("─");
|
|
42
|
+
// Layout the text - use full width
|
|
43
|
+
const layoutLines = this.layoutText(width);
|
|
44
|
+
const result = [];
|
|
45
|
+
// Render top border
|
|
46
|
+
result.push(horizontal.repeat(width));
|
|
47
|
+
// Render each layout line
|
|
48
|
+
for (const layoutLine of layoutLines) {
|
|
49
|
+
let displayText = layoutLine.text;
|
|
50
|
+
let visibleLength = layoutLine.text.length;
|
|
51
|
+
// Add cursor if this line has it
|
|
52
|
+
if (layoutLine.hasCursor && layoutLine.cursorPos !== undefined) {
|
|
53
|
+
const before = displayText.slice(0, layoutLine.cursorPos);
|
|
54
|
+
const after = displayText.slice(layoutLine.cursorPos);
|
|
55
|
+
if (after.length > 0) {
|
|
56
|
+
// Cursor is on a character - replace it with highlighted version
|
|
57
|
+
const cursor = `\x1b[7m${after[0]}\x1b[0m`;
|
|
58
|
+
const restAfter = after.slice(1);
|
|
59
|
+
displayText = before + cursor + restAfter;
|
|
60
|
+
// visibleLength stays the same - we're replacing, not adding
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// Cursor is at the end - check if we have room for the space
|
|
64
|
+
if (layoutLine.text.length < width) {
|
|
65
|
+
// We have room - add highlighted space
|
|
66
|
+
const cursor = "\x1b[7m \x1b[0m";
|
|
67
|
+
displayText = before + cursor;
|
|
68
|
+
// visibleLength increases by 1 - we're adding a space
|
|
69
|
+
visibleLength = layoutLine.text.length + 1;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Line is at full width - use reverse video on last character if possible
|
|
73
|
+
// or just show cursor at the end without adding space
|
|
74
|
+
if (before.length > 0) {
|
|
75
|
+
const lastChar = before[before.length - 1];
|
|
76
|
+
const cursor = `\x1b[7m${lastChar}\x1b[0m`;
|
|
77
|
+
displayText = before.slice(0, -1) + cursor;
|
|
78
|
+
}
|
|
79
|
+
// visibleLength stays the same
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Calculate padding based on actual visible length
|
|
84
|
+
const padding = " ".repeat(Math.max(0, width - visibleLength));
|
|
85
|
+
// Render the line (no side borders, just horizontal lines above and below)
|
|
86
|
+
result.push(displayText + padding);
|
|
87
|
+
}
|
|
88
|
+
// Render bottom border
|
|
89
|
+
result.push(horizontal.repeat(width));
|
|
90
|
+
// Add autocomplete list if active
|
|
91
|
+
if (this.isAutocompleting && this.autocompleteList) {
|
|
92
|
+
const autocompleteResult = this.autocompleteList.render(width);
|
|
93
|
+
result.push(...autocompleteResult);
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
handleInput(data) {
|
|
98
|
+
// Handle bracketed paste mode
|
|
99
|
+
// Start of paste: \x1b[200~
|
|
100
|
+
// End of paste: \x1b[201~
|
|
101
|
+
// Check if we're starting a bracketed paste
|
|
102
|
+
if (data.includes("\x1b[200~")) {
|
|
103
|
+
this.isInPaste = true;
|
|
104
|
+
this.pasteBuffer = "";
|
|
105
|
+
// Remove the start marker and keep the rest
|
|
106
|
+
const cleanedData = data.replace("\x1b[200~", "");
|
|
107
|
+
// Process the remaining data
|
|
108
|
+
this.processInputData(cleanedData);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// If we're in a paste, buffer the data
|
|
112
|
+
if (this.isInPaste) {
|
|
113
|
+
// Append data to buffer first (end marker could be split across chunks)
|
|
114
|
+
this.pasteBuffer += data;
|
|
115
|
+
// Check if the accumulated buffer contains the end marker
|
|
116
|
+
const endIndex = this.pasteBuffer.indexOf("\x1b[201~");
|
|
117
|
+
if (endIndex !== -1) {
|
|
118
|
+
// Extract content before the end marker
|
|
119
|
+
const pasteContent = this.pasteBuffer.substring(0, endIndex);
|
|
120
|
+
// Process the complete paste
|
|
121
|
+
this.handlePaste(pasteContent);
|
|
122
|
+
// Reset paste state
|
|
123
|
+
this.isInPaste = false;
|
|
124
|
+
// Process any remaining data after the end marker
|
|
125
|
+
const remaining = this.pasteBuffer.substring(endIndex + 6); // 6 = length of \x1b[201~
|
|
126
|
+
this.pasteBuffer = "";
|
|
127
|
+
if (remaining.length > 0) {
|
|
128
|
+
this.handleInput(remaining);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Still accumulating, wait for more data
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Intercept Escape key - but only if autocomplete is NOT active
|
|
136
|
+
// (let parent handle escape for autocomplete cancellation)
|
|
137
|
+
if (data === "\x1b" && this.onEscape && !this.isShowingAutocomplete()) {
|
|
138
|
+
this.onEscape();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// Intercept Ctrl+C
|
|
142
|
+
if (data === "\x03" && this.onCtrlC) {
|
|
143
|
+
this.onCtrlC();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// Process regular input data
|
|
147
|
+
this.processInputData(data);
|
|
148
|
+
}
|
|
149
|
+
processInputData(data) {
|
|
150
|
+
// Handle special key combinations first
|
|
151
|
+
// Ctrl+C - Exit (let parent handle this)
|
|
152
|
+
if (data.charCodeAt(0) === 3) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// Handle autocomplete special keys first (but don't block other input)
|
|
156
|
+
if (this.isAutocompleting && this.autocompleteList) {
|
|
157
|
+
// Escape - cancel autocomplete
|
|
158
|
+
if (data === "\x1b") {
|
|
159
|
+
this.cancelAutocomplete();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// Let the autocomplete list handle navigation and selection
|
|
163
|
+
if (data === "\x1b[A" ||
|
|
164
|
+
data === "\x1b[B" ||
|
|
165
|
+
data === "\r" ||
|
|
166
|
+
data === "\t") {
|
|
167
|
+
// Pass arrow keys and Tab to the list for navigation
|
|
168
|
+
if (data === "\x1b[A" || data === "\x1b[B" || data === "\t") {
|
|
169
|
+
this.autocompleteList.handleInput(data);
|
|
170
|
+
}
|
|
171
|
+
// Only Enter applies the selection
|
|
172
|
+
if (data === "\r") {
|
|
173
|
+
const selected = this.autocompleteList.getSelectedItem();
|
|
174
|
+
if (selected && this.autocompleteProvider) {
|
|
175
|
+
const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, selected, this.autocompletePrefix);
|
|
176
|
+
this.state.lines = result.lines;
|
|
177
|
+
this.state.cursorLine = result.cursorLine;
|
|
178
|
+
this.state.cursorCol = result.cursorCol;
|
|
179
|
+
this.cancelAutocomplete();
|
|
180
|
+
if (this.onChange) {
|
|
181
|
+
this.onChange(this.getText());
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// For other keys, handle normally within autocomplete
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// For other keys (like regular typing), DON'T return here
|
|
190
|
+
// Let them fall through to normal character handling
|
|
191
|
+
}
|
|
192
|
+
// Tab key - context-aware completion (but not when already autocompleting)
|
|
193
|
+
if (data === "\t" && !this.isAutocompleting) {
|
|
194
|
+
void this.handleTabCompletion();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// Continue with rest of input handling
|
|
198
|
+
// Ctrl+K - Delete current line
|
|
199
|
+
if (data.charCodeAt(0) === 11) {
|
|
200
|
+
this.deleteCurrentLine();
|
|
201
|
+
}
|
|
202
|
+
// Ctrl+A - Move to start of line
|
|
203
|
+
else if (data.charCodeAt(0) === 1) {
|
|
204
|
+
this.moveToLineStart();
|
|
205
|
+
}
|
|
206
|
+
// Ctrl+E - Move to end of line
|
|
207
|
+
else if (data.charCodeAt(0) === 5) {
|
|
208
|
+
this.moveToLineEnd();
|
|
209
|
+
}
|
|
210
|
+
// Modified Enter keys (Shift+Enter, Ctrl+Enter, etc.) - create new line
|
|
211
|
+
else if (this.isModifiedEnter(data)) {
|
|
212
|
+
// Modifier + Enter = new line
|
|
213
|
+
this.addNewLine();
|
|
214
|
+
}
|
|
215
|
+
// Plain Enter (char code 13 for CR) - only CR submits, LF adds new line
|
|
216
|
+
else if (data.charCodeAt(0) === 13 && data.length === 1) {
|
|
217
|
+
// If submit is disabled, do nothing
|
|
218
|
+
if (this.disableSubmit) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// Get text and substitute paste markers with actual content
|
|
222
|
+
let result = this.state.lines.join("\n").trim();
|
|
223
|
+
// Replace all [paste #N +xxx lines] or [paste #N xxx chars] markers with actual paste content
|
|
224
|
+
for (const [pasteId, pasteContent] of this.pastes) {
|
|
225
|
+
// Match formats: [paste #N], [paste #N +xxx lines], or [paste #N xxx chars]
|
|
226
|
+
const markerRegex = new RegExp(`\\[paste #${pasteId}( (\\+\\d+ lines|\\d+ chars))?\\]`, "g");
|
|
227
|
+
result = result.replace(markerRegex, pasteContent);
|
|
228
|
+
}
|
|
229
|
+
// Reset editor and clear pastes
|
|
230
|
+
this.state = {
|
|
231
|
+
lines: [""],
|
|
232
|
+
cursorLine: 0,
|
|
233
|
+
cursorCol: 0,
|
|
234
|
+
};
|
|
235
|
+
this.pastes.clear();
|
|
236
|
+
this.pasteCounter = 0;
|
|
237
|
+
// Notify that editor is now empty
|
|
238
|
+
if (this.onChange) {
|
|
239
|
+
this.onChange("");
|
|
240
|
+
}
|
|
241
|
+
if (this.onSubmit) {
|
|
242
|
+
this.onSubmit(result);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Backspace
|
|
246
|
+
else if (data.charCodeAt(0) === 127 || data.charCodeAt(0) === 8) {
|
|
247
|
+
this.handleBackspace();
|
|
248
|
+
}
|
|
249
|
+
// Line navigation shortcuts (Home/End keys)
|
|
250
|
+
else if (data === "\x1b[H" || data === "\x1b[1~" || data === "\x1b[7~") {
|
|
251
|
+
// Home key
|
|
252
|
+
this.moveToLineStart();
|
|
253
|
+
}
|
|
254
|
+
else if (data === "\x1b[F" || data === "\x1b[4~" || data === "\x1b[8~") {
|
|
255
|
+
// End key
|
|
256
|
+
this.moveToLineEnd();
|
|
257
|
+
}
|
|
258
|
+
// Forward delete (Fn+Backspace or Delete key)
|
|
259
|
+
else if (data === "\x1b[3~") {
|
|
260
|
+
// Delete key
|
|
261
|
+
this.handleForwardDelete();
|
|
262
|
+
}
|
|
263
|
+
// Arrow keys
|
|
264
|
+
else if (data === "\x1b[A") {
|
|
265
|
+
// Up
|
|
266
|
+
this.moveCursor(-1, 0);
|
|
267
|
+
}
|
|
268
|
+
else if (data === "\x1b[B") {
|
|
269
|
+
// Down
|
|
270
|
+
this.moveCursor(1, 0);
|
|
271
|
+
}
|
|
272
|
+
else if (data === "\x1b[C") {
|
|
273
|
+
// Right
|
|
274
|
+
this.moveCursor(0, 1);
|
|
275
|
+
}
|
|
276
|
+
else if (data === "\x1b[D") {
|
|
277
|
+
// Left
|
|
278
|
+
this.moveCursor(0, -1);
|
|
279
|
+
}
|
|
280
|
+
// Regular characters (printable ASCII)
|
|
281
|
+
else if (data.charCodeAt(0) >= 32 && data.charCodeAt(0) <= 126) {
|
|
282
|
+
this.insertCharacter(data);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
layoutText(contentWidth) {
|
|
286
|
+
const layoutLines = [];
|
|
287
|
+
if (this.state.lines.length === 0 ||
|
|
288
|
+
(this.state.lines.length === 1 && this.state.lines[0] === "")) {
|
|
289
|
+
// Empty editor
|
|
290
|
+
layoutLines.push({
|
|
291
|
+
text: "",
|
|
292
|
+
hasCursor: true,
|
|
293
|
+
cursorPos: 0,
|
|
294
|
+
});
|
|
295
|
+
return layoutLines;
|
|
296
|
+
}
|
|
297
|
+
// Process each logical line
|
|
298
|
+
for (let i = 0; i < this.state.lines.length; i++) {
|
|
299
|
+
const line = this.state.lines[i] || "";
|
|
300
|
+
const isCurrentLine = i === this.state.cursorLine;
|
|
301
|
+
const maxLineLength = contentWidth;
|
|
302
|
+
if (line.length <= maxLineLength) {
|
|
303
|
+
// Line fits in one layout line
|
|
304
|
+
if (isCurrentLine) {
|
|
305
|
+
layoutLines.push({
|
|
306
|
+
text: line,
|
|
307
|
+
hasCursor: true,
|
|
308
|
+
cursorPos: this.state.cursorCol,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
layoutLines.push({
|
|
313
|
+
text: line,
|
|
314
|
+
hasCursor: false,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
// Line needs wrapping
|
|
320
|
+
const chunks = [];
|
|
321
|
+
for (let pos = 0; pos < line.length; pos += maxLineLength) {
|
|
322
|
+
chunks.push(line.slice(pos, pos + maxLineLength));
|
|
323
|
+
}
|
|
324
|
+
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
325
|
+
const chunk = chunks[chunkIndex];
|
|
326
|
+
if (!chunk)
|
|
327
|
+
continue;
|
|
328
|
+
const chunkStart = chunkIndex * maxLineLength;
|
|
329
|
+
const chunkEnd = chunkStart + chunk.length;
|
|
330
|
+
const cursorPos = this.state.cursorCol;
|
|
331
|
+
const hasCursorInChunk = isCurrentLine && cursorPos >= chunkStart && cursorPos <= chunkEnd;
|
|
332
|
+
if (hasCursorInChunk) {
|
|
333
|
+
layoutLines.push({
|
|
334
|
+
text: chunk,
|
|
335
|
+
hasCursor: true,
|
|
336
|
+
cursorPos: cursorPos - chunkStart,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
layoutLines.push({
|
|
341
|
+
text: chunk,
|
|
342
|
+
hasCursor: false,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return layoutLines;
|
|
349
|
+
}
|
|
350
|
+
getText() {
|
|
351
|
+
return this.state.lines.join("\n");
|
|
352
|
+
}
|
|
353
|
+
setText(text) {
|
|
354
|
+
// Split text into lines, handling different line endings
|
|
355
|
+
const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
356
|
+
// Ensure at least one empty line
|
|
357
|
+
this.state.lines = lines.length === 0 ? [""] : lines;
|
|
358
|
+
// Reset cursor to end of text
|
|
359
|
+
this.state.cursorLine = this.state.lines.length - 1;
|
|
360
|
+
this.state.cursorCol = this.state.lines[this.state.cursorLine]?.length || 0;
|
|
361
|
+
// Notify of change
|
|
362
|
+
if (this.onChange) {
|
|
363
|
+
this.onChange(this.getText());
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// All the editor methods from before...
|
|
367
|
+
insertCharacter(char) {
|
|
368
|
+
const line = this.state.lines[this.state.cursorLine] || "";
|
|
369
|
+
const before = line.slice(0, this.state.cursorCol);
|
|
370
|
+
const after = line.slice(this.state.cursorCol);
|
|
371
|
+
this.state.lines[this.state.cursorLine] = before + char + after;
|
|
372
|
+
this.state.cursorCol += char.length; // Fix: increment by the length of the inserted string
|
|
373
|
+
if (this.onChange) {
|
|
374
|
+
this.onChange(this.getText());
|
|
375
|
+
}
|
|
376
|
+
// Check if we should trigger or update autocomplete
|
|
377
|
+
if (!this.isAutocompleting) {
|
|
378
|
+
// Auto-trigger for "/" at the start of a line (slash commands)
|
|
379
|
+
if (char === "/" && this.isAtStartOfMessage()) {
|
|
380
|
+
void this.tryTriggerAutocomplete();
|
|
381
|
+
}
|
|
382
|
+
// Also auto-trigger when typing letters in a slash command context
|
|
383
|
+
else if (/[a-zA-Z0-9]/.test(char)) {
|
|
384
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
385
|
+
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
386
|
+
// Check if we're in a slash command with a space (i.e., typing arguments)
|
|
387
|
+
if (textBeforeCursor.startsWith("/") &&
|
|
388
|
+
textBeforeCursor.includes(" ")) {
|
|
389
|
+
void this.tryTriggerAutocomplete();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
void this.updateAutocomplete();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
handlePaste(pastedText) {
|
|
398
|
+
// Clean the pasted text
|
|
399
|
+
const cleanText = pastedText.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
400
|
+
// Convert tabs to spaces (4 spaces per tab)
|
|
401
|
+
const tabExpandedText = cleanText.replace(/\t/g, " ");
|
|
402
|
+
// Filter out non-printable characters except newlines
|
|
403
|
+
const filteredText = tabExpandedText
|
|
404
|
+
.split("")
|
|
405
|
+
.filter((char) => char === "\n" || (char >= " " && char <= "~"))
|
|
406
|
+
.join("");
|
|
407
|
+
// Split into lines
|
|
408
|
+
const pastedLines = filteredText.split("\n");
|
|
409
|
+
// Check if this is a large paste (> 10 lines or > 1000 characters)
|
|
410
|
+
const totalChars = filteredText.length;
|
|
411
|
+
if (pastedLines.length > 10 || totalChars > 1000) {
|
|
412
|
+
// Store the paste and insert a marker
|
|
413
|
+
this.pasteCounter++;
|
|
414
|
+
const pasteId = this.pasteCounter;
|
|
415
|
+
this.pastes.set(pasteId, filteredText);
|
|
416
|
+
// Insert marker like "[paste #1 +123 lines]" or "[paste #1 1234 chars]"
|
|
417
|
+
const marker = pastedLines.length > 10
|
|
418
|
+
? `[paste #${pasteId} +${pastedLines.length} lines]`
|
|
419
|
+
: `[paste #${pasteId} ${totalChars} chars]`;
|
|
420
|
+
for (const char of marker) {
|
|
421
|
+
this.insertCharacter(char);
|
|
422
|
+
}
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (pastedLines.length === 1) {
|
|
426
|
+
// Single line - just insert each character
|
|
427
|
+
const text = pastedLines[0] || "";
|
|
428
|
+
for (const char of text) {
|
|
429
|
+
this.insertCharacter(char);
|
|
430
|
+
}
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
// Multi-line paste - be very careful with array manipulation
|
|
434
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
435
|
+
const beforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
436
|
+
const afterCursor = currentLine.slice(this.state.cursorCol);
|
|
437
|
+
// Build the new lines array step by step
|
|
438
|
+
const newLines = [];
|
|
439
|
+
// Add all lines before current line
|
|
440
|
+
for (let i = 0; i < this.state.cursorLine; i++) {
|
|
441
|
+
newLines.push(this.state.lines[i] || "");
|
|
442
|
+
}
|
|
443
|
+
// Add the first pasted line merged with before cursor text
|
|
444
|
+
newLines.push(beforeCursor + (pastedLines[0] || ""));
|
|
445
|
+
// Add all middle pasted lines
|
|
446
|
+
for (let i = 1; i < pastedLines.length - 1; i++) {
|
|
447
|
+
newLines.push(pastedLines[i] || "");
|
|
448
|
+
}
|
|
449
|
+
// Add the last pasted line with after cursor text
|
|
450
|
+
newLines.push((pastedLines[pastedLines.length - 1] || "") + afterCursor);
|
|
451
|
+
// Add all lines after current line
|
|
452
|
+
for (let i = this.state.cursorLine + 1; i < this.state.lines.length; i++) {
|
|
453
|
+
newLines.push(this.state.lines[i] || "");
|
|
454
|
+
}
|
|
455
|
+
// Replace the entire lines array
|
|
456
|
+
this.state.lines = newLines;
|
|
457
|
+
// Update cursor position to end of pasted content
|
|
458
|
+
this.state.cursorLine += pastedLines.length - 1;
|
|
459
|
+
this.state.cursorCol = (pastedLines[pastedLines.length - 1] || "").length;
|
|
460
|
+
// Notify of change
|
|
461
|
+
if (this.onChange) {
|
|
462
|
+
this.onChange(this.getText());
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
addNewLine() {
|
|
466
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
467
|
+
const before = currentLine.slice(0, this.state.cursorCol);
|
|
468
|
+
const after = currentLine.slice(this.state.cursorCol);
|
|
469
|
+
// Split current line
|
|
470
|
+
this.state.lines[this.state.cursorLine] = before;
|
|
471
|
+
this.state.lines.splice(this.state.cursorLine + 1, 0, after);
|
|
472
|
+
// Move cursor to start of new line
|
|
473
|
+
this.state.cursorLine++;
|
|
474
|
+
this.state.cursorCol = 0;
|
|
475
|
+
if (this.onChange) {
|
|
476
|
+
this.onChange(this.getText());
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
handleBackspace() {
|
|
480
|
+
if (this.state.cursorCol > 0) {
|
|
481
|
+
// Delete character in current line
|
|
482
|
+
const line = this.state.lines[this.state.cursorLine] || "";
|
|
483
|
+
const before = line.slice(0, this.state.cursorCol - 1);
|
|
484
|
+
const after = line.slice(this.state.cursorCol);
|
|
485
|
+
this.state.lines[this.state.cursorLine] = before + after;
|
|
486
|
+
this.state.cursorCol--;
|
|
487
|
+
}
|
|
488
|
+
else if (this.state.cursorLine > 0) {
|
|
489
|
+
// Merge with previous line
|
|
490
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
491
|
+
const previousLine = this.state.lines[this.state.cursorLine - 1] || "";
|
|
492
|
+
this.state.lines[this.state.cursorLine - 1] = previousLine + currentLine;
|
|
493
|
+
this.state.lines.splice(this.state.cursorLine, 1);
|
|
494
|
+
this.state.cursorLine--;
|
|
495
|
+
this.state.cursorCol = previousLine.length;
|
|
496
|
+
}
|
|
497
|
+
if (this.onChange) {
|
|
498
|
+
this.onChange(this.getText());
|
|
499
|
+
}
|
|
500
|
+
// Update autocomplete after backspace
|
|
501
|
+
if (this.isAutocompleting) {
|
|
502
|
+
void this.updateAutocomplete();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
moveToLineStart() {
|
|
506
|
+
this.state.cursorCol = 0;
|
|
507
|
+
}
|
|
508
|
+
moveToLineEnd() {
|
|
509
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
510
|
+
this.state.cursorCol = currentLine.length;
|
|
511
|
+
}
|
|
512
|
+
handleForwardDelete() {
|
|
513
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
514
|
+
if (this.state.cursorCol < currentLine.length) {
|
|
515
|
+
// Delete character at cursor position (forward delete)
|
|
516
|
+
const before = currentLine.slice(0, this.state.cursorCol);
|
|
517
|
+
const after = currentLine.slice(this.state.cursorCol + 1);
|
|
518
|
+
this.state.lines[this.state.cursorLine] = before + after;
|
|
519
|
+
}
|
|
520
|
+
else if (this.state.cursorLine < this.state.lines.length - 1) {
|
|
521
|
+
// At end of line - merge with next line
|
|
522
|
+
const nextLine = this.state.lines[this.state.cursorLine + 1] || "";
|
|
523
|
+
this.state.lines[this.state.cursorLine] = currentLine + nextLine;
|
|
524
|
+
this.state.lines.splice(this.state.cursorLine + 1, 1);
|
|
525
|
+
}
|
|
526
|
+
if (this.onChange) {
|
|
527
|
+
this.onChange(this.getText());
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
deleteCurrentLine() {
|
|
531
|
+
if (this.state.lines.length === 1) {
|
|
532
|
+
// Only one line - just clear it
|
|
533
|
+
this.state.lines[0] = "";
|
|
534
|
+
this.state.cursorCol = 0;
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
// Multiple lines - remove current line
|
|
538
|
+
this.state.lines.splice(this.state.cursorLine, 1);
|
|
539
|
+
// Adjust cursor position
|
|
540
|
+
if (this.state.cursorLine >= this.state.lines.length) {
|
|
541
|
+
// Was on last line, move to new last line
|
|
542
|
+
this.state.cursorLine = this.state.lines.length - 1;
|
|
543
|
+
}
|
|
544
|
+
// Clamp cursor column to new line length
|
|
545
|
+
const newLine = this.state.lines[this.state.cursorLine] || "";
|
|
546
|
+
this.state.cursorCol = Math.min(this.state.cursorCol, newLine.length);
|
|
547
|
+
}
|
|
548
|
+
if (this.onChange) {
|
|
549
|
+
this.onChange(this.getText());
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
moveCursor(deltaLine, deltaCol) {
|
|
553
|
+
if (deltaLine !== 0) {
|
|
554
|
+
const newLine = this.state.cursorLine + deltaLine;
|
|
555
|
+
if (newLine >= 0 && newLine < this.state.lines.length) {
|
|
556
|
+
this.state.cursorLine = newLine;
|
|
557
|
+
// Clamp cursor column to new line length
|
|
558
|
+
const line = this.state.lines[this.state.cursorLine] || "";
|
|
559
|
+
this.state.cursorCol = Math.min(this.state.cursorCol, line.length);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (deltaCol !== 0) {
|
|
563
|
+
// Move column
|
|
564
|
+
const newCol = this.state.cursorCol + deltaCol;
|
|
565
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
566
|
+
const maxCol = currentLine.length;
|
|
567
|
+
this.state.cursorCol = Math.max(0, Math.min(maxCol, newCol));
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
// Helper method to check if cursor is at start of message (for slash command detection)
|
|
571
|
+
isAtStartOfMessage() {
|
|
572
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
573
|
+
const beforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
574
|
+
// At start if line is empty, only contains whitespace, or is just "/"
|
|
575
|
+
return beforeCursor.trim() === "" || beforeCursor.trim() === "/";
|
|
576
|
+
}
|
|
577
|
+
// Autocomplete methods
|
|
578
|
+
async tryTriggerAutocomplete(explicitTab = false) {
|
|
579
|
+
if (!this.autocompleteProvider)
|
|
580
|
+
return;
|
|
581
|
+
// Check if we should trigger file completion on Tab
|
|
582
|
+
if (explicitTab) {
|
|
583
|
+
const provider = this
|
|
584
|
+
.autocompleteProvider;
|
|
585
|
+
// Only check file completion triggering if the provider has the method
|
|
586
|
+
// For slash commands, we always want to show autocomplete
|
|
587
|
+
if (provider.shouldTriggerFileCompletion &&
|
|
588
|
+
!provider.shouldTriggerFileCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol)) {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const suggestions = await this.autocompleteProvider.getSuggestions(this.state.lines, this.state.cursorLine, this.state.cursorCol);
|
|
593
|
+
if (suggestions && suggestions.items.length > 0) {
|
|
594
|
+
this.autocompletePrefix = suggestions.prefix;
|
|
595
|
+
this.isAutocompleting = true;
|
|
596
|
+
if (this.autocompleteList) {
|
|
597
|
+
this.autocompleteList.updateItems(suggestions.items);
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
this.autocompleteList = new SelectList(suggestions.items, 5);
|
|
601
|
+
}
|
|
602
|
+
// Request re-render to show autocomplete list
|
|
603
|
+
this.onRenderRequested?.();
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
this.cancelAutocomplete();
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
async handleTabCompletion() {
|
|
610
|
+
if (!this.autocompleteProvider)
|
|
611
|
+
return;
|
|
612
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
613
|
+
const beforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
614
|
+
// Check if we're in a slash command context
|
|
615
|
+
if (beforeCursor.trimStart().startsWith("/")) {
|
|
616
|
+
await this.handleSlashCommandCompletion();
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
await this.forceFileAutocomplete();
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async handleSlashCommandCompletion() {
|
|
623
|
+
// For now, fall back to regular autocomplete (slash commands)
|
|
624
|
+
// This can be extended later to handle command-specific argument completion
|
|
625
|
+
await this.tryTriggerAutocomplete(true);
|
|
626
|
+
}
|
|
627
|
+
async forceFileAutocomplete() {
|
|
628
|
+
if (!this.autocompleteProvider)
|
|
629
|
+
return;
|
|
630
|
+
// Check if provider has the force method
|
|
631
|
+
const provider = this.autocompleteProvider;
|
|
632
|
+
if (!provider.getForceFileSuggestions) {
|
|
633
|
+
await this.tryTriggerAutocomplete(true);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const suggestions = await provider.getForceFileSuggestions(this.state.lines, this.state.cursorLine, this.state.cursorCol);
|
|
637
|
+
if (suggestions && suggestions.items.length > 0) {
|
|
638
|
+
this.autocompletePrefix = suggestions.prefix;
|
|
639
|
+
if (this.autocompleteList) {
|
|
640
|
+
this.autocompleteList.updateItems(suggestions.items);
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
this.autocompleteList = new SelectList(suggestions.items, 5);
|
|
644
|
+
}
|
|
645
|
+
this.isAutocompleting = true;
|
|
646
|
+
// Request re-render to show autocomplete list
|
|
647
|
+
this.onRenderRequested?.();
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
this.cancelAutocomplete();
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
cancelAutocomplete() {
|
|
654
|
+
this.isAutocompleting = false;
|
|
655
|
+
this.autocompleteList = undefined;
|
|
656
|
+
this.autocompletePrefix = "";
|
|
657
|
+
if (this.autocompleteDebounceTimer) {
|
|
658
|
+
clearTimeout(this.autocompleteDebounceTimer);
|
|
659
|
+
this.autocompleteDebounceTimer = undefined;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
isShowingAutocomplete() {
|
|
663
|
+
return this.isAutocompleting;
|
|
664
|
+
}
|
|
665
|
+
async updateAutocomplete() {
|
|
666
|
+
if (!this.isAutocompleting || !this.autocompleteProvider)
|
|
667
|
+
return;
|
|
668
|
+
// Check if the current text still matches our autocomplete context
|
|
669
|
+
// This prevents unnecessary updates when typing unrelated text
|
|
670
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
671
|
+
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
672
|
+
// If we're no longer in the context that triggered autocomplete, cancel it
|
|
673
|
+
// For slash commands, check if we're still in slash command context
|
|
674
|
+
// For file paths, check if we're still in the same path context
|
|
675
|
+
if (textBeforeCursor.startsWith("/")) {
|
|
676
|
+
// For slash commands, we should continue autocomplete as long as we're in slash command context
|
|
677
|
+
// Don't cancel based on prefix matching for progressive typing
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
// For file paths, check if we're still in the same path context
|
|
681
|
+
if (!textBeforeCursor.endsWith(this.autocompletePrefix)) {
|
|
682
|
+
this.cancelAutocomplete();
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
// Clear any existing debounce timer
|
|
687
|
+
if (this.autocompleteDebounceTimer) {
|
|
688
|
+
clearTimeout(this.autocompleteDebounceTimer);
|
|
689
|
+
}
|
|
690
|
+
// Debounce autocomplete updates to prevent rapid-fire file system operations
|
|
691
|
+
this.autocompleteDebounceTimer = setTimeout(async () => {
|
|
692
|
+
const suggestions = await this.autocompleteProvider?.getSuggestions(this.state.lines, this.state.cursorLine, this.state.cursorCol);
|
|
693
|
+
if (suggestions && suggestions.items.length > 0) {
|
|
694
|
+
this.autocompletePrefix = suggestions.prefix;
|
|
695
|
+
if (this.autocompleteList) {
|
|
696
|
+
// Update the existing list with new items
|
|
697
|
+
this.autocompleteList.updateItems(suggestions.items);
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
this.autocompleteList = new SelectList(suggestions.items, 5);
|
|
701
|
+
}
|
|
702
|
+
this.isAutocompleting = true;
|
|
703
|
+
// Request re-render to show updated autocomplete list
|
|
704
|
+
this.onRenderRequested?.();
|
|
705
|
+
}
|
|
706
|
+
else {
|
|
707
|
+
// No more matches, cancel autocomplete
|
|
708
|
+
this.cancelAutocomplete();
|
|
709
|
+
// Request re-render to hide autocomplete
|
|
710
|
+
this.onRenderRequested?.();
|
|
711
|
+
}
|
|
712
|
+
}, 50); // 50ms debounce delay
|
|
713
|
+
}
|
|
714
|
+
isModifiedEnter(data) {
|
|
715
|
+
// Common modified Enter sequences across terminals
|
|
716
|
+
const sequences = [
|
|
717
|
+
// Shift+Enter sequences
|
|
718
|
+
"\x1b[13;2~", // Some terminals
|
|
719
|
+
"\x1bOM", // Some terminals
|
|
720
|
+
"\\\r", // VS Code terminal
|
|
721
|
+
"\x1b\r", // Option+Enter (macOS)
|
|
722
|
+
// Ctrl+Enter sequences
|
|
723
|
+
"\x1b[13;5~", // Some terminals
|
|
724
|
+
];
|
|
725
|
+
// Check for known sequences
|
|
726
|
+
if (sequences.includes(data)) {
|
|
727
|
+
return true;
|
|
728
|
+
}
|
|
729
|
+
// Check for Enter with escape sequences (general case)
|
|
730
|
+
if (data.length > 1 &&
|
|
731
|
+
data.includes("\x1b") &&
|
|
732
|
+
(data.includes("\r") || data.includes("\n"))) {
|
|
733
|
+
return true;
|
|
734
|
+
}
|
|
735
|
+
// Check for Ctrl+Enter (Ctrl + CR)
|
|
736
|
+
if (data.charCodeAt(0) === 13 && data.length > 1) {
|
|
737
|
+
return true;
|
|
738
|
+
}
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
getCursorPosition() {
|
|
742
|
+
// Return cursor position relative to the editor component
|
|
743
|
+
// The editor has a top border line, then content lines, then a bottom border line
|
|
744
|
+
// So cursor position within editor is: row = layoutLineIndex + 1
|
|
745
|
+
const width = 80; // Use a reasonable default width for calculation
|
|
746
|
+
const layoutLines = this.layoutText(width);
|
|
747
|
+
// Find which layout line contains the cursor
|
|
748
|
+
for (let i = 0; i < layoutLines.length; i++) {
|
|
749
|
+
const layoutLine = layoutLines[i];
|
|
750
|
+
if (layoutLine.hasCursor && layoutLine.cursorPos !== undefined) {
|
|
751
|
+
// Add 1 to account for the top border line
|
|
752
|
+
return [i + 1, layoutLine.cursorPos];
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
// If no cursor found, return position at start of first content line (after top border)
|
|
756
|
+
return [1, 0];
|
|
757
|
+
}
|
|
758
|
+
}
|