@linxiraos/pi-tui 1.1.26 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -491
- package/README.md +13 -7
- package/dist/types/active-terminal.d.ts +30 -0
- package/dist/types/app-keybindings.d.ts +7 -2
- package/dist/types/apps/cleanse-board.d.ts +1 -1
- package/dist/types/apps/debug/log-viewer.d.ts +4 -0
- package/dist/types/apps/debug/raw-sse-buffer.d.ts +2 -0
- package/dist/types/apps/git/git-tui.d.ts +5 -4
- package/dist/types/apps/git/help.d.ts +3 -1
- package/dist/types/apps/if-bench-board.d.ts +2 -2
- package/dist/types/apps/setup-model-picker.d.ts +1 -1
- package/dist/types/autocomplete.d.ts +5 -2
- package/dist/types/charts/chart-plan.d.ts +674 -0
- package/dist/types/charts/chart-svg.d.ts +59 -0
- package/dist/types/charts/table-data.d.ts +175 -0
- package/dist/types/chat/assistant-message.d.ts +11 -1
- package/dist/types/chat/chat-transcript-builder.d.ts +3 -0
- package/dist/types/chat/compaction-summary-message.d.ts +1 -0
- package/dist/types/chat/execution-shared.d.ts +6 -5
- package/dist/types/chat/fence-figure.d.ts +42 -0
- package/dist/types/chat/figure-markdown.d.ts +42 -0
- package/dist/types/chat/messages.d.ts +1 -1
- package/dist/types/chat/svg-figure.d.ts +33 -0
- package/dist/types/chat/svg-source.d.ts +49 -0
- package/dist/types/chat/table-chart.d.ts +89 -0
- package/dist/types/chat/tool-execution.d.ts +2 -0
- package/dist/types/chat/user-message.d.ts +17 -4
- package/dist/types/chrome/shared.d.ts +9 -0
- package/dist/types/chrome/status-notice.d.ts +15 -3
- package/dist/types/chrome/transcript-container.d.ts +6 -0
- package/dist/types/components/box.d.ts +1 -0
- package/dist/types/components/disclosure.d.ts +1 -0
- package/dist/types/components/editor.d.ts +23 -5
- package/dist/types/components/form.d.ts +1 -0
- package/dist/types/components/image.d.ts +26 -44
- package/dist/types/components/input.d.ts +2 -1
- package/dist/types/components/key-value-list.d.ts +1 -0
- package/dist/types/components/layout/row.d.ts +1 -0
- package/dist/types/components/layout/stack.d.ts +1 -0
- package/dist/types/components/loader.d.ts +5 -6
- package/dist/types/components/markdown.d.ts +47 -5
- package/dist/types/components/scroll-view.d.ts +1 -0
- package/dist/types/components/section.d.ts +1 -0
- package/dist/types/components/table.d.ts +1 -0
- package/dist/types/components/text.d.ts +1 -0
- package/dist/types/components/tree-view.d.ts +7 -0
- package/dist/types/components/truncated-text.d.ts +1 -0
- package/dist/types/fuzzy.d.ts +29 -1
- package/dist/types/key-hint-format.d.ts +9 -1
- package/dist/types/keybinding-matchers.d.ts +3 -0
- package/dist/types/keybindings.d.ts +1 -1
- package/dist/types/kitty-graphics.d.ts +2 -2
- package/dist/types/loop-watchdog.d.ts +8 -7
- package/dist/types/multiplexers/cmux.d.ts +16 -0
- package/dist/types/multiplexers/herdr.d.ts +26 -0
- package/dist/types/multiplexers/notify.d.ts +13 -0
- package/dist/types/multiplexers/orca.d.ts +12 -0
- package/dist/types/multiplexers/screen.d.ts +9 -0
- package/dist/types/multiplexers/tmux.d.ts +29 -0
- package/dist/types/multiplexers/types.d.ts +100 -0
- package/dist/types/multiplexers/wmux.d.ts +12 -0
- package/dist/types/multiplexers/zellij.d.ts +12 -0
- package/dist/types/native/apply.d.ts +4 -3
- package/dist/types/native/backend.d.ts +5 -4
- package/dist/types/native/blobs.d.ts +7 -3
- package/dist/types/native/encode.d.ts +8 -6
- package/dist/types/native/node.d.ts +36 -5
- package/dist/types/native/overlay.d.ts +8 -3
- package/dist/types/native/settle.d.ts +4 -3
- package/dist/types/overlays/agent-hub-projection.d.ts +6 -5
- package/dist/types/overlays/agent-hub-types.d.ts +9 -0
- package/dist/types/overlays/agent-hub.d.ts +3 -0
- package/dist/types/overlays/agent-transcript-viewer.d.ts +12 -2
- package/dist/types/overlays/agents-hub.d.ts +9 -0
- package/dist/types/overlays/annotation-overlay.d.ts +19 -2
- package/dist/types/overlays/annotation-types.d.ts +2 -0
- package/dist/types/overlays/ask-dialog.d.ts +5 -2
- package/dist/types/overlays/btw-history-panel.d.ts +13 -1
- package/dist/types/overlays/copy-selector.d.ts +1 -0
- package/dist/types/overlays/history-search.d.ts +10 -0
- package/dist/types/overlays/hook-selector.d.ts +9 -4
- package/dist/types/overlays/hub-frame.d.ts +4 -1
- package/dist/types/overlays/jobs-panel.d.ts +1 -7
- package/dist/types/overlays/model-browser.d.ts +54 -7
- package/dist/types/overlays/model-hub.d.ts +37 -0
- package/dist/types/overlays/oauth-selector.d.ts +0 -1
- package/dist/types/overlays/plan-review-overlay.d.ts +3 -2
- package/dist/types/overlays/report-panel.d.ts +68 -0
- package/dist/types/overlays/rewind-selector.d.ts +1 -0
- package/dist/types/overlays/session-observer-registry.d.ts +2 -0
- package/dist/types/overlays/session-selector.d.ts +6 -2
- package/dist/types/overlays/settings-defs.d.ts +1 -0
- package/dist/types/overlays/thinking-selector.d.ts +2 -2
- package/dist/types/overlays/usage-dashboard.d.ts +12 -8
- package/dist/types/overlays/usage-display.d.ts +3 -1
- package/dist/types/prompt/composer-cache.d.ts +0 -16
- package/dist/types/prompt/composer.d.ts +18 -3
- package/dist/types/prompt/custom-editor.d.ts +25 -13
- package/dist/types/prompt/image-source.d.ts +2 -1
- package/dist/types/prompt/interactive-context-helpers.d.ts +5 -1
- package/dist/types/prompt/model-mention-autocomplete.d.ts +1 -1
- package/dist/types/prompt/welcome.d.ts +7 -2
- package/dist/types/render/composite.d.ts +2 -0
- package/dist/types/render/output-pane.d.ts +1 -0
- package/dist/types/render/render-utils.d.ts +1 -0
- package/dist/types/render/tool-card.d.ts +6 -0
- package/dist/types/render/width-aware-text.d.ts +2 -0
- package/dist/types/setup/scenes/types.d.ts +1 -0
- package/dist/types/space-hold.d.ts +40 -25
- package/dist/types/status-line/component.d.ts +3 -0
- package/dist/types/status-line/context-usage.d.ts +7 -5
- package/dist/types/status-line/host.d.ts +4 -1
- package/dist/types/status-line/segments.d.ts +2 -0
- package/dist/types/status-line/types.d.ts +14 -3
- package/dist/types/terminal-capabilities.d.ts +41 -36
- package/dist/types/terminal-multiplexer.d.ts +127 -14
- package/dist/types/terminal.d.ts +23 -16
- package/dist/types/theme/active-symbols.d.ts +1 -1
- package/dist/types/theme/schema.d.ts +11 -2
- package/dist/types/theme/symbols.d.ts +1 -1
- package/dist/types/theme/theme-class.d.ts +7 -0
- package/dist/types/theme/theme.d.ts +1 -1
- package/dist/types/theme/tui-adapters.d.ts +3 -0
- package/dist/types/thinking.d.ts +1 -1
- package/dist/types/tmux.d.ts +11 -4
- package/dist/types/tools/bash.d.ts +7 -2
- package/dist/types/tools/daemon.d.ts +1 -1
- package/dist/types/tools/eval.d.ts +22 -3
- package/dist/types/tools/glob.d.ts +4 -0
- package/dist/types/tools/native-view.d.ts +47 -0
- package/dist/types/tools/output-meta.d.ts +11 -1
- package/dist/types/tools/renderer.d.ts +37 -2
- package/dist/types/tools/streaming-output.d.ts +20 -1
- package/dist/types/tools/task.d.ts +2 -3
- package/dist/types/tools/write.d.ts +9 -2
- package/dist/types/tools/yield.d.ts +22 -0
- package/dist/types/tui.d.ts +36 -0
- package/dist/types/utils.d.ts +6 -0
- package/dist/types/vim.d.ts +19 -5
- package/dist/types/windows-input-mode.d.ts +33 -1
- package/package.json +13 -9
- package/src/active-terminal.d.ts +30 -0
- package/src/active-terminal.ts +80 -0
- package/src/app-keybindings.d.ts +7 -2
- package/src/app-keybindings.ts +7 -2
- package/src/apps/autoresearch-dashboard.ts +7 -3
- package/src/apps/cleanse-board.ts +2 -2
- package/src/apps/cleanse-picker.ts +1 -1
- package/src/apps/debug/log-viewer.ts +188 -72
- package/src/apps/debug/raw-sse-buffer.ts +8 -0
- package/src/apps/debug/raw-sse.ts +39 -5
- package/src/apps/git/diff-pane.ts +96 -23
- package/src/apps/git/git-tui.ts +23 -16
- package/src/apps/git/help.ts +29 -20
- package/src/apps/git/sidebar.ts +8 -3
- package/src/apps/if-bench-board.ts +2 -2
- package/src/apps/ps-top.ts +2 -2
- package/src/apps/setup-model-picker.ts +1 -1
- package/src/autocomplete.d.ts +5 -2
- package/src/autocomplete.ts +67 -43
- package/src/bracketed-paste.ts +10 -3
- package/src/charts/chart-plan.d.ts +674 -0
- package/src/charts/chart-plan.ts +5081 -0
- package/src/charts/chart-svg.d.ts +59 -0
- package/src/charts/chart-svg.ts +4657 -0
- package/src/charts/table-data.d.ts +175 -0
- package/src/charts/table-data.ts +1478 -0
- package/src/chat/advisor-message.ts +11 -0
- package/src/chat/assistant-message.d.ts +11 -1
- package/src/chat/assistant-message.ts +245 -54
- package/src/chat/chat-transcript-builder.d.ts +3 -0
- package/src/chat/chat-transcript-builder.ts +9 -3
- package/src/chat/compaction-summary-message.d.ts +1 -0
- package/src/chat/compaction-summary-message.ts +8 -0
- package/src/chat/execution-shared.d.ts +6 -5
- package/src/chat/execution-shared.ts +42 -25
- package/src/chat/fence-figure.d.ts +42 -0
- package/src/chat/fence-figure.ts +80 -0
- package/src/chat/figure-markdown.d.ts +42 -0
- package/src/chat/figure-markdown.ts +174 -0
- package/src/chat/messages.d.ts +1 -1
- package/src/chat/messages.ts +1 -1
- package/src/chat/read-tool-group.ts +6 -4
- package/src/chat/skill-message.ts +6 -0
- package/src/chat/svg-figure.d.ts +33 -0
- package/src/chat/svg-figure.ts +399 -0
- package/src/chat/svg-source.d.ts +49 -0
- package/src/chat/svg-source.ts +188 -0
- package/src/chat/table-chart.d.ts +89 -0
- package/src/chat/table-chart.ts +294 -0
- package/src/chat/tool-execution.d.ts +2 -0
- package/src/chat/tool-execution.ts +86 -7
- package/src/chat/user-message.d.ts +17 -4
- package/src/chat/user-message.ts +71 -8
- package/src/chrome/shared.ts +53 -0
- package/src/chrome/status-notice.ts +25 -7
- package/src/chrome/transcript-container.d.ts +6 -0
- package/src/chrome/transcript-container.ts +162 -19
- package/src/components/box.d.ts +1 -0
- package/src/components/box.ts +5 -0
- package/src/components/disclosure.d.ts +1 -0
- package/src/components/disclosure.ts +8 -0
- package/src/components/editor.d.ts +23 -5
- package/src/components/editor.ts +316 -47
- package/src/components/form.d.ts +1 -0
- package/src/components/form.ts +8 -0
- package/src/components/image.d.ts +26 -44
- package/src/components/image.ts +115 -152
- package/src/components/input.d.ts +2 -1
- package/src/components/input.ts +17 -7
- package/src/components/key-value-list.d.ts +1 -0
- package/src/components/key-value-list.ts +4 -0
- package/src/components/layout/row.d.ts +1 -0
- package/src/components/layout/row.ts +7 -0
- package/src/components/layout/stack.d.ts +1 -0
- package/src/components/layout/stack.ts +7 -0
- package/src/components/loader.d.ts +5 -6
- package/src/components/loader.ts +10 -17
- package/src/components/markdown.d.ts +47 -5
- package/src/components/markdown.ts +786 -303
- package/src/components/scroll-view.d.ts +1 -0
- package/src/components/scroll-view.ts +5 -0
- package/src/components/section.d.ts +1 -0
- package/src/components/section.ts +5 -0
- package/src/components/settings-list.ts +2 -0
- package/src/components/table.d.ts +1 -0
- package/src/components/table.ts +4 -0
- package/src/components/text.d.ts +1 -0
- package/src/components/text.ts +7 -0
- package/src/components/tree-view.d.ts +7 -0
- package/src/components/tree-view.ts +38 -8
- package/src/components/truncated-text.d.ts +1 -0
- package/src/components/truncated-text.ts +5 -0
- package/src/desktop-notify.ts +3 -2
- package/src/fuzzy.d.ts +29 -1
- package/src/fuzzy.ts +115 -30
- package/src/hotkeys-markdown.ts +9 -2
- package/src/key-hint-format.d.ts +9 -1
- package/src/key-hint-format.ts +26 -11
- package/src/keybinding-matchers.d.ts +3 -0
- package/src/keybinding-matchers.ts +6 -2
- package/src/keybindings.d.ts +1 -1
- package/src/keybindings.ts +1 -1
- package/src/kitty-graphics.d.ts +2 -2
- package/src/kitty-graphics.ts +11 -5
- package/src/loop-watchdog.d.ts +8 -7
- package/src/loop-watchdog.ts +18 -13
- package/src/multiplexers/cmux.d.ts +16 -0
- package/src/multiplexers/cmux.ts +29 -0
- package/src/multiplexers/herdr.d.ts +26 -0
- package/src/multiplexers/herdr.ts +64 -0
- package/src/multiplexers/notify.d.ts +13 -0
- package/src/multiplexers/notify.ts +24 -0
- package/src/multiplexers/orca.d.ts +12 -0
- package/src/multiplexers/orca.ts +16 -0
- package/src/multiplexers/screen.d.ts +9 -0
- package/src/multiplexers/screen.ts +15 -0
- package/src/multiplexers/tmux.d.ts +29 -0
- package/src/multiplexers/tmux.ts +40 -0
- package/src/multiplexers/types.d.ts +100 -0
- package/src/multiplexers/types.ts +110 -0
- package/src/multiplexers/wmux.d.ts +12 -0
- package/src/multiplexers/wmux.ts +16 -0
- package/src/multiplexers/zellij.d.ts +12 -0
- package/src/multiplexers/zellij.ts +24 -0
- package/src/native/apply.d.ts +4 -3
- package/src/native/apply.ts +4 -3
- package/src/native/backend.d.ts +5 -4
- package/src/native/backend.ts +199 -22
- package/src/native/blobs.d.ts +7 -3
- package/src/native/blobs.ts +47 -12
- package/src/native/encode.d.ts +8 -6
- package/src/native/encode.ts +76 -34
- package/src/native/node.d.ts +36 -5
- package/src/native/node.ts +31 -5
- package/src/native/overlay.d.ts +8 -3
- package/src/native/overlay.ts +17 -4
- package/src/native/reconcile.ts +16 -4
- package/src/native/settle.d.ts +4 -3
- package/src/native/settle.ts +4 -3
- package/src/overlays/agent-hub-projection.d.ts +6 -5
- package/src/overlays/agent-hub-projection.ts +124 -13
- package/src/overlays/agent-hub-types.d.ts +9 -0
- package/src/overlays/agent-hub-types.ts +6 -0
- package/src/overlays/agent-hub.ts +48 -29
- package/src/overlays/agent-transcript-viewer.ts +93 -23
- package/src/overlays/agents-hub.ts +28 -8
- package/src/overlays/annotation-overlay.ts +293 -90
- package/src/overlays/annotation-types.ts +2 -0
- package/src/overlays/ask-dialog.ts +10 -17
- package/src/overlays/btw-history-panel.ts +136 -39
- package/src/overlays/copy-selector.d.ts +1 -0
- package/src/overlays/copy-selector.ts +32 -8
- package/src/overlays/extensions/extension-dashboard.ts +4 -4
- package/src/overlays/extensions/state-manager.ts +52 -12
- package/src/overlays/history-search.ts +100 -7
- package/src/overlays/hook-selector.ts +12 -5
- package/src/overlays/hub-frame.ts +19 -1
- package/src/overlays/jobs-panel.ts +33 -52
- package/src/overlays/model-browser.ts +367 -122
- package/src/overlays/model-hub.ts +435 -59
- package/src/overlays/move-overlay.ts +28 -14
- package/src/overlays/oauth-selector.ts +54 -70
- package/src/overlays/pause-screen.ts +2 -2
- package/src/overlays/plan-review-overlay.ts +208 -70
- package/src/overlays/plugin-settings.ts +3 -3
- package/src/overlays/report-panel.ts +191 -0
- package/src/overlays/rewind-selector.ts +88 -17
- package/src/overlays/session-observer-registry.d.ts +2 -0
- package/src/overlays/session-observer-registry.ts +10 -2
- package/src/overlays/session-selector.ts +64 -26
- package/src/overlays/settings-defs.ts +5 -4
- package/src/overlays/settings-selector.ts +33 -8
- package/src/overlays/snapcompact-shape-preview.ts +2 -3
- package/src/overlays/stats-notice.ts +1 -1
- package/src/overlays/thinking-selector.ts +11 -10
- package/src/overlays/tree-selector.ts +41 -20
- package/src/overlays/usage-dashboard.ts +117 -58
- package/src/overlays/usage-display.ts +10 -4
- package/src/overlays/usage-row.ts +22 -8
- package/src/prompt/attachment-chips.ts +15 -11
- package/src/prompt/composer-attachments.ts +6 -4
- package/src/prompt/composer-cache.ts +7 -86
- package/src/prompt/composer.ts +37 -26
- package/src/prompt/custom-editor.ts +282 -105
- package/src/prompt/image-source.ts +2 -1
- package/src/prompt/interactive-context-helpers.ts +32 -18
- package/src/prompt/macos-spelling.ts +7 -0
- package/src/prompt/magic-keywords.ts +11 -2
- package/src/prompt/model-mention-autocomplete.ts +14 -14
- package/src/prompt/queued-messages.ts +6 -4
- package/src/prompt/tips.txt +13 -9
- package/src/prompt/welcome.ts +64 -46
- package/src/prompt/word-completion.ts +38 -5
- package/src/render/composite.d.ts +2 -0
- package/src/render/composite.ts +64 -0
- package/src/render/output-pane.d.ts +1 -0
- package/src/render/output-pane.ts +6 -0
- package/src/render/render-utils.d.ts +1 -0
- package/src/render/render-utils.ts +16 -1
- package/src/render/tool-card.d.ts +6 -0
- package/src/render/tool-card.ts +15 -0
- package/src/render/width-aware-text.d.ts +2 -0
- package/src/render/width-aware-text.ts +5 -0
- package/src/setup/scenes/model.ts +2 -1
- package/src/setup/scenes/types.ts +1 -0
- package/src/space-hold.d.ts +40 -25
- package/src/space-hold.ts +156 -52
- package/src/status-line/component.ts +292 -78
- package/src/status-line/context-usage.ts +21 -46
- package/src/status-line/host.ts +4 -1
- package/src/status-line/segments.ts +40 -22
- package/src/status-line/types.ts +15 -3
- package/src/terminal-capabilities.d.ts +41 -36
- package/src/terminal-capabilities.ts +137 -213
- package/src/terminal-multiplexer.d.ts +127 -14
- package/src/terminal-multiplexer.ts +112 -33
- package/src/terminal.d.ts +23 -16
- package/src/terminal.ts +203 -99
- package/src/theme/active-symbols.d.ts +1 -1
- package/src/theme/active-symbols.ts +1 -1
- package/src/theme/schema-validation.ts +1 -0
- package/src/theme/schema.d.ts +11 -2
- package/src/theme/schema.ts +14 -1
- package/src/theme/session-color.ts +1 -7
- package/src/theme/symbols.d.ts +1 -1
- package/src/theme/symbols.ts +11 -4
- package/src/theme/theme-class.d.ts +7 -0
- package/src/theme/theme-class.ts +26 -1
- package/src/theme/theme-schema.json +5 -1
- package/src/theme/theme.d.ts +1 -1
- package/src/theme/theme.ts +8 -4
- package/src/theme/tui-adapters.d.ts +3 -0
- package/src/theme/tui-adapters.ts +68 -0
- package/src/thinking.d.ts +1 -1
- package/src/thinking.ts +1 -1
- package/src/tmux.d.ts +11 -4
- package/src/tmux.ts +47 -11
- package/src/tools/ast-edit.ts +59 -35
- package/src/tools/ast-grep.ts +35 -16
- package/src/tools/bash.d.ts +7 -2
- package/src/tools/bash.ts +116 -47
- package/src/tools/daemon.d.ts +1 -1
- package/src/tools/daemon.ts +1 -1
- package/src/tools/default-renderer.ts +35 -19
- package/src/tools/edit.ts +142 -120
- package/src/tools/eval-format/index.ts +28 -2
- package/src/tools/eval-format/javascript.ts +7 -5
- package/src/tools/eval.d.ts +22 -3
- package/src/tools/eval.ts +364 -208
- package/src/tools/fetch.ts +5 -6
- package/src/tools/glob.d.ts +4 -0
- package/src/tools/glob.ts +33 -65
- package/src/tools/grep.ts +81 -48
- package/src/tools/index.ts +2 -0
- package/src/tools/lsp.ts +81 -59
- package/src/tools/mcp.ts +111 -68
- package/src/tools/native-view.d.ts +47 -0
- package/src/tools/native-view.ts +102 -1
- package/src/tools/output-meta.d.ts +11 -1
- package/src/tools/output-meta.ts +23 -3
- package/src/tools/read.ts +7 -5
- package/src/tools/renderer.d.ts +37 -2
- package/src/tools/renderer.ts +42 -2
- package/src/tools/streaming-output.d.ts +20 -1
- package/src/tools/streaming-output.ts +154 -50
- package/src/tools/task.d.ts +2 -3
- package/src/tools/task.ts +115 -62
- package/src/tools/todo.ts +119 -30
- package/src/tools/wait.ts +34 -24
- package/src/tools/web-search.ts +1 -1
- package/src/tools/write.d.ts +9 -2
- package/src/tools/write.ts +147 -95
- package/src/tools/yield.d.ts +22 -0
- package/src/tools/yield.ts +225 -0
- package/src/tui.d.ts +36 -0
- package/src/tui.ts +666 -217
- package/src/utils.d.ts +6 -0
- package/src/utils.ts +19 -0
- package/src/vim.d.ts +19 -5
- package/src/vim.ts +333 -18
- package/src/windows-input-mode.d.ts +33 -1
- package/src/windows-input-mode.ts +212 -6
package/src/components/editor.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { BracketedPasteHandler, decodeReencodedPasteControls } from "../brackete
|
|
|
14
14
|
import { canonicalKeyId, getKeybindings, type KeybindingsManager } from "../keybindings";
|
|
15
15
|
import { extractPrintableText, matchesKey, parseKey } from "../keys";
|
|
16
16
|
import { KillRing } from "../kill-ring";
|
|
17
|
-
import type { TspEditorDecoration, TspEditorProps, TspTone } from "@linxiraos/pi-wire";
|
|
17
|
+
import type { TspEditorDecoration, TspEditorProps, TspText, TspTone } from "@linxiraos/pi-wire";
|
|
18
18
|
import { col, node } from "../native/describe";
|
|
19
19
|
import { sameItems, sameProps } from "../native/memo";
|
|
20
20
|
import { plainText } from "../native/spans";
|
|
@@ -74,12 +74,16 @@ export interface NativeEditorLayout {
|
|
|
74
74
|
readonly caret: string;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Vim mode as the TSP `editor.mode` label. Insert carries none: Insert edits like a plain field,
|
|
79
|
+
* and a host such as Tern stops native editing (selection, ⌘A/⌘C/⌘X) for any labelled mode.
|
|
80
|
+
*/
|
|
81
|
+
const VIM_MODE_LABELS: Record<VimMode, string | undefined> = {
|
|
82
|
+
insert: undefined,
|
|
80
83
|
normal: "NORMAL",
|
|
81
84
|
visual: "VISUAL",
|
|
82
85
|
"visual-line": "VISUAL",
|
|
86
|
+
replace: "REPLACE",
|
|
83
87
|
};
|
|
84
88
|
|
|
85
89
|
const PASSTHROUGH_COLOR = (text: string): string => text;
|
|
@@ -625,8 +629,11 @@ export class Editor implements Component, Focusable {
|
|
|
625
629
|
* dialog's text field; only the prompt composer claims `zeta.editor`).
|
|
626
630
|
*/
|
|
627
631
|
describeLayout: ((input: NativeNode, cx: DescribeContext) => NativeEditorLayout) | undefined;
|
|
628
|
-
/**
|
|
629
|
-
|
|
632
|
+
/**
|
|
633
|
+
* TSP placeholder for the empty buffer; natively it replaces the rotating ANSI {@link placeholder} hints.
|
|
634
|
+
* Return a stable value (the same spans array) while it is unchanged: the editor node is reused by identity.
|
|
635
|
+
*/
|
|
636
|
+
describePlaceholder: (() => TspText) | undefined;
|
|
630
637
|
#promptGutter: string | undefined;
|
|
631
638
|
/** Bumped by {@link invalidate}: host-side decoration inputs (spelling results, settings) changed. */
|
|
632
639
|
#nativeGeneration = 0;
|
|
@@ -666,7 +673,9 @@ export class Editor implements Component, Focusable {
|
|
|
666
673
|
// Emacs-style kill ring
|
|
667
674
|
#killRing = new KillRing();
|
|
668
675
|
/** Previous edit, for kill/yank chaining, undo coalescing, and the provisional space after a Tab word accept. */
|
|
669
|
-
#lastAction: "kill" | "yank" | "type-word" | "accept-word" | null = null;
|
|
676
|
+
#lastAction: "kill" | "yank" | "type-word" | "accept-word" | "replace" | null = null;
|
|
677
|
+
/** Graphemes overwritten in the current `R` session, so Backspace can restore them. */
|
|
678
|
+
#replaceLog: { line: number; col: number; removed: string; text: string }[] = [];
|
|
670
679
|
|
|
671
680
|
// Character jump mode
|
|
672
681
|
#jumpMode: "forward" | "backward" | null = null;
|
|
@@ -713,6 +722,7 @@ export class Editor implements Component, Focusable {
|
|
|
713
722
|
|
|
714
723
|
// Host-registered atomic chip tokens: exact buffer label → expansion emitted on submit.
|
|
715
724
|
#atoms: Map<string, string> = new Map();
|
|
725
|
+
#atomsRevision = 0;
|
|
716
726
|
|
|
717
727
|
/** Optional pattern matching atomic placeholder tokens (e.g. `[Image #1, 800x600]` or
|
|
718
728
|
* `[Paste #2, +30 lines]`) that the editor treats as indivisible: a backspace or forward-delete
|
|
@@ -737,9 +747,6 @@ export class Editor implements Component, Focusable {
|
|
|
737
747
|
#undoStack: EditorState[] = [];
|
|
738
748
|
#suspendUndo = false;
|
|
739
749
|
|
|
740
|
-
// Debounce timer for autocomplete updates
|
|
741
|
-
#autocompleteTimeout?: NodeJS.Timeout;
|
|
742
|
-
|
|
743
750
|
onSubmit?: (text: string) => void | Promise<void>;
|
|
744
751
|
onAltEnter?: (text: string) => void;
|
|
745
752
|
onChange?: (text: string) => void;
|
|
@@ -754,11 +761,22 @@ export class Editor implements Component, Focusable {
|
|
|
754
761
|
onLargePaste?: (text: string, lineCount: number, options: PasteOptions) => boolean;
|
|
755
762
|
onAutocompleteCancel?: () => void;
|
|
756
763
|
disableSubmit: boolean = false;
|
|
764
|
+
/** Base editors accept native edits, not atomic sends. Implementations that
|
|
765
|
+
* handle `send` override this readiness check for their submission path. */
|
|
766
|
+
protected get nativeSendable(): boolean {
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
757
769
|
/** Placeholder painted right-aligned on the cursor row while the editor is empty and no
|
|
758
770
|
* autocomplete is open; hidden when it can't keep {@link PLACEHOLDER_MIN_GAP} cells from the
|
|
759
771
|
* cursor. The host styles it (ANSI allowed). Re-evaluated on every render, so hosts can derive
|
|
760
772
|
* it from live state. */
|
|
761
773
|
placeholder?: () => string | undefined;
|
|
774
|
+
/** Whole-message prediction for the next prompt, painted as ghost text after the cursor while
|
|
775
|
+
* the single-line buffer is a strict prefix of it, the cursor sits at its end, and no
|
|
776
|
+
* autocomplete is open. A shown prediction replaces the {@link placeholder}; Tab or Right inserts
|
|
777
|
+
* the rest without submitting. Must be a single line. Re-evaluated on every render, so hosts can
|
|
778
|
+
* derive it from live state. */
|
|
779
|
+
prediction?: () => string | undefined;
|
|
762
780
|
|
|
763
781
|
// Custom top border (for status line integration). Either an eager `content`
|
|
764
782
|
// (set once, reused every frame) or a `provider` that recomputes lazily just
|
|
@@ -1023,6 +1041,7 @@ export class Editor implements Component, Focusable {
|
|
|
1023
1041
|
this.#pastes.clear();
|
|
1024
1042
|
this.#pasteCounter = 0;
|
|
1025
1043
|
this.#atoms.clear();
|
|
1044
|
+
this.#atomsRevision++;
|
|
1026
1045
|
this.#historyDraftActive = false;
|
|
1027
1046
|
}
|
|
1028
1047
|
|
|
@@ -1062,6 +1081,7 @@ export class Editor implements Component, Focusable {
|
|
|
1062
1081
|
if (entry?.draft || this.#historyDraftActive) {
|
|
1063
1082
|
this.#pastes = new Map(entry?.draft?.pastes);
|
|
1064
1083
|
this.#atoms = new Map(entry?.draft?.atoms);
|
|
1084
|
+
this.#atomsRevision++;
|
|
1065
1085
|
this.#pasteCounter = entry?.draft?.pasteCounter ?? 0;
|
|
1066
1086
|
this.restoreHistoryState(entry?.draft?.restore);
|
|
1067
1087
|
this.#historyDraftActive = entry?.draft !== undefined;
|
|
@@ -1664,7 +1684,10 @@ export class Editor implements Component, Focusable {
|
|
|
1664
1684
|
|
|
1665
1685
|
const placeholder = this.#getPlaceholder();
|
|
1666
1686
|
const nativePlaceholder =
|
|
1667
|
-
this.describePlaceholder !== undefined &&
|
|
1687
|
+
this.describePlaceholder !== undefined &&
|
|
1688
|
+
!this.#autocompleteState &&
|
|
1689
|
+
this.#isEditorEmpty() &&
|
|
1690
|
+
!this.#getPredictionSuffix()
|
|
1668
1691
|
? this.describePlaceholder()
|
|
1669
1692
|
: placeholder && plainText(placeholder).trim();
|
|
1670
1693
|
const atLineEnd = cursorCol >= (lines[cursorLine]?.length ?? 0);
|
|
@@ -1687,6 +1710,7 @@ export class Editor implements Component, Focusable {
|
|
|
1687
1710
|
const props: TspEditorProps = {
|
|
1688
1711
|
text,
|
|
1689
1712
|
cursor,
|
|
1713
|
+
sendable: this.nativeSendable,
|
|
1690
1714
|
anchor: anchor ?? undefined,
|
|
1691
1715
|
decor: decor.decor,
|
|
1692
1716
|
ghost: ghost ? plainText(ghost) : undefined,
|
|
@@ -1906,7 +1930,7 @@ export class Editor implements Component, Focusable {
|
|
|
1906
1930
|
if (kb.matchesCanonical(canonical, "tui.input.tab") || rightArrowAccepts) {
|
|
1907
1931
|
const selected = this.#autocompleteList.getSelectedItem();
|
|
1908
1932
|
// Check for stale autocomplete state due to buffer edits since last refresh
|
|
1909
|
-
// (destructive keys or paste can outrun the
|
|
1933
|
+
// (destructive keys or paste can outrun the async refresh).
|
|
1910
1934
|
const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
|
|
1911
1935
|
const currentTextBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
1912
1936
|
if (!this.#autocompletePrefixMatchesCursorText(currentTextBeforeCursor, selected)) {
|
|
@@ -1916,7 +1940,7 @@ export class Editor implements Component, Focusable {
|
|
|
1916
1940
|
}
|
|
1917
1941
|
if (!selected) {
|
|
1918
1942
|
// An `@` popup whose narrowing filter matched nothing stays open with no
|
|
1919
|
-
// candidate (see #
|
|
1943
|
+
// candidate (see #refreshAutocomplete). Nothing to accept: cancel the
|
|
1920
1944
|
// popup and fall through so Tab keeps its normal completion role and a right
|
|
1921
1945
|
// arrow at end of line moves the cursor.
|
|
1922
1946
|
this.#cancelAutocomplete();
|
|
@@ -1938,7 +1962,7 @@ export class Editor implements Component, Focusable {
|
|
|
1938
1962
|
!this.#selectedCompletionIsSkillNamespace()
|
|
1939
1963
|
) {
|
|
1940
1964
|
const selected = this.#autocompleteList.getSelectedItem();
|
|
1941
|
-
// Check for stale autocomplete state
|
|
1965
|
+
// Check for stale autocomplete state while an async refresh is pending
|
|
1942
1966
|
const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
|
|
1943
1967
|
const currentTextBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
1944
1968
|
if (!this.#autocompletePrefixMatchesCursorText(currentTextBeforeCursor, selected)) {
|
|
@@ -2115,15 +2139,18 @@ export class Editor implements Component, Focusable {
|
|
|
2115
2139
|
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
2116
2140
|
if (
|
|
2117
2141
|
findLeadingSlashCommandStart(textBeforeCursor) !== null &&
|
|
2142
|
+
textBeforeCursor.trim() !== "/" &&
|
|
2118
2143
|
this.#isInSubmittedSlashCommandContext() &&
|
|
2119
2144
|
this.#autocompleteProvider?.trySyncSlashCompletion
|
|
2120
2145
|
) {
|
|
2121
2146
|
const syncResult = this.#autocompleteProvider.trySyncSlashCompletion(textBeforeCursor);
|
|
2122
|
-
|
|
2147
|
+
// The collapsed `/skill:` namespace row is not a runnable command; Enter submits
|
|
2148
|
+
// immediately after applying, so only a real command may be completed here.
|
|
2149
|
+
const selected = syncResult?.items.find(item => item.value !== SKILL_NAMESPACE);
|
|
2150
|
+
if (syncResult && selected) {
|
|
2123
2151
|
// Invalidate any pending async autocomplete so its stale results are discarded
|
|
2124
2152
|
this.#autocompleteRequestId += 1;
|
|
2125
2153
|
// Apply the best match and submit the completed command
|
|
2126
|
-
const selected = syncResult.items[0]!;
|
|
2127
2154
|
const result = this.#autocompleteProvider.applyCompletion(
|
|
2128
2155
|
this.#state.lines,
|
|
2129
2156
|
this.#state.cursorLine,
|
|
@@ -2198,8 +2225,8 @@ export class Editor implements Component, Focusable {
|
|
|
2198
2225
|
this.#moveCursor(1, 0); // Cursor movement (within text or history entry)
|
|
2199
2226
|
}
|
|
2200
2227
|
} else if (kb.matchesCanonical(canonical, "tui.editor.cursorRight")) {
|
|
2201
|
-
// Right walks over the ghost word completion as if it were typed: no trailing space.
|
|
2202
|
-
if (this.#acceptWordCompletion({ space: false })) return;
|
|
2228
|
+
// Right walks over the ghost prediction or word completion as if it were typed: no trailing space.
|
|
2229
|
+
if (this.#acceptPrediction() || this.#acceptWordCompletion({ space: false })) return;
|
|
2203
2230
|
this.#moveCursor(0, 1);
|
|
2204
2231
|
} else if (kb.matchesCanonical(canonical, "tui.editor.cursorLeft")) {
|
|
2205
2232
|
// Left
|
|
@@ -2242,18 +2269,43 @@ export class Editor implements Component, Focusable {
|
|
|
2242
2269
|
return this.isShowingAutocomplete() ? false : this.#runVimKey("escape", vim);
|
|
2243
2270
|
}
|
|
2244
2271
|
if (vim.mode === "insert") return false;
|
|
2272
|
+
if (vim.mode === "replace") {
|
|
2273
|
+
if (canonical === "backspace" || matchesKey(data, "backspace") || matchesKey(data, "shift+backspace")) {
|
|
2274
|
+
this.#replaceBackspace();
|
|
2275
|
+
return true;
|
|
2276
|
+
}
|
|
2277
|
+
const replacing = extractPrintableText(data);
|
|
2278
|
+
if (replacing) {
|
|
2279
|
+
for (const seg of segmenter.segment(replacing)) this.#overwriteReplaceGrapheme(seg.segment);
|
|
2280
|
+
return true;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2245
2283
|
|
|
2284
|
+
// `f`/`t`/`r` are waiting for a character. Space is that character; arrows and other named
|
|
2285
|
+
// keys cancel, instead of being rewritten to `h`/`l` and inserted or searched for.
|
|
2286
|
+
const awaitingTarget = /[fFtTr]$/.test(vim.pendingText);
|
|
2287
|
+
if (awaitingTarget && (canonical === "space" || data === " ")) return this.#runVimKey(" ", vim);
|
|
2246
2288
|
const mapped = canonical === undefined ? undefined : VIM_NAV_KEYS[canonical];
|
|
2289
|
+
if (awaitingTarget && mapped !== undefined) return this.#runVimKey("escape", vim);
|
|
2247
2290
|
if (mapped !== undefined) return this.#runVimKey(mapped, vim);
|
|
2248
2291
|
|
|
2249
|
-
// Control chords carry no printable text and stay with the host.
|
|
2292
|
+
// Control chords, Enter, and Tab carry no printable text and stay with the host. A half-typed
|
|
2293
|
+
// command (`r`, `f`, `d`) is cancelled first, like character-jump mode, so it cannot outlive a
|
|
2294
|
+
// submit and swallow the first key of the next prompt.
|
|
2250
2295
|
const printable = extractPrintableText(data);
|
|
2251
|
-
if (!printable)
|
|
2296
|
+
if (!printable) {
|
|
2297
|
+
if (vim.pending) this.#runVimKey("escape", vim);
|
|
2298
|
+
return false;
|
|
2299
|
+
}
|
|
2252
2300
|
|
|
2253
2301
|
// Batched stdin can deliver several keystrokes at once, so replay the run one grapheme at a
|
|
2254
|
-
// time. A command that drops out of Normal mode part-way (`iabc`) turns the rest of
|
|
2255
|
-
//
|
|
2302
|
+
// time. A command that drops out of Normal mode part-way (`iabc`, `Rxx`) turns the rest of
|
|
2303
|
+
// the run into literal text or replace-mode overwrites rather than more Normal commands.
|
|
2256
2304
|
for (const seg of segmenter.segment(printable)) {
|
|
2305
|
+
if (vim.mode === "replace") {
|
|
2306
|
+
this.#overwriteReplaceGrapheme(seg.segment);
|
|
2307
|
+
continue;
|
|
2308
|
+
}
|
|
2257
2309
|
if (this.#runVimKey(seg.segment, vim)) continue;
|
|
2258
2310
|
this.#insertCharacter(printable.slice(seg.index));
|
|
2259
2311
|
return true;
|
|
@@ -2274,11 +2326,22 @@ export class Editor implements Component, Focusable {
|
|
|
2274
2326
|
if (vim.mode !== before || vim.pendingText !== pendingBefore || this.vimSelectedLines !== selectedLinesBefore) {
|
|
2275
2327
|
this.onVimModeChange?.(vim.mode);
|
|
2276
2328
|
}
|
|
2329
|
+
if (before === "replace" && vim.mode !== "replace") this.#replaceLog.length = 0;
|
|
2330
|
+
if (vim.mode === "replace" && before !== "replace") this.#replaceLog.length = 0;
|
|
2277
2331
|
return true;
|
|
2278
2332
|
}
|
|
2279
2333
|
|
|
2280
2334
|
#applyVimCommands(commands: readonly VimCommand[]): void {
|
|
2281
2335
|
for (const command of commands) {
|
|
2336
|
+
// A delete or other edit in replace mode invalidates Backspace restores. Motions do not.
|
|
2337
|
+
if (
|
|
2338
|
+
this.#vim?.mode === "replace" &&
|
|
2339
|
+
command.kind !== "move" &&
|
|
2340
|
+
command.kind !== "mode" &&
|
|
2341
|
+
command.kind !== "yank"
|
|
2342
|
+
) {
|
|
2343
|
+
this.#replaceLog.length = 0;
|
|
2344
|
+
}
|
|
2282
2345
|
switch (command.kind) {
|
|
2283
2346
|
case "move":
|
|
2284
2347
|
this.#moveVimCursor(command.to);
|
|
@@ -2310,12 +2373,133 @@ export class Editor implements Component, Focusable {
|
|
|
2310
2373
|
case "undo":
|
|
2311
2374
|
this.#applyUndo();
|
|
2312
2375
|
break;
|
|
2376
|
+
case "replace":
|
|
2377
|
+
this.#replaceVimSpan(command.from, command.to, command.text);
|
|
2378
|
+
break;
|
|
2379
|
+
case "join":
|
|
2380
|
+
this.#joinVimLines(command.fromLine, command.lines);
|
|
2381
|
+
break;
|
|
2382
|
+
case "indent":
|
|
2383
|
+
this.#indentVimLines(command.fromLine, command.toLine, command.out);
|
|
2384
|
+
break;
|
|
2313
2385
|
}
|
|
2314
2386
|
}
|
|
2315
2387
|
this.#clampVimCursor();
|
|
2316
2388
|
this.invalidate();
|
|
2317
2389
|
}
|
|
2318
2390
|
|
|
2391
|
+
#replaceVimSpan(from: VimPosition, to: VimPosition, text: string): void {
|
|
2392
|
+
const line = this.#state.lines[from.line] ?? "";
|
|
2393
|
+
if (this.#spanCutsAtomicToken(line, from.col, to.col)) return;
|
|
2394
|
+
this.#recordUndoState();
|
|
2395
|
+
this.#lastAction = null;
|
|
2396
|
+
this.#state.lines[from.line] = line.slice(0, from.col) + text + line.slice(to.col);
|
|
2397
|
+
this.#state.cursorLine = from.line;
|
|
2398
|
+
let last = from.col;
|
|
2399
|
+
for (const seg of segmenter.segment(text)) last = from.col + seg.index;
|
|
2400
|
+
this.#setCursorCol(last);
|
|
2401
|
+
this.#afterVimEdit();
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
#overwriteReplaceGrapheme(grapheme: string): void {
|
|
2405
|
+
const lineIdx = this.#state.cursorLine;
|
|
2406
|
+
const line = this.#state.lines[lineIdx] ?? "";
|
|
2407
|
+
const col = this.#state.cursorCol;
|
|
2408
|
+
if (this.#atomicTokenAt(line, col)) return;
|
|
2409
|
+
// Any other edit since the last overwrite (setText, `x`, paste) resets `#lastAction`, which
|
|
2410
|
+
// makes the logged columns meaningless; start a fresh undo step and a fresh log.
|
|
2411
|
+
if (this.#lastAction !== "replace") {
|
|
2412
|
+
this.#recordUndoState();
|
|
2413
|
+
this.#replaceLog.length = 0;
|
|
2414
|
+
}
|
|
2415
|
+
this.#lastAction = "replace";
|
|
2416
|
+
// Editing a recalled prompt makes it a draft, so Up/Down at an edge move the cursor again
|
|
2417
|
+
// instead of navigating history over the overwritten text (same as `#afterVimEdit`).
|
|
2418
|
+
this.#historyIndex = -1;
|
|
2419
|
+
const end = col >= line.length ? col : nextGraphemeStart(line, col);
|
|
2420
|
+
const removed = line.slice(col, end);
|
|
2421
|
+
this.#state.lines[lineIdx] = line.slice(0, col) + grapheme + line.slice(end);
|
|
2422
|
+
this.#replaceLog.push({ line: lineIdx, col, removed, text: grapheme });
|
|
2423
|
+
this.#setCursorCol(col + grapheme.length);
|
|
2424
|
+
this.#notifyChange();
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
#replaceBackspace(): void {
|
|
2428
|
+
if (this.#lastAction !== "replace") {
|
|
2429
|
+
this.#replaceLog.length = 0;
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
const entry = this.#replaceLog.pop();
|
|
2433
|
+
if (!entry) return;
|
|
2434
|
+
const line = this.#state.lines[entry.line] ?? "";
|
|
2435
|
+
const stillThere = line.slice(entry.col, entry.col + entry.text.length) === entry.text;
|
|
2436
|
+
const atCursor = this.#state.cursorLine === entry.line && this.#state.cursorCol === entry.col + entry.text.length;
|
|
2437
|
+
if (!stillThere) return;
|
|
2438
|
+
if (!atCursor) {
|
|
2439
|
+
this.#replaceLog.push(entry);
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
this.#state.lines[entry.line] =
|
|
2443
|
+
line.slice(0, entry.col) + entry.removed + line.slice(entry.col + entry.text.length);
|
|
2444
|
+
this.#setCursorCol(entry.col);
|
|
2445
|
+
this.#notifyChange();
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
#joinVimLines(fromLine: number, span: number): void {
|
|
2449
|
+
const start = Math.max(0, Math.min(fromLine, this.#state.lines.length - 1));
|
|
2450
|
+
const last = Math.min(start + Math.max(span, 2) - 1, this.#state.lines.length - 1);
|
|
2451
|
+
if (last <= start) return;
|
|
2452
|
+
this.#recordUndoState();
|
|
2453
|
+
let text = this.#state.lines[start] ?? "";
|
|
2454
|
+
let cursor = text.length;
|
|
2455
|
+
for (let i = start + 1; i <= last; i++) {
|
|
2456
|
+
const raw = this.#state.lines[i] ?? "";
|
|
2457
|
+
let trim = /^[ \t]*/.exec(raw)?.[0].length ?? 0;
|
|
2458
|
+
const token = this.#atomicTokenAt(raw, 0);
|
|
2459
|
+
if (token && token.start < trim) trim = token.start;
|
|
2460
|
+
const next = raw.slice(trim);
|
|
2461
|
+
const at = text.length;
|
|
2462
|
+
if (i === start + 1) cursor = at;
|
|
2463
|
+
// Vim join: no extra space after whitespace, before `)`, or for an empty line; two spaces after `.!?`.
|
|
2464
|
+
const gap =
|
|
2465
|
+
text.length === 0 || next.length === 0 || /[ \t]$/.test(text) || next.startsWith(")")
|
|
2466
|
+
? ""
|
|
2467
|
+
: /[.!?]$/.test(text)
|
|
2468
|
+
? " "
|
|
2469
|
+
: " ";
|
|
2470
|
+
text += gap + next;
|
|
2471
|
+
}
|
|
2472
|
+
this.#state.lines.splice(start, last - start + 1, text);
|
|
2473
|
+
this.#state.cursorLine = start;
|
|
2474
|
+
this.#setCursorCol(cursor);
|
|
2475
|
+
this.#lastAction = null;
|
|
2476
|
+
this.#afterVimEdit();
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
#indentVimLines(fromLine: number, toLine: number, out: boolean): void {
|
|
2480
|
+
const first = Math.max(0, Math.min(fromLine, this.#state.lines.length - 1));
|
|
2481
|
+
const last = Math.max(first, Math.min(toLine, this.#state.lines.length - 1));
|
|
2482
|
+
// Two spaces, not Vim's shiftwidth of 8: a prompt draft should not jump a tab stop. A leading tab shifts by one tab so the line keeps its own indent style.
|
|
2483
|
+
// Empty lines stay empty, as in Vim, so shifting a paragraph never leaves trailing blanks.
|
|
2484
|
+
const next = this.#state.lines.slice(first, last + 1).map(line => {
|
|
2485
|
+
const text = line ?? "";
|
|
2486
|
+
if (!out) return text.length === 0 ? text : (text.startsWith("\t") ? "\t" : " ") + text;
|
|
2487
|
+
if (text.startsWith("\t")) return text.slice(1);
|
|
2488
|
+
const spaces = /^ */.exec(text)?.[0].length ?? 0;
|
|
2489
|
+
return text.slice(Math.min(2, spaces));
|
|
2490
|
+
});
|
|
2491
|
+
if (next.every((line, i) => line === (this.#state.lines[first + i] ?? ""))) return;
|
|
2492
|
+
this.#recordUndoState();
|
|
2493
|
+
for (let i = first; i <= last; i++) this.#state.lines[i] = next[i - first] ?? "";
|
|
2494
|
+
this.#state.cursorLine = first;
|
|
2495
|
+
const landed = this.#state.lines[first] ?? "";
|
|
2496
|
+
let col = 0;
|
|
2497
|
+
while (col < landed.length && /\s/.test(landed.charAt(col))) col++;
|
|
2498
|
+
this.#setCursorCol(col);
|
|
2499
|
+
this.#lastAction = null;
|
|
2500
|
+
this.#afterVimEdit();
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2319
2503
|
#moveVimCursor(to: VimPosition): void {
|
|
2320
2504
|
this.#state.cursorLine = Math.max(0, Math.min(to.line, this.#state.lines.length - 1));
|
|
2321
2505
|
const line = this.#state.lines[this.#state.cursorLine] ?? "";
|
|
@@ -2660,6 +2844,11 @@ export class Editor implements Component, Focusable {
|
|
|
2660
2844
|
return this.#textRevision;
|
|
2661
2845
|
}
|
|
2662
2846
|
|
|
2847
|
+
/** Monotonic atom-table revision for render caches: advances whenever {@link atoms} is mutated or replaced. */
|
|
2848
|
+
get atomsRevision(): number {
|
|
2849
|
+
return this.#atomsRevision;
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2663
2852
|
#notifyChange(text?: string): void {
|
|
2664
2853
|
this.#textRevision++;
|
|
2665
2854
|
this.onChange?.(text ?? this.getText());
|
|
@@ -2698,6 +2887,7 @@ export class Editor implements Component, Focusable {
|
|
|
2698
2887
|
* it — for hosts that re-collapse restored draft text via {@link setText}. */
|
|
2699
2888
|
registerAtom(label: string, expansion: string): void {
|
|
2700
2889
|
this.#atoms.set(label, expansion);
|
|
2890
|
+
this.#atomsRevision++;
|
|
2701
2891
|
}
|
|
2702
2892
|
|
|
2703
2893
|
/** Insert `label` (plus a trailing space) at the cursor and register it as an atom expanding
|
|
@@ -2734,6 +2924,7 @@ export class Editor implements Component, Focusable {
|
|
|
2734
2924
|
/** Drop every registered atom expansion (draft cleared or replaced by the host). */
|
|
2735
2925
|
clearAtoms(): void {
|
|
2736
2926
|
this.#atoms.clear();
|
|
2927
|
+
this.#atomsRevision++;
|
|
2737
2928
|
}
|
|
2738
2929
|
|
|
2739
2930
|
/**
|
|
@@ -2773,7 +2964,7 @@ export class Editor implements Component, Focusable {
|
|
|
2773
2964
|
* Mirrors the transient state the key dispatch tears down before this action so the two
|
|
2774
2965
|
* cannot diverge: a pending character jump is cancelled by any other key, and an open
|
|
2775
2966
|
* spelling-assist popup is dismissed by anything that is not one of its accept keys (its
|
|
2776
|
-
*
|
|
2967
|
+
* refresh skips assist mode, so a surviving list would hang around forever).
|
|
2777
2968
|
* While Vim owns the buffer (Normal or Visual) the operation is Vim's `x` — deleting the
|
|
2778
2969
|
* selection and returning to Normal in Visual mode, the grapheme under the cursor
|
|
2779
2970
|
* otherwise. Only Insert mode and Vim-off editors delete straight through. */
|
|
@@ -2867,9 +3058,10 @@ export class Editor implements Component, Focusable {
|
|
|
2867
3058
|
this.#insertTextAtCursor(text);
|
|
2868
3059
|
}
|
|
2869
3060
|
|
|
2870
|
-
/** Terminal-side selection edits on the `editor` node (see {@link applyHostEdit}). */
|
|
3061
|
+
/** Terminal-side selection edits and undo on the `editor` node (see {@link applyHostEdit}). */
|
|
2871
3062
|
handleNativeEvent(event: NativeUiEvent): void {
|
|
2872
3063
|
if (event.type === "edit") this.applyHostEdit(event);
|
|
3064
|
+
else if (event.type === "undo") this.#applyUndo();
|
|
2873
3065
|
}
|
|
2874
3066
|
|
|
2875
3067
|
/**
|
|
@@ -3040,6 +3232,20 @@ export class Editor implements Component, Focusable {
|
|
|
3040
3232
|
return true;
|
|
3041
3233
|
}
|
|
3042
3234
|
|
|
3235
|
+
/** Whether a character-jump hotkey is waiting for its target character. */
|
|
3236
|
+
get isJumpPending(): boolean {
|
|
3237
|
+
return this.#jumpMode !== null;
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
/** Type literal text through the character-editing pipeline without keybinding dispatch. */
|
|
3241
|
+
typeCharacter(text: string): void {
|
|
3242
|
+
// Same as #handleInputChunk: typed input wins over a stale in-flight provider lookup.
|
|
3243
|
+
if (this.#autocompleteRequestRunning && this.#autocompleteState === null) {
|
|
3244
|
+
this.#invalidateAutocompleteRequests();
|
|
3245
|
+
}
|
|
3246
|
+
this.#insertCharacter(text);
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3043
3249
|
#insertCharacter(char: string): void {
|
|
3044
3250
|
this.#exitHistoryForEditing();
|
|
3045
3251
|
// Undo coalescing: consecutive word typing collapses into one undo unit
|
|
@@ -3168,7 +3374,7 @@ export class Editor implements Component, Focusable {
|
|
|
3168
3374
|
}
|
|
3169
3375
|
}
|
|
3170
3376
|
} else {
|
|
3171
|
-
this.#
|
|
3377
|
+
this.#refreshAutocomplete();
|
|
3172
3378
|
}
|
|
3173
3379
|
}
|
|
3174
3380
|
|
|
@@ -3264,7 +3470,7 @@ export class Editor implements Component, Focusable {
|
|
|
3264
3470
|
/** Re-evaluate autocomplete triggers for the text ending at the cursor (used after bulk edits). */
|
|
3265
3471
|
#retriggerAutocompleteAtCursor(): void {
|
|
3266
3472
|
if (this.#autocompleteState) {
|
|
3267
|
-
this.#
|
|
3473
|
+
this.#refreshAutocomplete();
|
|
3268
3474
|
return;
|
|
3269
3475
|
}
|
|
3270
3476
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
@@ -3377,6 +3583,23 @@ export class Editor implements Component, Focusable {
|
|
|
3377
3583
|
return undefined;
|
|
3378
3584
|
}
|
|
3379
3585
|
|
|
3586
|
+
/** True when `[from, to)` overlaps a placeholder. Counted `r` must no-op rather than split it.
|
|
3587
|
+
* One pass over the line's matches, so a long counted replace stays linear. */
|
|
3588
|
+
#spanCutsAtomicToken(line: string, from: number, to: number): boolean {
|
|
3589
|
+
const re = this.#getAtomicTokenRe();
|
|
3590
|
+
if (re === undefined) return false;
|
|
3591
|
+
re.lastIndex = 0;
|
|
3592
|
+
for (;;) {
|
|
3593
|
+
const match = re.exec(line);
|
|
3594
|
+
if (match === null || match.index >= to) return false;
|
|
3595
|
+
if (match[0].length === 0) {
|
|
3596
|
+
re.lastIndex = match.index + 1;
|
|
3597
|
+
continue;
|
|
3598
|
+
}
|
|
3599
|
+
if (match.index + match[0].length > from) return true;
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3380
3603
|
/** Expand the half-open range [start, end) so it never cuts through an atomic
|
|
3381
3604
|
* placeholder token: a boundary landing inside a token pulls the whole token in. */
|
|
3382
3605
|
#expandRangeOverAtomicTokens(line: string, start: number, end: number): { start: number; end: number } {
|
|
@@ -3446,7 +3669,7 @@ export class Editor implements Component, Focusable {
|
|
|
3446
3669
|
this.#cancelAutocomplete();
|
|
3447
3670
|
this.onAutocompleteUpdate?.();
|
|
3448
3671
|
} else {
|
|
3449
|
-
this.#
|
|
3672
|
+
this.#refreshAutocomplete();
|
|
3450
3673
|
}
|
|
3451
3674
|
} else {
|
|
3452
3675
|
// If autocomplete was cancelled (no matches), re-trigger if we're in a completable context
|
|
@@ -3626,7 +3849,7 @@ export class Editor implements Component, Focusable {
|
|
|
3626
3849
|
this.#notifyChange();
|
|
3627
3850
|
|
|
3628
3851
|
if (this.#autocompleteState) {
|
|
3629
|
-
this.#
|
|
3852
|
+
this.#refreshAutocomplete();
|
|
3630
3853
|
} else {
|
|
3631
3854
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
3632
3855
|
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
@@ -3960,7 +4183,7 @@ export class Editor implements Component, Focusable {
|
|
|
3960
4183
|
|
|
3961
4184
|
// Update or re-trigger autocomplete after forward delete
|
|
3962
4185
|
if (this.#autocompleteState) {
|
|
3963
|
-
this.#
|
|
4186
|
+
this.#refreshAutocomplete();
|
|
3964
4187
|
} else {
|
|
3965
4188
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
3966
4189
|
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
@@ -4244,7 +4467,7 @@ export class Editor implements Component, Focusable {
|
|
|
4244
4467
|
if (!token.includes(" ") && !token.slice(1).includes("/")) {
|
|
4245
4468
|
// Guard the timing window where the popup was built for an earlier
|
|
4246
4469
|
// query (e.g. bare `/`) and the user typed further characters before
|
|
4247
|
-
// the
|
|
4470
|
+
// the async refresh landed: accept the stale skill only
|
|
4248
4471
|
// when the refreshed popup would still surface it (same gate as
|
|
4249
4472
|
// buildMidPromptSkillCompletions). `tmp` after a bare slash
|
|
4250
4473
|
// therefore falls through to file completion instead of rewriting
|
|
@@ -4417,6 +4640,7 @@ export class Editor implements Component, Focusable {
|
|
|
4417
4640
|
}
|
|
4418
4641
|
|
|
4419
4642
|
async #handleTabCompletion(): Promise<void> {
|
|
4643
|
+
if (this.#acceptPrediction()) return;
|
|
4420
4644
|
if (this.#acceptWordCompletion({ space: true })) return;
|
|
4421
4645
|
if (!this.#autocompleteProvider) return;
|
|
4422
4646
|
|
|
@@ -4454,6 +4678,13 @@ export class Editor implements Component, Focusable {
|
|
|
4454
4678
|
if (space) this.#lastAction = "accept-word";
|
|
4455
4679
|
return true;
|
|
4456
4680
|
}
|
|
4681
|
+
/** Insert the rest of the shown {@link prediction}, if any. Never submits. */
|
|
4682
|
+
#acceptPrediction(): boolean {
|
|
4683
|
+
const suffix = this.#getPredictionSuffix();
|
|
4684
|
+
if (!suffix) return false;
|
|
4685
|
+
this.#insertTextAtCursor(suffix);
|
|
4686
|
+
return true;
|
|
4687
|
+
}
|
|
4457
4688
|
|
|
4458
4689
|
async #showSpellingSuggestions(): Promise<void> {
|
|
4459
4690
|
const cursorLine = this.#state.cursorLine;
|
|
@@ -4531,7 +4762,6 @@ export class Editor implements Component, Focusable {
|
|
|
4531
4762
|
|
|
4532
4763
|
#cancelAutocomplete(notifyCancel: boolean = false): void {
|
|
4533
4764
|
const wasAutocompleting = this.#autocompleteState !== null;
|
|
4534
|
-
this.#clearAutocompleteTimeout();
|
|
4535
4765
|
this.#invalidateAutocompleteRequests();
|
|
4536
4766
|
this.#autocompleteState = null;
|
|
4537
4767
|
this.#autocompleteList = undefined;
|
|
@@ -4650,7 +4880,7 @@ export class Editor implements Component, Focusable {
|
|
|
4650
4880
|
this.onAutocompleteUpdate?.();
|
|
4651
4881
|
}
|
|
4652
4882
|
|
|
4653
|
-
/** Filter a shown `@` file list to the live `@` token until the
|
|
4883
|
+
/** Filter a shown `@` file list to the live `@` token until the async refresh replaces it. */
|
|
4654
4884
|
#narrowAtFileList(): void {
|
|
4655
4885
|
if (!this.#autocompleteList || !this.#autocompletePrefix.startsWith("@")) return;
|
|
4656
4886
|
const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
|
|
@@ -4666,27 +4896,50 @@ export class Editor implements Component, Focusable {
|
|
|
4666
4896
|
for (const resolve of waiters) resolve();
|
|
4667
4897
|
}
|
|
4668
4898
|
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4899
|
+
/**
|
|
4900
|
+
* Re-query suggestions for the live text after an edit while the popup is open. Runs on
|
|
4901
|
+
* every edit; the request queue coalesces bursts (a newer request replaces the pending one
|
|
4902
|
+
* and aborts the in-flight one), so no timer delays the popup behind the typing.
|
|
4903
|
+
*/
|
|
4904
|
+
#refreshAutocomplete(): void {
|
|
4905
|
+
if (this.#autocompleteState === "assist") return;
|
|
4906
|
+
if (this.#refreshSlashCommandListSync()) return;
|
|
4907
|
+
this.#narrowAtFileList();
|
|
4908
|
+
void this.#updateAutocomplete();
|
|
4678
4909
|
}
|
|
4679
4910
|
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4911
|
+
/**
|
|
4912
|
+
* Rebuild a shown slash command-name popup from the live token synchronously. An async
|
|
4913
|
+
* refresh resolves a microtask later at best, so Enter/Tab arriving in the same input
|
|
4914
|
+
* batch would accept a row computed for an earlier query (`/` → `mod` → Enter ran `/login`).
|
|
4915
|
+
* Returns false when the async refresh must handle the edit (arguments, mid-prompt
|
|
4916
|
+
* skills, absolute-path fall-through).
|
|
4917
|
+
*/
|
|
4918
|
+
#refreshSlashCommandListSync(): boolean {
|
|
4919
|
+
const provider = this.#autocompleteProvider;
|
|
4920
|
+
if (this.#autocompleteState !== "regular" || !provider?.trySyncSlashCompletion) return false;
|
|
4921
|
+
if (!this.#isInSubmittedSlashCommandContext()) return false;
|
|
4922
|
+
const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
|
|
4923
|
+
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
4924
|
+
const token = textBeforeCursor.trimStart();
|
|
4925
|
+
if (token.includes(" ")) return false;
|
|
4926
|
+
const suggestions = provider.trySyncSlashCompletion(textBeforeCursor);
|
|
4927
|
+
if (suggestions) {
|
|
4928
|
+
this.#invalidateAutocompleteRequests();
|
|
4929
|
+
this.#showAutocompleteSuggestions(suggestions, "regular");
|
|
4930
|
+
return true;
|
|
4684
4931
|
}
|
|
4932
|
+
// No command matches: an inner slash may still be an absolute path, which needs the
|
|
4933
|
+
// async file lookup. Otherwise the async refresh would only close the popup.
|
|
4934
|
+
if (token.slice(1).includes("/")) return false;
|
|
4935
|
+
this.#cancelAutocomplete();
|
|
4936
|
+
this.onAutocompleteUpdate?.();
|
|
4937
|
+
return true;
|
|
4685
4938
|
}
|
|
4686
4939
|
|
|
4687
4940
|
/**
|
|
4688
4941
|
* Get inline hint text to show as dim ghost text after the cursor.
|
|
4689
|
-
* Checks selected autocomplete item's hint first, then
|
|
4942
|
+
* Checks the selected autocomplete item's hint first, then the prediction, then the provider.
|
|
4690
4943
|
*/
|
|
4691
4944
|
#getInlineHint(): string | null {
|
|
4692
4945
|
// Check selected autocomplete item for a hint
|
|
@@ -4695,6 +4948,9 @@ export class Editor implements Component, Focusable {
|
|
|
4695
4948
|
return selected?.hint ?? null;
|
|
4696
4949
|
}
|
|
4697
4950
|
|
|
4951
|
+
const prediction = this.#getPredictionSuffix();
|
|
4952
|
+
if (prediction) return prediction;
|
|
4953
|
+
|
|
4698
4954
|
// Fall back to provider's getInlineHint
|
|
4699
4955
|
if (this.#autocompleteProvider?.getInlineHint) {
|
|
4700
4956
|
const hint = this.#autocompleteProvider.getInlineHint(
|
|
@@ -4708,9 +4964,22 @@ export class Editor implements Component, Focusable {
|
|
|
4708
4964
|
return this.#getWordCompletion();
|
|
4709
4965
|
}
|
|
4710
4966
|
#getPlaceholder(): string | undefined {
|
|
4711
|
-
if (this.#autocompleteState || !this.#isEditorEmpty()) return undefined;
|
|
4967
|
+
if (this.#autocompleteState || !this.#isEditorEmpty() || this.#getPredictionSuffix()) return undefined;
|
|
4712
4968
|
return this.placeholder?.() || undefined;
|
|
4713
4969
|
}
|
|
4970
|
+
/** The part of {@link prediction} not yet typed, or `null` when no prediction is shown. */
|
|
4971
|
+
#getPredictionSuffix(): string | null {
|
|
4972
|
+
if (this.#autocompleteState) return null;
|
|
4973
|
+
const prediction = this.prediction?.();
|
|
4974
|
+
if (!prediction) return null;
|
|
4975
|
+
const { lines, cursorCol } = this.#state;
|
|
4976
|
+
if (lines.length !== 1) return null;
|
|
4977
|
+
const typed = lines[0] ?? "";
|
|
4978
|
+
if (cursorCol !== typed.length || typed.length >= prediction.length || !prediction.startsWith(typed)) {
|
|
4979
|
+
return null;
|
|
4980
|
+
}
|
|
4981
|
+
return prediction.slice(typed.length);
|
|
4982
|
+
}
|
|
4714
4983
|
/** Typing `typed` where it diverges from the shown ghost completion rejects that ghost. */
|
|
4715
4984
|
#reportTypedPastWordCompletion(typed: string): void {
|
|
4716
4985
|
if (!this.#textAssistProvider?.wordCompletionFeedback) return;
|
package/src/components/form.d.ts
CHANGED
|
@@ -44,6 +44,7 @@ export declare class FormField implements Component, Focusable, MouseRoutable {
|
|
|
44
44
|
/** Label, description, details, preview, the control, error, summary, hint and footer as one column. */
|
|
45
45
|
describe(_cx: DescribeContext): NativeNode;
|
|
46
46
|
get focused(): boolean;
|
|
47
|
+
get hasError(): boolean;
|
|
47
48
|
get debugChildren(): readonly Component[];
|
|
48
49
|
set focused(value: boolean);
|
|
49
50
|
setUseTerminalCursor(useTerminalCursor: boolean): void;
|