@cueloop/client 0.1.0-alpha.10 → 0.1.0-alpha.101
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 +369 -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 +1243 -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 +20 -0
- package/src/app/App.toast.test.tsx +124 -0
- package/src/app/App.tsx +1667 -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.agent.test.tsx +50 -0
- package/src/integrations/components/ShareDialog.stories.tsx +121 -0
- package/src/integrations/components/ShareDialog.tsx +574 -0
- package/src/integrations/components/share-dialog-store.ts +59 -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 +935 -0
- package/src/thread/components/AgentThreadPane.tsx +377 -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 +2026 -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 +117 -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,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The native SyntaxStyle for the inline markdown editor: maps each markdown
|
|
3
|
+
* token group to a theme color and attribute set, and resolves the styleId the
|
|
4
|
+
* edit buffer needs for `addHighlightByCharRange`. Cached per theme, mirroring
|
|
5
|
+
* syntaxStyleFor. Syntax punctuation (the "marker" group) is dimmed rather than
|
|
6
|
+
* hidden, since the editor shows raw source.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { SyntaxStyle, type StyleDefinitionInput } from "@opentui/core";
|
|
10
|
+
import { type Theme } from "../appearance/theme";
|
|
11
|
+
import { MARKDOWN_HIGHLIGHT_GROUPS, type MarkdownHighlightGroup } from "./markdown-highlight";
|
|
12
|
+
|
|
13
|
+
/** A markdown SyntaxStyle plus the lookup from token group to its native styleId. */
|
|
14
|
+
export interface MarkdownEditorStyle {
|
|
15
|
+
style: SyntaxStyle;
|
|
16
|
+
styleIdFor: (group: MarkdownHighlightGroup) => number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function markdownGroupStyles(theme: Theme): Record<MarkdownHighlightGroup, StyleDefinitionInput> {
|
|
20
|
+
return {
|
|
21
|
+
heading: { bold: true },
|
|
22
|
+
marker: { fg: theme.textDim },
|
|
23
|
+
link: { fg: theme.blue, underline: true },
|
|
24
|
+
code: { fg: theme.textDim },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const markdownEditorStyleCache = new WeakMap<Theme, MarkdownEditorStyle>();
|
|
29
|
+
|
|
30
|
+
/** Build (or reuse) the markdown editor's SyntaxStyle for a theme, with a group-to-styleId lookup. */
|
|
31
|
+
export function markdownEditorStyle(theme: Theme): MarkdownEditorStyle {
|
|
32
|
+
const cached = markdownEditorStyleCache.get(theme);
|
|
33
|
+
|
|
34
|
+
if (cached) return cached;
|
|
35
|
+
const groupStyles = markdownGroupStyles(theme);
|
|
36
|
+
const style = SyntaxStyle.fromStyles(groupStyles);
|
|
37
|
+
const styleIds = new Map<MarkdownHighlightGroup, number>();
|
|
38
|
+
|
|
39
|
+
for (const group of MARKDOWN_HIGHLIGHT_GROUPS) {
|
|
40
|
+
styleIds.set(group, style.getStyleId(group) ?? 0);
|
|
41
|
+
}
|
|
42
|
+
const entry: MarkdownEditorStyle = {
|
|
43
|
+
style,
|
|
44
|
+
styleIdFor: (group) => styleIds.get(group) ?? 0,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
markdownEditorStyleCache.set(theme, entry);
|
|
48
|
+
|
|
49
|
+
return entry;
|
|
50
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
layoutFrontmatterGrid,
|
|
4
|
+
layoutMarkdownTable,
|
|
5
|
+
parseFrontmatterRows,
|
|
6
|
+
parseMarkdownTable,
|
|
7
|
+
} from "./markdown-grid";
|
|
8
|
+
|
|
9
|
+
/** The plain text of a grid line, segments concatenated. */
|
|
10
|
+
function lineText(line: { text: string }[]): string {
|
|
11
|
+
return line.map((segment) => segment.text).join("");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("parseMarkdownTable", () => {
|
|
15
|
+
test("reads header cells, alignments, and body rows", () => {
|
|
16
|
+
const table = parseMarkdownTable("| Name | Size |\n| :--- | ---: |\n| a | 1 |\n| bb | 22 |");
|
|
17
|
+
|
|
18
|
+
expect(table.header).toEqual(["Name", "Size"]);
|
|
19
|
+
expect(table.aligns).toEqual(["left", "right"]);
|
|
20
|
+
expect(table.rows).toEqual([
|
|
21
|
+
["a", "1"],
|
|
22
|
+
["bb", "22"],
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("center alignment reads from a :---: delimiter", () => {
|
|
27
|
+
expect(parseMarkdownTable("| a |\n| :---: |\n| x |").aligns).toEqual(["center"]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("an escaped pipe stays inside its cell instead of splitting the column", () => {
|
|
31
|
+
const table = parseMarkdownTable("| Name | Note |\n| --- | --- |\n| a | b \\| c |");
|
|
32
|
+
|
|
33
|
+
expect(table.rows).toEqual([["a", "b | c"]]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("a blank cell between border pipes survives the border strip", () => {
|
|
37
|
+
expect(parseMarkdownTable("| | b |\n| --- | --- |\n| x | y |").header).toEqual(["", "b"]);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("layoutMarkdownTable", () => {
|
|
42
|
+
test("bolds the header, rules under it, and right-aligns a numeric column", () => {
|
|
43
|
+
const lines = layoutMarkdownTable(
|
|
44
|
+
parseMarkdownTable("| Name | Size |\n| --- | ---: |\n| a | 1 |"),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(lines[0]![0]!.bold).toBe(true);
|
|
48
|
+
expect(lineText(lines[0]!)).toBe("Name Size");
|
|
49
|
+
expect(lines[1]![0]!.dim).toBe(true);
|
|
50
|
+
expect(lineText(lines[1]!)).toBe("─".repeat("Name Size".length));
|
|
51
|
+
// "a" padded to the "Name" column, "1" right-aligned under "Size"
|
|
52
|
+
expect(lineText(lines[2]!)).toBe("a" + " ".repeat(8) + "1");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("parseFrontmatterRows", () => {
|
|
57
|
+
test("splits top-level key: value pairs", () => {
|
|
58
|
+
expect(parseFrontmatterRows("title: Plan\nowner: me")).toEqual([
|
|
59
|
+
{ key: "title", value: "Plan" },
|
|
60
|
+
{ key: "owner", value: "me" },
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("folds an indented continuation into the row above", () => {
|
|
65
|
+
expect(parseFrontmatterRows("tags:\n - one\n - two")).toEqual([
|
|
66
|
+
{ key: "tags", value: "- one\n- two" },
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("layoutFrontmatterGrid", () => {
|
|
72
|
+
test("draws a full border with a bold key and a rule between rows", () => {
|
|
73
|
+
const lines = layoutFrontmatterGrid(
|
|
74
|
+
[
|
|
75
|
+
{ key: "title", value: "Plan" },
|
|
76
|
+
{ key: "owner", value: "me" },
|
|
77
|
+
],
|
|
78
|
+
40,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
expect(lineText(lines[0]!).startsWith("┌")).toBe(true);
|
|
82
|
+
expect(lineText(lines.at(-1)!).startsWith("└")).toBe(true);
|
|
83
|
+
// the key cell is bold, the border pieces are dim
|
|
84
|
+
const firstRow = lines[1]!;
|
|
85
|
+
|
|
86
|
+
expect(firstRow.find((segment) => segment.text.includes("title"))!.bold).toBe(true);
|
|
87
|
+
expect(firstRow[0]!.dim).toBe(true);
|
|
88
|
+
// a divider separates the two rows
|
|
89
|
+
expect(lines.some((line) => lineText(line).startsWith("├"))).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("wraps a long value across cell lines, keeping the key on the first", () => {
|
|
93
|
+
const lines = layoutFrontmatterGrid([{ key: "desc", value: "one two three four five" }], 24);
|
|
94
|
+
const bodyLines = lines.filter((line) => lineText(line).startsWith("│"));
|
|
95
|
+
|
|
96
|
+
expect(bodyLines.length).toBeGreaterThan(1);
|
|
97
|
+
expect(bodyLines[0]!.some((segment) => segment.text.includes("desc"))).toBe(true);
|
|
98
|
+
// the continuation line keeps the key column blank
|
|
99
|
+
expect(bodyLines[1]!.some((segment) => segment.text.includes("desc"))).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid layout for the two block kinds the read-only thread renders as tables:
|
|
3
|
+
* a GFM table (bold header over a rule, columns aligned per the delimiter row,
|
|
4
|
+
* no vertical borders) and leading YAML
|
|
5
|
+
* frontmatter (a full-bordered key/value grid, bold nowrap keys, wrapped
|
|
6
|
+
* values). Both are pure: they turn a block's source text and a width into
|
|
7
|
+
* positioned, styled segments the renderer prints verbatim, so a char-frame
|
|
8
|
+
* test can assert the exact grid.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { wrapLines } from "./mark-runs";
|
|
12
|
+
|
|
13
|
+
/** One painted piece of a grid line: its text plus whether it reads bold or dim (a border). */
|
|
14
|
+
export interface GridSegment {
|
|
15
|
+
text: string;
|
|
16
|
+
bold: boolean;
|
|
17
|
+
dim: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A grid line is left-to-right segments; concatenating their text gives the row. */
|
|
21
|
+
export type GridLine = GridSegment[];
|
|
22
|
+
|
|
23
|
+
export type ColumnAlign = "left" | "right" | "center";
|
|
24
|
+
|
|
25
|
+
export interface MarkdownTable {
|
|
26
|
+
aligns: ColumnAlign[];
|
|
27
|
+
header: string[];
|
|
28
|
+
rows: string[][];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Split a table row into trimmed cells on unescaped pipes, unescaping `\|` into a
|
|
33
|
+
* literal pipe, and dropping the empty cells an optional leading or trailing
|
|
34
|
+
* border pipe produces (an intentional blank cell keeps its surrounding spaces).
|
|
35
|
+
*/
|
|
36
|
+
function splitTableRow(line: string): string[] {
|
|
37
|
+
const trimmed = line.trim();
|
|
38
|
+
const parts: string[] = [];
|
|
39
|
+
let cell = "";
|
|
40
|
+
|
|
41
|
+
for (let index = 0; index < trimmed.length; index++) {
|
|
42
|
+
const char = trimmed[index]!;
|
|
43
|
+
|
|
44
|
+
if (char === "\\" && trimmed[index + 1] === "|") {
|
|
45
|
+
cell += "|";
|
|
46
|
+
index++;
|
|
47
|
+
} else if (char === "|") {
|
|
48
|
+
parts.push(cell);
|
|
49
|
+
cell = "";
|
|
50
|
+
} else {
|
|
51
|
+
cell += char;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
parts.push(cell);
|
|
55
|
+
if (parts.length > 1 && parts[0] === "") parts.shift();
|
|
56
|
+
if (parts.length > 1 && parts[parts.length - 1] === "") parts.pop();
|
|
57
|
+
|
|
58
|
+
return parts.map((part) => part.trim());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Parse a GFM table block's verbatim source into header cells, column alignments, and body rows. */
|
|
62
|
+
export function parseMarkdownTable(text: string): MarkdownTable {
|
|
63
|
+
const lines = text.split("\n").filter((line) => line.trim() !== "");
|
|
64
|
+
const header = splitTableRow(lines[0] ?? "");
|
|
65
|
+
const aligns: ColumnAlign[] = splitTableRow(lines[1] ?? "").map((cell) => {
|
|
66
|
+
const left = cell.startsWith(":");
|
|
67
|
+
const right = cell.endsWith(":");
|
|
68
|
+
|
|
69
|
+
return left && right ? "center" : right ? "right" : "left";
|
|
70
|
+
});
|
|
71
|
+
const rows = lines.slice(2).map(splitTableRow);
|
|
72
|
+
|
|
73
|
+
return { aligns, header, rows };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Pad `text` to `width` cells under an alignment. */
|
|
77
|
+
function pad(text: string, width: number, align: ColumnAlign): string {
|
|
78
|
+
const slack = Math.max(0, width - text.length);
|
|
79
|
+
|
|
80
|
+
if (align === "right") return " ".repeat(slack) + text;
|
|
81
|
+
if (align === "center") {
|
|
82
|
+
const left = Math.floor(slack / 2);
|
|
83
|
+
|
|
84
|
+
return " ".repeat(left) + text + " ".repeat(slack - left);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return text + " ".repeat(slack);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Two spaces separate table columns; the read-only render draws no vertical rule. */
|
|
91
|
+
const COLUMN_GAP = " ";
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Lay out a GFM table: a bold header row, a dim rule spanning the table width,
|
|
95
|
+
* then the body rows, every column padded to its widest cell under its alignment.
|
|
96
|
+
*/
|
|
97
|
+
export function layoutMarkdownTable(table: MarkdownTable): GridLine[] {
|
|
98
|
+
const columnCount = Math.max(table.header.length, ...table.rows.map((row) => row.length), 1);
|
|
99
|
+
const alignOf = (column: number): ColumnAlign => table.aligns[column] ?? "left";
|
|
100
|
+
const cellAt = (cells: string[], column: number): string => cells[column] ?? "";
|
|
101
|
+
const widths = Array.from({ length: columnCount }, (_unused, column) =>
|
|
102
|
+
Math.max(
|
|
103
|
+
cellAt(table.header, column).length,
|
|
104
|
+
...table.rows.map((row) => cellAt(row, column).length),
|
|
105
|
+
1,
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
const rowText = (cells: string[]): string =>
|
|
109
|
+
widths
|
|
110
|
+
.map((width, column) => pad(cellAt(cells, column), width, alignOf(column)))
|
|
111
|
+
.join(COLUMN_GAP);
|
|
112
|
+
const totalWidth =
|
|
113
|
+
widths.reduce((sum, width) => sum + width, 0) + COLUMN_GAP.length * (columnCount - 1);
|
|
114
|
+
const lines: GridLine[] = [
|
|
115
|
+
[{ text: rowText(table.header), bold: true, dim: false }],
|
|
116
|
+
[{ text: "─".repeat(totalWidth), bold: false, dim: true }],
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
for (const row of table.rows) lines.push([{ text: rowText(row), bold: false, dim: false }]);
|
|
120
|
+
|
|
121
|
+
return lines;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface FrontmatterRow {
|
|
125
|
+
key: string;
|
|
126
|
+
value: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Parse frontmatter YAML into top-level key/value rows; indented lines fold into the row above. */
|
|
130
|
+
export function parseFrontmatterRows(text: string): FrontmatterRow[] {
|
|
131
|
+
const rows: FrontmatterRow[] = [];
|
|
132
|
+
|
|
133
|
+
for (const line of text.split("\n")) {
|
|
134
|
+
if (line.trim() === "") continue;
|
|
135
|
+
const match = /^([^:\s][^:]*):\s?(.*)$/.exec(line);
|
|
136
|
+
|
|
137
|
+
if (match) {
|
|
138
|
+
rows.push({ key: match[1]!.trim(), value: match[2]! });
|
|
139
|
+
} else if (rows.length > 0) {
|
|
140
|
+
const previous = rows[rows.length - 1]!;
|
|
141
|
+
|
|
142
|
+
previous.value = previous.value === "" ? line.trim() : `${previous.value}\n${line.trim()}`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return rows;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The fixed cells a frontmatter grid row spends on borders and padding: `│ key │ value │`. */
|
|
150
|
+
const FRONTMATTER_CHROME = 7;
|
|
151
|
+
|
|
152
|
+
/** Wrap a cell value into lines no wider than `width`, honouring its own line breaks. */
|
|
153
|
+
function wrapCell(value: string, width: number): string[] {
|
|
154
|
+
if (value === "") return [""];
|
|
155
|
+
|
|
156
|
+
return wrapLines(value, width).map((line) => value.slice(line.start, line.end));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Lay out leading frontmatter as a full-bordered key/value grid: bold nowrap
|
|
161
|
+
* keys on the left, values wrapped to the remaining width on the right, a rule
|
|
162
|
+
* between every row.
|
|
163
|
+
*/
|
|
164
|
+
export function layoutFrontmatterGrid(rows: FrontmatterRow[], maxWidth: number): GridLine[] {
|
|
165
|
+
if (rows.length === 0) return [];
|
|
166
|
+
const keyWidth = Math.max(3, ...rows.map((row) => row.key.length));
|
|
167
|
+
// size the value column to its content, capped at the width left after the key column and borders,
|
|
168
|
+
// so a short frontmatter table stays compact and a long value wraps instead of overflowing
|
|
169
|
+
const available = Math.max(3, maxWidth - keyWidth - FRONTMATTER_CHROME);
|
|
170
|
+
const naturalWidth = Math.max(
|
|
171
|
+
3,
|
|
172
|
+
...rows.map((row) => Math.max(0, ...row.value.split("\n").map((line) => line.length))),
|
|
173
|
+
);
|
|
174
|
+
const valueWidth = Math.min(naturalWidth, available);
|
|
175
|
+
const rule = (left: string, mid: string, right: string): GridLine => [
|
|
176
|
+
{
|
|
177
|
+
text: left + "─".repeat(keyWidth + 2) + mid + "─".repeat(valueWidth + 2) + right,
|
|
178
|
+
bold: false,
|
|
179
|
+
dim: true,
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
const lines: GridLine[] = [rule("┌", "┬", "┐")];
|
|
183
|
+
|
|
184
|
+
rows.forEach((row, rowIndex) => {
|
|
185
|
+
if (rowIndex > 0) lines.push(rule("├", "┼", "┤"));
|
|
186
|
+
const valueLines = wrapCell(row.value, valueWidth);
|
|
187
|
+
|
|
188
|
+
valueLines.forEach((valueLine, valueIndex) => {
|
|
189
|
+
const keyCell = valueIndex === 0 ? row.key.padEnd(keyWidth) : " ".repeat(keyWidth);
|
|
190
|
+
|
|
191
|
+
lines.push([
|
|
192
|
+
{ text: "│ ", bold: false, dim: true },
|
|
193
|
+
{ text: keyCell, bold: valueIndex === 0, dim: false },
|
|
194
|
+
{ text: " │ ", bold: false, dim: true },
|
|
195
|
+
{ text: valueLine.padEnd(valueWidth), bold: false, dim: false },
|
|
196
|
+
{ text: " │", bold: false, dim: true },
|
|
197
|
+
]);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
lines.push(rule("└", "┴", "┘"));
|
|
201
|
+
|
|
202
|
+
return lines;
|
|
203
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { markdownHighlightRanges, type MarkdownHighlightGroup } from "./markdown-highlight";
|
|
3
|
+
|
|
4
|
+
/** The substring a range covers, paired with its group, for readable assertions. */
|
|
5
|
+
function painted(source: string): Array<{ text: string; group: MarkdownHighlightGroup }> {
|
|
6
|
+
return markdownHighlightRanges(source).map((range) => ({
|
|
7
|
+
text: source.slice(range.start, range.end),
|
|
8
|
+
group: range.group,
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("markdownHighlightRanges", () => {
|
|
13
|
+
test("a heading dims its marker and bolds the text without coloring it", () => {
|
|
14
|
+
expect(painted("## Rollout Plan")).toEqual([
|
|
15
|
+
{ text: "## ", group: "marker" },
|
|
16
|
+
{ text: "Rollout Plan", group: "heading" },
|
|
17
|
+
]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("a plain link colors its brackets and href but leaves the label plain", () => {
|
|
21
|
+
const runs = painted("see [docs](https://x.dev)");
|
|
22
|
+
|
|
23
|
+
expect(runs).toContainEqual({ text: "[", group: "link" });
|
|
24
|
+
expect(runs).toContainEqual({ text: "](https://x.dev)", group: "link" });
|
|
25
|
+
expect(runs.some((run) => run.text.includes("docs"))).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("an @-scope link label colors along with the brackets and href", () => {
|
|
29
|
+
expect(painted("[@pierre/diffs](https://npm/@pierre/diffs)")).toEqual([
|
|
30
|
+
{ text: "[@pierre/diffs](https://npm/@pierre/diffs)", group: "link" },
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("inline code grays the whole backtick span", () => {
|
|
35
|
+
expect(painted("call `run()` now")).toContainEqual({ text: "`run()`", group: "code" });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("a fenced code block grays the whole block", () => {
|
|
39
|
+
const source = "```ts\nconst a = 1;\n```";
|
|
40
|
+
|
|
41
|
+
expect(painted(source)).toEqual([{ text: source, group: "code" }]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("inline code inside a list item resolves against the whole source offset", () => {
|
|
45
|
+
const source = "- run `cueloop dev`";
|
|
46
|
+
const code = markdownHighlightRanges(source).find((range) => range.group === "code");
|
|
47
|
+
|
|
48
|
+
expect(code && source.slice(code.start, code.end)).toBe("`cueloop dev`");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("bold, emphasis, list markers, quotes, and rules stay plain", () => {
|
|
52
|
+
expect(markdownHighlightRanges("ship **now** or _later_")).toEqual([]);
|
|
53
|
+
expect(markdownHighlightRanges("- a task")).toEqual([]);
|
|
54
|
+
expect(markdownHighlightRanges("> quoted line")).toEqual([]);
|
|
55
|
+
expect(markdownHighlightRanges("---")).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("plain prose yields no ranges", () => {
|
|
59
|
+
expect(markdownHighlightRanges("just words here")).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scan raw markdown source into highlight ranges for the inline thread editor.
|
|
3
|
+
* The scheme is deliberately restrained, matching a plain markdown editor: a
|
|
4
|
+
* heading dims its `#` marker and bolds the title text (no color); links render
|
|
5
|
+
* in one link color; inline and fenced code gray out. Everything else - bold,
|
|
6
|
+
* emphasis, list and quote markers, rules - stays plain source. Block structure
|
|
7
|
+
* reuses the schema parser so a heading or code fence is defined in one place.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { parseBlocks, LEADING_BLOCK_MARKER, type Block } from "@cueloop/schema";
|
|
11
|
+
|
|
12
|
+
/** Every markdown token class the editor paints; "marker" is the dimmed heading punctuation. */
|
|
13
|
+
export const MARKDOWN_HIGHLIGHT_GROUPS = ["heading", "marker", "link", "code"] as const;
|
|
14
|
+
|
|
15
|
+
/** A markdown token class the editor paints. */
|
|
16
|
+
export type MarkdownHighlightGroup = (typeof MARKDOWN_HIGHLIGHT_GROUPS)[number];
|
|
17
|
+
|
|
18
|
+
/** A char range in the raw markdown source to paint with one group's style; end is exclusive. */
|
|
19
|
+
export interface MarkdownHighlightRange {
|
|
20
|
+
start: number;
|
|
21
|
+
end: number;
|
|
22
|
+
group: MarkdownHighlightGroup;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The absolute source offset where each 0-based line begins. */
|
|
26
|
+
function lineStartOffsets(source: string): number[] {
|
|
27
|
+
const offsets = [0];
|
|
28
|
+
|
|
29
|
+
for (let index = 0; index < source.length; index++) {
|
|
30
|
+
if (source[index] === "\n") offsets.push(index + 1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return offsets;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// gray a balanced backtick run and color a [text](href) link; bold and emphasis stay plain source
|
|
37
|
+
const INLINE_TOKEN = /(`+)(?:.+?)\1|\[[^\]]+\]\([^)]+\)/g;
|
|
38
|
+
|
|
39
|
+
/** Gray inline code and color links found in one text span; other inline markup stays plain. */
|
|
40
|
+
function scanInlineTokens(text: string, base: number, into: MarkdownHighlightRange[]): void {
|
|
41
|
+
INLINE_TOKEN.lastIndex = 0;
|
|
42
|
+
let match: RegExpExecArray | null;
|
|
43
|
+
|
|
44
|
+
while ((match = INLINE_TOKEN.exec(text)) !== null) {
|
|
45
|
+
const token = match[0];
|
|
46
|
+
const start = base + match.index;
|
|
47
|
+
const end = start + token.length;
|
|
48
|
+
|
|
49
|
+
if (token.startsWith("`")) {
|
|
50
|
+
into.push({ start, end, group: "code" });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
// a link colors its brackets and href; the [] label stays plain unless it is an @-scope, matching how a
|
|
54
|
+
// markdown source view paints links
|
|
55
|
+
const closeBracket = token.indexOf("]");
|
|
56
|
+
|
|
57
|
+
if (token.slice(1, closeBracket).startsWith("@")) {
|
|
58
|
+
into.push({ start, end, group: "link" });
|
|
59
|
+
} else {
|
|
60
|
+
into.push({ start, end: start + 1, group: "link" });
|
|
61
|
+
into.push({ start: start + closeBracket, end, group: "link" });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isHeadingKind(kind: Block["kind"]): boolean {
|
|
67
|
+
return kind === "h1" || kind === "h2" || kind === "h3";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Dim a heading's `#` marker and bold the rest of the line (no color). */
|
|
71
|
+
function scanHeadingBlock(
|
|
72
|
+
firstLineStart: number,
|
|
73
|
+
source: string,
|
|
74
|
+
lineStarts: number[],
|
|
75
|
+
block: Block,
|
|
76
|
+
into: MarkdownHighlightRange[],
|
|
77
|
+
): void {
|
|
78
|
+
const lineText = source.slice(firstLineStart, lineStarts[block.lineStart + 1] ?? source.length);
|
|
79
|
+
const marker = lineText.match(LEADING_BLOCK_MARKER)?.[0] ?? "";
|
|
80
|
+
|
|
81
|
+
into.push({ start: firstLineStart, end: firstLineStart + marker.length, group: "marker" });
|
|
82
|
+
into.push({
|
|
83
|
+
start: firstLineStart + marker.length,
|
|
84
|
+
end: firstLineStart + lineText.trimEnd().length,
|
|
85
|
+
group: "heading",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Paint one block: heading marker and title, grayed code block, or inline code and links in prose. */
|
|
90
|
+
function scanBlock(
|
|
91
|
+
block: Block,
|
|
92
|
+
source: string,
|
|
93
|
+
lineStarts: number[],
|
|
94
|
+
into: MarkdownHighlightRange[],
|
|
95
|
+
): void {
|
|
96
|
+
const firstLineStart = lineStarts[block.lineStart] ?? 0;
|
|
97
|
+
|
|
98
|
+
if (isHeadingKind(block.kind)) {
|
|
99
|
+
scanHeadingBlock(firstLineStart, source, lineStarts, block, into);
|
|
100
|
+
} else if (block.kind === "code") {
|
|
101
|
+
into.push({
|
|
102
|
+
start: firstLineStart,
|
|
103
|
+
end: lineStarts[block.lineEnd + 1] ?? source.length,
|
|
104
|
+
group: "code",
|
|
105
|
+
});
|
|
106
|
+
} else if (block.kind !== "hr") {
|
|
107
|
+
for (let line = block.lineStart; line <= block.lineEnd; line++) {
|
|
108
|
+
const lineStart = lineStarts[line] ?? source.length;
|
|
109
|
+
const lineText = source.slice(lineStart, lineStarts[line + 1] ?? source.length);
|
|
110
|
+
|
|
111
|
+
scanInlineTokens(lineText.replace(/\r?\n$/, ""), lineStart, into);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Scan raw markdown source into highlight ranges over the source (markers included), for the inline editor. */
|
|
117
|
+
export function markdownHighlightRanges(source: string): MarkdownHighlightRange[] {
|
|
118
|
+
const lineStarts = lineStartOffsets(source);
|
|
119
|
+
const ranges: MarkdownHighlightRange[] = [];
|
|
120
|
+
|
|
121
|
+
for (const block of parseBlocks(source)) scanBlock(block, source, lineStarts, ranges);
|
|
122
|
+
|
|
123
|
+
return ranges;
|
|
124
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Tree-sitter colors for fenced code blocks in a Thread, in rendered-text offsets. */
|
|
2
|
+
|
|
3
|
+
import { getTreeSitterClient } from "@opentui/core";
|
|
4
|
+
import type { DisplayBlock } from "./view-plan";
|
|
5
|
+
import { renderedText } from "./view-plan";
|
|
6
|
+
import { spansByLine, type SyntaxSpan } from "../diff/diff-syntax";
|
|
7
|
+
import { filetypeFor } from "../ui/components/syntax-highlight";
|
|
8
|
+
|
|
9
|
+
export type { SyntaxSpan } from "../diff/diff-syntax";
|
|
10
|
+
|
|
11
|
+
/** Highlight whole code blocks so multi-line syntax stays intact. */
|
|
12
|
+
export async function highlightThreadCodeBlocks(
|
|
13
|
+
display: DisplayBlock[],
|
|
14
|
+
): Promise<Map<number, SyntaxSpan[]>> {
|
|
15
|
+
const jobs = display.flatMap((block, blockIndex) => {
|
|
16
|
+
if (block.kind !== "code") return [];
|
|
17
|
+
const filetype = filetypeFor(block.work?.lang ?? block.base?.lang);
|
|
18
|
+
|
|
19
|
+
return filetype ? [{ blockIndex, filetype, source: renderedText(block) }] : [];
|
|
20
|
+
});
|
|
21
|
+
const byBlock = new Map<number, SyntaxSpan[]>();
|
|
22
|
+
|
|
23
|
+
if (jobs.length === 0) return byBlock;
|
|
24
|
+
const client = getTreeSitterClient();
|
|
25
|
+
|
|
26
|
+
await client.initialize();
|
|
27
|
+
for (const job of jobs) {
|
|
28
|
+
const result = await client.highlightOnce(job.source, job.filetype);
|
|
29
|
+
|
|
30
|
+
if (!result.highlights) continue;
|
|
31
|
+
const spans: SyntaxSpan[] = [];
|
|
32
|
+
let offset = 0;
|
|
33
|
+
const lines = job.source.split("\n");
|
|
34
|
+
|
|
35
|
+
spansByLine(job.source, result.highlights).forEach((lineSpans, lineIndex) => {
|
|
36
|
+
for (const span of lineSpans) {
|
|
37
|
+
spans.push({ start: offset + span.start, end: offset + span.end, group: span.group });
|
|
38
|
+
}
|
|
39
|
+
offset += lines[lineIndex]!.length + 1;
|
|
40
|
+
});
|
|
41
|
+
byBlock.set(job.blockIndex, spans);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return byBlock;
|
|
45
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The thread pane's inline edit mode: owns whether the native markdown editor is
|
|
3
|
+
* open over the body, the editor element itself, and the open and exit handles.
|
|
4
|
+
* Opening is gated to an editable thread (the caller passes canEdit - owner,
|
|
5
|
+
* unresolved, not a diff); exiting saves the edited body as the working copy
|
|
6
|
+
* through the controller, so leaving the editor never loses work. The header
|
|
7
|
+
* edit/normal toggle exits through requestExit; switching threads closes the
|
|
8
|
+
* editor. Rendering the editor lives here too, so the pane pulls in one concept
|
|
9
|
+
* rather than wiring the component and its handle ref by hand.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import React, { useRef, useState } from "react";
|
|
13
|
+
import type { Theme } from "../appearance/theme";
|
|
14
|
+
import type { ReviewController } from "../thread/thread-controller";
|
|
15
|
+
import { MarkdownThreadEditor, type MarkdownEditorHandle } from "./components/MarkdownThreadEditor";
|
|
16
|
+
|
|
17
|
+
export interface ThreadBodyEditing {
|
|
18
|
+
editing: boolean;
|
|
19
|
+
openEditor: () => void;
|
|
20
|
+
requestExit: () => void;
|
|
21
|
+
renderEditor: (theme: Theme) => React.ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useThreadBodyEditing(options: {
|
|
25
|
+
controller: ReviewController;
|
|
26
|
+
sessionId: string | undefined;
|
|
27
|
+
canEdit: boolean;
|
|
28
|
+
}): ThreadBodyEditing {
|
|
29
|
+
const { controller, sessionId, canEdit } = options;
|
|
30
|
+
// key editing by thread id so a thread switch closes the editor with no effect or ref
|
|
31
|
+
const [editingSessionId, setEditingSessionId] = useState<string | null>(null);
|
|
32
|
+
const handleRef = useRef<MarkdownEditorHandle | null>(null);
|
|
33
|
+
const editing = editingSessionId !== null && editingSessionId === sessionId;
|
|
34
|
+
const exitEditor = (text: string): void => {
|
|
35
|
+
controller.saveEditedBody(text);
|
|
36
|
+
setEditingSessionId(null);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
editing,
|
|
41
|
+
openEditor: () => {
|
|
42
|
+
if (canEdit && sessionId !== undefined) setEditingSessionId(sessionId);
|
|
43
|
+
},
|
|
44
|
+
requestExit: () => handleRef.current?.requestExit(),
|
|
45
|
+
renderEditor: (theme) => (
|
|
46
|
+
<MarkdownThreadEditor
|
|
47
|
+
ref={handleRef}
|
|
48
|
+
initialText={controller.working()}
|
|
49
|
+
theme={theme}
|
|
50
|
+
onExitEditor={exitEditor}
|
|
51
|
+
/>
|
|
52
|
+
),
|
|
53
|
+
};
|
|
54
|
+
}
|