@travisennis/acai 0.0.6 → 0.0.7
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 +186 -17
- package/bin/acai-wrapper.js +26 -0
- package/dist/agent/index.d.ts +15 -2
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +202 -174
- package/dist/api/exa/index.js +1 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +40 -7
- package/dist/commands/add-directory-command.d.ts +1 -1
- package/dist/commands/add-directory-command.d.ts.map +1 -1
- package/dist/commands/add-directory-command.js +1 -32
- package/dist/commands/application-log-command.d.ts +1 -1
- package/dist/commands/application-log-command.d.ts.map +1 -1
- package/dist/commands/application-log-command.js +2 -38
- package/dist/commands/clear-command.d.ts +1 -1
- package/dist/commands/clear-command.d.ts.map +1 -1
- package/dist/commands/clear-command.js +1 -5
- package/dist/commands/compact-command.d.ts.map +1 -1
- package/dist/commands/compact-command.js +0 -9
- package/dist/commands/context-command.d.ts +1 -1
- package/dist/commands/context-command.d.ts.map +1 -1
- package/dist/commands/context-command.js +13 -72
- package/dist/commands/copy-command.d.ts.map +1 -1
- package/dist/commands/copy-command.js +0 -19
- package/dist/commands/edit-command.d.ts +1 -1
- package/dist/commands/edit-command.d.ts.map +1 -1
- package/dist/commands/edit-command.js +3 -49
- package/dist/commands/edit-prompt-command.d.ts +1 -1
- package/dist/commands/edit-prompt-command.d.ts.map +1 -1
- package/dist/commands/edit-prompt-command.js +1 -26
- package/dist/commands/exit-command.d.ts +1 -4
- package/dist/commands/exit-command.d.ts.map +1 -1
- package/dist/commands/exit-command.js +2 -18
- package/dist/commands/files-command.d.ts +1 -1
- package/dist/commands/files-command.d.ts.map +1 -1
- package/dist/commands/files-command.js +1 -54
- package/dist/commands/generate-rules-command.d.ts +1 -1
- package/dist/commands/generate-rules-command.d.ts.map +1 -1
- package/dist/commands/generate-rules-command.js +18 -60
- package/dist/commands/handoff-command.d.ts.map +1 -1
- package/dist/commands/handoff-command.js +0 -11
- package/dist/commands/health-command.d.ts +1 -1
- package/dist/commands/health-command.d.ts.map +1 -1
- package/dist/commands/health-command.js +8 -103
- package/dist/commands/help-command.d.ts +1 -1
- package/dist/commands/help-command.d.ts.map +1 -1
- package/dist/commands/help-command.js +6 -14
- package/dist/commands/history-command.d.ts +1 -1
- package/dist/commands/history-command.d.ts.map +1 -1
- package/dist/commands/history-command.js +30 -106
- 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 +4 -23
- package/dist/commands/last-log-command.d.ts +1 -1
- package/dist/commands/last-log-command.d.ts.map +1 -1
- package/dist/commands/last-log-command.js +1 -28
- package/dist/commands/list-directories-command.d.ts +1 -1
- package/dist/commands/list-directories-command.d.ts.map +1 -1
- package/dist/commands/list-directories-command.js +1 -14
- package/dist/commands/list-tools-command.d.ts.map +1 -1
- package/dist/commands/list-tools-command.js +55 -78
- package/dist/commands/manager.d.ts +1 -8
- package/dist/commands/manager.d.ts.map +1 -1
- package/dist/commands/manager.js +7 -42
- package/dist/commands/model-command.d.ts +0 -22
- package/dist/commands/model-command.d.ts.map +1 -1
- package/dist/commands/model-command.js +5 -126
- package/dist/commands/paste-command.d.ts +1 -1
- package/dist/commands/paste-command.d.ts.map +1 -1
- package/dist/commands/paste-command.js +1 -79
- package/dist/commands/pickup-command.d.ts.map +1 -1
- package/dist/commands/pickup-command.js +3 -55
- package/dist/commands/prompt-command.d.ts +19 -1
- package/dist/commands/prompt-command.d.ts.map +1 -1
- package/dist/commands/prompt-command.js +172 -194
- package/dist/commands/remove-directory-command.d.ts +1 -1
- package/dist/commands/remove-directory-command.d.ts.map +1 -1
- package/dist/commands/remove-directory-command.js +1 -33
- 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 +3 -11
- package/dist/commands/rules-command.d.ts +1 -1
- package/dist/commands/rules-command.d.ts.map +1 -1
- package/dist/commands/rules-command.js +1 -63
- package/dist/commands/save-command.d.ts +1 -1
- package/dist/commands/save-command.d.ts.map +1 -1
- package/dist/commands/save-command.js +1 -8
- package/dist/commands/shell-command.d.ts.map +1 -1
- package/dist/commands/shell-command.js +1 -48
- package/dist/commands/types.d.ts +0 -3
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/commands/usage-command.d.ts +1 -1
- package/dist/commands/usage-command.d.ts.map +1 -1
- package/dist/commands/usage-command.js +5 -16
- package/dist/config.d.ts +17 -6
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +86 -53
- package/dist/execution/index.d.ts +17 -2
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/index.js +62 -20
- package/dist/formatting.d.ts +19 -0
- package/dist/formatting.d.ts.map +1 -1
- package/dist/formatting.js +54 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +212 -153
- package/dist/messages.d.ts +3 -0
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +67 -3
- package/dist/models/anthropic-provider.d.ts.map +1 -1
- package/dist/models/anthropic-provider.js +0 -7
- package/dist/models/deepseek-provider.d.ts.map +1 -1
- package/dist/models/deepseek-provider.js +0 -2
- package/dist/models/google-provider.d.ts.map +1 -1
- package/dist/models/google-provider.js +0 -3
- package/dist/models/groq-provider.d.ts.map +1 -1
- package/dist/models/groq-provider.js +0 -1
- package/dist/models/openai-provider.d.ts.map +1 -1
- package/dist/models/openai-provider.js +0 -4
- package/dist/models/openrouter-provider.d.ts +10 -9
- package/dist/models/openrouter-provider.d.ts.map +1 -1
- package/dist/models/openrouter-provider.js +82 -88
- package/dist/models/providers.d.ts +4 -14
- package/dist/models/providers.d.ts.map +1 -1
- package/dist/models/providers.js +1 -57
- package/dist/models/xai-provider.d.ts.map +1 -1
- package/dist/models/xai-provider.js +0 -2
- package/dist/prompts.d.ts +9 -4
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +427 -99
- package/dist/repl/project-status-line.d.ts +1 -0
- package/dist/repl/project-status-line.d.ts.map +1 -1
- package/dist/repl/project-status-line.js +57 -27
- package/dist/repl-new.d.ts +0 -2
- package/dist/repl-new.d.ts.map +1 -1
- package/dist/repl-new.js +34 -54
- package/dist/skills.d.ts +20 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +192 -0
- package/dist/terminal/control.d.ts +55 -0
- package/dist/terminal/control.d.ts.map +1 -0
- package/dist/terminal/control.js +109 -0
- package/dist/terminal/default-theme.d.ts +1 -1
- package/dist/terminal/default-theme.d.ts.map +1 -1
- package/dist/terminal/default-theme.js +24 -28
- package/dist/terminal/formatting.d.ts +23 -25
- package/dist/terminal/formatting.d.ts.map +1 -1
- package/dist/terminal/formatting.js +35 -52
- package/dist/terminal/highlight/index.d.ts.map +1 -1
- package/dist/terminal/highlight/index.js +3 -6
- package/dist/terminal/highlight/theme.d.ts.map +1 -1
- package/dist/terminal/highlight/theme.js +2 -6
- package/dist/terminal/index.d.ts +2 -101
- package/dist/terminal/index.d.ts.map +1 -1
- package/dist/terminal/index.js +2 -464
- package/dist/terminal/markdown.js +7 -5
- package/dist/terminal/strip-ansi.js +4 -4
- package/dist/terminal/table/cell.d.ts +114 -0
- package/dist/terminal/table/cell.d.ts.map +1 -0
- package/dist/terminal/table/cell.js +407 -0
- package/dist/terminal/table/debug.d.ts +15 -0
- package/dist/terminal/table/debug.d.ts.map +1 -0
- package/dist/terminal/table/debug.js +32 -0
- package/dist/terminal/table/index.d.ts +3 -0
- package/dist/terminal/table/index.d.ts.map +1 -0
- package/dist/terminal/table/index.js +2 -0
- package/dist/terminal/table/layout-manager.d.ts +27 -0
- package/dist/terminal/table/layout-manager.d.ts.map +1 -0
- package/dist/terminal/table/layout-manager.js +257 -0
- package/dist/terminal/table/table.d.ts +9 -0
- package/dist/terminal/table/table.d.ts.map +1 -0
- package/dist/terminal/table/table.js +97 -0
- package/dist/terminal/table/utils.d.ts +63 -0
- package/dist/terminal/table/utils.d.ts.map +1 -0
- package/dist/terminal/table/utils.js +326 -0
- package/dist/tokens/threshold.d.ts +6 -21
- package/dist/tokens/threshold.d.ts.map +1 -1
- package/dist/tokens/threshold.js +13 -31
- package/dist/tools/advanced-edit-file.d.ts.map +1 -1
- package/dist/tools/advanced-edit-file.js +5 -1
- package/dist/tools/agent.d.ts.map +1 -1
- package/dist/tools/agent.js +19 -5
- package/dist/tools/bash.d.ts +3 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +204 -42
- package/dist/tools/batch.d.ts +34 -0
- package/dist/tools/batch.d.ts.map +1 -0
- package/dist/tools/batch.js +174 -0
- package/dist/tools/code-interpreter.d.ts.map +1 -1
- package/dist/tools/code-interpreter.js +25 -9
- package/dist/tools/delete-file.d.ts.map +1 -1
- package/dist/tools/delete-file.js +9 -2
- package/dist/tools/directory-tree.d.ts +0 -6
- package/dist/tools/directory-tree.d.ts.map +1 -1
- package/dist/tools/directory-tree.js +29 -18
- package/dist/tools/dynamic-tool-loader.d.ts +0 -4
- package/dist/tools/dynamic-tool-loader.d.ts.map +1 -1
- package/dist/tools/dynamic-tool-loader.js +2 -2
- package/dist/tools/edit-file.d.ts.map +1 -1
- package/dist/tools/edit-file.js +16 -3
- package/dist/tools/glob.d.ts.map +1 -1
- package/dist/tools/glob.js +24 -13
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +40 -25
- package/dist/tools/index.d.ts +17 -3
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +43 -1
- package/dist/tools/llm-edit-fixer.d.ts +0 -1
- package/dist/tools/llm-edit-fixer.d.ts.map +1 -1
- package/dist/tools/llm-edit-fixer.js +14 -28
- package/dist/tools/move-file.d.ts.map +1 -1
- package/dist/tools/move-file.js +8 -1
- package/dist/tools/read-file.d.ts.map +1 -1
- package/dist/tools/read-file.js +32 -23
- package/dist/tools/read-multiple-files.d.ts.map +1 -1
- package/dist/tools/read-multiple-files.js +102 -45
- package/dist/tools/save-file.d.ts +4 -4
- package/dist/tools/save-file.d.ts.map +1 -1
- package/dist/tools/save-file.js +20 -2
- package/dist/tools/think.d.ts.map +1 -1
- package/dist/tools/think.js +7 -1
- package/dist/tools/types.d.ts +5 -1
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/web-fetch.js +1 -1
- package/dist/tools/web-search.d.ts.map +1 -1
- package/dist/tools/web-search.js +24 -9
- package/dist/tui/components/assistant-message.js +1 -1
- package/dist/tui/components/box.d.ts +20 -0
- package/dist/tui/components/box.d.ts.map +1 -0
- package/dist/tui/components/box.js +81 -0
- package/dist/tui/components/editor.d.ts +60 -5
- package/dist/tui/components/editor.d.ts.map +1 -1
- package/dist/tui/components/editor.js +577 -115
- package/dist/tui/components/footer.d.ts +0 -12
- package/dist/tui/components/footer.d.ts.map +1 -1
- package/dist/tui/components/footer.js +19 -7
- package/dist/tui/components/header.d.ts +21 -0
- package/dist/tui/components/header.d.ts.map +1 -0
- package/dist/tui/components/header.js +63 -0
- package/dist/tui/components/loader.d.ts +5 -1
- package/dist/tui/components/loader.d.ts.map +1 -1
- package/dist/tui/components/loader.js +2 -2
- package/dist/tui/components/markdown.d.ts +26 -23
- package/dist/tui/components/markdown.d.ts.map +1 -1
- package/dist/tui/components/markdown.js +107 -54
- package/dist/tui/components/modal.d.ts +0 -11
- package/dist/tui/components/modal.d.ts.map +1 -1
- package/dist/tui/components/modal.js +0 -29
- package/dist/tui/components/progress-bar.d.ts +19 -0
- package/dist/tui/components/progress-bar.d.ts.map +1 -0
- package/dist/tui/components/progress-bar.js +78 -0
- package/dist/tui/components/prompt-status.d.ts +2 -1
- package/dist/tui/components/prompt-status.d.ts.map +1 -1
- package/dist/tui/components/prompt-status.js +7 -2
- package/dist/tui/components/select-list.d.ts +27 -1
- package/dist/tui/components/select-list.d.ts.map +1 -1
- package/dist/tui/components/select-list.js +93 -29
- package/dist/tui/components/spacer.d.ts +1 -1
- package/dist/tui/components/spacer.d.ts.map +1 -1
- package/dist/tui/components/spacer.js +2 -2
- package/dist/tui/components/table.d.ts +27 -0
- package/dist/tui/components/table.d.ts.map +1 -0
- package/dist/tui/components/table.js +125 -0
- package/dist/tui/components/thinking-block.d.ts.map +1 -1
- package/dist/tui/components/thinking-block.js +4 -1
- package/dist/tui/components/tool-execution.d.ts +8 -4
- package/dist/tui/components/tool-execution.d.ts.map +1 -1
- package/dist/tui/components/tool-execution.js +88 -80
- package/dist/tui/components/user-message.d.ts.map +1 -1
- package/dist/tui/components/user-message.js +6 -4
- package/dist/tui/index.d.ts +9 -5
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +5 -1
- package/dist/tui/terminal.d.ts +2 -1
- package/dist/tui/terminal.d.ts.map +1 -1
- package/dist/tui/terminal.js +28 -38
- package/dist/tui/tui.d.ts +2 -0
- package/dist/tui/tui.d.ts.map +1 -1
- package/dist/tui/tui.js +53 -33
- package/dist/tui/utils.d.ts +5 -0
- package/dist/tui/utils.d.ts.map +1 -1
- package/dist/tui/utils.js +81 -1
- package/dist/{tools/bash-utils.d.ts → utils/bash.d.ts} +3 -3
- package/dist/utils/bash.d.ts.map +1 -0
- package/dist/{tools/bash-utils.js → utils/bash.js} +22 -11
- package/dist/utils/{filesystem.d.ts → filesystem/operations.d.ts} +1 -1
- package/dist/utils/filesystem/operations.d.ts.map +1 -0
- package/dist/{tools/filesystem-utils.d.ts → utils/filesystem/security.d.ts} +3 -2
- package/dist/utils/filesystem/security.d.ts.map +1 -0
- package/dist/{tools/filesystem-utils.js → utils/filesystem/security.js} +62 -4
- package/dist/utils/funcs.d.ts +6 -0
- package/dist/utils/funcs.d.ts.map +1 -0
- package/dist/utils/funcs.js +6 -0
- package/dist/{tools/git-utils.d.ts → utils/git.d.ts} +1 -1
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/{tools/git-utils.js → utils/git.js} +0 -6
- package/dist/utils/glob.js +1 -1
- package/dist/utils/{zod-utils.d.ts → zod.d.ts} +1 -1
- package/dist/utils/zod.d.ts.map +1 -0
- package/package.json +17 -17
- package/dist/agent/manual-loop.d.ts +0 -41
- package/dist/agent/manual-loop.d.ts.map +0 -1
- package/dist/agent/manual-loop.js +0 -278
- package/dist/repl/display-tool-messages.d.ts +0 -4
- package/dist/repl/display-tool-messages.d.ts.map +0 -1
- package/dist/repl/display-tool-messages.js +0 -58
- package/dist/repl/display-tool-use.d.ts +0 -14
- package/dist/repl/display-tool-use.d.ts.map +0 -1
- package/dist/repl/display-tool-use.js +0 -63
- package/dist/repl/get-prompt-header.d.ts +0 -8
- package/dist/repl/get-prompt-header.d.ts.map +0 -1
- package/dist/repl/get-prompt-header.js +0 -9
- package/dist/repl/prompt.d.ts +0 -21
- package/dist/repl/prompt.d.ts.map +0 -1
- package/dist/repl/prompt.js +0 -244
- package/dist/repl.d.ts +0 -29
- package/dist/repl.d.ts.map +0 -1
- package/dist/repl.js +0 -218
- package/dist/terminal/checkbox-prompt.d.ts +0 -36
- package/dist/terminal/checkbox-prompt.d.ts.map +0 -1
- package/dist/terminal/checkbox-prompt.js +0 -368
- package/dist/terminal/editor-prompt.d.ts +0 -10
- package/dist/terminal/editor-prompt.d.ts.map +0 -1
- package/dist/terminal/editor-prompt.js +0 -61
- package/dist/terminal/errors.d.ts +0 -19
- package/dist/terminal/errors.d.ts.map +0 -1
- package/dist/terminal/errors.js +0 -37
- package/dist/terminal/input-prompt.d.ts +0 -17
- package/dist/terminal/input-prompt.d.ts.map +0 -1
- package/dist/terminal/input-prompt.js +0 -181
- package/dist/terminal/search-prompt.d.ts +0 -20
- package/dist/terminal/search-prompt.d.ts.map +0 -1
- package/dist/terminal/search-prompt.js +0 -280
- package/dist/terminal/types.d.ts +0 -35
- package/dist/terminal/types.d.ts.map +0 -1
- package/dist/terminal/types.js +0 -1
- package/dist/tools/bash-utils.d.ts.map +0 -1
- package/dist/tools/filesystem-utils.d.ts.map +0 -1
- package/dist/tools/git-utils.d.ts.map +0 -1
- package/dist/utils/filesystem.d.ts.map +0 -1
- package/dist/utils/zod-utils.d.ts.map +0 -1
- /package/dist/utils/{filesystem.js → filesystem/operations.js} +0 -0
- /package/dist/utils/{zod-utils.js → zod.js} +0 -0
|
@@ -1,12 +1,76 @@
|
|
|
1
1
|
import style from "../../terminal/style.js";
|
|
2
|
-
import {
|
|
2
|
+
import { visibleWidth } from "../utils.js";
|
|
3
|
+
import { isNavigationKey, isTab, SelectList } from "./select-list.js";
|
|
4
|
+
// Grapheme segmenter for proper Unicode iteration (handles emojis, etc.)
|
|
5
|
+
const segmenter = new Intl.Segmenter();
|
|
6
|
+
// Cache for line metrics to avoid repeated segmentation
|
|
7
|
+
const lineMetricsCache = {
|
|
8
|
+
maxSize: 1000,
|
|
9
|
+
cache: new Map(),
|
|
10
|
+
get(line) {
|
|
11
|
+
let cached = this.cache.get(line);
|
|
12
|
+
if (!cached) {
|
|
13
|
+
// Fast path for ASCII-only lines (common case)
|
|
14
|
+
if (/^[\x20-\x7E\t]*$/.test(line)) {
|
|
15
|
+
// ASCII characters (including tabs)
|
|
16
|
+
const graphemes = line.split(""); // Simple split for ASCII
|
|
17
|
+
const widths = graphemes.map((char) => (char === "\t" ? 3 : 1));
|
|
18
|
+
const totalWidth = widths.reduce((sum, w) => sum + w, 0);
|
|
19
|
+
cached = { graphemes, widths, totalWidth };
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
// Complex Unicode line, use full segmentation
|
|
23
|
+
const graphemes = [...segmenter.segment(line)].map((seg) => seg.segment);
|
|
24
|
+
const widths = graphemes.map((g) => visibleWidth(g));
|
|
25
|
+
const totalWidth = widths.reduce((sum, w) => sum + w, 0);
|
|
26
|
+
cached = { graphemes, widths, totalWidth };
|
|
27
|
+
}
|
|
28
|
+
this.cache.set(line, cached);
|
|
29
|
+
// Enforce size limit
|
|
30
|
+
if (this.cache.size > this.maxSize) {
|
|
31
|
+
// Delete first (oldest) entry - simple but not LRU; okay for our use case
|
|
32
|
+
const firstKey = this.cache.keys().next().value;
|
|
33
|
+
if (firstKey !== undefined) {
|
|
34
|
+
this.cache.delete(firstKey);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return cached;
|
|
39
|
+
},
|
|
40
|
+
clear() {
|
|
41
|
+
this.cache.clear();
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Text editor component with support for multi-line input and autocomplete.
|
|
46
|
+
*
|
|
47
|
+
* Key bindings:
|
|
48
|
+
* - Enter: Create new line
|
|
49
|
+
* - Shift+Enter / Ctrl+Enter / Option+Enter: Submit prompt
|
|
50
|
+
* - Tab: Trigger autocomplete
|
|
51
|
+
* - Escape: Cancel autocomplete or custom handler
|
|
52
|
+
* - Ctrl+C: Custom handler
|
|
53
|
+
* - Arrow keys: Navigate text
|
|
54
|
+
* - Backspace/Delete: Delete characters
|
|
55
|
+
* - Ctrl+A: Move to start of line
|
|
56
|
+
* - Ctrl+E: Move to end of line
|
|
57
|
+
* - Ctrl+K: Delete to end of line
|
|
58
|
+
* - Ctrl+U: Delete to start of line
|
|
59
|
+
* - Ctrl+W / Option+Backspace: Delete word backwards
|
|
60
|
+
* - Ctrl+Left/Right / Option+Left/Right: Word navigation
|
|
61
|
+
* - Up/Down: History navigation when editor is empty
|
|
62
|
+
*/
|
|
3
63
|
export class Editor {
|
|
4
64
|
state = {
|
|
5
65
|
lines: [""],
|
|
6
66
|
cursorLine: 0,
|
|
7
67
|
cursorCol: 0,
|
|
8
68
|
};
|
|
9
|
-
|
|
69
|
+
theme;
|
|
70
|
+
// Store last render width for cursor navigation
|
|
71
|
+
lastWidth = 80;
|
|
72
|
+
// Border color (can be changed dynamically)
|
|
73
|
+
borderColor;
|
|
10
74
|
// Autocomplete support
|
|
11
75
|
autocompleteProvider;
|
|
12
76
|
autocompleteList;
|
|
@@ -19,6 +83,9 @@ export class Editor {
|
|
|
19
83
|
// Bracketed paste mode buffering
|
|
20
84
|
pasteBuffer = "";
|
|
21
85
|
isInPaste = false;
|
|
86
|
+
// Prompt history for up/down navigation
|
|
87
|
+
history = [];
|
|
88
|
+
historyIndex = -1; // -1 = not browsing, 0 = most recent, 1 = older, etc.
|
|
22
89
|
onSubmit;
|
|
23
90
|
onChange;
|
|
24
91
|
disableSubmit = false;
|
|
@@ -26,19 +93,78 @@ export class Editor {
|
|
|
26
93
|
onEscape;
|
|
27
94
|
onCtrlC;
|
|
28
95
|
onRenderRequested;
|
|
29
|
-
constructor(
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
96
|
+
constructor(theme) {
|
|
97
|
+
// Default theme if none provided (backward compatibility)
|
|
98
|
+
this.theme = theme || {
|
|
99
|
+
borderColor: style.gray,
|
|
100
|
+
};
|
|
101
|
+
this.borderColor = this.theme.borderColor;
|
|
33
102
|
}
|
|
34
|
-
|
|
35
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Add a prompt to history for up/down arrow navigation.
|
|
105
|
+
* Called after successful submission.
|
|
106
|
+
*/
|
|
107
|
+
addToHistory(text) {
|
|
108
|
+
const trimmed = text.trim();
|
|
109
|
+
if (!trimmed)
|
|
110
|
+
return;
|
|
111
|
+
// Don't add consecutive duplicates
|
|
112
|
+
if (this.history.length > 0 && this.history[0] === trimmed)
|
|
113
|
+
return;
|
|
114
|
+
this.history.unshift(trimmed);
|
|
115
|
+
// Limit history size
|
|
116
|
+
if (this.history.length > 100) {
|
|
117
|
+
this.history.pop();
|
|
118
|
+
}
|
|
36
119
|
}
|
|
37
120
|
setAutocompleteProvider(provider) {
|
|
38
121
|
this.autocompleteProvider = provider;
|
|
39
122
|
}
|
|
123
|
+
isEditorEmpty() {
|
|
124
|
+
return this.state.lines.length === 1 && this.state.lines[0] === "";
|
|
125
|
+
}
|
|
126
|
+
isOnFirstVisualLine() {
|
|
127
|
+
const visualLines = this.buildVisualLineMap(this.lastWidth);
|
|
128
|
+
const currentVisualLine = this.findCurrentVisualLine(visualLines);
|
|
129
|
+
return currentVisualLine === 0;
|
|
130
|
+
}
|
|
131
|
+
isOnLastVisualLine() {
|
|
132
|
+
const visualLines = this.buildVisualLineMap(this.lastWidth);
|
|
133
|
+
const currentVisualLine = this.findCurrentVisualLine(visualLines);
|
|
134
|
+
return currentVisualLine === visualLines.length - 1;
|
|
135
|
+
}
|
|
136
|
+
navigateHistory(direction) {
|
|
137
|
+
if (this.history.length === 0)
|
|
138
|
+
return;
|
|
139
|
+
const newIndex = this.historyIndex - direction; // Up(-1) increases index, Down(1) decreases
|
|
140
|
+
if (newIndex < -1 || newIndex >= this.history.length)
|
|
141
|
+
return;
|
|
142
|
+
this.historyIndex = newIndex;
|
|
143
|
+
if (this.historyIndex === -1) {
|
|
144
|
+
// Returned to "current" state - clear editor
|
|
145
|
+
this.setTextInternal("");
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
this.setTextInternal(this.history[this.historyIndex] || "");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** Internal setText that doesn't reset history state - used by navigateHistory */
|
|
152
|
+
setTextInternal(text) {
|
|
153
|
+
const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
154
|
+
this.state.lines = lines.length === 0 ? [""] : lines;
|
|
155
|
+
this.state.cursorLine = this.state.lines.length - 1;
|
|
156
|
+
this.state.cursorCol = this.state.lines[this.state.cursorLine]?.length || 0;
|
|
157
|
+
if (this.onChange) {
|
|
158
|
+
this.onChange(this.getText());
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
invalidate() {
|
|
162
|
+
// No cached state to invalidate currently
|
|
163
|
+
}
|
|
40
164
|
render(width) {
|
|
41
|
-
|
|
165
|
+
// Store width for cursor navigation
|
|
166
|
+
this.lastWidth = width;
|
|
167
|
+
const horizontal = this.borderColor("─");
|
|
42
168
|
// Layout the text - use full width
|
|
43
169
|
const layoutLines = this.layoutText(width);
|
|
44
170
|
const result = [];
|
|
@@ -47,41 +173,50 @@ export class Editor {
|
|
|
47
173
|
// Render each layout line
|
|
48
174
|
for (const layoutLine of layoutLines) {
|
|
49
175
|
let displayText = layoutLine.text;
|
|
50
|
-
let
|
|
176
|
+
let lineVisibleWidth = layoutLine.width;
|
|
51
177
|
// Add cursor if this line has it
|
|
52
178
|
if (layoutLine.hasCursor && layoutLine.cursorPos !== undefined) {
|
|
53
179
|
const before = displayText.slice(0, layoutLine.cursorPos);
|
|
54
180
|
const after = displayText.slice(layoutLine.cursorPos);
|
|
55
181
|
if (after.length > 0) {
|
|
56
|
-
// Cursor is on a character - replace it with highlighted version
|
|
57
|
-
|
|
58
|
-
const
|
|
182
|
+
// Cursor is on a character (grapheme) - replace it with highlighted version
|
|
183
|
+
// Get the first grapheme from 'after'
|
|
184
|
+
const afterGraphemes = [...segmenter.segment(after)];
|
|
185
|
+
const firstGrapheme = afterGraphemes[0]?.segment || "";
|
|
186
|
+
const restAfter = after.slice(firstGrapheme.length);
|
|
187
|
+
const cursor = `\x1b[7m${firstGrapheme}\x1b[0m`;
|
|
59
188
|
displayText = before + cursor + restAfter;
|
|
60
|
-
//
|
|
189
|
+
// lineVisibleWidth stays the same - we're replacing, not adding
|
|
61
190
|
}
|
|
62
191
|
else {
|
|
63
192
|
// Cursor is at the end - check if we have room for the space
|
|
64
|
-
if (
|
|
193
|
+
if (lineVisibleWidth < width) {
|
|
65
194
|
// We have room - add highlighted space
|
|
66
195
|
const cursor = "\x1b[7m \x1b[0m";
|
|
67
196
|
displayText = before + cursor;
|
|
68
|
-
//
|
|
69
|
-
|
|
197
|
+
// lineVisibleWidth increases by 1 - we're adding a space
|
|
198
|
+
lineVisibleWidth = lineVisibleWidth + 1;
|
|
70
199
|
}
|
|
71
200
|
else {
|
|
72
|
-
// Line is at full width - use reverse video on last
|
|
201
|
+
// Line is at full width - use reverse video on last grapheme if possible
|
|
73
202
|
// or just show cursor at the end without adding space
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
203
|
+
const beforeGraphemes = [...segmenter.segment(before)];
|
|
204
|
+
if (beforeGraphemes.length > 0) {
|
|
205
|
+
const lastGrapheme = beforeGraphemes[beforeGraphemes.length - 1]?.segment || "";
|
|
206
|
+
const cursor = `\x1b[7m${lastGrapheme}\x1b[0m`;
|
|
207
|
+
// Rebuild 'before' without the last grapheme
|
|
208
|
+
const beforeWithoutLast = beforeGraphemes
|
|
209
|
+
.slice(0, -1)
|
|
210
|
+
.map((g) => g.segment)
|
|
211
|
+
.join("");
|
|
212
|
+
displayText = beforeWithoutLast + cursor;
|
|
78
213
|
}
|
|
79
|
-
//
|
|
214
|
+
// lineVisibleWidth stays the same
|
|
80
215
|
}
|
|
81
216
|
}
|
|
82
217
|
}
|
|
83
|
-
// Calculate padding based on actual visible
|
|
84
|
-
const padding = " ".repeat(Math.max(0, width -
|
|
218
|
+
// Calculate padding based on actual visible width
|
|
219
|
+
const padding = " ".repeat(Math.max(0, width - lineVisibleWidth));
|
|
85
220
|
// Render the line (no side borders, just horizontal lines above and below)
|
|
86
221
|
result.push(displayText + padding);
|
|
87
222
|
}
|
|
@@ -159,45 +294,50 @@ export class Editor {
|
|
|
159
294
|
this.cancelAutocomplete();
|
|
160
295
|
return;
|
|
161
296
|
}
|
|
162
|
-
//
|
|
163
|
-
if (data === "\
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
this.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
}
|
|
297
|
+
// Enter - apply selection
|
|
298
|
+
if (data === "\r") {
|
|
299
|
+
const selected = this.autocompleteList.getSelectedItem();
|
|
300
|
+
if (selected && this.autocompleteProvider) {
|
|
301
|
+
const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, selected, this.autocompletePrefix);
|
|
302
|
+
this.state.lines = result.lines;
|
|
303
|
+
this.state.cursorLine = result.cursorLine;
|
|
304
|
+
this.state.cursorCol = result.cursorCol;
|
|
305
|
+
this.cancelAutocomplete();
|
|
306
|
+
if (this.onChange) {
|
|
307
|
+
this.onChange(this.getText());
|
|
183
308
|
}
|
|
184
|
-
return;
|
|
185
309
|
}
|
|
186
|
-
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// Navigation keys (arrows, Tab, Shift+Tab) - pass to autocomplete list
|
|
313
|
+
if (isNavigationKey(data)) {
|
|
314
|
+
this.autocompleteList.handleInput(data);
|
|
187
315
|
return;
|
|
188
316
|
}
|
|
189
317
|
// For other keys (like regular typing), DON'T return here
|
|
190
318
|
// Let them fall through to normal character handling
|
|
191
319
|
}
|
|
192
320
|
// Tab key - context-aware completion (but not when already autocompleting)
|
|
193
|
-
if (data
|
|
321
|
+
if (isTab(data) && !this.isAutocompleting) {
|
|
194
322
|
void this.handleTabCompletion();
|
|
195
323
|
return;
|
|
196
324
|
}
|
|
197
325
|
// Continue with rest of input handling
|
|
198
|
-
// Ctrl+K - Delete
|
|
326
|
+
// Ctrl+K - Delete to end of line
|
|
199
327
|
if (data.charCodeAt(0) === 11) {
|
|
200
|
-
this.
|
|
328
|
+
this.deleteToEndOfLine();
|
|
329
|
+
}
|
|
330
|
+
// Ctrl+U - Delete to start of line
|
|
331
|
+
else if (data.charCodeAt(0) === 21) {
|
|
332
|
+
this.deleteToStartOfLine();
|
|
333
|
+
}
|
|
334
|
+
// Ctrl+W - Delete word backwards
|
|
335
|
+
else if (data.charCodeAt(0) === 23) {
|
|
336
|
+
this.deleteWordBackwards();
|
|
337
|
+
}
|
|
338
|
+
// Option/Alt+Backspace (e.g. Ghostty sends ESC + DEL)
|
|
339
|
+
else if (data === "\x1b\x7f") {
|
|
340
|
+
this.deleteWordBackwards();
|
|
201
341
|
}
|
|
202
342
|
// Ctrl+A - Move to start of line
|
|
203
343
|
else if (data.charCodeAt(0) === 1) {
|
|
@@ -207,13 +347,12 @@ export class Editor {
|
|
|
207
347
|
else if (data.charCodeAt(0) === 5) {
|
|
208
348
|
this.moveToLineEnd();
|
|
209
349
|
}
|
|
210
|
-
//
|
|
211
|
-
else if (
|
|
212
|
-
// Modifier + Enter = new line
|
|
350
|
+
// Plain Enter (char code 13 for CR) - create new line
|
|
351
|
+
else if (data.charCodeAt(0) === 13 && data.length === 1) {
|
|
213
352
|
this.addNewLine();
|
|
214
353
|
}
|
|
215
|
-
//
|
|
216
|
-
else if (
|
|
354
|
+
// Modified Enter keys (Shift+Enter, Ctrl+Enter, etc.) - submit
|
|
355
|
+
else if (this.isModifiedEnter(data)) {
|
|
217
356
|
// If submit is disabled, do nothing
|
|
218
357
|
if (this.disableSubmit) {
|
|
219
358
|
return;
|
|
@@ -234,6 +373,7 @@ export class Editor {
|
|
|
234
373
|
};
|
|
235
374
|
this.pastes.clear();
|
|
236
375
|
this.pasteCounter = 0;
|
|
376
|
+
this.historyIndex = -1; // Exit history browsing mode
|
|
237
377
|
// Notify that editor is now empty
|
|
238
378
|
if (this.onChange) {
|
|
239
379
|
this.onChange("");
|
|
@@ -260,14 +400,42 @@ export class Editor {
|
|
|
260
400
|
// Delete key
|
|
261
401
|
this.handleForwardDelete();
|
|
262
402
|
}
|
|
403
|
+
// Word navigation (Option/Alt + Arrow or Ctrl + Arrow)
|
|
404
|
+
// Option+Left: \x1b[1;3D or \x1bb
|
|
405
|
+
// Option+Right: \x1b[1;3C or \x1bf
|
|
406
|
+
// Ctrl+Left: \x1b[1;5D
|
|
407
|
+
// Ctrl+Right: \x1b[1;5C
|
|
408
|
+
else if (data === "\x1b[1;3D" || data === "\x1bb" || data === "\x1b[1;5D") {
|
|
409
|
+
// Word left
|
|
410
|
+
this.moveWordBackwards();
|
|
411
|
+
}
|
|
412
|
+
else if (data === "\x1b[1;3C" ||
|
|
413
|
+
data === "\x1bf" ||
|
|
414
|
+
data === "\x1b[1;5C") {
|
|
415
|
+
// Word right
|
|
416
|
+
this.moveWordForwards();
|
|
417
|
+
}
|
|
263
418
|
// Arrow keys
|
|
264
419
|
else if (data === "\x1b[A") {
|
|
265
|
-
// Up
|
|
266
|
-
this.
|
|
420
|
+
// Up - history navigation or cursor movement
|
|
421
|
+
if (this.isEditorEmpty()) {
|
|
422
|
+
this.navigateHistory(-1); // Start browsing history
|
|
423
|
+
}
|
|
424
|
+
else if (this.historyIndex > -1 && this.isOnFirstVisualLine()) {
|
|
425
|
+
this.navigateHistory(-1); // Navigate to older history entry
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
this.moveCursor(-1, 0); // Cursor movement (within text or history entry)
|
|
429
|
+
}
|
|
267
430
|
}
|
|
268
431
|
else if (data === "\x1b[B") {
|
|
269
|
-
// Down
|
|
270
|
-
this.
|
|
432
|
+
// Down - history navigation or cursor movement
|
|
433
|
+
if (this.historyIndex > -1 && this.isOnLastVisualLine()) {
|
|
434
|
+
this.navigateHistory(1); // Navigate to newer history entry or clear
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
this.moveCursor(1, 0); // Cursor movement (within text or history entry)
|
|
438
|
+
}
|
|
271
439
|
}
|
|
272
440
|
else if (data === "\x1b[C") {
|
|
273
441
|
// Right
|
|
@@ -277,8 +445,8 @@ export class Editor {
|
|
|
277
445
|
// Left
|
|
278
446
|
this.moveCursor(0, -1);
|
|
279
447
|
}
|
|
280
|
-
// Regular characters (printable
|
|
281
|
-
else if (data.charCodeAt(0) >= 32
|
|
448
|
+
// Regular characters (printable characters and unicode, but not control characters)
|
|
449
|
+
else if (data.charCodeAt(0) >= 32) {
|
|
282
450
|
this.insertCharacter(data);
|
|
283
451
|
}
|
|
284
452
|
}
|
|
@@ -291,6 +459,7 @@ export class Editor {
|
|
|
291
459
|
text: "",
|
|
292
460
|
hasCursor: true,
|
|
293
461
|
cursorPos: 0,
|
|
462
|
+
width: 0,
|
|
294
463
|
});
|
|
295
464
|
return layoutLines;
|
|
296
465
|
}
|
|
@@ -298,48 +467,89 @@ export class Editor {
|
|
|
298
467
|
for (let i = 0; i < this.state.lines.length; i++) {
|
|
299
468
|
const line = this.state.lines[i] || "";
|
|
300
469
|
const isCurrentLine = i === this.state.cursorLine;
|
|
301
|
-
const
|
|
302
|
-
|
|
470
|
+
const metrics = lineMetricsCache.get(line);
|
|
471
|
+
const lineVisibleWidth = metrics.totalWidth;
|
|
472
|
+
if (lineVisibleWidth <= contentWidth) {
|
|
303
473
|
// Line fits in one layout line
|
|
304
474
|
if (isCurrentLine) {
|
|
305
475
|
layoutLines.push({
|
|
306
476
|
text: line,
|
|
307
477
|
hasCursor: true,
|
|
308
478
|
cursorPos: this.state.cursorCol,
|
|
479
|
+
width: lineVisibleWidth,
|
|
309
480
|
});
|
|
310
481
|
}
|
|
311
482
|
else {
|
|
312
483
|
layoutLines.push({
|
|
313
484
|
text: line,
|
|
314
485
|
hasCursor: false,
|
|
486
|
+
width: lineVisibleWidth,
|
|
315
487
|
});
|
|
316
488
|
}
|
|
317
489
|
}
|
|
318
490
|
else {
|
|
319
|
-
// Line needs wrapping
|
|
491
|
+
// Line needs wrapping - use cached graphemes and widths
|
|
320
492
|
const chunks = [];
|
|
321
|
-
|
|
322
|
-
|
|
493
|
+
let currentChunk = "";
|
|
494
|
+
let currentWidth = 0;
|
|
495
|
+
let chunkStartIndex = 0;
|
|
496
|
+
let currentIndex = 0;
|
|
497
|
+
for (let g = 0; g < metrics.graphemes.length; g++) {
|
|
498
|
+
const grapheme = metrics.graphemes[g];
|
|
499
|
+
const graphemeWidth = metrics.widths[g];
|
|
500
|
+
if (currentWidth + graphemeWidth > contentWidth &&
|
|
501
|
+
currentChunk !== "") {
|
|
502
|
+
// Start a new chunk
|
|
503
|
+
chunks.push({
|
|
504
|
+
text: currentChunk,
|
|
505
|
+
startIndex: chunkStartIndex,
|
|
506
|
+
endIndex: currentIndex,
|
|
507
|
+
width: currentWidth,
|
|
508
|
+
});
|
|
509
|
+
currentChunk = grapheme;
|
|
510
|
+
currentWidth = graphemeWidth;
|
|
511
|
+
chunkStartIndex = currentIndex;
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
currentChunk += grapheme;
|
|
515
|
+
currentWidth += graphemeWidth;
|
|
516
|
+
}
|
|
517
|
+
currentIndex += grapheme.length;
|
|
518
|
+
}
|
|
519
|
+
// Push the last chunk
|
|
520
|
+
if (currentChunk !== "") {
|
|
521
|
+
chunks.push({
|
|
522
|
+
text: currentChunk,
|
|
523
|
+
startIndex: chunkStartIndex,
|
|
524
|
+
endIndex: currentIndex,
|
|
525
|
+
width: currentWidth,
|
|
526
|
+
});
|
|
323
527
|
}
|
|
324
528
|
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
325
529
|
const chunk = chunks[chunkIndex];
|
|
326
530
|
if (!chunk)
|
|
327
531
|
continue;
|
|
328
|
-
const chunkStart = chunkIndex * maxLineLength;
|
|
329
|
-
const chunkEnd = chunkStart + chunk.length;
|
|
330
532
|
const cursorPos = this.state.cursorCol;
|
|
331
|
-
const
|
|
533
|
+
const isLastChunk = chunkIndex === chunks.length - 1;
|
|
534
|
+
// For non-last chunks, cursor at endIndex belongs to the next chunk
|
|
535
|
+
const hasCursorInChunk = isCurrentLine &&
|
|
536
|
+
cursorPos >= chunk.startIndex &&
|
|
537
|
+
(isLastChunk
|
|
538
|
+
? cursorPos <= chunk.endIndex
|
|
539
|
+
: cursorPos < chunk.endIndex);
|
|
332
540
|
if (hasCursorInChunk) {
|
|
333
541
|
layoutLines.push({
|
|
334
|
-
text: chunk,
|
|
542
|
+
text: chunk.text,
|
|
335
543
|
hasCursor: true,
|
|
336
|
-
cursorPos: cursorPos -
|
|
544
|
+
cursorPos: cursorPos - chunk.startIndex,
|
|
545
|
+
width: chunk.width,
|
|
337
546
|
});
|
|
338
547
|
}
|
|
339
548
|
else {
|
|
340
549
|
layoutLines.push({
|
|
341
|
-
text: chunk,
|
|
550
|
+
text: chunk.text,
|
|
342
551
|
hasCursor: false,
|
|
552
|
+
width: chunk.width,
|
|
343
553
|
});
|
|
344
554
|
}
|
|
345
555
|
}
|
|
@@ -351,20 +561,12 @@ export class Editor {
|
|
|
351
561
|
return this.state.lines.join("\n");
|
|
352
562
|
}
|
|
353
563
|
setText(text) {
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
}
|
|
564
|
+
this.historyIndex = -1; // Exit history browsing mode
|
|
565
|
+
this.setTextInternal(text);
|
|
365
566
|
}
|
|
366
567
|
// All the editor methods from before...
|
|
367
568
|
insertCharacter(char) {
|
|
569
|
+
this.historyIndex = -1; // Exit history browsing mode
|
|
368
570
|
const line = this.state.lines[this.state.cursorLine] || "";
|
|
369
571
|
const before = line.slice(0, this.state.cursorCol);
|
|
370
572
|
const after = line.slice(this.state.cursorCol);
|
|
@@ -379,13 +581,28 @@ export class Editor {
|
|
|
379
581
|
if (char === "/" && this.isAtStartOfMessage()) {
|
|
380
582
|
void this.tryTriggerAutocomplete();
|
|
381
583
|
}
|
|
584
|
+
// Auto-trigger for "@" file reference (fuzzy search)
|
|
585
|
+
else if (char === "@") {
|
|
586
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
587
|
+
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
588
|
+
// Only trigger if @ is after whitespace or at start of line
|
|
589
|
+
const charBeforeAt = textBeforeCursor[textBeforeCursor.length - 2];
|
|
590
|
+
if (textBeforeCursor.length === 1 ||
|
|
591
|
+
charBeforeAt === " " ||
|
|
592
|
+
charBeforeAt === "\t") {
|
|
593
|
+
void this.tryTriggerAutocomplete();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
382
596
|
// Also auto-trigger when typing letters in a slash command context
|
|
383
597
|
else if (/[a-zA-Z0-9]/.test(char)) {
|
|
384
598
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
385
599
|
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
386
|
-
// Check if we're in a slash command with
|
|
387
|
-
if (textBeforeCursor.startsWith("/")
|
|
388
|
-
|
|
600
|
+
// Check if we're in a slash command (with or without space for arguments)
|
|
601
|
+
if (textBeforeCursor.trimStart().startsWith("/")) {
|
|
602
|
+
void this.tryTriggerAutocomplete();
|
|
603
|
+
}
|
|
604
|
+
// Check if we're in an @ file reference context
|
|
605
|
+
else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
|
|
389
606
|
void this.tryTriggerAutocomplete();
|
|
390
607
|
}
|
|
391
608
|
}
|
|
@@ -501,6 +718,15 @@ export class Editor {
|
|
|
501
718
|
if (this.isAutocompleting) {
|
|
502
719
|
void this.updateAutocomplete();
|
|
503
720
|
}
|
|
721
|
+
else {
|
|
722
|
+
// Check if we should trigger autocomplete after backspace in slash command context
|
|
723
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
724
|
+
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
725
|
+
// Trigger autocomplete if we're in a slash command context (typing command name)
|
|
726
|
+
if (textBeforeCursor.startsWith("/") && !textBeforeCursor.includes(" ")) {
|
|
727
|
+
void this.tryTriggerAutocomplete();
|
|
728
|
+
}
|
|
729
|
+
}
|
|
504
730
|
}
|
|
505
731
|
moveToLineStart() {
|
|
506
732
|
this.state.cursorCol = 0;
|
|
@@ -527,45 +753,276 @@ export class Editor {
|
|
|
527
753
|
this.onChange(this.getText());
|
|
528
754
|
}
|
|
529
755
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
756
|
+
deleteToStartOfLine() {
|
|
757
|
+
this.historyIndex = -1; // Exit history browsing mode
|
|
758
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
759
|
+
if (this.state.cursorCol > 0) {
|
|
760
|
+
// Delete from start of line up to cursor
|
|
761
|
+
this.state.lines[this.state.cursorLine] = currentLine.slice(this.state.cursorCol);
|
|
534
762
|
this.state.cursorCol = 0;
|
|
535
763
|
}
|
|
536
|
-
else {
|
|
537
|
-
//
|
|
764
|
+
else if (this.state.cursorLine > 0) {
|
|
765
|
+
// At start of line - merge with previous line
|
|
766
|
+
const previousLine = this.state.lines[this.state.cursorLine - 1] || "";
|
|
767
|
+
this.state.lines[this.state.cursorLine - 1] = previousLine + currentLine;
|
|
538
768
|
this.state.lines.splice(this.state.cursorLine, 1);
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
769
|
+
this.state.cursorLine--;
|
|
770
|
+
this.state.cursorCol = previousLine.length;
|
|
771
|
+
}
|
|
772
|
+
if (this.onChange) {
|
|
773
|
+
this.onChange(this.getText());
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
deleteToEndOfLine() {
|
|
777
|
+
this.historyIndex = -1; // Exit history browsing mode
|
|
778
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
779
|
+
if (this.state.cursorCol < currentLine.length) {
|
|
780
|
+
// Delete from cursor to end of line
|
|
781
|
+
this.state.lines[this.state.cursorLine] = currentLine.slice(0, this.state.cursorCol);
|
|
782
|
+
}
|
|
783
|
+
else if (this.state.cursorLine < this.state.lines.length - 1) {
|
|
784
|
+
// At end of line - merge with next line
|
|
785
|
+
const nextLine = this.state.lines[this.state.cursorLine + 1] || "";
|
|
786
|
+
this.state.lines[this.state.cursorLine] = currentLine + nextLine;
|
|
787
|
+
this.state.lines.splice(this.state.cursorLine + 1, 1);
|
|
788
|
+
}
|
|
789
|
+
if (this.onChange) {
|
|
790
|
+
this.onChange(this.getText());
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
deleteWordBackwards() {
|
|
794
|
+
this.historyIndex = -1; // Exit history browsing mode
|
|
795
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
796
|
+
// If at start of line, behave like backspace at column 0 (merge with previous line)
|
|
797
|
+
if (this.state.cursorCol === 0) {
|
|
798
|
+
if (this.state.cursorLine > 0) {
|
|
799
|
+
const previousLine = this.state.lines[this.state.cursorLine - 1] || "";
|
|
800
|
+
this.state.lines[this.state.cursorLine - 1] =
|
|
801
|
+
previousLine + currentLine;
|
|
802
|
+
this.state.lines.splice(this.state.cursorLine, 1);
|
|
803
|
+
this.state.cursorLine--;
|
|
804
|
+
this.state.cursorCol = previousLine.length;
|
|
543
805
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
808
|
+
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
809
|
+
const isWhitespace = (char) => /\s/.test(char);
|
|
810
|
+
const isPunctuation = (char) => {
|
|
811
|
+
// Treat obvious code punctuation as boundaries
|
|
812
|
+
return /[(){}[\]<>.,;:'"!?+\-=*/\\|&%^$#@~`]/.test(char);
|
|
813
|
+
};
|
|
814
|
+
let deleteFrom = this.state.cursorCol;
|
|
815
|
+
const lastChar = textBeforeCursor[deleteFrom - 1] ?? "";
|
|
816
|
+
// If immediately on whitespace or punctuation, delete that single boundary char
|
|
817
|
+
if (isWhitespace(lastChar) || isPunctuation(lastChar)) {
|
|
818
|
+
deleteFrom -= 1;
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
// Otherwise, delete a run of non-boundary characters (the "word")
|
|
822
|
+
while (deleteFrom > 0) {
|
|
823
|
+
const ch = textBeforeCursor[deleteFrom - 1] ?? "";
|
|
824
|
+
if (isWhitespace(ch) || isPunctuation(ch)) {
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
deleteFrom -= 1;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
this.state.lines[this.state.cursorLine] =
|
|
831
|
+
currentLine.slice(0, deleteFrom) +
|
|
832
|
+
currentLine.slice(this.state.cursorCol);
|
|
833
|
+
this.state.cursorCol = deleteFrom;
|
|
547
834
|
}
|
|
548
835
|
if (this.onChange) {
|
|
549
836
|
this.onChange(this.getText());
|
|
550
837
|
}
|
|
551
838
|
}
|
|
839
|
+
/**
|
|
840
|
+
* Build a mapping from visual lines to logical positions.
|
|
841
|
+
* Returns an array where each element represents a visual line with:
|
|
842
|
+
* - logicalLine: index into this.state.lines
|
|
843
|
+
* - startCol: starting column in the logical line
|
|
844
|
+
* - length: length of this visual line segment
|
|
845
|
+
*/
|
|
846
|
+
buildVisualLineMap(width) {
|
|
847
|
+
const visualLines = [];
|
|
848
|
+
for (let i = 0; i < this.state.lines.length; i++) {
|
|
849
|
+
const line = this.state.lines[i] || "";
|
|
850
|
+
const metrics = lineMetricsCache.get(line);
|
|
851
|
+
const lineVisWidth = metrics.totalWidth;
|
|
852
|
+
if (line.length === 0) {
|
|
853
|
+
// Empty line still takes one visual line
|
|
854
|
+
visualLines.push({ logicalLine: i, startCol: 0, length: 0 });
|
|
855
|
+
}
|
|
856
|
+
else if (lineVisWidth <= width) {
|
|
857
|
+
visualLines.push({ logicalLine: i, startCol: 0, length: line.length });
|
|
858
|
+
}
|
|
859
|
+
else {
|
|
860
|
+
// Line needs wrapping - use cached graphemes and widths
|
|
861
|
+
let currentWidth = 0;
|
|
862
|
+
let chunkStartIndex = 0;
|
|
863
|
+
let currentIndex = 0;
|
|
864
|
+
for (let g = 0; g < metrics.graphemes.length; g++) {
|
|
865
|
+
const grapheme = metrics.graphemes[g];
|
|
866
|
+
const graphemeWidth = metrics.widths[g];
|
|
867
|
+
if (currentWidth + graphemeWidth > width &&
|
|
868
|
+
currentIndex > chunkStartIndex) {
|
|
869
|
+
// Start a new chunk
|
|
870
|
+
visualLines.push({
|
|
871
|
+
logicalLine: i,
|
|
872
|
+
startCol: chunkStartIndex,
|
|
873
|
+
length: currentIndex - chunkStartIndex,
|
|
874
|
+
});
|
|
875
|
+
chunkStartIndex = currentIndex;
|
|
876
|
+
currentWidth = graphemeWidth;
|
|
877
|
+
}
|
|
878
|
+
else {
|
|
879
|
+
currentWidth += graphemeWidth;
|
|
880
|
+
}
|
|
881
|
+
currentIndex += grapheme.length;
|
|
882
|
+
}
|
|
883
|
+
// Push the last chunk
|
|
884
|
+
if (currentIndex > chunkStartIndex) {
|
|
885
|
+
visualLines.push({
|
|
886
|
+
logicalLine: i,
|
|
887
|
+
startCol: chunkStartIndex,
|
|
888
|
+
length: currentIndex - chunkStartIndex,
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return visualLines;
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Find the visual line index for the current cursor position.
|
|
897
|
+
*/
|
|
898
|
+
findCurrentVisualLine(visualLines) {
|
|
899
|
+
for (let i = 0; i < visualLines.length; i++) {
|
|
900
|
+
const vl = visualLines[i];
|
|
901
|
+
if (!vl)
|
|
902
|
+
continue;
|
|
903
|
+
if (vl.logicalLine === this.state.cursorLine) {
|
|
904
|
+
const colInSegment = this.state.cursorCol - vl.startCol;
|
|
905
|
+
// Cursor is in this segment if it's within range
|
|
906
|
+
// For the last segment of a logical line, cursor can be at length (end position)
|
|
907
|
+
const isLastSegmentOfLine = i === visualLines.length - 1 ||
|
|
908
|
+
visualLines[i + 1]?.logicalLine !== vl.logicalLine;
|
|
909
|
+
if (colInSegment >= 0 &&
|
|
910
|
+
(colInSegment < vl.length ||
|
|
911
|
+
(isLastSegmentOfLine && colInSegment <= vl.length))) {
|
|
912
|
+
return i;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
// Fallback: return last visual line
|
|
917
|
+
return visualLines.length - 1;
|
|
918
|
+
}
|
|
552
919
|
moveCursor(deltaLine, deltaCol) {
|
|
920
|
+
const width = this.lastWidth;
|
|
553
921
|
if (deltaLine !== 0) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
922
|
+
// Build visual line map for navigation
|
|
923
|
+
const visualLines = this.buildVisualLineMap(width);
|
|
924
|
+
const currentVisualLine = this.findCurrentVisualLine(visualLines);
|
|
925
|
+
// Calculate column position within current visual line
|
|
926
|
+
const currentVl = visualLines[currentVisualLine];
|
|
927
|
+
const visualCol = currentVl
|
|
928
|
+
? this.state.cursorCol - currentVl.startCol
|
|
929
|
+
: 0;
|
|
930
|
+
// Move to target visual line
|
|
931
|
+
const targetVisualLine = currentVisualLine + deltaLine;
|
|
932
|
+
if (targetVisualLine >= 0 && targetVisualLine < visualLines.length) {
|
|
933
|
+
const targetVl = visualLines[targetVisualLine];
|
|
934
|
+
if (targetVl) {
|
|
935
|
+
this.state.cursorLine = targetVl.logicalLine;
|
|
936
|
+
// Try to maintain visual column position, clamped to line length
|
|
937
|
+
const targetCol = targetVl.startCol + Math.min(visualCol, targetVl.length);
|
|
938
|
+
const logicalLine = this.state.lines[targetVl.logicalLine] || "";
|
|
939
|
+
this.state.cursorCol = Math.min(targetCol, logicalLine.length);
|
|
940
|
+
}
|
|
560
941
|
}
|
|
561
942
|
}
|
|
562
943
|
if (deltaCol !== 0) {
|
|
563
|
-
// Move column
|
|
564
|
-
const newCol = this.state.cursorCol + deltaCol;
|
|
565
944
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
566
|
-
|
|
567
|
-
|
|
945
|
+
if (deltaCol > 0) {
|
|
946
|
+
// Moving right
|
|
947
|
+
if (this.state.cursorCol < currentLine.length) {
|
|
948
|
+
this.state.cursorCol++;
|
|
949
|
+
}
|
|
950
|
+
else if (this.state.cursorLine < this.state.lines.length - 1) {
|
|
951
|
+
// Wrap to start of next logical line
|
|
952
|
+
this.state.cursorLine++;
|
|
953
|
+
this.state.cursorCol = 0;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
else {
|
|
957
|
+
// Moving left
|
|
958
|
+
if (this.state.cursorCol > 0) {
|
|
959
|
+
this.state.cursorCol--;
|
|
960
|
+
}
|
|
961
|
+
else if (this.state.cursorLine > 0) {
|
|
962
|
+
// Wrap to end of previous logical line
|
|
963
|
+
this.state.cursorLine--;
|
|
964
|
+
const prevLine = this.state.lines[this.state.cursorLine] || "";
|
|
965
|
+
this.state.cursorCol = prevLine.length;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
isWordBoundary(char) {
|
|
971
|
+
return /\s/.test(char) || /[(){}[\]<>.,;:'"!?+\-=*/\\|&%^$#@~`]/.test(char);
|
|
972
|
+
}
|
|
973
|
+
moveWordBackwards() {
|
|
974
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
975
|
+
// If at start of line, move to end of previous line
|
|
976
|
+
if (this.state.cursorCol === 0) {
|
|
977
|
+
if (this.state.cursorLine > 0) {
|
|
978
|
+
this.state.cursorLine--;
|
|
979
|
+
const prevLine = this.state.lines[this.state.cursorLine] || "";
|
|
980
|
+
this.state.cursorCol = prevLine.length;
|
|
981
|
+
}
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
985
|
+
let newCol = this.state.cursorCol;
|
|
986
|
+
const lastChar = textBeforeCursor[newCol - 1] ?? "";
|
|
987
|
+
// If immediately on whitespace or punctuation, skip that single boundary char
|
|
988
|
+
if (this.isWordBoundary(lastChar)) {
|
|
989
|
+
newCol -= 1;
|
|
990
|
+
}
|
|
991
|
+
// Now skip the "word" (non-boundary characters)
|
|
992
|
+
while (newCol > 0) {
|
|
993
|
+
const ch = textBeforeCursor[newCol - 1] ?? "";
|
|
994
|
+
if (this.isWordBoundary(ch)) {
|
|
995
|
+
break;
|
|
996
|
+
}
|
|
997
|
+
newCol -= 1;
|
|
998
|
+
}
|
|
999
|
+
this.state.cursorCol = newCol;
|
|
1000
|
+
}
|
|
1001
|
+
moveWordForwards() {
|
|
1002
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1003
|
+
// If at end of line, move to start of next line
|
|
1004
|
+
if (this.state.cursorCol >= currentLine.length) {
|
|
1005
|
+
if (this.state.cursorLine < this.state.lines.length - 1) {
|
|
1006
|
+
this.state.cursorLine++;
|
|
1007
|
+
this.state.cursorCol = 0;
|
|
1008
|
+
}
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
let newCol = this.state.cursorCol;
|
|
1012
|
+
const charAtCursor = currentLine[newCol] ?? "";
|
|
1013
|
+
// If on whitespace or punctuation, skip it
|
|
1014
|
+
if (this.isWordBoundary(charAtCursor)) {
|
|
1015
|
+
newCol += 1;
|
|
1016
|
+
}
|
|
1017
|
+
// Skip the "word" (non-boundary characters)
|
|
1018
|
+
while (newCol < currentLine.length) {
|
|
1019
|
+
const ch = currentLine[newCol] ?? "";
|
|
1020
|
+
if (this.isWordBoundary(ch)) {
|
|
1021
|
+
break;
|
|
1022
|
+
}
|
|
1023
|
+
newCol += 1;
|
|
568
1024
|
}
|
|
1025
|
+
this.state.cursorCol = newCol;
|
|
569
1026
|
}
|
|
570
1027
|
// Helper method to check if cursor is at start of message (for slash command detection)
|
|
571
1028
|
isAtStartOfMessage() {
|
|
@@ -597,7 +1054,7 @@ export class Editor {
|
|
|
597
1054
|
this.autocompleteList.updateItems(suggestions.items);
|
|
598
1055
|
}
|
|
599
1056
|
else {
|
|
600
|
-
this.autocompleteList = new SelectList(suggestions.items, 5);
|
|
1057
|
+
this.autocompleteList = new SelectList(suggestions.items, 5, this.theme.selectList);
|
|
601
1058
|
}
|
|
602
1059
|
// Request re-render to show autocomplete list
|
|
603
1060
|
this.onRenderRequested?.();
|
|
@@ -640,7 +1097,7 @@ export class Editor {
|
|
|
640
1097
|
this.autocompleteList.updateItems(suggestions.items);
|
|
641
1098
|
}
|
|
642
1099
|
else {
|
|
643
|
-
this.autocompleteList = new SelectList(suggestions.items, 5);
|
|
1100
|
+
this.autocompleteList = new SelectList(suggestions.items, 5, this.theme.selectList);
|
|
644
1101
|
}
|
|
645
1102
|
this.isAutocompleting = true;
|
|
646
1103
|
// Request re-render to show autocomplete list
|
|
@@ -697,7 +1154,7 @@ export class Editor {
|
|
|
697
1154
|
this.autocompleteList.updateItems(suggestions.items);
|
|
698
1155
|
}
|
|
699
1156
|
else {
|
|
700
|
-
this.autocompleteList = new SelectList(suggestions.items, 5);
|
|
1157
|
+
this.autocompleteList = new SelectList(suggestions.items, 5, this.theme.selectList);
|
|
701
1158
|
}
|
|
702
1159
|
this.isAutocompleting = true;
|
|
703
1160
|
// Request re-render to show updated autocomplete list
|
|
@@ -719,8 +1176,12 @@ export class Editor {
|
|
|
719
1176
|
"\x1bOM", // Some terminals
|
|
720
1177
|
"\\\r", // VS Code terminal
|
|
721
1178
|
"\x1b\r", // Option+Enter (macOS)
|
|
1179
|
+
"\x1b[27;2;13~", // xterm shift+enter
|
|
1180
|
+
"\x1b[13;2u", // libtermkey shift+enter
|
|
722
1181
|
// Ctrl+Enter sequences
|
|
723
1182
|
"\x1b[13;5~", // Some terminals
|
|
1183
|
+
"\x1b[27;5;13~", // xterm ctrl+enter
|
|
1184
|
+
"\x1b[13;5u", // libtermkey ctrl+enter
|
|
724
1185
|
];
|
|
725
1186
|
// Check for known sequences
|
|
726
1187
|
if (sequences.includes(data)) {
|
|
@@ -732,8 +1193,9 @@ export class Editor {
|
|
|
732
1193
|
(data.includes("\r") || data.includes("\n"))) {
|
|
733
1194
|
return true;
|
|
734
1195
|
}
|
|
735
|
-
// Check for Ctrl+Enter (Ctrl + CR)
|
|
736
|
-
if (data.charCodeAt(0) === 13
|
|
1196
|
+
// Check for Ctrl+Enter (Ctrl + CR) or Ctrl+Enter with LF
|
|
1197
|
+
if ((data.charCodeAt(0) === 13 || data.charCodeAt(0) === 10) &&
|
|
1198
|
+
data.length > 1) {
|
|
737
1199
|
return true;
|
|
738
1200
|
}
|
|
739
1201
|
return false;
|