@linxiraos/pi-tui 1.1.27 → 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 +47 -463
- package/README.md +7 -5
- package/dist/types/app-keybindings.d.ts +1 -1
- 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 +2 -2
- 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 +5 -0
- package/dist/types/chat/chat-transcript-builder.d.ts +2 -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 +15 -2
- package/dist/types/chrome/shared.d.ts +9 -0
- 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 +15 -4
- package/dist/types/components/form.d.ts +1 -0
- package/dist/types/components/image.d.ts +12 -44
- 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/markdown.d.ts +36 -3
- 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 +1 -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/backend.d.ts +3 -2
- package/dist/types/native/blobs.d.ts +7 -3
- package/dist/types/native/encode.d.ts +3 -4
- package/dist/types/native/overlay.d.ts +5 -0
- 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 +2 -0
- package/dist/types/overlays/agent-transcript-viewer.d.ts +11 -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/history-search.d.ts +10 -0
- package/dist/types/overlays/hook-selector.d.ts +9 -4
- 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 +23 -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/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/usage-dashboard.d.ts +8 -4
- package/dist/types/overlays/usage-display.d.ts +3 -1
- package/dist/types/prompt/composer.d.ts +1 -1
- package/dist/types/prompt/custom-editor.d.ts +11 -8
- package/dist/types/prompt/image-source.d.ts +2 -1
- package/dist/types/prompt/interactive-context-helpers.d.ts +4 -1
- package/dist/types/prompt/model-mention-autocomplete.d.ts +1 -1
- package/dist/types/prompt/welcome.d.ts +6 -1
- 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/status-line/component.d.ts +3 -0
- 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 +27 -35
- package/dist/types/terminal-multiplexer.d.ts +127 -14
- package/dist/types/terminal.d.ts +22 -6
- 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/tmux.d.ts +11 -4
- 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 +0 -4
- package/dist/types/tools/write.d.ts +9 -2
- package/dist/types/tools/yield.d.ts +22 -0
- package/dist/types/tui.d.ts +31 -0
- package/dist/types/utils.d.ts +6 -0
- package/dist/types/vim.d.ts +19 -5
- package/package.json +13 -9
- package/src/app-keybindings.d.ts +1 -1
- package/src/app-keybindings.ts +1 -1
- 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 +26 -5
- package/src/apps/git/git-tui.ts +2 -2
- package/src/apps/git/help.ts +29 -20
- 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/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 +5 -0
- package/src/chat/assistant-message.ts +219 -50
- package/src/chat/chat-transcript-builder.d.ts +2 -0
- package/src/chat/chat-transcript-builder.ts +5 -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 +80 -6
- package/src/chat/user-message.d.ts +15 -2
- package/src/chat/user-message.ts +55 -3
- package/src/chrome/shared.ts +53 -0
- package/src/chrome/transcript-container.d.ts +6 -0
- package/src/chrome/transcript-container.ts +93 -13
- 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 +15 -4
- package/src/components/editor.ts +294 -46
- package/src/components/form.d.ts +1 -0
- package/src/components/form.ts +8 -0
- package/src/components/image.d.ts +12 -44
- package/src/components/image.ts +47 -151
- 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/markdown.d.ts +36 -3
- package/src/components/markdown.ts +433 -318
- 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 +1 -1
- package/src/fuzzy.d.ts +29 -1
- package/src/fuzzy.ts +115 -30
- package/src/key-hint-format.d.ts +1 -1
- package/src/key-hint-format.ts +1 -1
- 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 +4 -4
- 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/backend.d.ts +3 -2
- package/src/native/backend.ts +174 -18
- package/src/native/blobs.d.ts +7 -3
- package/src/native/blobs.ts +47 -12
- package/src/native/encode.d.ts +3 -4
- package/src/native/encode.ts +58 -31
- package/src/native/overlay.d.ts +5 -0
- package/src/native/overlay.ts +13 -0
- package/src/native/reconcile.ts +4 -0
- 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 +44 -29
- package/src/overlays/agent-transcript-viewer.ts +91 -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 +7 -10
- package/src/overlays/btw-history-panel.ts +12 -1
- package/src/overlays/copy-selector.ts +30 -8
- 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/jobs-panel.ts +28 -46
- package/src/overlays/model-browser.ts +341 -110
- package/src/overlays/model-hub.ts +300 -49
- package/src/overlays/move-overlay.ts +28 -14
- package/src/overlays/oauth-selector.ts +54 -70
- package/src/overlays/plan-review-overlay.ts +194 -70
- package/src/overlays/plugin-settings.ts +3 -3
- package/src/overlays/report-panel.ts +2 -2
- package/src/overlays/rewind-selector.ts +86 -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 +4 -3
- 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/tree-selector.ts +41 -20
- package/src/overlays/usage-dashboard.ts +100 -50
- package/src/overlays/usage-display.ts +10 -4
- package/src/prompt/attachment-chips.ts +15 -11
- package/src/prompt/composer-attachments.ts +6 -4
- package/src/prompt/composer.ts +4 -15
- package/src/prompt/custom-editor.ts +54 -26
- package/src/prompt/image-source.ts +2 -1
- package/src/prompt/interactive-context-helpers.ts +30 -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/tips.txt +6 -5
- package/src/prompt/welcome.ts +55 -12
- 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 +11 -0
- 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/status-line/component.ts +292 -78
- 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 +27 -35
- package/src/terminal-capabilities.ts +75 -189
- package/src/terminal-multiplexer.d.ts +127 -14
- package/src/terminal-multiplexer.ts +112 -33
- package/src/terminal.d.ts +22 -6
- package/src/terminal.ts +139 -37
- 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 +6 -1
- 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/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.ts +92 -42
- 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 +149 -50
- package/src/tools/task.d.ts +0 -4
- package/src/tools/task.ts +112 -59
- 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 +31 -0
- package/src/tui.ts +309 -160
- 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.ts +8 -0
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;
|
|
@@ -764,6 +771,12 @@ export class Editor implements Component, Focusable {
|
|
|
764
771
|
* cursor. The host styles it (ANSI allowed). Re-evaluated on every render, so hosts can derive
|
|
765
772
|
* it from live state. */
|
|
766
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;
|
|
767
780
|
|
|
768
781
|
// Custom top border (for status line integration). Either an eager `content`
|
|
769
782
|
// (set once, reused every frame) or a `provider` that recomputes lazily just
|
|
@@ -1028,6 +1041,7 @@ export class Editor implements Component, Focusable {
|
|
|
1028
1041
|
this.#pastes.clear();
|
|
1029
1042
|
this.#pasteCounter = 0;
|
|
1030
1043
|
this.#atoms.clear();
|
|
1044
|
+
this.#atomsRevision++;
|
|
1031
1045
|
this.#historyDraftActive = false;
|
|
1032
1046
|
}
|
|
1033
1047
|
|
|
@@ -1067,6 +1081,7 @@ export class Editor implements Component, Focusable {
|
|
|
1067
1081
|
if (entry?.draft || this.#historyDraftActive) {
|
|
1068
1082
|
this.#pastes = new Map(entry?.draft?.pastes);
|
|
1069
1083
|
this.#atoms = new Map(entry?.draft?.atoms);
|
|
1084
|
+
this.#atomsRevision++;
|
|
1070
1085
|
this.#pasteCounter = entry?.draft?.pasteCounter ?? 0;
|
|
1071
1086
|
this.restoreHistoryState(entry?.draft?.restore);
|
|
1072
1087
|
this.#historyDraftActive = entry?.draft !== undefined;
|
|
@@ -1669,7 +1684,10 @@ export class Editor implements Component, Focusable {
|
|
|
1669
1684
|
|
|
1670
1685
|
const placeholder = this.#getPlaceholder();
|
|
1671
1686
|
const nativePlaceholder =
|
|
1672
|
-
this.describePlaceholder !== undefined &&
|
|
1687
|
+
this.describePlaceholder !== undefined &&
|
|
1688
|
+
!this.#autocompleteState &&
|
|
1689
|
+
this.#isEditorEmpty() &&
|
|
1690
|
+
!this.#getPredictionSuffix()
|
|
1673
1691
|
? this.describePlaceholder()
|
|
1674
1692
|
: placeholder && plainText(placeholder).trim();
|
|
1675
1693
|
const atLineEnd = cursorCol >= (lines[cursorLine]?.length ?? 0);
|
|
@@ -1912,7 +1930,7 @@ export class Editor implements Component, Focusable {
|
|
|
1912
1930
|
if (kb.matchesCanonical(canonical, "tui.input.tab") || rightArrowAccepts) {
|
|
1913
1931
|
const selected = this.#autocompleteList.getSelectedItem();
|
|
1914
1932
|
// Check for stale autocomplete state due to buffer edits since last refresh
|
|
1915
|
-
// (destructive keys or paste can outrun the
|
|
1933
|
+
// (destructive keys or paste can outrun the async refresh).
|
|
1916
1934
|
const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
|
|
1917
1935
|
const currentTextBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
1918
1936
|
if (!this.#autocompletePrefixMatchesCursorText(currentTextBeforeCursor, selected)) {
|
|
@@ -1922,7 +1940,7 @@ export class Editor implements Component, Focusable {
|
|
|
1922
1940
|
}
|
|
1923
1941
|
if (!selected) {
|
|
1924
1942
|
// An `@` popup whose narrowing filter matched nothing stays open with no
|
|
1925
|
-
// candidate (see #
|
|
1943
|
+
// candidate (see #refreshAutocomplete). Nothing to accept: cancel the
|
|
1926
1944
|
// popup and fall through so Tab keeps its normal completion role and a right
|
|
1927
1945
|
// arrow at end of line moves the cursor.
|
|
1928
1946
|
this.#cancelAutocomplete();
|
|
@@ -1944,7 +1962,7 @@ export class Editor implements Component, Focusable {
|
|
|
1944
1962
|
!this.#selectedCompletionIsSkillNamespace()
|
|
1945
1963
|
) {
|
|
1946
1964
|
const selected = this.#autocompleteList.getSelectedItem();
|
|
1947
|
-
// Check for stale autocomplete state
|
|
1965
|
+
// Check for stale autocomplete state while an async refresh is pending
|
|
1948
1966
|
const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
|
|
1949
1967
|
const currentTextBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
1950
1968
|
if (!this.#autocompletePrefixMatchesCursorText(currentTextBeforeCursor, selected)) {
|
|
@@ -2121,15 +2139,18 @@ export class Editor implements Component, Focusable {
|
|
|
2121
2139
|
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
2122
2140
|
if (
|
|
2123
2141
|
findLeadingSlashCommandStart(textBeforeCursor) !== null &&
|
|
2142
|
+
textBeforeCursor.trim() !== "/" &&
|
|
2124
2143
|
this.#isInSubmittedSlashCommandContext() &&
|
|
2125
2144
|
this.#autocompleteProvider?.trySyncSlashCompletion
|
|
2126
2145
|
) {
|
|
2127
2146
|
const syncResult = this.#autocompleteProvider.trySyncSlashCompletion(textBeforeCursor);
|
|
2128
|
-
|
|
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) {
|
|
2129
2151
|
// Invalidate any pending async autocomplete so its stale results are discarded
|
|
2130
2152
|
this.#autocompleteRequestId += 1;
|
|
2131
2153
|
// Apply the best match and submit the completed command
|
|
2132
|
-
const selected = syncResult.items[0]!;
|
|
2133
2154
|
const result = this.#autocompleteProvider.applyCompletion(
|
|
2134
2155
|
this.#state.lines,
|
|
2135
2156
|
this.#state.cursorLine,
|
|
@@ -2204,8 +2225,8 @@ export class Editor implements Component, Focusable {
|
|
|
2204
2225
|
this.#moveCursor(1, 0); // Cursor movement (within text or history entry)
|
|
2205
2226
|
}
|
|
2206
2227
|
} else if (kb.matchesCanonical(canonical, "tui.editor.cursorRight")) {
|
|
2207
|
-
// Right walks over the ghost word completion as if it were typed: no trailing space.
|
|
2208
|
-
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;
|
|
2209
2230
|
this.#moveCursor(0, 1);
|
|
2210
2231
|
} else if (kb.matchesCanonical(canonical, "tui.editor.cursorLeft")) {
|
|
2211
2232
|
// Left
|
|
@@ -2248,18 +2269,43 @@ export class Editor implements Component, Focusable {
|
|
|
2248
2269
|
return this.isShowingAutocomplete() ? false : this.#runVimKey("escape", vim);
|
|
2249
2270
|
}
|
|
2250
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
|
+
}
|
|
2251
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);
|
|
2252
2288
|
const mapped = canonical === undefined ? undefined : VIM_NAV_KEYS[canonical];
|
|
2289
|
+
if (awaitingTarget && mapped !== undefined) return this.#runVimKey("escape", vim);
|
|
2253
2290
|
if (mapped !== undefined) return this.#runVimKey(mapped, vim);
|
|
2254
2291
|
|
|
2255
|
-
// 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.
|
|
2256
2295
|
const printable = extractPrintableText(data);
|
|
2257
|
-
if (!printable)
|
|
2296
|
+
if (!printable) {
|
|
2297
|
+
if (vim.pending) this.#runVimKey("escape", vim);
|
|
2298
|
+
return false;
|
|
2299
|
+
}
|
|
2258
2300
|
|
|
2259
2301
|
// Batched stdin can deliver several keystrokes at once, so replay the run one grapheme at a
|
|
2260
|
-
// time. A command that drops out of Normal mode part-way (`iabc`) turns the rest of
|
|
2261
|
-
//
|
|
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.
|
|
2262
2304
|
for (const seg of segmenter.segment(printable)) {
|
|
2305
|
+
if (vim.mode === "replace") {
|
|
2306
|
+
this.#overwriteReplaceGrapheme(seg.segment);
|
|
2307
|
+
continue;
|
|
2308
|
+
}
|
|
2263
2309
|
if (this.#runVimKey(seg.segment, vim)) continue;
|
|
2264
2310
|
this.#insertCharacter(printable.slice(seg.index));
|
|
2265
2311
|
return true;
|
|
@@ -2280,11 +2326,22 @@ export class Editor implements Component, Focusable {
|
|
|
2280
2326
|
if (vim.mode !== before || vim.pendingText !== pendingBefore || this.vimSelectedLines !== selectedLinesBefore) {
|
|
2281
2327
|
this.onVimModeChange?.(vim.mode);
|
|
2282
2328
|
}
|
|
2329
|
+
if (before === "replace" && vim.mode !== "replace") this.#replaceLog.length = 0;
|
|
2330
|
+
if (vim.mode === "replace" && before !== "replace") this.#replaceLog.length = 0;
|
|
2283
2331
|
return true;
|
|
2284
2332
|
}
|
|
2285
2333
|
|
|
2286
2334
|
#applyVimCommands(commands: readonly VimCommand[]): void {
|
|
2287
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
|
+
}
|
|
2288
2345
|
switch (command.kind) {
|
|
2289
2346
|
case "move":
|
|
2290
2347
|
this.#moveVimCursor(command.to);
|
|
@@ -2316,12 +2373,133 @@ export class Editor implements Component, Focusable {
|
|
|
2316
2373
|
case "undo":
|
|
2317
2374
|
this.#applyUndo();
|
|
2318
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;
|
|
2319
2385
|
}
|
|
2320
2386
|
}
|
|
2321
2387
|
this.#clampVimCursor();
|
|
2322
2388
|
this.invalidate();
|
|
2323
2389
|
}
|
|
2324
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
|
+
|
|
2325
2503
|
#moveVimCursor(to: VimPosition): void {
|
|
2326
2504
|
this.#state.cursorLine = Math.max(0, Math.min(to.line, this.#state.lines.length - 1));
|
|
2327
2505
|
const line = this.#state.lines[this.#state.cursorLine] ?? "";
|
|
@@ -2666,6 +2844,11 @@ export class Editor implements Component, Focusable {
|
|
|
2666
2844
|
return this.#textRevision;
|
|
2667
2845
|
}
|
|
2668
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
|
+
|
|
2669
2852
|
#notifyChange(text?: string): void {
|
|
2670
2853
|
this.#textRevision++;
|
|
2671
2854
|
this.onChange?.(text ?? this.getText());
|
|
@@ -2704,6 +2887,7 @@ export class Editor implements Component, Focusable {
|
|
|
2704
2887
|
* it — for hosts that re-collapse restored draft text via {@link setText}. */
|
|
2705
2888
|
registerAtom(label: string, expansion: string): void {
|
|
2706
2889
|
this.#atoms.set(label, expansion);
|
|
2890
|
+
this.#atomsRevision++;
|
|
2707
2891
|
}
|
|
2708
2892
|
|
|
2709
2893
|
/** Insert `label` (plus a trailing space) at the cursor and register it as an atom expanding
|
|
@@ -2740,6 +2924,7 @@ export class Editor implements Component, Focusable {
|
|
|
2740
2924
|
/** Drop every registered atom expansion (draft cleared or replaced by the host). */
|
|
2741
2925
|
clearAtoms(): void {
|
|
2742
2926
|
this.#atoms.clear();
|
|
2927
|
+
this.#atomsRevision++;
|
|
2743
2928
|
}
|
|
2744
2929
|
|
|
2745
2930
|
/**
|
|
@@ -2779,7 +2964,7 @@ export class Editor implements Component, Focusable {
|
|
|
2779
2964
|
* Mirrors the transient state the key dispatch tears down before this action so the two
|
|
2780
2965
|
* cannot diverge: a pending character jump is cancelled by any other key, and an open
|
|
2781
2966
|
* spelling-assist popup is dismissed by anything that is not one of its accept keys (its
|
|
2782
|
-
*
|
|
2967
|
+
* refresh skips assist mode, so a surviving list would hang around forever).
|
|
2783
2968
|
* While Vim owns the buffer (Normal or Visual) the operation is Vim's `x` — deleting the
|
|
2784
2969
|
* selection and returning to Normal in Visual mode, the grapheme under the cursor
|
|
2785
2970
|
* otherwise. Only Insert mode and Vim-off editors delete straight through. */
|
|
@@ -3189,7 +3374,7 @@ export class Editor implements Component, Focusable {
|
|
|
3189
3374
|
}
|
|
3190
3375
|
}
|
|
3191
3376
|
} else {
|
|
3192
|
-
this.#
|
|
3377
|
+
this.#refreshAutocomplete();
|
|
3193
3378
|
}
|
|
3194
3379
|
}
|
|
3195
3380
|
|
|
@@ -3285,7 +3470,7 @@ export class Editor implements Component, Focusable {
|
|
|
3285
3470
|
/** Re-evaluate autocomplete triggers for the text ending at the cursor (used after bulk edits). */
|
|
3286
3471
|
#retriggerAutocompleteAtCursor(): void {
|
|
3287
3472
|
if (this.#autocompleteState) {
|
|
3288
|
-
this.#
|
|
3473
|
+
this.#refreshAutocomplete();
|
|
3289
3474
|
return;
|
|
3290
3475
|
}
|
|
3291
3476
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
@@ -3398,6 +3583,23 @@ export class Editor implements Component, Focusable {
|
|
|
3398
3583
|
return undefined;
|
|
3399
3584
|
}
|
|
3400
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
|
+
|
|
3401
3603
|
/** Expand the half-open range [start, end) so it never cuts through an atomic
|
|
3402
3604
|
* placeholder token: a boundary landing inside a token pulls the whole token in. */
|
|
3403
3605
|
#expandRangeOverAtomicTokens(line: string, start: number, end: number): { start: number; end: number } {
|
|
@@ -3467,7 +3669,7 @@ export class Editor implements Component, Focusable {
|
|
|
3467
3669
|
this.#cancelAutocomplete();
|
|
3468
3670
|
this.onAutocompleteUpdate?.();
|
|
3469
3671
|
} else {
|
|
3470
|
-
this.#
|
|
3672
|
+
this.#refreshAutocomplete();
|
|
3471
3673
|
}
|
|
3472
3674
|
} else {
|
|
3473
3675
|
// If autocomplete was cancelled (no matches), re-trigger if we're in a completable context
|
|
@@ -3647,7 +3849,7 @@ export class Editor implements Component, Focusable {
|
|
|
3647
3849
|
this.#notifyChange();
|
|
3648
3850
|
|
|
3649
3851
|
if (this.#autocompleteState) {
|
|
3650
|
-
this.#
|
|
3852
|
+
this.#refreshAutocomplete();
|
|
3651
3853
|
} else {
|
|
3652
3854
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
3653
3855
|
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
@@ -3981,7 +4183,7 @@ export class Editor implements Component, Focusable {
|
|
|
3981
4183
|
|
|
3982
4184
|
// Update or re-trigger autocomplete after forward delete
|
|
3983
4185
|
if (this.#autocompleteState) {
|
|
3984
|
-
this.#
|
|
4186
|
+
this.#refreshAutocomplete();
|
|
3985
4187
|
} else {
|
|
3986
4188
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
3987
4189
|
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
@@ -4265,7 +4467,7 @@ export class Editor implements Component, Focusable {
|
|
|
4265
4467
|
if (!token.includes(" ") && !token.slice(1).includes("/")) {
|
|
4266
4468
|
// Guard the timing window where the popup was built for an earlier
|
|
4267
4469
|
// query (e.g. bare `/`) and the user typed further characters before
|
|
4268
|
-
// the
|
|
4470
|
+
// the async refresh landed: accept the stale skill only
|
|
4269
4471
|
// when the refreshed popup would still surface it (same gate as
|
|
4270
4472
|
// buildMidPromptSkillCompletions). `tmp` after a bare slash
|
|
4271
4473
|
// therefore falls through to file completion instead of rewriting
|
|
@@ -4438,6 +4640,7 @@ export class Editor implements Component, Focusable {
|
|
|
4438
4640
|
}
|
|
4439
4641
|
|
|
4440
4642
|
async #handleTabCompletion(): Promise<void> {
|
|
4643
|
+
if (this.#acceptPrediction()) return;
|
|
4441
4644
|
if (this.#acceptWordCompletion({ space: true })) return;
|
|
4442
4645
|
if (!this.#autocompleteProvider) return;
|
|
4443
4646
|
|
|
@@ -4475,6 +4678,13 @@ export class Editor implements Component, Focusable {
|
|
|
4475
4678
|
if (space) this.#lastAction = "accept-word";
|
|
4476
4679
|
return true;
|
|
4477
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
|
+
}
|
|
4478
4688
|
|
|
4479
4689
|
async #showSpellingSuggestions(): Promise<void> {
|
|
4480
4690
|
const cursorLine = this.#state.cursorLine;
|
|
@@ -4552,7 +4762,6 @@ export class Editor implements Component, Focusable {
|
|
|
4552
4762
|
|
|
4553
4763
|
#cancelAutocomplete(notifyCancel: boolean = false): void {
|
|
4554
4764
|
const wasAutocompleting = this.#autocompleteState !== null;
|
|
4555
|
-
this.#clearAutocompleteTimeout();
|
|
4556
4765
|
this.#invalidateAutocompleteRequests();
|
|
4557
4766
|
this.#autocompleteState = null;
|
|
4558
4767
|
this.#autocompleteList = undefined;
|
|
@@ -4671,7 +4880,7 @@ export class Editor implements Component, Focusable {
|
|
|
4671
4880
|
this.onAutocompleteUpdate?.();
|
|
4672
4881
|
}
|
|
4673
4882
|
|
|
4674
|
-
/** 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. */
|
|
4675
4884
|
#narrowAtFileList(): void {
|
|
4676
4885
|
if (!this.#autocompleteList || !this.#autocompletePrefix.startsWith("@")) return;
|
|
4677
4886
|
const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
|
|
@@ -4687,27 +4896,50 @@ export class Editor implements Component, Focusable {
|
|
|
4687
4896
|
for (const resolve of waiters) resolve();
|
|
4688
4897
|
}
|
|
4689
4898
|
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
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();
|
|
4699
4909
|
}
|
|
4700
4910
|
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
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;
|
|
4705
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;
|
|
4706
4938
|
}
|
|
4707
4939
|
|
|
4708
4940
|
/**
|
|
4709
4941
|
* Get inline hint text to show as dim ghost text after the cursor.
|
|
4710
|
-
* Checks selected autocomplete item's hint first, then
|
|
4942
|
+
* Checks the selected autocomplete item's hint first, then the prediction, then the provider.
|
|
4711
4943
|
*/
|
|
4712
4944
|
#getInlineHint(): string | null {
|
|
4713
4945
|
// Check selected autocomplete item for a hint
|
|
@@ -4716,6 +4948,9 @@ export class Editor implements Component, Focusable {
|
|
|
4716
4948
|
return selected?.hint ?? null;
|
|
4717
4949
|
}
|
|
4718
4950
|
|
|
4951
|
+
const prediction = this.#getPredictionSuffix();
|
|
4952
|
+
if (prediction) return prediction;
|
|
4953
|
+
|
|
4719
4954
|
// Fall back to provider's getInlineHint
|
|
4720
4955
|
if (this.#autocompleteProvider?.getInlineHint) {
|
|
4721
4956
|
const hint = this.#autocompleteProvider.getInlineHint(
|
|
@@ -4729,9 +4964,22 @@ export class Editor implements Component, Focusable {
|
|
|
4729
4964
|
return this.#getWordCompletion();
|
|
4730
4965
|
}
|
|
4731
4966
|
#getPlaceholder(): string | undefined {
|
|
4732
|
-
if (this.#autocompleteState || !this.#isEditorEmpty()) return undefined;
|
|
4967
|
+
if (this.#autocompleteState || !this.#isEditorEmpty() || this.#getPredictionSuffix()) return undefined;
|
|
4733
4968
|
return this.placeholder?.() || undefined;
|
|
4734
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
|
+
}
|
|
4735
4983
|
/** Typing `typed` where it diverges from the shown ghost completion rejects that ghost. */
|
|
4736
4984
|
#reportTypedPastWordCompletion(typed: string): void {
|
|
4737
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;
|
package/src/components/form.ts
CHANGED
|
@@ -97,6 +97,10 @@ class OptionalText implements Component {
|
|
|
97
97
|
this.#text = new StyledText("", style, token);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
get value(): string {
|
|
101
|
+
return this.#value;
|
|
102
|
+
}
|
|
103
|
+
|
|
100
104
|
describe(cx: DescribeContext): NativeNode {
|
|
101
105
|
return this.#value ? this.#text.describe(cx) : this.#hidden;
|
|
102
106
|
}
|
|
@@ -200,6 +204,10 @@ export class FormField implements Component, Focusable, MouseRoutable {
|
|
|
200
204
|
return this.#focused;
|
|
201
205
|
}
|
|
202
206
|
|
|
207
|
+
get hasError(): boolean {
|
|
208
|
+
return this.#error.value !== "";
|
|
209
|
+
}
|
|
210
|
+
|
|
203
211
|
get debugChildren(): readonly Component[] {
|
|
204
212
|
return [this.control];
|
|
205
213
|
}
|