@cueloop/client 0.1.0-alpha.10 → 0.1.0-alpha.100
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/native/build-pty.sh +51 -0
- package/native/build.sh +82 -0
- package/native/darwin-arm64/libcuelooppty.dylib +0 -0
- package/native/darwin-arm64/libcueloopvt.dylib +0 -0
- package/native/src/pty.zig +193 -0
- package/native/src/shim.zig +114 -0
- package/package.json +42 -34
- package/src/annotations/annotation-palette.ts +62 -0
- package/src/annotations/components/AnnotationCards.stories.tsx +86 -0
- package/src/annotations/components/AnnotationCards.test.tsx +42 -0
- package/src/annotations/components/AnnotationCards.tsx +364 -0
- package/src/annotations/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/annotations/components/DiscussionMarkerRail.tsx +186 -0
- package/src/annotations/discussions.ts +77 -0
- package/src/annotations/thread-selection.test.ts +210 -0
- package/src/annotations/thread-selection.ts +242 -0
- package/src/annotations/use-annotation-surface.tsx +1238 -0
- package/src/app/App.appearance.test.tsx +53 -0
- package/src/{App.autoclose.test.tsx → app/App.autoclose.test.tsx} +77 -51
- package/src/app/App.deleted-scroll.test.tsx +187 -0
- package/src/app/App.diff.test.tsx +313 -0
- package/src/app/App.export.test.tsx +105 -0
- package/src/app/App.focus.test.ts +89 -0
- package/src/app/App.identity.test.tsx +84 -0
- package/src/app/App.plan-render.test.tsx +209 -0
- package/src/app/App.plan-review.test.tsx +428 -0
- package/src/app/App.prototype-markdown.test.tsx +128 -0
- package/src/app/App.prototype.test.tsx +176 -0
- package/src/app/App.readonly.test.tsx +210 -0
- package/src/app/App.regression.test.tsx +99 -0
- package/src/app/App.rendering.test.tsx +134 -0
- package/src/app/App.share-dialog.test.tsx +177 -0
- package/src/app/App.submit-confirm.test.tsx +267 -0
- package/src/app/App.test.tsx +496 -0
- package/src/app/App.thread-agent.test.ts +12 -0
- package/src/app/App.toast.test.tsx +124 -0
- package/src/app/App.tsx +1650 -0
- package/src/app/App.visual.test.tsx +59 -0
- package/src/app/App.walk.test.tsx +299 -0
- package/src/app/App.workbench.test.tsx +1149 -0
- package/src/app/app-key-state.ts +61 -0
- package/src/app/app-screens.tsx +606 -0
- package/src/app/app-view-model.ts +168 -0
- package/src/app/components/AppHeader.stories.tsx +50 -0
- package/src/app/components/AppHeader.tsx +81 -0
- package/src/app/components/AppShell.brand.test.tsx +58 -0
- package/src/app/components/AppShell.extensions.test.tsx +191 -0
- package/src/app/components/AppShell.stories.tsx +89 -0
- package/src/app/components/AppShell.tsx +358 -0
- package/src/app/components/AppShell.zoom.test.tsx +149 -0
- package/src/app/components/Breadcrumb.stories.tsx +19 -0
- package/src/app/components/Breadcrumb.tsx +45 -0
- package/src/app/components/MenuBar.stories.tsx +28 -0
- package/src/app/components/MenuBar.tsx +44 -0
- package/src/app/components/PaneGrid.stories.tsx +84 -0
- package/src/app/components/PaneGrid.tsx +128 -0
- package/src/app/components/PanelColumn.stories.tsx +25 -0
- package/src/app/components/PanelColumn.tsx +93 -0
- package/src/app/components/ShellHeader.stories.tsx +50 -0
- package/src/app/components/ShellHeader.tsx +137 -0
- package/src/app/components/WorkspacePanels.stories.tsx +36 -0
- package/src/app/components/WorkspacePanels.tsx +120 -0
- package/src/app/components/pages/Workspace.stories.tsx +181 -0
- package/src/app/intent-dispatch.test.ts +728 -0
- package/src/app/intent-dispatch.ts +687 -0
- package/src/app/launch-layout.test.ts +78 -0
- package/src/app/launch-layout.ts +57 -0
- package/src/app/ready-signal.test.ts +85 -0
- package/src/app/ready-signal.ts +52 -0
- package/src/app/run.ts +109 -0
- package/src/{serve.test.ts → app/serve.test.ts} +24 -12
- package/src/app/serve.ts +111 -0
- package/src/app/version.ts +5 -0
- package/src/appearance/components/theme-context.stories.tsx +46 -0
- package/src/appearance/components/theme-context.tsx +37 -0
- package/src/appearance/theme-presets.test.ts +51 -0
- package/src/appearance/theme-presets.ts +219 -0
- package/src/appearance/theme.ts +112 -0
- package/src/appearance/visual/contrast.test.ts +35 -0
- package/src/appearance/visual/contrast.ts +33 -0
- package/src/diff/components/AnnotatableFileView.stories.tsx +42 -0
- package/src/diff/components/AnnotatableFileView.test.tsx +157 -0
- package/src/diff/components/AnnotatableFileView.tsx +141 -0
- package/src/diff/components/BareWorkbenchFileView.stories.tsx +27 -0
- package/src/diff/components/BareWorkbenchFileView.tsx +63 -0
- package/src/diff/components/DiffContentView.scroll.test.tsx +434 -0
- package/src/diff/components/DiffContentView.split.test.tsx +140 -0
- package/src/diff/components/DiffContentView.stories.tsx +80 -0
- package/src/diff/components/DiffContentView.syntax.test.tsx +82 -0
- package/src/diff/components/DiffContentView.tsx +1055 -0
- package/src/diff/components/WalkWizard.stories.tsx +100 -0
- package/src/diff/components/WalkWizard.tsx +146 -0
- package/src/diff/components/diff-content-view-layout.test.ts +198 -0
- package/src/diff/components/diff-content-view-layout.ts +84 -0
- package/src/diff/diff-fold.test.ts +78 -0
- package/src/diff/diff-fold.ts +126 -0
- package/src/diff/diff-hunk-curate.test.ts +315 -0
- package/src/diff/diff-hunk-curate.ts +152 -0
- package/src/diff/diff-intraline.test.ts +239 -0
- package/src/diff/diff-intraline.ts +238 -0
- package/src/diff/diff-syntax.test.ts +125 -0
- package/src/diff/diff-syntax.ts +186 -0
- package/src/diff/split-diff.test.ts +110 -0
- package/src/diff/split-diff.ts +144 -0
- package/src/diff/view-diff.test.ts +325 -0
- package/src/diff/view-diff.ts +522 -0
- package/src/diff/walk.test.ts +121 -0
- package/src/diff/walk.ts +73 -0
- package/src/index.ts +31 -2
- package/src/integrations/attribution.test.ts +31 -0
- package/src/integrations/attribution.ts +42 -0
- package/src/integrations/client-extension-registry.test.tsx +61 -0
- package/src/integrations/client-extension-registry.ts +159 -0
- package/src/integrations/clipboard.test.ts +23 -0
- package/src/integrations/clipboard.ts +33 -0
- package/src/integrations/components/ExtensionZone.stories.tsx +28 -0
- package/src/integrations/components/ExtensionZone.tsx +145 -0
- package/src/integrations/components/ShareDialog.stories.tsx +121 -0
- package/src/integrations/components/ShareDialog.tsx +550 -0
- package/src/integrations/components/share-dialog-store.ts +55 -0
- package/src/integrations/editor.test.ts +149 -0
- package/src/integrations/editor.ts +160 -0
- package/src/integrations/github-identity.test.ts +58 -0
- package/src/integrations/github-identity.ts +41 -0
- package/src/integrations/herdr.test.ts +43 -0
- package/src/integrations/herdr.ts +38 -0
- package/src/integrations/integrations.test.ts +41 -0
- package/src/integrations/integrations.ts +32 -0
- package/src/integrations/pasted-image.test.ts +67 -0
- package/src/integrations/pasted-image.ts +60 -0
- package/src/integrations/share.ts +238 -0
- package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
- package/src/keyboard/components/CompletionOverlay.tsx +75 -0
- package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
- package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
- package/src/keyboard/components/HandleChipsInput.tsx +143 -0
- package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
- package/src/keyboard/components/KeybindsDialog.tsx +103 -0
- package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
- package/src/keyboard/components/NavModeHint.tsx +31 -0
- package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
- package/src/keyboard/components/SlashComposer.tsx +87 -0
- package/src/keyboard/key-bindings.test.ts +156 -0
- package/src/keyboard/key-bindings.ts +459 -0
- package/src/keyboard/keymap.test.ts +611 -0
- package/src/keyboard/keymap.ts +395 -0
- package/src/keyboard/skills.test.ts +74 -0
- package/src/keyboard/skills.ts +75 -0
- package/src/keyboard/slash-palette.test.ts +103 -0
- package/src/keyboard/slash-palette.ts +129 -0
- package/src/keyboard/thread-chords.test.ts +79 -0
- package/src/keyboard/thread-chords.ts +163 -0
- package/src/keyboard/use-shared-keyboard.ts +55 -0
- package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
- package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/markdown/components/MarkdownThreadEditor.tsx +160 -0
- package/src/markdown/components/ThreadView.selection.test.tsx +951 -0
- package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
- package/src/markdown/components/ThreadView.stories.tsx +26 -0
- package/src/markdown/components/ThreadView.tsx +560 -0
- package/src/markdown/mark-runs.ts +121 -0
- package/src/markdown/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown/markdown-grid.test.ts +101 -0
- package/src/markdown/markdown-grid.ts +203 -0
- package/src/markdown/markdown-highlight.test.ts +61 -0
- package/src/markdown/markdown-highlight.ts +124 -0
- package/src/markdown/thread-code-syntax.ts +45 -0
- package/src/markdown/use-thread-body-editing.tsx +54 -0
- package/src/markdown/view-plan.regression.test.ts +60 -0
- package/src/markdown/view-plan.test.ts +545 -0
- package/src/markdown/view-plan.ts +1009 -0
- package/src/performance/otlp-trace.test.ts +119 -0
- package/src/performance/otlp-trace.ts +134 -0
- package/src/performance/perf-report.ts +18 -0
- package/src/performance/perf-timings.test.ts +18 -0
- package/src/performance/perf-timings.ts +39 -0
- package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
- package/src/prototype/components/PrototypeContentView.tsx +442 -0
- package/src/prototype/prototype-browser.test.ts +81 -0
- package/src/prototype/prototype-browser.ts +321 -0
- package/src/prototype/prototype-pixels.tsx +35 -0
- package/src/settings/components/SettingsDialog.stories.tsx +117 -0
- package/src/settings/components/SettingsDialog.tsx +183 -0
- package/src/settings/components/SettingsRows.stories.tsx +18 -0
- package/src/settings/components/SettingsRows.tsx +112 -0
- package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
- package/src/settings/components/quick-actions-editor.test.tsx +120 -0
- package/src/settings/components/quick-actions-editor.tsx +222 -0
- package/src/settings/config-identity.test.ts +78 -0
- package/src/settings/config.test.ts +676 -0
- package/src/settings/config.ts +628 -0
- package/src/settings/use-settings-dialog.tsx +351 -0
- package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
- package/src/stories/stories-app.tsx +196 -0
- package/src/stories/stories.test.tsx +74 -0
- package/src/stories/story-fixtures.ts +102 -0
- package/src/stories/story-tree.test.ts +108 -0
- package/src/stories/story.ts +154 -0
- package/src/terminal/ansi-screen.test.ts +28 -0
- package/src/terminal/ansi-screen.ts +72 -0
- package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
- package/src/terminal/components/AnsiScreen.tsx +28 -0
- package/src/terminal/components/terminal-pane.test.tsx +40 -0
- package/src/terminal/components/terminal-pane.ts +196 -0
- package/src/terminal/ghostty-terminal.test.ts +85 -0
- package/src/terminal/ghostty-terminal.ts +197 -0
- package/src/terminal/kitty-image.test.ts +66 -0
- package/src/terminal/kitty-image.ts +137 -0
- package/src/terminal/pty.test.ts +81 -0
- package/src/terminal/pty.ts +246 -0
- package/src/testing/test-support.test.ts +264 -0
- package/src/testing/test-support.ts +367 -0
- package/src/thread/agent-continuation.test.ts +32 -0
- package/src/thread/agent-continuation.ts +13 -0
- package/src/thread/agent-transcript.test.ts +161 -0
- package/src/thread/agent-transcript.ts +336 -0
- package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
- package/src/thread/components/AgentConfigControls.test.tsx +112 -0
- package/src/thread/components/AgentConfigControls.tsx +114 -0
- package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
- package/src/thread/components/AgentThreadPane.test.tsx +829 -0
- package/src/thread/components/AgentThreadPane.tsx +367 -0
- package/src/thread/components/ThreadFooter.stories.tsx +30 -0
- package/src/thread/components/ThreadFooter.test.tsx +49 -0
- package/src/thread/components/ThreadFooter.tsx +77 -0
- package/src/thread/components/ThreadTree.stories.tsx +46 -0
- package/src/thread/components/ThreadTree.test.tsx +41 -0
- package/src/thread/components/ThreadTree.tsx +267 -0
- package/src/thread/components/thread-tree.test.ts +137 -0
- package/src/thread/components/thread-tree.ts +112 -0
- package/src/thread/thread-controller.curate.test.ts +502 -0
- package/src/thread/thread-controller.derivation.test.ts +62 -0
- package/src/thread/thread-controller.live-diff.test.ts +357 -0
- package/src/thread/thread-controller.optimistic.test.ts +69 -0
- package/src/thread/thread-controller.share.test.ts +573 -0
- package/src/thread/thread-controller.switch-cache.test.ts +174 -0
- package/src/thread/thread-controller.tree.test.ts +263 -0
- package/src/thread/thread-controller.ts +2018 -0
- package/src/thread/tree-view.test.ts +128 -0
- package/src/thread/tree-view.ts +146 -0
- package/src/thread/use-thread-agent.test.tsx +208 -0
- package/src/thread/use-thread-agent.ts +111 -0
- package/src/ui/components/CodeBlock.stories.tsx +42 -0
- package/src/ui/components/CodeBlock.tsx +80 -0
- package/src/ui/components/ConfirmCard.stories.tsx +37 -0
- package/src/ui/components/ConfirmCard.tsx +151 -0
- package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
- package/src/ui/components/ConfirmDialog.tsx +72 -0
- package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
- package/src/ui/components/OverlayScrollbar.test.tsx +139 -0
- package/src/ui/components/OverlayScrollbar.tsx +129 -0
- package/src/ui/components/PromptDialog.stories.tsx +36 -0
- package/src/ui/components/PromptDialog.test.tsx +65 -0
- package/src/ui/components/PromptDialog.tsx +99 -0
- package/src/ui/components/RootOverlay.stories.tsx +45 -0
- package/src/ui/components/RootOverlay.tsx +49 -0
- package/src/ui/components/ScrollArea.stories.tsx +20 -0
- package/src/ui/components/ScrollArea.tsx +61 -0
- package/src/ui/components/Toast.stories.tsx +19 -0
- package/src/ui/components/Toast.tsx +54 -0
- package/src/ui/components/Tooltip.stories.tsx +26 -0
- package/src/ui/components/Tooltip.tsx +72 -0
- package/src/ui/components/menu-control.stories.tsx +35 -0
- package/src/ui/components/menu-control.tsx +43 -0
- package/src/ui/components/primitives/Button.stories.tsx +32 -0
- package/src/ui/components/primitives/Button.tsx +57 -0
- package/src/ui/components/primitives/Card.stories.tsx +25 -0
- package/src/ui/components/primitives/Card.tsx +62 -0
- package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
- package/src/ui/components/primitives/Dialog.test.tsx +38 -0
- package/src/ui/components/primitives/Dialog.tsx +69 -0
- package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/ui/components/primitives/DialogActions.tsx +39 -0
- package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
- package/src/ui/components/primitives/IconButton.tsx +58 -0
- package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/ui/components/primitives/StatusBar.tsx +23 -0
- package/src/ui/components/primitives/Switch.stories.tsx +17 -0
- package/src/ui/components/primitives/Switch.tsx +32 -0
- package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
- package/src/ui/components/primitives/Tabs.tsx +101 -0
- package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/ui/components/primitives/Toolbar.tsx +14 -0
- package/src/ui/components/primitives/Tree.stories.tsx +124 -0
- package/src/ui/components/primitives/Tree.tsx +133 -0
- package/src/ui/components/primitives/frame.test.ts +12 -0
- package/src/ui/components/primitives/frame.ts +17 -0
- package/src/ui/components/primitives/icons.ts +61 -0
- package/src/ui/components/primitives/tree-model.test.ts +99 -0
- package/src/ui/components/primitives/tree-model.ts +137 -0
- package/src/ui/components/syntax-highlight.ts +133 -0
- package/src/ui/components/truncate-text.ts +7 -0
- package/src/ui/components/truncate-title.test.ts +31 -0
- package/src/ui/components/truncate-title.ts +15 -0
- package/src/ui/use-frame-measure.ts +74 -0
- package/src/ui/use-terminal-virtualizer.ts +259 -0
- package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
- package/src/workbench/components/ChangesColumn.tsx +203 -0
- package/src/workbench/components/ChangesFileTree.test.tsx +181 -0
- package/src/workbench/components/EditorGrid.split.test.tsx +118 -0
- package/src/workbench/components/EditorGrid.stories.tsx +86 -0
- package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/workbench/components/EditorGrid.tsx +544 -0
- package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
- package/src/workbench/components/FileContentsView.stories.tsx +16 -0
- package/src/workbench/components/FileContentsView.test.tsx +29 -0
- package/src/workbench/components/FileContentsView.tsx +100 -0
- package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
- package/src/workbench/components/GridTabContent.stories.tsx +59 -0
- package/src/workbench/components/GridTabContent.tsx +193 -0
- package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
- package/src/workbench/components/ProjectTreeView.test.tsx +236 -0
- package/src/workbench/components/ProjectTreeView.tsx +151 -0
- package/src/workbench/components/TreePane.stories.tsx +106 -0
- package/src/workbench/components/TreePane.tsx +155 -0
- package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
- package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
- package/src/workbench/components/WelcomePlayground.tsx +138 -0
- package/src/workbench/components/editor-grid.test.ts +184 -0
- package/src/workbench/components/editor-grid.ts +281 -0
- package/src/workbench/components/file-tree.test.ts +60 -0
- package/src/workbench/components/file-tree.ts +112 -0
- package/src/workbench/use-changes-workbench.ts +205 -0
- package/src/workbench/use-remember-layout.ts +31 -0
- package/src/workbench/use-repo-changes.ts +91 -0
- package/src/workbench/workbench-snapshot.test.ts +99 -0
- package/src/workbench/workbench-snapshot.ts +59 -0
- package/src/App.diff.test.tsx +0 -99
- package/src/App.export.test.tsx +0 -88
- package/src/App.plan-review.test.tsx +0 -259
- package/src/App.readonly.test.tsx +0 -148
- package/src/App.rendering.test.tsx +0 -108
- package/src/App.submit-confirm.test.tsx +0 -183
- package/src/App.test.tsx +0 -218
- package/src/App.tsx +0 -1249
- package/src/config.test.ts +0 -116
- package/src/config.ts +0 -150
- package/src/editor.ts +0 -34
- package/src/herdr.test.ts +0 -68
- package/src/herdr.ts +0 -34
- package/src/keymap.test.ts +0 -270
- package/src/keymap.ts +0 -173
- package/src/run.ts +0 -30
- package/src/serve.ts +0 -75
- package/src/session-controller.ts +0 -398
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/theme.ts +0 -46
- package/src/view-diff.test.ts +0 -55
- package/src/view-diff.ts +0 -86
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -364
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Syntax highlighting for diff rows: tree-sitter token colors mapped onto the
|
|
3
|
+
* diff sheet's hand-rolled rows, so annotation cards still interleave. A hunk is
|
|
4
|
+
* highlighted as a contiguous fragment (its context+addition lines as the new
|
|
5
|
+
* side, context+deletion lines as the old side) - never per single line, which
|
|
6
|
+
* would lose the multi-line grammar context - and the resulting token spans are
|
|
7
|
+
* mapped back to each row in line-relative columns. Async: the shared tree-sitter
|
|
8
|
+
* client resolves off the render path, so callers draw unstyled first.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { getTreeSitterClient, type SimpleHighlight } from "@opentui/core";
|
|
12
|
+
import type { DiffRow } from "./view-diff";
|
|
13
|
+
import { filetypeForPath } from "../ui/components/syntax-highlight";
|
|
14
|
+
|
|
15
|
+
/** A syntax-colored run within one row, in line-relative columns [start, end). */
|
|
16
|
+
export interface SyntaxSpan {
|
|
17
|
+
start: number;
|
|
18
|
+
end: number;
|
|
19
|
+
group: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function stripTrailingNewline(text: string): string {
|
|
23
|
+
return text.replace(/\n$/, "");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface HighlightJob {
|
|
27
|
+
filetype: string;
|
|
28
|
+
source: string;
|
|
29
|
+
rowIndexByLine: number[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Split diff rows into per-hunk highlight jobs, one per side. Plain project
|
|
34
|
+
* files contain only context rows, so detect their language from the row path.
|
|
35
|
+
*/
|
|
36
|
+
export function highlightJobs(rows: DiffRow[]): HighlightJob[] {
|
|
37
|
+
const jobs: HighlightJob[] = [];
|
|
38
|
+
let oldLines: string[] = [];
|
|
39
|
+
let newLines: string[] = [];
|
|
40
|
+
let oldRowIndexByLine: number[] = [];
|
|
41
|
+
let newRowIndexByLine: number[] = [];
|
|
42
|
+
let filetype: string | undefined;
|
|
43
|
+
let path: string | undefined;
|
|
44
|
+
let hasChange = false;
|
|
45
|
+
|
|
46
|
+
const flush = (): void => {
|
|
47
|
+
if (filetype) {
|
|
48
|
+
if (oldLines.length && (hasChange || newLines.length === 0))
|
|
49
|
+
jobs.push({ filetype, source: oldLines.join("\n"), rowIndexByLine: oldRowIndexByLine });
|
|
50
|
+
if (newLines.length)
|
|
51
|
+
jobs.push({ filetype, source: newLines.join("\n"), rowIndexByLine: newRowIndexByLine });
|
|
52
|
+
}
|
|
53
|
+
oldLines = [];
|
|
54
|
+
newLines = [];
|
|
55
|
+
oldRowIndexByLine = [];
|
|
56
|
+
newRowIndexByLine = [];
|
|
57
|
+
hasChange = false;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
|
|
61
|
+
const row = rows[rowIndex]!;
|
|
62
|
+
|
|
63
|
+
if (row.file !== path) {
|
|
64
|
+
flush();
|
|
65
|
+
path = row.file;
|
|
66
|
+
filetype = filetypeForPath(path);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (row.kind === "file" || row.kind === "hunk") {
|
|
70
|
+
flush();
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const text = stripTrailingNewline(row.text);
|
|
74
|
+
|
|
75
|
+
if (row.kind === "ctx" || row.kind === "del") {
|
|
76
|
+
oldLines.push(text);
|
|
77
|
+
oldRowIndexByLine.push(rowIndex);
|
|
78
|
+
}
|
|
79
|
+
if (row.kind === "ctx" || row.kind === "add") {
|
|
80
|
+
newLines.push(text);
|
|
81
|
+
newRowIndexByLine.push(rowIndex);
|
|
82
|
+
}
|
|
83
|
+
if (row.kind === "add" || row.kind === "del") hasChange = true;
|
|
84
|
+
}
|
|
85
|
+
flush();
|
|
86
|
+
|
|
87
|
+
return jobs;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve one source's flat, possibly-overlapping token highlights into
|
|
92
|
+
* per-line spans. Offsets are absolute into `source`; later highlights win on
|
|
93
|
+
* overlap, and a highlight is clamped to the line its start falls on.
|
|
94
|
+
*/
|
|
95
|
+
export function spansByLine(source: string, highlights: SimpleHighlight[]): SyntaxSpan[][] {
|
|
96
|
+
const lineStarts: number[] = [0];
|
|
97
|
+
|
|
98
|
+
for (let index = 0; index < source.length; index++) {
|
|
99
|
+
if (source[index] === "\n") lineStarts.push(index + 1);
|
|
100
|
+
}
|
|
101
|
+
const lineOf = (offset: number): number => {
|
|
102
|
+
let low = 0;
|
|
103
|
+
let high = lineStarts.length - 1;
|
|
104
|
+
|
|
105
|
+
while (low < high) {
|
|
106
|
+
const mid = (low + high + 1) >> 1;
|
|
107
|
+
|
|
108
|
+
if (lineStarts[mid]! <= offset) low = mid;
|
|
109
|
+
else high = mid - 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return low;
|
|
113
|
+
};
|
|
114
|
+
// Per-line char->group, later highlights overriding, then coalesce into spans.
|
|
115
|
+
const groupsByLine: Array<Array<string | undefined>> = lineStarts.map((start, line) => {
|
|
116
|
+
const end = line + 1 < lineStarts.length ? lineStarts[line + 1]! - 1 : source.length;
|
|
117
|
+
|
|
118
|
+
return Array.from({ length: Math.max(0, end - start) }, () => undefined);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
for (const [start, rawEnd, group] of highlights) {
|
|
122
|
+
// A capture can span newlines (block comments, template literals); color the
|
|
123
|
+
// covered columns on every line it crosses, not just the starting line. Offsets
|
|
124
|
+
// past the source (byte offsets from a multi-byte line) clamp to its end, or the
|
|
125
|
+
// walk below would sit on the last line forever.
|
|
126
|
+
const end = Math.min(rawEnd, source.length);
|
|
127
|
+
let cursor = Math.max(0, start);
|
|
128
|
+
|
|
129
|
+
while (cursor < end) {
|
|
130
|
+
const line = lineOf(cursor);
|
|
131
|
+
const lineStart = lineStarts[line]!;
|
|
132
|
+
const lineEnd = line + 1 < lineStarts.length ? lineStarts[line + 1]! - 1 : source.length;
|
|
133
|
+
const chars = groupsByLine[line]!;
|
|
134
|
+
const to = Math.min(end, lineEnd) - lineStart;
|
|
135
|
+
|
|
136
|
+
for (let column = cursor - lineStart; column < to; column++) chars[column] = group;
|
|
137
|
+
cursor = lineEnd + 1;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return groupsByLine.map((chars) => {
|
|
142
|
+
const spans: SyntaxSpan[] = [];
|
|
143
|
+
|
|
144
|
+
for (let column = 0; column < chars.length; column++) {
|
|
145
|
+
const group = chars[column];
|
|
146
|
+
|
|
147
|
+
if (group === undefined) continue;
|
|
148
|
+
const previous = spans[spans.length - 1];
|
|
149
|
+
|
|
150
|
+
if (previous && previous.group === group && previous.end === column)
|
|
151
|
+
previous.end = column + 1;
|
|
152
|
+
else spans.push({ start: column, end: column + 1, group });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return spans;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Highlight the diff's rows and return the token spans per row index. Rows in a
|
|
161
|
+
* file with no known filetype, and any row while highlighting is still resolving,
|
|
162
|
+
* are absent - the caller renders those unstyled.
|
|
163
|
+
*/
|
|
164
|
+
export async function highlightDiffRows(rows: DiffRow[]): Promise<Map<number, SyntaxSpan[]>> {
|
|
165
|
+
const spansByRow = new Map<number, SyntaxSpan[]>();
|
|
166
|
+
const jobs = highlightJobs(rows);
|
|
167
|
+
|
|
168
|
+
if (!jobs.length) return spansByRow;
|
|
169
|
+
const client = getTreeSitterClient();
|
|
170
|
+
|
|
171
|
+
await client.initialize();
|
|
172
|
+
for (const job of jobs) {
|
|
173
|
+
const result = await client.highlightOnce(job.source, job.filetype);
|
|
174
|
+
|
|
175
|
+
if (!result.highlights) continue;
|
|
176
|
+
const lineSpans = spansByLine(job.source, result.highlights);
|
|
177
|
+
|
|
178
|
+
job.rowIndexByLine.forEach((rowIndex, line) => {
|
|
179
|
+
const spans = lineSpans[line];
|
|
180
|
+
|
|
181
|
+
if (spans && spans.length) spansByRow.set(rowIndex, spans);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return spansByRow;
|
|
186
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { diffRows } from "./view-diff";
|
|
3
|
+
import { splitDiffRows, splitRowOffsets, type SplitRow } from "./split-diff";
|
|
4
|
+
|
|
5
|
+
const PATCH = `diff --git a/greet.ts b/greet.ts
|
|
6
|
+
--- a/greet.ts
|
|
7
|
+
+++ b/greet.ts
|
|
8
|
+
@@ -1,4 +1,4 @@
|
|
9
|
+
const name = "world";
|
|
10
|
+
-console.log("hi " + name);
|
|
11
|
+
-console.log("done");
|
|
12
|
+
+console.log("hello " + name);
|
|
13
|
+
+console.log("finished");
|
|
14
|
+
export {};
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
function pairs(rows: SplitRow[]): SplitRow[] {
|
|
18
|
+
return rows.filter((row) => row.kind === "pair");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("splitDiffRows", () => {
|
|
22
|
+
test("carries the file and hunk headers through unchanged", () => {
|
|
23
|
+
// Act
|
|
24
|
+
const split = splitDiffRows(diffRows(PATCH));
|
|
25
|
+
|
|
26
|
+
// Assert
|
|
27
|
+
expect(split[0]).toMatchObject({ kind: "file", file: "greet.ts", text: "greet.ts" });
|
|
28
|
+
expect(split[1]?.kind).toBe("hunk");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("a context line sits on both sides with its own line numbers", () => {
|
|
32
|
+
// Act
|
|
33
|
+
const first = pairs(splitDiffRows(diffRows(PATCH)))[0]!;
|
|
34
|
+
|
|
35
|
+
// Assert
|
|
36
|
+
expect(first.left).toMatchObject({ kind: "ctx", lineNumber: 1 });
|
|
37
|
+
expect(first.right).toMatchObject({ kind: "ctx", lineNumber: 1 });
|
|
38
|
+
expect(first.left?.text).toBe(first.right?.text);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("a balanced change block pairs each deletion against an addition", () => {
|
|
42
|
+
// Act - the two deletions zip against the two additions, one per row
|
|
43
|
+
const changed = pairs(splitDiffRows(diffRows(PATCH))).filter(
|
|
44
|
+
(row) => row.left?.kind === "del" || row.right?.kind === "add",
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Assert
|
|
48
|
+
expect(changed).toHaveLength(2);
|
|
49
|
+
expect(changed[0]).toMatchObject({
|
|
50
|
+
left: { kind: "del", lineNumber: 2 },
|
|
51
|
+
right: { kind: "add", lineNumber: 2 },
|
|
52
|
+
});
|
|
53
|
+
expect(changed[1]).toMatchObject({
|
|
54
|
+
left: { kind: "del", lineNumber: 3 },
|
|
55
|
+
right: { kind: "add", lineNumber: 3 },
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("an unbalanced block blank-fills the shorter side", () => {
|
|
60
|
+
// Arrange - one deletion, three additions
|
|
61
|
+
const patch = `diff --git a/x.ts b/x.ts
|
|
62
|
+
--- a/x.ts
|
|
63
|
+
+++ b/x.ts
|
|
64
|
+
@@ -1,2 +1,4 @@
|
|
65
|
+
keep;
|
|
66
|
+
-old;
|
|
67
|
+
+new1;
|
|
68
|
+
+new2;
|
|
69
|
+
+new3;
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
// Act
|
|
73
|
+
const changed = pairs(splitDiffRows(diffRows(patch))).filter(
|
|
74
|
+
(row) => row.left?.kind === "del" || row.right?.kind === "add",
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Assert - three rows tall, only the first has a left deletion
|
|
78
|
+
expect(changed).toHaveLength(3);
|
|
79
|
+
expect(changed[0]?.left?.kind).toBe("del");
|
|
80
|
+
expect(changed[1]?.left).toBeUndefined();
|
|
81
|
+
expect(changed[2]?.left).toBeUndefined();
|
|
82
|
+
expect(changed.map((row) => row.right?.text.trim())).toEqual(["new1;", "new2;", "new3;"]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("each side keeps its originating unified row for anchoring", () => {
|
|
86
|
+
// Act
|
|
87
|
+
const base = diffRows(PATCH);
|
|
88
|
+
const changed = pairs(splitDiffRows(base)).find((row) => row.right?.kind === "add")!;
|
|
89
|
+
|
|
90
|
+
// Assert - the split line points back at a row from the base list
|
|
91
|
+
expect(base[changed.right!.rowIndex]).toBe(changed.right!.row);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("splitRowOffsets", () => {
|
|
96
|
+
test("maps both sides of a pair to the same y and steps a file band by three", () => {
|
|
97
|
+
// Arrange
|
|
98
|
+
const base = diffRows(PATCH);
|
|
99
|
+
const split = splitDiffRows(base);
|
|
100
|
+
|
|
101
|
+
// Act
|
|
102
|
+
const offsets = splitRowOffsets(split);
|
|
103
|
+
|
|
104
|
+
// Assert - the file band is three rows and the hunk one, so the first context pair sits at y 4
|
|
105
|
+
const firstContext = split.find((row) => row.kind === "pair" && row.left?.kind === "ctx")!;
|
|
106
|
+
|
|
107
|
+
expect(offsets.get(firstContext.left!.rowIndex)).toBe(4);
|
|
108
|
+
expect(offsets.get(firstContext.right!.rowIndex)).toBe(4);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side-by-side diff projection: fold the unified DiffRow list into paired rows,
|
|
3
|
+
* old on the left and new on the right. A context line sits on both sides; a
|
|
4
|
+
* change block zips its deletions against its additions to the taller side, with
|
|
5
|
+
* a blank filler where one side runs out. Each side keeps its originating unified
|
|
6
|
+
* row, so the cursor, anchors, and curation keep working through the base rows.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { DiffRow } from "./view-diff";
|
|
10
|
+
|
|
11
|
+
/** One code line on one side of the split, carrying its originating unified row. */
|
|
12
|
+
export interface SplitLine {
|
|
13
|
+
row: DiffRow;
|
|
14
|
+
rowIndex: number;
|
|
15
|
+
kind: "ctx" | "del" | "add";
|
|
16
|
+
text: string;
|
|
17
|
+
lineNumber?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A rendered split row: a file band, a hunk header, or an old|new line pair. */
|
|
21
|
+
export interface SplitRow {
|
|
22
|
+
kind: "file" | "hunk" | "pair";
|
|
23
|
+
file: string;
|
|
24
|
+
text?: string;
|
|
25
|
+
rowIndex?: number;
|
|
26
|
+
left?: SplitLine;
|
|
27
|
+
right?: SplitLine;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A base row paired with its index in the base list. */
|
|
31
|
+
interface IndexedRow {
|
|
32
|
+
row: DiffRow;
|
|
33
|
+
index: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function contextLine({ row, index }: IndexedRow, side: "left" | "right"): SplitLine {
|
|
37
|
+
return {
|
|
38
|
+
row,
|
|
39
|
+
rowIndex: index,
|
|
40
|
+
kind: "ctx",
|
|
41
|
+
text: row.text,
|
|
42
|
+
lineNumber: side === "left" ? row.oldLine : row.newLine,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function deletionLine({ row, index }: IndexedRow): SplitLine {
|
|
47
|
+
return { row, rowIndex: index, kind: "del", text: row.text, lineNumber: row.oldLine };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function additionLine({ row, index }: IndexedRow): SplitLine {
|
|
51
|
+
return { row, rowIndex: index, kind: "add", text: row.text, lineNumber: row.newLine };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Zip a change block's deletions against its additions, blank-filling the shorter side. */
|
|
55
|
+
function zipChangeBlock(
|
|
56
|
+
deletions: IndexedRow[],
|
|
57
|
+
additions: IndexedRow[],
|
|
58
|
+
file: string,
|
|
59
|
+
): SplitRow[] {
|
|
60
|
+
const pairs: SplitRow[] = [];
|
|
61
|
+
const height = Math.max(deletions.length, additions.length);
|
|
62
|
+
|
|
63
|
+
for (let index = 0; index < height; index++) {
|
|
64
|
+
const del = deletions[index];
|
|
65
|
+
const add = additions[index];
|
|
66
|
+
|
|
67
|
+
pairs.push({
|
|
68
|
+
kind: "pair",
|
|
69
|
+
file,
|
|
70
|
+
left: del ? deletionLine(del) : undefined,
|
|
71
|
+
right: add ? additionLine(add) : undefined,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return pairs;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Project unified diff rows into side-by-side split rows. */
|
|
79
|
+
export function splitDiffRows(rows: DiffRow[]): SplitRow[] {
|
|
80
|
+
const split: SplitRow[] = [];
|
|
81
|
+
let pendingDeletions: IndexedRow[] = [];
|
|
82
|
+
let pendingAdditions: IndexedRow[] = [];
|
|
83
|
+
let blockFile = "";
|
|
84
|
+
|
|
85
|
+
const flushChangeBlock = (): void => {
|
|
86
|
+
if (pendingDeletions.length === 0 && pendingAdditions.length === 0) return;
|
|
87
|
+
split.push(...zipChangeBlock(pendingDeletions, pendingAdditions, blockFile));
|
|
88
|
+
pendingDeletions = [];
|
|
89
|
+
pendingAdditions = [];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
rows.forEach((row, index) => {
|
|
93
|
+
if (row.kind === "add") {
|
|
94
|
+
blockFile = row.file;
|
|
95
|
+
pendingAdditions.push({ row, index });
|
|
96
|
+
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (row.kind === "del") {
|
|
100
|
+
blockFile = row.file;
|
|
101
|
+
pendingDeletions.push({ row, index });
|
|
102
|
+
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// any non-change row closes the current change block before it renders
|
|
106
|
+
flushChangeBlock();
|
|
107
|
+
if (row.kind === "file" || row.kind === "hunk") {
|
|
108
|
+
split.push({ kind: row.kind, file: row.file, text: row.text, rowIndex: index });
|
|
109
|
+
} else
|
|
110
|
+
split.push({
|
|
111
|
+
kind: "pair",
|
|
112
|
+
file: row.file,
|
|
113
|
+
left: contextLine({ row, index }, "left"),
|
|
114
|
+
right: contextLine({ row, index }, "right"),
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
flushChangeBlock();
|
|
118
|
+
|
|
119
|
+
return split;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Visual y of each base row index in the split layout, so the cursor scrolls into
|
|
124
|
+
* view. A file band spans three rows (rule, name, rule), a hunk one, a pair one; a
|
|
125
|
+
* pair's two sides share the row, so both base indices map to the same y.
|
|
126
|
+
*/
|
|
127
|
+
export function splitRowOffsets(splitRows: SplitRow[]): Map<number, number> {
|
|
128
|
+
const offsets = new Map<number, number>();
|
|
129
|
+
let contentY = 0;
|
|
130
|
+
|
|
131
|
+
for (const row of splitRows) {
|
|
132
|
+
if (row.kind === "file") {
|
|
133
|
+
contentY += 3;
|
|
134
|
+
} else if (row.kind === "hunk") {
|
|
135
|
+
contentY += 1;
|
|
136
|
+
} else {
|
|
137
|
+
if (row.left) offsets.set(row.left.rowIndex, contentY);
|
|
138
|
+
if (row.right) offsets.set(row.right.rowIndex, contentY);
|
|
139
|
+
contentY += 1;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return offsets;
|
|
144
|
+
}
|