@travisennis/acai 0.0.5 → 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 +190 -19
- package/bin/acai-wrapper.js +26 -0
- package/dist/agent/index.d.ts +132 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +434 -0
- package/dist/api/exa/index.js +1 -1
- package/dist/cli.d.ts +4 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +67 -40
- 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 +54 -0
- 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 +18 -20
- 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 +7 -3
- package/dist/commands/compact-command.d.ts.map +1 -1
- package/dist/commands/compact-command.js +9 -5
- 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 +124 -0
- package/dist/commands/copy-command.d.ts.map +1 -1
- package/dist/commands/copy-command.js +14 -5
- 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 +21 -34
- 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 +18 -15
- 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 +9 -5
- 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 +20 -16
- 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 +307 -39
- 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 +191 -0
- 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 +49 -27
- 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 +25 -5
- 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 +458 -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 +40 -22
- 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 +15 -15
- 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 +35 -0
- package/dist/commands/list-tools-command.d.ts.map +1 -1
- package/dist/commands/list-tools-command.js +61 -21
- package/dist/commands/manager.d.ts +9 -4
- package/dist/commands/manager.d.ts.map +1 -1
- package/dist/commands/manager.js +64 -39
- package/dist/commands/model-command.d.ts.map +1 -1
- package/dist/commands/model-command.js +201 -66
- 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 +23 -9
- 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 +109 -0
- 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 +191 -98
- 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 +55 -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 +8 -5
- 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 +25 -22
- 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 +8 -3
- package/dist/commands/shell-command.d.ts.map +1 -1
- package/dist/commands/shell-command.js +45 -24
- package/dist/commands/types.d.ts +9 -7
- 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 +18 -7
- package/dist/config.d.ts +21 -11
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +90 -63
- 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 +127 -0
- package/dist/formatting.d.ts.map +1 -1
- package/dist/formatting.js +201 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +263 -102
- 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 +11 -0
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +122 -21
- 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 -67
- 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/manager.d.ts +2 -1
- package/dist/models/manager.d.ts.map +1 -1
- package/dist/models/manager.js +26 -2
- 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 +16 -22
- package/dist/models/openrouter-provider.d.ts.map +1 -1
- package/dist/models/openrouter-provider.js +175 -236
- 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 +10 -4
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +447 -70
- package/dist/repl/project-status-line.d.ts +3 -0
- package/dist/repl/project-status-line.d.ts.map +1 -0
- package/dist/repl/project-status-line.js +61 -0
- 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 +51 -0
- package/dist/repl-new.d.ts.map +1 -0
- package/dist/repl-new.js +354 -0
- 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 -94
- package/dist/terminal/index.d.ts.map +1 -1
- package/dist/terminal/index.js +2 -370
- package/dist/terminal/markdown.js +10 -5
- 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/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 +20 -0
- package/dist/tokens/threshold.d.ts.map +1 -0
- package/dist/tokens/threshold.js +67 -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 +285 -0
- package/dist/tools/agent.d.ts +16 -5
- package/dist/tools/agent.d.ts.map +1 -1
- package/dist/tools/agent.js +86 -59
- package/dist/tools/bash.d.ts +23 -12
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +243 -128
- 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 +21 -9
- package/dist/tools/code-interpreter.d.ts.map +1 -1
- package/dist/tools/code-interpreter.js +151 -134
- 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 +60 -97
- package/dist/tools/directory-tree.d.ts +17 -12
- package/dist/tools/directory-tree.d.ts.map +1 -1
- package/dist/tools/directory-tree.js +57 -48
- package/dist/tools/dynamic-tool-loader.d.ts +16 -10
- package/dist/tools/dynamic-tool-loader.d.ts.map +1 -1
- package/dist/tools/dynamic-tool-loader.js +122 -130
- 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 +127 -114
- package/dist/tools/glob.d.ts +36 -0
- package/dist/tools/glob.d.ts.map +1 -0
- package/dist/tools/glob.js +154 -0
- package/dist/tools/grep.d.ts +73 -12
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +425 -165
- package/dist/tools/index.d.ts +220 -126
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +284 -135
- package/dist/tools/llm-edit-fixer.d.ts +24 -0
- package/dist/tools/llm-edit-fixer.d.ts.map +1 -0
- package/dist/tools/llm-edit-fixer.js +136 -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 +48 -34
- 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 +84 -70
- 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 +132 -72
- 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 +76 -101
- package/dist/tools/think.d.ts +15 -7
- package/dist/tools/think.d.ts.map +1 -1
- package/dist/tools/think.js +34 -20
- package/dist/tools/types.d.ts +8 -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 +64 -31
- 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/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 +106 -0
- package/dist/tui/components/editor.d.ts.map +1 -0
- package/dist/tui/components/editor.js +1220 -0
- package/dist/tui/components/footer.d.ts +12 -0
- package/dist/tui/components/footer.d.ts.map +1 -0
- package/dist/tui/components/footer.js +209 -0
- 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/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 +23 -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 +106 -0
- package/dist/tui/components/markdown.d.ts.map +1 -0
- package/dist/tui/components/markdown.js +586 -0
- package/dist/tui/components/modal.d.ts +29 -0
- package/dist/tui/components/modal.d.ts.map +1 -0
- package/dist/tui/components/modal.js +263 -0
- 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 +17 -0
- package/dist/tui/components/prompt-status.d.ts.map +1 -0
- package/dist/tui/components/prompt-status.js +26 -0
- package/dist/tui/components/select-list.d.ts +48 -0
- package/dist/tui/components/select-list.d.ts.map +1 -0
- package/dist/tui/components/select-list.js +207 -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/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/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 +33 -0
- package/dist/tui/components/tool-execution.d.ts +21 -0
- package/dist/tui/components/tool-execution.d.ts.map +1 -0
- package/dist/tui/components/tool-execution.js +161 -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 +23 -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 +18 -0
- package/dist/tui/index.d.ts.map +1 -0
- package/dist/tui/index.js +22 -0
- package/dist/tui/terminal.d.ts +38 -0
- package/dist/tui/terminal.d.ts.map +1 -0
- package/dist/tui/terminal.js +94 -0
- package/dist/tui/tui.d.ts +69 -0
- package/dist/tui/tui.d.ts.map +1 -0
- package/dist/tui/tui.js +204 -0
- package/dist/tui/utils.d.ts +24 -0
- package/dist/tui/utils.d.ts.map +1 -0
- package/dist/tui/utils.js +111 -0
- package/dist/utils/bash.d.ts +7 -0
- package/dist/utils/bash.d.ts.map +1 -0
- package/dist/{tools/bash-utils.js → utils/bash.js} +31 -12
- package/dist/utils/{filesystem.d.ts → filesystem/operations.d.ts} +1 -1
- package/dist/utils/filesystem/operations.d.ts.map +1 -0
- package/dist/utils/filesystem/security.d.ts +9 -0
- package/dist/utils/filesystem/security.d.ts.map +1 -0
- package/dist/{tools/filesystem-utils.js → utils/filesystem/security.js} +93 -21
- 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/utils/generators.d.ts +3 -0
- package/dist/utils/generators.d.ts.map +1 -0
- package/dist/utils/generators.js +25 -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/iterables.d.ts +2 -0
- package/dist/utils/iterables.d.ts.map +1 -0
- package/dist/utils/iterables.js +6 -0
- package/dist/utils/{zod-utils.d.ts → zod.d.ts} +1 -1
- package/dist/utils/zod.d.ts.map +1 -0
- package/package.json +21 -21
- 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/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 -55
- 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 -38
- package/dist/repl-prompt.d.ts +0 -15
- package/dist/repl-prompt.d.ts.map +0 -1
- package/dist/repl-prompt.js +0 -147
- package/dist/repl.d.ts +0 -31
- package/dist/repl.d.ts.map +0 -1
- package/dist/repl.js +0 -310
- 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 -362
- 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 -16
- 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 -279
- 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/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/bash-utils.d.ts +0 -7
- package/dist/tools/bash-utils.d.ts.map +0 -1
- 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
- package/dist/tools/filesystem-utils.d.ts +0 -7
- 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
|
@@ -0,0 +1,1220 @@
|
|
|
1
|
+
import style from "../../terminal/style.js";
|
|
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
|
+
*/
|
|
63
|
+
export class Editor {
|
|
64
|
+
state = {
|
|
65
|
+
lines: [""],
|
|
66
|
+
cursorLine: 0,
|
|
67
|
+
cursorCol: 0,
|
|
68
|
+
};
|
|
69
|
+
theme;
|
|
70
|
+
// Store last render width for cursor navigation
|
|
71
|
+
lastWidth = 80;
|
|
72
|
+
// Border color (can be changed dynamically)
|
|
73
|
+
borderColor;
|
|
74
|
+
// Autocomplete support
|
|
75
|
+
autocompleteProvider;
|
|
76
|
+
autocompleteList;
|
|
77
|
+
isAutocompleting = false;
|
|
78
|
+
autocompletePrefix = "";
|
|
79
|
+
autocompleteDebounceTimer;
|
|
80
|
+
// Paste tracking for large pastes
|
|
81
|
+
pastes = new Map();
|
|
82
|
+
pasteCounter = 0;
|
|
83
|
+
// Bracketed paste mode buffering
|
|
84
|
+
pasteBuffer = "";
|
|
85
|
+
isInPaste = false;
|
|
86
|
+
// Prompt history for up/down navigation
|
|
87
|
+
history = [];
|
|
88
|
+
historyIndex = -1; // -1 = not browsing, 0 = most recent, 1 = older, etc.
|
|
89
|
+
onSubmit;
|
|
90
|
+
onChange;
|
|
91
|
+
disableSubmit = false;
|
|
92
|
+
// Custom key handlers for coding-agent
|
|
93
|
+
onEscape;
|
|
94
|
+
onCtrlC;
|
|
95
|
+
onRenderRequested;
|
|
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;
|
|
102
|
+
}
|
|
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
|
+
}
|
|
119
|
+
}
|
|
120
|
+
setAutocompleteProvider(provider) {
|
|
121
|
+
this.autocompleteProvider = provider;
|
|
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
|
+
}
|
|
164
|
+
render(width) {
|
|
165
|
+
// Store width for cursor navigation
|
|
166
|
+
this.lastWidth = width;
|
|
167
|
+
const horizontal = this.borderColor("─");
|
|
168
|
+
// Layout the text - use full width
|
|
169
|
+
const layoutLines = this.layoutText(width);
|
|
170
|
+
const result = [];
|
|
171
|
+
// Render top border
|
|
172
|
+
result.push(horizontal.repeat(width));
|
|
173
|
+
// Render each layout line
|
|
174
|
+
for (const layoutLine of layoutLines) {
|
|
175
|
+
let displayText = layoutLine.text;
|
|
176
|
+
let lineVisibleWidth = layoutLine.width;
|
|
177
|
+
// Add cursor if this line has it
|
|
178
|
+
if (layoutLine.hasCursor && layoutLine.cursorPos !== undefined) {
|
|
179
|
+
const before = displayText.slice(0, layoutLine.cursorPos);
|
|
180
|
+
const after = displayText.slice(layoutLine.cursorPos);
|
|
181
|
+
if (after.length > 0) {
|
|
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`;
|
|
188
|
+
displayText = before + cursor + restAfter;
|
|
189
|
+
// lineVisibleWidth stays the same - we're replacing, not adding
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
// Cursor is at the end - check if we have room for the space
|
|
193
|
+
if (lineVisibleWidth < width) {
|
|
194
|
+
// We have room - add highlighted space
|
|
195
|
+
const cursor = "\x1b[7m \x1b[0m";
|
|
196
|
+
displayText = before + cursor;
|
|
197
|
+
// lineVisibleWidth increases by 1 - we're adding a space
|
|
198
|
+
lineVisibleWidth = lineVisibleWidth + 1;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// Line is at full width - use reverse video on last grapheme if possible
|
|
202
|
+
// or just show cursor at the end without adding space
|
|
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;
|
|
213
|
+
}
|
|
214
|
+
// lineVisibleWidth stays the same
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Calculate padding based on actual visible width
|
|
219
|
+
const padding = " ".repeat(Math.max(0, width - lineVisibleWidth));
|
|
220
|
+
// Render the line (no side borders, just horizontal lines above and below)
|
|
221
|
+
result.push(displayText + padding);
|
|
222
|
+
}
|
|
223
|
+
// Render bottom border
|
|
224
|
+
result.push(horizontal.repeat(width));
|
|
225
|
+
// Add autocomplete list if active
|
|
226
|
+
if (this.isAutocompleting && this.autocompleteList) {
|
|
227
|
+
const autocompleteResult = this.autocompleteList.render(width);
|
|
228
|
+
result.push(...autocompleteResult);
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
handleInput(data) {
|
|
233
|
+
// Handle bracketed paste mode
|
|
234
|
+
// Start of paste: \x1b[200~
|
|
235
|
+
// End of paste: \x1b[201~
|
|
236
|
+
// Check if we're starting a bracketed paste
|
|
237
|
+
if (data.includes("\x1b[200~")) {
|
|
238
|
+
this.isInPaste = true;
|
|
239
|
+
this.pasteBuffer = "";
|
|
240
|
+
// Remove the start marker and keep the rest
|
|
241
|
+
const cleanedData = data.replace("\x1b[200~", "");
|
|
242
|
+
// Process the remaining data
|
|
243
|
+
this.processInputData(cleanedData);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
// If we're in a paste, buffer the data
|
|
247
|
+
if (this.isInPaste) {
|
|
248
|
+
// Append data to buffer first (end marker could be split across chunks)
|
|
249
|
+
this.pasteBuffer += data;
|
|
250
|
+
// Check if the accumulated buffer contains the end marker
|
|
251
|
+
const endIndex = this.pasteBuffer.indexOf("\x1b[201~");
|
|
252
|
+
if (endIndex !== -1) {
|
|
253
|
+
// Extract content before the end marker
|
|
254
|
+
const pasteContent = this.pasteBuffer.substring(0, endIndex);
|
|
255
|
+
// Process the complete paste
|
|
256
|
+
this.handlePaste(pasteContent);
|
|
257
|
+
// Reset paste state
|
|
258
|
+
this.isInPaste = false;
|
|
259
|
+
// Process any remaining data after the end marker
|
|
260
|
+
const remaining = this.pasteBuffer.substring(endIndex + 6); // 6 = length of \x1b[201~
|
|
261
|
+
this.pasteBuffer = "";
|
|
262
|
+
if (remaining.length > 0) {
|
|
263
|
+
this.handleInput(remaining);
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
// Still accumulating, wait for more data
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
// Intercept Escape key - but only if autocomplete is NOT active
|
|
271
|
+
// (let parent handle escape for autocomplete cancellation)
|
|
272
|
+
if (data === "\x1b" && this.onEscape && !this.isShowingAutocomplete()) {
|
|
273
|
+
this.onEscape();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
// Intercept Ctrl+C
|
|
277
|
+
if (data === "\x03" && this.onCtrlC) {
|
|
278
|
+
this.onCtrlC();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
// Process regular input data
|
|
282
|
+
this.processInputData(data);
|
|
283
|
+
}
|
|
284
|
+
processInputData(data) {
|
|
285
|
+
// Handle special key combinations first
|
|
286
|
+
// Ctrl+C - Exit (let parent handle this)
|
|
287
|
+
if (data.charCodeAt(0) === 3) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
// Handle autocomplete special keys first (but don't block other input)
|
|
291
|
+
if (this.isAutocompleting && this.autocompleteList) {
|
|
292
|
+
// Escape - cancel autocomplete
|
|
293
|
+
if (data === "\x1b") {
|
|
294
|
+
this.cancelAutocomplete();
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
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());
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// Navigation keys (arrows, Tab, Shift+Tab) - pass to autocomplete list
|
|
313
|
+
if (isNavigationKey(data)) {
|
|
314
|
+
this.autocompleteList.handleInput(data);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
// For other keys (like regular typing), DON'T return here
|
|
318
|
+
// Let them fall through to normal character handling
|
|
319
|
+
}
|
|
320
|
+
// Tab key - context-aware completion (but not when already autocompleting)
|
|
321
|
+
if (isTab(data) && !this.isAutocompleting) {
|
|
322
|
+
void this.handleTabCompletion();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// Continue with rest of input handling
|
|
326
|
+
// Ctrl+K - Delete to end of line
|
|
327
|
+
if (data.charCodeAt(0) === 11) {
|
|
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();
|
|
341
|
+
}
|
|
342
|
+
// Ctrl+A - Move to start of line
|
|
343
|
+
else if (data.charCodeAt(0) === 1) {
|
|
344
|
+
this.moveToLineStart();
|
|
345
|
+
}
|
|
346
|
+
// Ctrl+E - Move to end of line
|
|
347
|
+
else if (data.charCodeAt(0) === 5) {
|
|
348
|
+
this.moveToLineEnd();
|
|
349
|
+
}
|
|
350
|
+
// Plain Enter (char code 13 for CR) - create new line
|
|
351
|
+
else if (data.charCodeAt(0) === 13 && data.length === 1) {
|
|
352
|
+
this.addNewLine();
|
|
353
|
+
}
|
|
354
|
+
// Modified Enter keys (Shift+Enter, Ctrl+Enter, etc.) - submit
|
|
355
|
+
else if (this.isModifiedEnter(data)) {
|
|
356
|
+
// If submit is disabled, do nothing
|
|
357
|
+
if (this.disableSubmit) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
// Get text and substitute paste markers with actual content
|
|
361
|
+
let result = this.state.lines.join("\n").trim();
|
|
362
|
+
// Replace all [paste #N +xxx lines] or [paste #N xxx chars] markers with actual paste content
|
|
363
|
+
for (const [pasteId, pasteContent] of this.pastes) {
|
|
364
|
+
// Match formats: [paste #N], [paste #N +xxx lines], or [paste #N xxx chars]
|
|
365
|
+
const markerRegex = new RegExp(`\\[paste #${pasteId}( (\\+\\d+ lines|\\d+ chars))?\\]`, "g");
|
|
366
|
+
result = result.replace(markerRegex, pasteContent);
|
|
367
|
+
}
|
|
368
|
+
// Reset editor and clear pastes
|
|
369
|
+
this.state = {
|
|
370
|
+
lines: [""],
|
|
371
|
+
cursorLine: 0,
|
|
372
|
+
cursorCol: 0,
|
|
373
|
+
};
|
|
374
|
+
this.pastes.clear();
|
|
375
|
+
this.pasteCounter = 0;
|
|
376
|
+
this.historyIndex = -1; // Exit history browsing mode
|
|
377
|
+
// Notify that editor is now empty
|
|
378
|
+
if (this.onChange) {
|
|
379
|
+
this.onChange("");
|
|
380
|
+
}
|
|
381
|
+
if (this.onSubmit) {
|
|
382
|
+
this.onSubmit(result);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// Backspace
|
|
386
|
+
else if (data.charCodeAt(0) === 127 || data.charCodeAt(0) === 8) {
|
|
387
|
+
this.handleBackspace();
|
|
388
|
+
}
|
|
389
|
+
// Line navigation shortcuts (Home/End keys)
|
|
390
|
+
else if (data === "\x1b[H" || data === "\x1b[1~" || data === "\x1b[7~") {
|
|
391
|
+
// Home key
|
|
392
|
+
this.moveToLineStart();
|
|
393
|
+
}
|
|
394
|
+
else if (data === "\x1b[F" || data === "\x1b[4~" || data === "\x1b[8~") {
|
|
395
|
+
// End key
|
|
396
|
+
this.moveToLineEnd();
|
|
397
|
+
}
|
|
398
|
+
// Forward delete (Fn+Backspace or Delete key)
|
|
399
|
+
else if (data === "\x1b[3~") {
|
|
400
|
+
// Delete key
|
|
401
|
+
this.handleForwardDelete();
|
|
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
|
+
}
|
|
418
|
+
// Arrow keys
|
|
419
|
+
else if (data === "\x1b[A") {
|
|
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
|
+
}
|
|
430
|
+
}
|
|
431
|
+
else if (data === "\x1b[B") {
|
|
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
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else if (data === "\x1b[C") {
|
|
441
|
+
// Right
|
|
442
|
+
this.moveCursor(0, 1);
|
|
443
|
+
}
|
|
444
|
+
else if (data === "\x1b[D") {
|
|
445
|
+
// Left
|
|
446
|
+
this.moveCursor(0, -1);
|
|
447
|
+
}
|
|
448
|
+
// Regular characters (printable characters and unicode, but not control characters)
|
|
449
|
+
else if (data.charCodeAt(0) >= 32) {
|
|
450
|
+
this.insertCharacter(data);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
layoutText(contentWidth) {
|
|
454
|
+
const layoutLines = [];
|
|
455
|
+
if (this.state.lines.length === 0 ||
|
|
456
|
+
(this.state.lines.length === 1 && this.state.lines[0] === "")) {
|
|
457
|
+
// Empty editor
|
|
458
|
+
layoutLines.push({
|
|
459
|
+
text: "",
|
|
460
|
+
hasCursor: true,
|
|
461
|
+
cursorPos: 0,
|
|
462
|
+
width: 0,
|
|
463
|
+
});
|
|
464
|
+
return layoutLines;
|
|
465
|
+
}
|
|
466
|
+
// Process each logical line
|
|
467
|
+
for (let i = 0; i < this.state.lines.length; i++) {
|
|
468
|
+
const line = this.state.lines[i] || "";
|
|
469
|
+
const isCurrentLine = i === this.state.cursorLine;
|
|
470
|
+
const metrics = lineMetricsCache.get(line);
|
|
471
|
+
const lineVisibleWidth = metrics.totalWidth;
|
|
472
|
+
if (lineVisibleWidth <= contentWidth) {
|
|
473
|
+
// Line fits in one layout line
|
|
474
|
+
if (isCurrentLine) {
|
|
475
|
+
layoutLines.push({
|
|
476
|
+
text: line,
|
|
477
|
+
hasCursor: true,
|
|
478
|
+
cursorPos: this.state.cursorCol,
|
|
479
|
+
width: lineVisibleWidth,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
layoutLines.push({
|
|
484
|
+
text: line,
|
|
485
|
+
hasCursor: false,
|
|
486
|
+
width: lineVisibleWidth,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
// Line needs wrapping - use cached graphemes and widths
|
|
492
|
+
const chunks = [];
|
|
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
|
+
});
|
|
527
|
+
}
|
|
528
|
+
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
529
|
+
const chunk = chunks[chunkIndex];
|
|
530
|
+
if (!chunk)
|
|
531
|
+
continue;
|
|
532
|
+
const cursorPos = this.state.cursorCol;
|
|
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);
|
|
540
|
+
if (hasCursorInChunk) {
|
|
541
|
+
layoutLines.push({
|
|
542
|
+
text: chunk.text,
|
|
543
|
+
hasCursor: true,
|
|
544
|
+
cursorPos: cursorPos - chunk.startIndex,
|
|
545
|
+
width: chunk.width,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
layoutLines.push({
|
|
550
|
+
text: chunk.text,
|
|
551
|
+
hasCursor: false,
|
|
552
|
+
width: chunk.width,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return layoutLines;
|
|
559
|
+
}
|
|
560
|
+
getText() {
|
|
561
|
+
return this.state.lines.join("\n");
|
|
562
|
+
}
|
|
563
|
+
setText(text) {
|
|
564
|
+
this.historyIndex = -1; // Exit history browsing mode
|
|
565
|
+
this.setTextInternal(text);
|
|
566
|
+
}
|
|
567
|
+
// All the editor methods from before...
|
|
568
|
+
insertCharacter(char) {
|
|
569
|
+
this.historyIndex = -1; // Exit history browsing mode
|
|
570
|
+
const line = this.state.lines[this.state.cursorLine] || "";
|
|
571
|
+
const before = line.slice(0, this.state.cursorCol);
|
|
572
|
+
const after = line.slice(this.state.cursorCol);
|
|
573
|
+
this.state.lines[this.state.cursorLine] = before + char + after;
|
|
574
|
+
this.state.cursorCol += char.length; // Fix: increment by the length of the inserted string
|
|
575
|
+
if (this.onChange) {
|
|
576
|
+
this.onChange(this.getText());
|
|
577
|
+
}
|
|
578
|
+
// Check if we should trigger or update autocomplete
|
|
579
|
+
if (!this.isAutocompleting) {
|
|
580
|
+
// Auto-trigger for "/" at the start of a line (slash commands)
|
|
581
|
+
if (char === "/" && this.isAtStartOfMessage()) {
|
|
582
|
+
void this.tryTriggerAutocomplete();
|
|
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
|
+
}
|
|
596
|
+
// Also auto-trigger when typing letters in a slash command context
|
|
597
|
+
else if (/[a-zA-Z0-9]/.test(char)) {
|
|
598
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
599
|
+
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
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]*$/)) {
|
|
606
|
+
void this.tryTriggerAutocomplete();
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
void this.updateAutocomplete();
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
handlePaste(pastedText) {
|
|
615
|
+
// Clean the pasted text
|
|
616
|
+
const cleanText = pastedText.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
617
|
+
// Convert tabs to spaces (4 spaces per tab)
|
|
618
|
+
const tabExpandedText = cleanText.replace(/\t/g, " ");
|
|
619
|
+
// Filter out non-printable characters except newlines
|
|
620
|
+
const filteredText = tabExpandedText
|
|
621
|
+
.split("")
|
|
622
|
+
.filter((char) => char === "\n" || (char >= " " && char <= "~"))
|
|
623
|
+
.join("");
|
|
624
|
+
// Split into lines
|
|
625
|
+
const pastedLines = filteredText.split("\n");
|
|
626
|
+
// Check if this is a large paste (> 10 lines or > 1000 characters)
|
|
627
|
+
const totalChars = filteredText.length;
|
|
628
|
+
if (pastedLines.length > 10 || totalChars > 1000) {
|
|
629
|
+
// Store the paste and insert a marker
|
|
630
|
+
this.pasteCounter++;
|
|
631
|
+
const pasteId = this.pasteCounter;
|
|
632
|
+
this.pastes.set(pasteId, filteredText);
|
|
633
|
+
// Insert marker like "[paste #1 +123 lines]" or "[paste #1 1234 chars]"
|
|
634
|
+
const marker = pastedLines.length > 10
|
|
635
|
+
? `[paste #${pasteId} +${pastedLines.length} lines]`
|
|
636
|
+
: `[paste #${pasteId} ${totalChars} chars]`;
|
|
637
|
+
for (const char of marker) {
|
|
638
|
+
this.insertCharacter(char);
|
|
639
|
+
}
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (pastedLines.length === 1) {
|
|
643
|
+
// Single line - just insert each character
|
|
644
|
+
const text = pastedLines[0] || "";
|
|
645
|
+
for (const char of text) {
|
|
646
|
+
this.insertCharacter(char);
|
|
647
|
+
}
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
// Multi-line paste - be very careful with array manipulation
|
|
651
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
652
|
+
const beforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
653
|
+
const afterCursor = currentLine.slice(this.state.cursorCol);
|
|
654
|
+
// Build the new lines array step by step
|
|
655
|
+
const newLines = [];
|
|
656
|
+
// Add all lines before current line
|
|
657
|
+
for (let i = 0; i < this.state.cursorLine; i++) {
|
|
658
|
+
newLines.push(this.state.lines[i] || "");
|
|
659
|
+
}
|
|
660
|
+
// Add the first pasted line merged with before cursor text
|
|
661
|
+
newLines.push(beforeCursor + (pastedLines[0] || ""));
|
|
662
|
+
// Add all middle pasted lines
|
|
663
|
+
for (let i = 1; i < pastedLines.length - 1; i++) {
|
|
664
|
+
newLines.push(pastedLines[i] || "");
|
|
665
|
+
}
|
|
666
|
+
// Add the last pasted line with after cursor text
|
|
667
|
+
newLines.push((pastedLines[pastedLines.length - 1] || "") + afterCursor);
|
|
668
|
+
// Add all lines after current line
|
|
669
|
+
for (let i = this.state.cursorLine + 1; i < this.state.lines.length; i++) {
|
|
670
|
+
newLines.push(this.state.lines[i] || "");
|
|
671
|
+
}
|
|
672
|
+
// Replace the entire lines array
|
|
673
|
+
this.state.lines = newLines;
|
|
674
|
+
// Update cursor position to end of pasted content
|
|
675
|
+
this.state.cursorLine += pastedLines.length - 1;
|
|
676
|
+
this.state.cursorCol = (pastedLines[pastedLines.length - 1] || "").length;
|
|
677
|
+
// Notify of change
|
|
678
|
+
if (this.onChange) {
|
|
679
|
+
this.onChange(this.getText());
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
addNewLine() {
|
|
683
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
684
|
+
const before = currentLine.slice(0, this.state.cursorCol);
|
|
685
|
+
const after = currentLine.slice(this.state.cursorCol);
|
|
686
|
+
// Split current line
|
|
687
|
+
this.state.lines[this.state.cursorLine] = before;
|
|
688
|
+
this.state.lines.splice(this.state.cursorLine + 1, 0, after);
|
|
689
|
+
// Move cursor to start of new line
|
|
690
|
+
this.state.cursorLine++;
|
|
691
|
+
this.state.cursorCol = 0;
|
|
692
|
+
if (this.onChange) {
|
|
693
|
+
this.onChange(this.getText());
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
handleBackspace() {
|
|
697
|
+
if (this.state.cursorCol > 0) {
|
|
698
|
+
// Delete character in current line
|
|
699
|
+
const line = this.state.lines[this.state.cursorLine] || "";
|
|
700
|
+
const before = line.slice(0, this.state.cursorCol - 1);
|
|
701
|
+
const after = line.slice(this.state.cursorCol);
|
|
702
|
+
this.state.lines[this.state.cursorLine] = before + after;
|
|
703
|
+
this.state.cursorCol--;
|
|
704
|
+
}
|
|
705
|
+
else if (this.state.cursorLine > 0) {
|
|
706
|
+
// Merge with previous line
|
|
707
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
708
|
+
const previousLine = this.state.lines[this.state.cursorLine - 1] || "";
|
|
709
|
+
this.state.lines[this.state.cursorLine - 1] = previousLine + currentLine;
|
|
710
|
+
this.state.lines.splice(this.state.cursorLine, 1);
|
|
711
|
+
this.state.cursorLine--;
|
|
712
|
+
this.state.cursorCol = previousLine.length;
|
|
713
|
+
}
|
|
714
|
+
if (this.onChange) {
|
|
715
|
+
this.onChange(this.getText());
|
|
716
|
+
}
|
|
717
|
+
// Update autocomplete after backspace
|
|
718
|
+
if (this.isAutocompleting) {
|
|
719
|
+
void this.updateAutocomplete();
|
|
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
|
+
}
|
|
730
|
+
}
|
|
731
|
+
moveToLineStart() {
|
|
732
|
+
this.state.cursorCol = 0;
|
|
733
|
+
}
|
|
734
|
+
moveToLineEnd() {
|
|
735
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
736
|
+
this.state.cursorCol = currentLine.length;
|
|
737
|
+
}
|
|
738
|
+
handleForwardDelete() {
|
|
739
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
740
|
+
if (this.state.cursorCol < currentLine.length) {
|
|
741
|
+
// Delete character at cursor position (forward delete)
|
|
742
|
+
const before = currentLine.slice(0, this.state.cursorCol);
|
|
743
|
+
const after = currentLine.slice(this.state.cursorCol + 1);
|
|
744
|
+
this.state.lines[this.state.cursorLine] = before + after;
|
|
745
|
+
}
|
|
746
|
+
else if (this.state.cursorLine < this.state.lines.length - 1) {
|
|
747
|
+
// At end of line - merge with next line
|
|
748
|
+
const nextLine = this.state.lines[this.state.cursorLine + 1] || "";
|
|
749
|
+
this.state.lines[this.state.cursorLine] = currentLine + nextLine;
|
|
750
|
+
this.state.lines.splice(this.state.cursorLine + 1, 1);
|
|
751
|
+
}
|
|
752
|
+
if (this.onChange) {
|
|
753
|
+
this.onChange(this.getText());
|
|
754
|
+
}
|
|
755
|
+
}
|
|
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);
|
|
762
|
+
this.state.cursorCol = 0;
|
|
763
|
+
}
|
|
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;
|
|
768
|
+
this.state.lines.splice(this.state.cursorLine, 1);
|
|
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;
|
|
805
|
+
}
|
|
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;
|
|
834
|
+
}
|
|
835
|
+
if (this.onChange) {
|
|
836
|
+
this.onChange(this.getText());
|
|
837
|
+
}
|
|
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
|
+
}
|
|
919
|
+
moveCursor(deltaLine, deltaCol) {
|
|
920
|
+
const width = this.lastWidth;
|
|
921
|
+
if (deltaLine !== 0) {
|
|
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
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (deltaCol !== 0) {
|
|
944
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
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;
|
|
1024
|
+
}
|
|
1025
|
+
this.state.cursorCol = newCol;
|
|
1026
|
+
}
|
|
1027
|
+
// Helper method to check if cursor is at start of message (for slash command detection)
|
|
1028
|
+
isAtStartOfMessage() {
|
|
1029
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1030
|
+
const beforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1031
|
+
// At start if line is empty, only contains whitespace, or is just "/"
|
|
1032
|
+
return beforeCursor.trim() === "" || beforeCursor.trim() === "/";
|
|
1033
|
+
}
|
|
1034
|
+
// Autocomplete methods
|
|
1035
|
+
async tryTriggerAutocomplete(explicitTab = false) {
|
|
1036
|
+
if (!this.autocompleteProvider)
|
|
1037
|
+
return;
|
|
1038
|
+
// Check if we should trigger file completion on Tab
|
|
1039
|
+
if (explicitTab) {
|
|
1040
|
+
const provider = this
|
|
1041
|
+
.autocompleteProvider;
|
|
1042
|
+
// Only check file completion triggering if the provider has the method
|
|
1043
|
+
// For slash commands, we always want to show autocomplete
|
|
1044
|
+
if (provider.shouldTriggerFileCompletion &&
|
|
1045
|
+
!provider.shouldTriggerFileCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol)) {
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
const suggestions = await this.autocompleteProvider.getSuggestions(this.state.lines, this.state.cursorLine, this.state.cursorCol);
|
|
1050
|
+
if (suggestions && suggestions.items.length > 0) {
|
|
1051
|
+
this.autocompletePrefix = suggestions.prefix;
|
|
1052
|
+
this.isAutocompleting = true;
|
|
1053
|
+
if (this.autocompleteList) {
|
|
1054
|
+
this.autocompleteList.updateItems(suggestions.items);
|
|
1055
|
+
}
|
|
1056
|
+
else {
|
|
1057
|
+
this.autocompleteList = new SelectList(suggestions.items, 5, this.theme.selectList);
|
|
1058
|
+
}
|
|
1059
|
+
// Request re-render to show autocomplete list
|
|
1060
|
+
this.onRenderRequested?.();
|
|
1061
|
+
}
|
|
1062
|
+
else {
|
|
1063
|
+
this.cancelAutocomplete();
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
async handleTabCompletion() {
|
|
1067
|
+
if (!this.autocompleteProvider)
|
|
1068
|
+
return;
|
|
1069
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1070
|
+
const beforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1071
|
+
// Check if we're in a slash command context
|
|
1072
|
+
if (beforeCursor.trimStart().startsWith("/")) {
|
|
1073
|
+
await this.handleSlashCommandCompletion();
|
|
1074
|
+
}
|
|
1075
|
+
else {
|
|
1076
|
+
await this.forceFileAutocomplete();
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
async handleSlashCommandCompletion() {
|
|
1080
|
+
// For now, fall back to regular autocomplete (slash commands)
|
|
1081
|
+
// This can be extended later to handle command-specific argument completion
|
|
1082
|
+
await this.tryTriggerAutocomplete(true);
|
|
1083
|
+
}
|
|
1084
|
+
async forceFileAutocomplete() {
|
|
1085
|
+
if (!this.autocompleteProvider)
|
|
1086
|
+
return;
|
|
1087
|
+
// Check if provider has the force method
|
|
1088
|
+
const provider = this.autocompleteProvider;
|
|
1089
|
+
if (!provider.getForceFileSuggestions) {
|
|
1090
|
+
await this.tryTriggerAutocomplete(true);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
const suggestions = await provider.getForceFileSuggestions(this.state.lines, this.state.cursorLine, this.state.cursorCol);
|
|
1094
|
+
if (suggestions && suggestions.items.length > 0) {
|
|
1095
|
+
this.autocompletePrefix = suggestions.prefix;
|
|
1096
|
+
if (this.autocompleteList) {
|
|
1097
|
+
this.autocompleteList.updateItems(suggestions.items);
|
|
1098
|
+
}
|
|
1099
|
+
else {
|
|
1100
|
+
this.autocompleteList = new SelectList(suggestions.items, 5, this.theme.selectList);
|
|
1101
|
+
}
|
|
1102
|
+
this.isAutocompleting = true;
|
|
1103
|
+
// Request re-render to show autocomplete list
|
|
1104
|
+
this.onRenderRequested?.();
|
|
1105
|
+
}
|
|
1106
|
+
else {
|
|
1107
|
+
this.cancelAutocomplete();
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
cancelAutocomplete() {
|
|
1111
|
+
this.isAutocompleting = false;
|
|
1112
|
+
this.autocompleteList = undefined;
|
|
1113
|
+
this.autocompletePrefix = "";
|
|
1114
|
+
if (this.autocompleteDebounceTimer) {
|
|
1115
|
+
clearTimeout(this.autocompleteDebounceTimer);
|
|
1116
|
+
this.autocompleteDebounceTimer = undefined;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
isShowingAutocomplete() {
|
|
1120
|
+
return this.isAutocompleting;
|
|
1121
|
+
}
|
|
1122
|
+
async updateAutocomplete() {
|
|
1123
|
+
if (!this.isAutocompleting || !this.autocompleteProvider)
|
|
1124
|
+
return;
|
|
1125
|
+
// Check if the current text still matches our autocomplete context
|
|
1126
|
+
// This prevents unnecessary updates when typing unrelated text
|
|
1127
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1128
|
+
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1129
|
+
// If we're no longer in the context that triggered autocomplete, cancel it
|
|
1130
|
+
// For slash commands, check if we're still in slash command context
|
|
1131
|
+
// For file paths, check if we're still in the same path context
|
|
1132
|
+
if (textBeforeCursor.startsWith("/")) {
|
|
1133
|
+
// For slash commands, we should continue autocomplete as long as we're in slash command context
|
|
1134
|
+
// Don't cancel based on prefix matching for progressive typing
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
// For file paths, check if we're still in the same path context
|
|
1138
|
+
if (!textBeforeCursor.endsWith(this.autocompletePrefix)) {
|
|
1139
|
+
this.cancelAutocomplete();
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
// Clear any existing debounce timer
|
|
1144
|
+
if (this.autocompleteDebounceTimer) {
|
|
1145
|
+
clearTimeout(this.autocompleteDebounceTimer);
|
|
1146
|
+
}
|
|
1147
|
+
// Debounce autocomplete updates to prevent rapid-fire file system operations
|
|
1148
|
+
this.autocompleteDebounceTimer = setTimeout(async () => {
|
|
1149
|
+
const suggestions = await this.autocompleteProvider?.getSuggestions(this.state.lines, this.state.cursorLine, this.state.cursorCol);
|
|
1150
|
+
if (suggestions && suggestions.items.length > 0) {
|
|
1151
|
+
this.autocompletePrefix = suggestions.prefix;
|
|
1152
|
+
if (this.autocompleteList) {
|
|
1153
|
+
// Update the existing list with new items
|
|
1154
|
+
this.autocompleteList.updateItems(suggestions.items);
|
|
1155
|
+
}
|
|
1156
|
+
else {
|
|
1157
|
+
this.autocompleteList = new SelectList(suggestions.items, 5, this.theme.selectList);
|
|
1158
|
+
}
|
|
1159
|
+
this.isAutocompleting = true;
|
|
1160
|
+
// Request re-render to show updated autocomplete list
|
|
1161
|
+
this.onRenderRequested?.();
|
|
1162
|
+
}
|
|
1163
|
+
else {
|
|
1164
|
+
// No more matches, cancel autocomplete
|
|
1165
|
+
this.cancelAutocomplete();
|
|
1166
|
+
// Request re-render to hide autocomplete
|
|
1167
|
+
this.onRenderRequested?.();
|
|
1168
|
+
}
|
|
1169
|
+
}, 50); // 50ms debounce delay
|
|
1170
|
+
}
|
|
1171
|
+
isModifiedEnter(data) {
|
|
1172
|
+
// Common modified Enter sequences across terminals
|
|
1173
|
+
const sequences = [
|
|
1174
|
+
// Shift+Enter sequences
|
|
1175
|
+
"\x1b[13;2~", // Some terminals
|
|
1176
|
+
"\x1bOM", // Some terminals
|
|
1177
|
+
"\\\r", // VS Code terminal
|
|
1178
|
+
"\x1b\r", // Option+Enter (macOS)
|
|
1179
|
+
"\x1b[27;2;13~", // xterm shift+enter
|
|
1180
|
+
"\x1b[13;2u", // libtermkey shift+enter
|
|
1181
|
+
// Ctrl+Enter sequences
|
|
1182
|
+
"\x1b[13;5~", // Some terminals
|
|
1183
|
+
"\x1b[27;5;13~", // xterm ctrl+enter
|
|
1184
|
+
"\x1b[13;5u", // libtermkey ctrl+enter
|
|
1185
|
+
];
|
|
1186
|
+
// Check for known sequences
|
|
1187
|
+
if (sequences.includes(data)) {
|
|
1188
|
+
return true;
|
|
1189
|
+
}
|
|
1190
|
+
// Check for Enter with escape sequences (general case)
|
|
1191
|
+
if (data.length > 1 &&
|
|
1192
|
+
data.includes("\x1b") &&
|
|
1193
|
+
(data.includes("\r") || data.includes("\n"))) {
|
|
1194
|
+
return true;
|
|
1195
|
+
}
|
|
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) {
|
|
1199
|
+
return true;
|
|
1200
|
+
}
|
|
1201
|
+
return false;
|
|
1202
|
+
}
|
|
1203
|
+
getCursorPosition() {
|
|
1204
|
+
// Return cursor position relative to the editor component
|
|
1205
|
+
// The editor has a top border line, then content lines, then a bottom border line
|
|
1206
|
+
// So cursor position within editor is: row = layoutLineIndex + 1
|
|
1207
|
+
const width = 80; // Use a reasonable default width for calculation
|
|
1208
|
+
const layoutLines = this.layoutText(width);
|
|
1209
|
+
// Find which layout line contains the cursor
|
|
1210
|
+
for (let i = 0; i < layoutLines.length; i++) {
|
|
1211
|
+
const layoutLine = layoutLines[i];
|
|
1212
|
+
if (layoutLine.hasCursor && layoutLine.cursorPos !== undefined) {
|
|
1213
|
+
// Add 1 to account for the top border line
|
|
1214
|
+
return [i + 1, layoutLine.cursorPos];
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
// If no cursor found, return position at start of first content line (after top border)
|
|
1218
|
+
return [1, 0];
|
|
1219
|
+
}
|
|
1220
|
+
}
|