@kabmo-labs/kabmo 0.7.0 → 0.9.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/README.md +7 -0
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +403 -384
- package/dist/client/static/js/{index-0wvbca0b.js → index-bpptfkz2.js} +905 -875
- package/dist/client/static/js/index-k315p1e8.css +1 -0
- package/package.json +10 -1
- package/src/client/components/common/code-surface/documents.test.ts +120 -0
- package/src/client/components/common/code-surface/documents.ts +201 -0
- package/src/client/components/common/code-surface/extensions.test.ts +110 -0
- package/src/client/components/common/code-surface/extensions.ts +162 -0
- package/src/client/components/common/code-surface/handle.ts +102 -0
- package/src/client/components/common/code-surface/index.tsx +253 -314
- package/src/client/components/common/code-surface/keymap.test.ts +133 -0
- package/src/client/components/common/code-surface/keymap.ts +267 -0
- package/src/client/components/common/code-surface/marks.ts +74 -0
- package/src/client/components/common/code-surface/shiki.ts +234 -0
- package/src/client/components/common/code-surface/sync.test.ts +185 -0
- package/src/client/components/common/code-surface/sync.ts +58 -0
- package/src/client/components/common/code-surface/theme.test.ts +202 -0
- package/src/client/components/common/code-surface/theme.ts +111 -0
- package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
- package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
- package/src/client/components/common/header-controls/index.tsx +24 -1
- package/src/client/components/common/header-controls/shell-chrome.test.tsx +272 -0
- package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
- package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
- package/src/client/components/common/shell-update-button/index.tsx +125 -0
- package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
- package/src/client/components/common/toolbar-row/index.tsx +189 -0
- package/src/client/components/common/workspace-options-menu/index.tsx +19 -1
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +5 -6
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
- package/src/client/components/layout/desktop-layout/index.tsx +55 -69
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
- package/src/client/components/layout/session-sidebar/CategoryHeader.tsx +24 -0
- package/src/client/components/layout/session-sidebar/CategoryHeader.worktree.test.tsx +126 -0
- package/src/client/components/layout/session-sidebar/CategoryItem.tsx +4 -0
- package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +12 -0
- package/src/client/components/layout/session-sidebar/SessionList.tsx +4 -0
- package/src/client/components/layout/session-sidebar/SessionsView.tsx +24 -0
- package/src/client/components/layout/session-sidebar/WorktreeBadge.test.ts +48 -0
- package/src/client/components/layout/session-sidebar/WorktreeBadge.tsx +55 -0
- package/src/client/components/mobile/LayoutWrapper.tsx +23 -16
- package/src/client/components/mobile/RightSidebar.tsx +21 -10
- package/src/client/components/mobile/SessionSidebar.tsx +8 -0
- package/src/client/components/mobile/header-titlebar.test.tsx +172 -0
- package/src/client/components/mobile/mobile-main-view/Header.tsx +10 -1
- package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
- package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
- package/src/client/components/mobile/mobile-session-sidebar/Header.tsx +6 -2
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -0
- package/src/client/components/mobile/mobile-session-sidebar/List.tsx +4 -0
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +5 -0
- package/src/client/components/mobile/mobile-session-sidebar/WorktreeDialog.tsx +49 -0
- package/src/client/components/mobile/worktree-session.ts +51 -0
- package/src/client/components/settings/Modal.tsx +8 -0
- package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
- package/src/client/components/settings/categories/ProjectSettings.tsx +22 -8
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
- package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +146 -0
- package/src/client/components/settings/categories/project-settings/worktree-row.test.ts +83 -0
- package/src/client/components/settings/categories/project-settings/worktree-row.ts +47 -0
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +36 -55
- package/src/client/components/workspace/chat-timeline/PendingChatView.tsx +8 -0
- package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
- package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
- package/src/client/components/workspace/chat-timeline/index.tsx +5 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.test.tsx +146 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +32 -17
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
- package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
- package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
- package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
- package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
- package/src/client/components/workspace/diff-panel/index.tsx +76 -145
- package/src/client/components/workspace/diff-panel/merge.ts +315 -0
- package/src/client/components/workspace/editor/Body.tsx +4 -7
- package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
- package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
- package/src/client/components/workspace/editor/FilePane.tsx +4 -7
- package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
- package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
- package/src/client/components/workspace/editor/index.tsx +7 -9
- package/src/client/components/workspace/file-explorer/ContextMenu.tsx +2 -1
- package/src/client/components/workspace/file-explorer/Modals.tsx +40 -12
- package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
- package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
- package/src/client/components/workspace/file-explorer/RootMenu.tsx +14 -5
- package/src/client/components/workspace/file-explorer/Toolbar.tsx +74 -0
- package/src/client/components/workspace/file-explorer/TreeItem.tsx +38 -3
- package/src/client/components/workspace/file-explorer/filter.ts +47 -0
- package/src/client/components/workspace/file-explorer/index.tsx +77 -64
- package/src/client/components/workspace/file-explorer/local-edit.test.ts +135 -0
- package/src/client/components/workspace/search-panel/Body.tsx +1 -1
- package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
- package/src/client/components/workspace/worktree-modal/index.tsx +261 -0
- package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
- package/src/client/hooks/chat/omp/reconnect.ts +187 -0
- package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
- package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
- package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
- package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
- package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
- package/src/client/hooks/chat/omp/session-list.tsx +69 -9
- package/src/client/hooks/chat/timeline/generating.ts +19 -2
- package/src/client/hooks/chat/timeline/index.ts +27 -28
- package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
- package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
- package/src/client/hooks/chat/timeline/send-execute.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send-steer.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send.ts +11 -1
- package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
- package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
- package/src/client/hooks/chat/timeline/session-load.ts +69 -37
- package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
- package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
- package/src/client/hooks/chat/timeline/spawn-target.ts +52 -0
- package/src/client/hooks/chat/timeline/tool-open.test.ts +74 -0
- package/src/client/hooks/chat/timeline/tool-open.ts +52 -8
- package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
- package/src/client/hooks/code/deferred-highlight.ts +129 -0
- package/src/client/hooks/editor/find-state.ts +1 -1
- package/src/client/hooks/editor/theme.test.ts +206 -0
- package/src/client/hooks/editor/theme.ts +82 -0
- package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
- package/src/client/hooks/editor/use-editor-drop.ts +67 -14
- package/src/client/hooks/editor/use-editor-find.ts +7 -7
- package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
- package/src/client/hooks/editor/use-outline-view.ts +94 -7
- package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
- package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
- package/src/client/hooks/editor/use-search-reveal.ts +1 -1
- package/src/client/hooks/ui/long-press.test.ts +174 -0
- package/src/client/hooks/ui/long-press.ts +112 -0
- package/src/client/hooks/ui/settings-modal.ts +56 -0
- package/src/client/hooks/ui/updates/shell-download.test.ts +75 -0
- package/src/client/hooks/ui/updates/shell-download.ts +153 -28
- package/src/client/hooks/ui/updates/shell-offer.test.ts +213 -0
- package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
- package/src/client/hooks/ui/updates/shell-update.ts +119 -0
- package/src/client/hooks/workspace/diff-file.ts +29 -10
- package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
- package/src/client/hooks/workspace/editor-view.ts +61 -0
- package/src/client/hooks/workspace/file-create.ts +41 -11
- package/src/client/hooks/workspace/file-listing.ts +56 -1
- package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
- package/src/client/hooks/workspace/file-tabs.ts +24 -11
- package/src/client/hooks/workspace/file-tree-actions.ts +37 -5
- package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
- package/src/client/hooks/workspace/worktree/index.ts +165 -0
- package/src/client/lib/desktop/bridge.ts +45 -0
- package/src/client/lib/search-reveal.ts +1 -1
- package/src/client/main.tsx +5 -1
- package/src/client/tailwind.css +56 -8
- package/src/server/index.ts +7 -2
- package/src/server/lib/auth/guard.test.ts +28 -0
- package/src/server/lib/auth/guard.ts +10 -0
- package/src/server/lib/bundler/titlebar-css.test.ts +42 -1
- package/src/server/lib/db/schema.ts +4 -0
- package/src/server/lib/db/worktree-registry.server.ts +91 -0
- package/src/server/lib/fs/canonical.ts +52 -0
- package/src/server/lib/fs/relative-path.ts +48 -0
- package/src/server/lib/fs/root-worktree.test.ts +116 -0
- package/src/server/lib/fs/root.ts +39 -10
- package/src/server/lib/fs/worktree/carry.ts +134 -0
- package/src/server/lib/fs/worktree/create.test.ts +249 -0
- package/src/server/lib/fs/worktree/create.ts +262 -0
- package/src/server/lib/fs/worktree/errors.ts +43 -0
- package/src/server/lib/fs/worktree/list.test.ts +207 -0
- package/src/server/lib/fs/worktree/list.ts +142 -0
- package/src/server/lib/fs/worktree/location.test.ts +206 -0
- package/src/server/lib/fs/worktree/location.ts +114 -0
- package/src/server/lib/fs/worktree/project-id.ts +53 -0
- package/src/server/lib/fs/worktree/remove.test.ts +242 -0
- package/src/server/lib/fs/worktree/remove.ts +152 -0
- package/src/server/lib/fs/worktree/settings.server.ts +56 -0
- package/src/server/lib/fs/worktree/topology.ts +142 -0
- package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
- package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
- package/src/server/lib/lifecycle/port-guard.ts +39 -7
- package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
- package/src/server/lib/lifecycle/probe.test.ts +77 -164
- package/src/server/lib/lifecycle/probe.ts +121 -13
- package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
- package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
- package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
- package/src/server/lib/omp/session/sidebar-data.canonical.test.ts +155 -0
- package/src/server/lib/omp/session/sidebar-data.server.ts +28 -1
- package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
- package/src/server/lib/omp/session/telemetry.test.ts +33 -0
- package/src/server/lib/panels/local-edits.server.ts +33 -0
- package/src/server/lib/panels/sync.fixture.test-util.ts +92 -0
- package/src/server/lib/panels/sync.local-edits.test.ts +150 -0
- package/src/server/lib/panels/sync.server.test.ts +125 -147
- package/src/server/lib/panels/sync.server.ts +153 -48
- package/src/server/lib/realtime/signals.server.ts +10 -0
- package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
- package/src/server/lib/realtime/topics.server.ts +16 -0
- package/src/server/lib/updates/shell-assets.test.ts +114 -0
- package/src/server/lib/updates/shell-assets.ts +200 -0
- package/src/server/lib/updates/shell-cache.ts +105 -0
- package/src/server/lib/updates/shell-pages.test.ts +187 -0
- package/src/server/lib/updates/shell-pages.ts +157 -0
- package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
- package/src/server/lib/updates/shell-release.test.ts +59 -71
- package/src/server/lib/updates/shell-release.ts +178 -198
- package/src/server/routes/fs/action-create.test.ts +18 -13
- package/src/server/routes/fs/action-nested.test.ts +184 -0
- package/src/server/routes/fs/action.ts +47 -16
- package/src/server/routes/fs/git-merge.test.ts +11 -1
- package/src/server/routes/fs/read.test.ts +24 -7
- package/src/server/routes/fs/scope.test.ts +15 -10
- package/src/server/routes/fs/scope.ts +3 -14
- package/src/server/routes/index.ts +2 -0
- package/src/server/routes/updates/android.ts +6 -1
- package/src/server/routes/updates/desktop.ts +59 -0
- package/src/server/routes/updates/index.ts +2 -0
- package/src/server/routes/updates/ios.ts +5 -1
- package/src/server/routes/worktrees/create.ts +138 -0
- package/src/server/routes/worktrees/index.ts +25 -0
- package/src/server/routes/worktrees/list.ts +134 -0
- package/src/server/routes/worktrees/remove.ts +102 -0
- package/src/server/routes/worktrees/routes.test.ts +326 -0
- package/src/server/routes/worktrees/shared.ts +90 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
- package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
- package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
- package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
- package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
- package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
- package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
- package/src/shared/lib/code/editor/find-scan.ts +177 -0
- package/src/shared/lib/code/editor/keymap.ts +3 -3
- package/src/shared/lib/code/editor/lines.test.ts +7 -55
- package/src/shared/lib/code/editor/lines.ts +8 -48
- package/src/shared/lib/code/editor/marks.test.ts +4 -5
- package/src/shared/lib/code/editor/marks.ts +13 -25
- package/src/shared/lib/code/editor/theme-loader.ts +97 -0
- package/src/shared/lib/code/editor/themes.generated.ts +156 -0
- package/src/shared/lib/code/editor/themes.test.ts +165 -0
- package/src/shared/lib/code/editor/themes.ts +165 -0
- package/src/shared/lib/code/highlight-memo.test.ts +103 -0
- package/src/shared/lib/code/highlight-memo.ts +149 -0
- package/src/shared/lib/code/highlighter-warmup.ts +112 -0
- package/src/shared/lib/code/highlighter.ts +3 -1
- package/src/shared/lib/code/idle.ts +37 -0
- package/src/shared/lib/code/outline/rows.test.ts +96 -0
- package/src/shared/lib/code/outline/rows.ts +94 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
- package/src/shared/lib/code/syntax-highlight.ts +2 -3
- package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
- package/src/shared/lib/fs/file-kind.ts +47 -13
- package/src/shared/lib/fs/file-tree.test.ts +183 -0
- package/src/shared/lib/fs/file-tree.ts +159 -0
- package/src/shared/lib/fs/file-url.test.ts +52 -0
- package/src/shared/lib/fs/file-url.ts +37 -0
- package/src/shared/lib/fs/patch-sides.test.ts +114 -0
- package/src/shared/lib/fs/patch-sides.ts +139 -0
- package/src/shared/lib/fs/paths.test.ts +24 -1
- package/src/shared/lib/fs/paths.ts +17 -0
- package/src/shared/lib/fs/worktree/settings-keys.ts +18 -0
- package/src/shared/lib/omp/session/prewarm.ts +7 -0
- package/src/shared/lib/realtime/protocol.ts +16 -0
- package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
- package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
- package/src/shared/lib/updates/shell-arch.ts +54 -0
- package/src/shared/lib/updates/shell-ua.ts +24 -4
- package/src/shared/lib/workspace/active-project.ts +23 -0
- package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
- package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
- package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
- package/src/shared/lib/workspace/panel-widths.ts +25 -5
- package/src/shared/lib/workspace/session-state/cache.ts +159 -0
- package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
- package/src/shared/lib/workspace/session-state/merge.ts +100 -0
- package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
- package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
- package/src/shared/lib/workspace/session-state/store.ts +92 -130
- package/src/shared/lib/workspace/workspace-modules.test.ts +49 -1
- package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
- package/src/shared/lib/workspace/worktree/branch-name.test.ts +95 -0
- package/src/shared/lib/workspace/worktree/branch-name.ts +115 -0
- package/src/shared/lib/workspace/worktree/types.ts +104 -0
- package/src/shared/types/fs.ts +10 -0
- package/src/shared/types/realtime.ts +29 -0
- package/src/shared/types/settings/state.ts +10 -0
- package/src/shared/types/workspace.ts +14 -0
- package/src/test-support/file-explorer-disk.test-util.ts +76 -0
- package/src/test-support/file-explorer.test-util.ts +71 -40
- package/src/test-support/loopback.ts +183 -0
- package/dist/client/static/js/index-nzyjf92m.css +0 -1
- package/src/client/components/common/code-editor/handler.test.ts +0 -274
- package/src/client/components/common/code-editor/handler.ts +0 -175
- package/src/client/components/common/code-editor/index.tsx +0 -314
- package/src/client/components/common/code-surface/measure.ts +0 -148
- package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
- package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
- package/src/client/hooks/editor/scroll-container.test.ts +0 -102
- package/src/client/hooks/editor/scroll-container.ts +0 -26
- package/src/client/hooks/editor/use-code-window.ts +0 -100
- package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
- package/src/client/hooks/editor/use-editor-history.ts +0 -134
- package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
- package/src/client/hooks/editor/use-occurrences.ts +0 -98
- package/src/shared/lib/code/editor/commands.test.ts +0 -155
- package/src/shared/lib/code/editor/commands.ts +0 -260
- package/src/shared/lib/code/editor/edits.test.ts +0 -100
- package/src/shared/lib/code/editor/edits.ts +0 -122
- package/src/shared/lib/code/editor/history.test.ts +0 -118
- package/src/shared/lib/code/editor/history.ts +0 -101
- package/src/shared/lib/code/editor/layers.test.ts +0 -77
- package/src/shared/lib/code/editor/layers.ts +0 -67
- package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
- package/src/shared/lib/code/editor/line-edits.ts +0 -159
- package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
- package/src/shared/lib/code/editor/occurrences.ts +0 -111
- package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
- package/src/shared/lib/code/editor/reset-css.ts +0 -32
- package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
- package/src/shared/lib/code/editor/text-commands.ts +0 -154
- package/src/shared/lib/code/lazy-window.test.ts +0 -161
- package/src/shared/lib/code/lazy-window.ts +0 -156
- package/src/shared/lib/code/windowed-highlight.ts +0 -114
- package/src/shared/lib/fs/diff-parser.test.ts +0 -116
- package/src/shared/lib/fs/diff-parser.ts +0 -213
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The editor theme catalogue, and the one property that makes it trustworthy:
|
|
8
|
+
* every colour it ships is the INSTALLED package's own.
|
|
9
|
+
*
|
|
10
|
+
* The grid draws 65 cards without fetching a theme module, so its colours are
|
|
11
|
+
* data. Data generated from a dependency drifts the moment the dependency
|
|
12
|
+
* updates — silently, because the picker still renders, just with a palette the
|
|
13
|
+
* highlighter no longer has. These cases re-derive each row from
|
|
14
|
+
* `@shikijs/themes` itself, which is the only check that can fail on a version
|
|
15
|
+
* bump rather than on a typo.
|
|
16
|
+
*
|
|
17
|
+
* The remaining cases pin the two rules the surfaces depend on: `auto` resolves
|
|
18
|
+
* to no theme (so nothing changes for an install that never opens the picker),
|
|
19
|
+
* and an explicit theme names ITSELF in both Shiki slots — which is what makes
|
|
20
|
+
* the editor ignore the app's light/dark variant without a second CSS path.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, expect, test } from 'bun:test';
|
|
24
|
+
import { readdirSync } from 'node:fs';
|
|
25
|
+
import { dirname, join } from 'node:path';
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
EDITOR_AUTO_THEME_ID,
|
|
29
|
+
EDITOR_DEFAULT_THEME_ID,
|
|
30
|
+
EDITOR_THEMES,
|
|
31
|
+
EDITOR_THEME_LOADERS,
|
|
32
|
+
editorShikiThemes,
|
|
33
|
+
editorThemeVars,
|
|
34
|
+
isEditorThemeId,
|
|
35
|
+
resolveEditorTheme,
|
|
36
|
+
} from '@/shared/lib/code/editor/themes';
|
|
37
|
+
import { SHIKI_THEMES } from '@/shared/lib/code/shiki-themes';
|
|
38
|
+
|
|
39
|
+
/** The installed package's `dist`, resolved through the package itself so a
|
|
40
|
+
* hoisted or linked install resolves the same way the app's own imports do. */
|
|
41
|
+
const THEMES_DIR = join(dirname(Bun.resolveSync('@shikijs/themes/package.json', import.meta.dir)), 'dist');
|
|
42
|
+
|
|
43
|
+
/** The ids the installed package actually ships, in the generator's order. */
|
|
44
|
+
function installedIds(): string[] {
|
|
45
|
+
return readdirSync(THEMES_DIR)
|
|
46
|
+
.filter((file) => file.endsWith('.mjs') && file !== 'index.mjs')
|
|
47
|
+
.map((file) => file.slice(0, -'.mjs'.length))
|
|
48
|
+
.sort();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('catalogue', () => {
|
|
52
|
+
test('one row per theme the installed package ships, in id order', () => {
|
|
53
|
+
expect(EDITOR_THEMES.map((theme) => theme.id)).toEqual(installedIds());
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('every row has a loader, and every loader has a row', () => {
|
|
57
|
+
expect(Object.keys(EDITOR_THEME_LOADERS).sort()).toEqual(EDITOR_THEMES.map((theme) => theme.id));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('a label is never empty and never the raw id with dashes', () => {
|
|
61
|
+
for (const theme of EDITOR_THEMES) {
|
|
62
|
+
expect(theme.label.length).toBeGreaterThan(0);
|
|
63
|
+
expect(theme.variant === 'light' || theme.variant === 'dark').toBe(true);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('generated colours', () => {
|
|
69
|
+
test('the surface and the ink are the theme’s own', async () => {
|
|
70
|
+
for (const theme of EDITOR_THEMES) {
|
|
71
|
+
// Dynamic on purpose: the specifier is the row's own id, read at runtime
|
|
72
|
+
// from a 65-entry catalogue — a static import cannot name it.
|
|
73
|
+
const module = (await import(`@shikijs/themes/${theme.id}`)).default as {
|
|
74
|
+
colors?: Record<string, string>;
|
|
75
|
+
};
|
|
76
|
+
const colors = module.colors ?? {};
|
|
77
|
+
expect(theme.bg).toBe(colors['editor.background'] as string);
|
|
78
|
+
// Five themes ship no `editor.foreground`; the generator falls back to the
|
|
79
|
+
// theme's own `foreground`, which is still ITS colour and not a borrow.
|
|
80
|
+
const own =
|
|
81
|
+
colors['editor.foreground'] ?? colors['foreground'] ?? colors['terminal.foreground'] ?? theme.fg;
|
|
82
|
+
expect(theme.fg).toBe(own);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('a chrome colour the theme omits is a mix of its own ink and surface', () => {
|
|
87
|
+
for (const theme of EDITOR_THEMES) {
|
|
88
|
+
for (const value of [theme.gutterFg, theme.selection, theme.find, theme.border]) {
|
|
89
|
+
expect(value.length).toBeGreaterThan(0);
|
|
90
|
+
// Either the theme's own hex, or a `color-mix` over its own two colours
|
|
91
|
+
// — never a bare colour borrowed from another theme.
|
|
92
|
+
const isOwnHex = /^#[0-9a-f]{3,8}$/i.test(value);
|
|
93
|
+
const isOwnMix = value.startsWith('color-mix(') && value.includes(theme.bg);
|
|
94
|
+
expect(isOwnHex || isOwnMix).toBe(true);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('the three accents differ enough to tell two themes apart', () => {
|
|
100
|
+
for (const theme of EDITOR_THEMES) {
|
|
101
|
+
expect(theme.keyword).toMatch(/^#[0-9a-f]{3,8}$/i);
|
|
102
|
+
expect(theme.string).toMatch(/^#[0-9a-f]{3,8}$/i);
|
|
103
|
+
expect(theme.comment).toMatch(/^#[0-9a-f]{3,8}$/i);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('resolution', () => {
|
|
109
|
+
test('auto is the default and resolves to no theme', () => {
|
|
110
|
+
expect(EDITOR_DEFAULT_THEME_ID).toBe(EDITOR_AUTO_THEME_ID);
|
|
111
|
+
expect(resolveEditorTheme(EDITOR_AUTO_THEME_ID)).toBeNull();
|
|
112
|
+
expect(resolveEditorTheme(undefined)).toBeNull();
|
|
113
|
+
expect(resolveEditorTheme(null)).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('an id this build does not ship falls back to auto rather than half a theme', () => {
|
|
117
|
+
expect(resolveEditorTheme('no-such-theme')).toBeNull();
|
|
118
|
+
expect(isEditorThemeId('no-such-theme')).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('a shipped id resolves to its own row', () => {
|
|
122
|
+
const dracula = resolveEditorTheme('dracula');
|
|
123
|
+
expect(dracula?.id).toBe('dracula');
|
|
124
|
+
expect(isEditorThemeId('dracula')).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('shiki themes', () => {
|
|
129
|
+
test('auto keeps the app’s dual pair, so an app theme flip repaints', () => {
|
|
130
|
+
expect(editorShikiThemes(null)).toEqual(SHIKI_THEMES);
|
|
131
|
+
expect(editorShikiThemes(EDITOR_AUTO_THEME_ID)).toEqual(SHIKI_THEMES);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('an explicit theme names itself in BOTH slots', () => {
|
|
135
|
+
// Both slots carrying one theme is what makes the app's variant attribute
|
|
136
|
+
// harmless: `--shiki-light` and `--shiki-dark` hold the same value, so
|
|
137
|
+
// either selector paints the chosen theme.
|
|
138
|
+
expect(editorShikiThemes('dracula')).toEqual({ light: 'dracula', dark: 'dracula' });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('an unknown id keeps the app pair', () => {
|
|
142
|
+
expect(editorShikiThemes('no-such-theme')).toEqual(SHIKI_THEMES);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('css variables', () => {
|
|
147
|
+
test('auto writes nothing, so every declaration keeps its palette fallback', () => {
|
|
148
|
+
expect(editorThemeVars(null)).toEqual({});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('an explicit theme writes the surface, the gutter and the diff bands', () => {
|
|
152
|
+
const theme = resolveEditorTheme('nord');
|
|
153
|
+
expect(theme).not.toBeNull();
|
|
154
|
+
const vars = editorThemeVars(theme);
|
|
155
|
+
expect(vars['--editor-bg']).toBe(theme!.bg);
|
|
156
|
+
expect(vars['--editor-fg']).toBe(theme!.fg);
|
|
157
|
+
expect(vars['--editor-gutter-fg']).toBe(theme!.gutterFg);
|
|
158
|
+
expect(vars['--editor-selection']).toBe(theme!.selection);
|
|
159
|
+
expect(vars['--editor-find-current']).toBe(theme!.findCurrent);
|
|
160
|
+
expect(vars['--editor-added']).toBe(theme!.added);
|
|
161
|
+
expect(vars['--editor-removed-gutter']).toBe(theme!.removedGutter);
|
|
162
|
+
// Every key is a custom property, never a plain one.
|
|
163
|
+
for (const key of Object.keys(vars)) expect(key.startsWith('--editor-')).toBe(true);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The EDITOR's colour themes, independent of the app palette.
|
|
8
|
+
*
|
|
9
|
+
* CodeMirror can wear any of the 65 TextMate themes `@shikijs/themes` ships, and
|
|
10
|
+
* a reader who works in Dracula or Nord wants the code surface to say so while
|
|
11
|
+
* the console around it stays in the app's own palette. That separation is the
|
|
12
|
+
* whole feature: `SettingsState.editorTheme` is a second axis beside `theme`,
|
|
13
|
+
* and the editor paints its own surface, gutter, selection and tokens from it.
|
|
14
|
+
*
|
|
15
|
+
* Three rules keep it honest:
|
|
16
|
+
*
|
|
17
|
+
* - **`auto` is the default and means "the app's own palette".** Nothing changes
|
|
18
|
+
* for an install that never opens the picker: the surface stays transparent
|
|
19
|
+
* over the panel, the chrome reads `--theme-*`, and the tokenizer keeps the
|
|
20
|
+
* dual `one-light`/`one-dark-pro` pair the app variant selects.
|
|
21
|
+
* - **The data is generated, never typed.** `themes.generated.ts` is written by
|
|
22
|
+
* `scripts/editor-themes.mjs` from the installed package, so the picker's
|
|
23
|
+
* swatches are the theme's OWN colours and a `@shikijs/themes` bump cannot
|
|
24
|
+
* leave the grid showing a palette that no longer exists.
|
|
25
|
+
* - **A theme module is only fetched when it is selected.** The rows carry
|
|
26
|
+
* enough colour to draw the grid; `EDITOR_THEME_LOADERS` is what the
|
|
27
|
+
* highlighter calls, and 65 modules nobody picks are never downloaded.
|
|
28
|
+
*
|
|
29
|
+
* The picker's swatch shows the editor surface, the ink and three syntax
|
|
30
|
+
* accents — the five colours that actually tell two themes apart at 120px.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { EDITOR_THEME_LOADERS, EDITOR_THEME_ROWS } from '@/shared/lib/code/editor/themes.generated';
|
|
34
|
+
import { SHIKI_THEMES } from '@/shared/lib/code/shiki-themes';
|
|
35
|
+
|
|
36
|
+
/** The setting value meaning "follow the app palette". */
|
|
37
|
+
export const EDITOR_AUTO_THEME_ID = 'auto';
|
|
38
|
+
|
|
39
|
+
/** What a settings row that predates this option holds. */
|
|
40
|
+
export const EDITOR_DEFAULT_THEME_ID = EDITOR_AUTO_THEME_ID;
|
|
41
|
+
|
|
42
|
+
export type EditorThemeVariant = 'light' | 'dark';
|
|
43
|
+
|
|
44
|
+
export interface EditorTheme {
|
|
45
|
+
/** Shiki's theme id, which is also the stored setting value. */
|
|
46
|
+
id: string;
|
|
47
|
+
label: string;
|
|
48
|
+
/** The theme's OWN light/dark verdict — never the app's. */
|
|
49
|
+
variant: EditorThemeVariant;
|
|
50
|
+
/** The editor surface and its ink. */
|
|
51
|
+
bg: string;
|
|
52
|
+
fg: string;
|
|
53
|
+
/** Line-number band. */
|
|
54
|
+
gutterFg: string;
|
|
55
|
+
/** Selection band, caret, and the two find-match bands. */
|
|
56
|
+
selection: string;
|
|
57
|
+
cursor: string;
|
|
58
|
+
find: string;
|
|
59
|
+
findCurrent: string;
|
|
60
|
+
/** Divider between the gutter and the text. */
|
|
61
|
+
border: string;
|
|
62
|
+
/** The two diff bands and their gutter markers. */
|
|
63
|
+
added: string;
|
|
64
|
+
removed: string;
|
|
65
|
+
addedGutter: string;
|
|
66
|
+
removedGutter: string;
|
|
67
|
+
/** Three syntax accents, for the picker's swatch only. */
|
|
68
|
+
keyword: string;
|
|
69
|
+
string: string;
|
|
70
|
+
comment: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The tuple's field order, stated once so the generated rows and this mapping
|
|
75
|
+
* cannot drift: a column added to the generator without a field here is a type
|
|
76
|
+
* error rather than a silently shifted colour.
|
|
77
|
+
*/
|
|
78
|
+
type EditorThemeRow = (typeof EDITOR_THEME_ROWS)[number];
|
|
79
|
+
|
|
80
|
+
const FIELD_ORDER = [
|
|
81
|
+
'id',
|
|
82
|
+
'label',
|
|
83
|
+
'variant',
|
|
84
|
+
'bg',
|
|
85
|
+
'fg',
|
|
86
|
+
'gutterFg',
|
|
87
|
+
'selection',
|
|
88
|
+
'cursor',
|
|
89
|
+
'find',
|
|
90
|
+
'findCurrent',
|
|
91
|
+
'border',
|
|
92
|
+
'added',
|
|
93
|
+
'removed',
|
|
94
|
+
'addedGutter',
|
|
95
|
+
'removedGutter',
|
|
96
|
+
'keyword',
|
|
97
|
+
'string',
|
|
98
|
+
'comment',
|
|
99
|
+
] as const satisfies ReadonlyArray<keyof EditorTheme>;
|
|
100
|
+
|
|
101
|
+
function toTheme(row: EditorThemeRow): EditorTheme {
|
|
102
|
+
const theme = {} as EditorTheme;
|
|
103
|
+
FIELD_ORDER.forEach((field, index) => {
|
|
104
|
+
// The tuple and the field list are asserted equal by the types, so this
|
|
105
|
+
// cast is the only place the two meet.
|
|
106
|
+
(theme as unknown as Record<string, string>)[field] = row[index];
|
|
107
|
+
});
|
|
108
|
+
return theme;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Every theme the installed Shiki ships, in id order (the generator's order). */
|
|
112
|
+
export const EDITOR_THEMES: readonly EditorTheme[] = EDITOR_THEME_ROWS.map(toTheme);
|
|
113
|
+
|
|
114
|
+
const BY_ID = new Map(EDITOR_THEMES.map((theme) => [theme.id, theme]));
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The theme a stored setting names, or `null` for `auto` — and for an id this
|
|
118
|
+
* build no longer ships (a downgrade, a hand-edited row), which must render the
|
|
119
|
+
* app palette rather than a half-applied theme.
|
|
120
|
+
*/
|
|
121
|
+
export function resolveEditorTheme(id: string | null | undefined): EditorTheme | null {
|
|
122
|
+
if (!id || id === EDITOR_AUTO_THEME_ID) return null;
|
|
123
|
+
return BY_ID.get(id) ?? null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** True when `id` is a theme this build can paint. */
|
|
127
|
+
export function isEditorThemeId(id: string): boolean {
|
|
128
|
+
return BY_ID.has(id);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The two theme names Shiki tokenizes with.
|
|
133
|
+
*
|
|
134
|
+
* `auto` keeps the app's dual pair, so the variant attribute picks the palette
|
|
135
|
+
* exactly as it does for every other surface. An explicit theme names ITSELF
|
|
136
|
+
* for both slots — one theme, both custom properties, identical values — which
|
|
137
|
+
* is what makes the editor ignore the app variant without a second CSS path.
|
|
138
|
+
*/
|
|
139
|
+
export function editorShikiThemes(id: string | null | undefined): { light: string; dark: string } {
|
|
140
|
+
const theme = resolveEditorTheme(id);
|
|
141
|
+
if (!theme) return SHIKI_THEMES;
|
|
142
|
+
return { light: theme.id, dark: theme.id };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The CSS custom properties an explicit theme writes, keyed by variable name. */
|
|
146
|
+
export function editorThemeVars(theme: EditorTheme | null): Record<string, string> {
|
|
147
|
+
if (!theme) return {};
|
|
148
|
+
return {
|
|
149
|
+
'--editor-bg': theme.bg,
|
|
150
|
+
'--editor-fg': theme.fg,
|
|
151
|
+
'--editor-gutter-fg': theme.gutterFg,
|
|
152
|
+
'--editor-selection': theme.selection,
|
|
153
|
+
'--editor-cursor': theme.cursor,
|
|
154
|
+
'--editor-find': theme.find,
|
|
155
|
+
'--editor-find-current': theme.findCurrent,
|
|
156
|
+
'--editor-border': theme.border,
|
|
157
|
+
'--editor-added': theme.added,
|
|
158
|
+
'--editor-removed': theme.removed,
|
|
159
|
+
'--editor-added-gutter': theme.addedGutter,
|
|
160
|
+
'--editor-removed-gutter': theme.removedGutter,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The lazy module loaders, so the highlighter can fetch one theme on demand. */
|
|
165
|
+
export { EDITOR_THEME_LOADERS };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The block memo in front of the tokenizer.
|
|
8
|
+
*
|
|
9
|
+
* Three properties matter and each was a decision:
|
|
10
|
+
*
|
|
11
|
+
* 1. **A repeat of the same `(language, code)` returns the same markup** — that
|
|
12
|
+
* is the whole point (a card closed and reopened re-tokenized a 1000-line
|
|
13
|
+
* read, measured as the dominant cost of its expand).
|
|
14
|
+
* 2. **A block whose grammar is not ready is NOT cached.** `highlightCode`
|
|
15
|
+
* answers escaped text in that state, and caching it would pin the plain
|
|
16
|
+
* fallback for the life of the entry.
|
|
17
|
+
* 3. **A find-in-progress bypasses the memo**, because its markup depends on the
|
|
18
|
+
* match ranges rather than on the code alone.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { beforeAll, beforeEach, describe, expect, test } from 'bun:test';
|
|
22
|
+
|
|
23
|
+
import { bootSyntax, onLanguageReady, requestLanguage } from '@/shared/lib/code/highlighter';
|
|
24
|
+
import {
|
|
25
|
+
clearHighlightMemo,
|
|
26
|
+
highlightBlock,
|
|
27
|
+
highlightBlockLines,
|
|
28
|
+
highlightMemoSize,
|
|
29
|
+
} from '@/shared/lib/code/highlight-memo';
|
|
30
|
+
|
|
31
|
+
const TOKEN = '--shiki-light:';
|
|
32
|
+
|
|
33
|
+
beforeAll(async () => {
|
|
34
|
+
Object.assign(globalThis, { window: globalThis });
|
|
35
|
+
await bootSyntax();
|
|
36
|
+
Reflect.deleteProperty(globalThis, 'window');
|
|
37
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
38
|
+
const settle = () => {
|
|
39
|
+
if (requestLanguage('json') && requestLanguage('typescript')) {
|
|
40
|
+
off();
|
|
41
|
+
resolve();
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const off = onLanguageReady(settle);
|
|
45
|
+
settle();
|
|
46
|
+
await promise;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
beforeEach(() => clearHighlightMemo());
|
|
50
|
+
|
|
51
|
+
describe('highlightBlock', () => {
|
|
52
|
+
test('a repeated block returns the same markup and is held once', () => {
|
|
53
|
+
const code = 'const value = 1;\n';
|
|
54
|
+
const first = highlightBlock(code, 'typescript');
|
|
55
|
+
const second = highlightBlock(code, 'typescript');
|
|
56
|
+
|
|
57
|
+
expect(first).toBe(second);
|
|
58
|
+
expect(first).toContain(TOKEN);
|
|
59
|
+
expect(highlightMemoSize()).toBe(1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('the same code in another language is a different entry', () => {
|
|
63
|
+
highlightBlock('{"a":1}', 'json');
|
|
64
|
+
highlightBlock('{"a":1}', 'typescript');
|
|
65
|
+
expect(highlightMemoSize()).toBe(2);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('a grammar that is not ready is never cached', () => {
|
|
69
|
+
const html = highlightBlock('const a = 1;', 'not-a-real-grammar');
|
|
70
|
+
// It still renders something — escaped text, via the javascript fallback —
|
|
71
|
+
// but caching that would freeze the fallback for good.
|
|
72
|
+
expect(html).toContain('const');
|
|
73
|
+
expect(highlightMemoSize()).toBe(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('a find-in-progress bypasses the memo', () => {
|
|
77
|
+
highlightBlock('const a = 1;', 'typescript', { marks: [{ start: 0, end: 5 }] });
|
|
78
|
+
expect(highlightMemoSize()).toBe(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('empty input is not an entry', () => {
|
|
82
|
+
expect(highlightBlock('', 'typescript')).toBe('');
|
|
83
|
+
expect(highlightMemoSize()).toBe(0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('the per-language bound holds', () => {
|
|
87
|
+
for (let index = 0; index < 30; index += 1) {
|
|
88
|
+
highlightBlock(`const value${index} = ${index};`, 'typescript');
|
|
89
|
+
}
|
|
90
|
+
expect(highlightMemoSize()).toBeLessThanOrEqual(24);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('highlightBlockLines', () => {
|
|
95
|
+
test('lines are cached and round-trip through the same key', () => {
|
|
96
|
+
const lines = highlightBlockLines('const a = 1;\nconst b = 2;', 'typescript');
|
|
97
|
+
expect(lines).toHaveLength(2);
|
|
98
|
+
expect(highlightMemoSize()).toBe(1);
|
|
99
|
+
|
|
100
|
+
const again = highlightBlockLines('const a = 1;\nconst b = 2;', 'typescript');
|
|
101
|
+
expect(again).toEqual(lines);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A bounded memo in front of the syntax highlighter, for BLOCK highlights — the
|
|
8
|
+
* code a tool card draws when it opens.
|
|
9
|
+
*
|
|
10
|
+
* Shiki tokenization is the expensive half of rendering a tool card: profiled on
|
|
11
|
+
* a throttled phone, a `read` expand spent 75% of its long task in
|
|
12
|
+
* `findNextMatchSync`/the grammar engine, and the same card re-expanded in 33 ms
|
|
13
|
+
* once its grammar was warm. Two consequences follow, and this module is the
|
|
14
|
+
* second half of both (the first is `idle.ts`, which moves the work off the tap):
|
|
15
|
+
*
|
|
16
|
+
* 1. A card that is closed and reopened is the same code and the same language,
|
|
17
|
+
* so its markup is the same string. Tokenizing it again is pure waste, and a
|
|
18
|
+
* reader paging through a transcript does exactly that repeatedly.
|
|
19
|
+
* 2. `Edit` asked for three highlights per render (old, new, preview) when at
|
|
20
|
+
* most one of them is ever drawn. Caching per `(language, code)` makes the
|
|
21
|
+
* duplicates free even before the call sites are gated.
|
|
22
|
+
*
|
|
23
|
+
* The key is a two-level map — language, then the code string itself — rather
|
|
24
|
+
* than one composite string, because the composite would COPY the whole file on
|
|
25
|
+
* every lookup, which is exactly the per-frame allocation this exists to remove.
|
|
26
|
+
* Insertion order is the LRU order.
|
|
27
|
+
*
|
|
28
|
+
* Nothing is cached while the language's grammar has not landed: `highlightCode`
|
|
29
|
+
* answers escaped text in that state, and caching it would pin the plain
|
|
30
|
+
* fallback for the life of the entry (the trap `highlight-cache.ts` documents
|
|
31
|
+
* for search rows). `useSyntaxReady` re-runs the caller when a grammar arrives,
|
|
32
|
+
* which is when the entry finally becomes cacheable.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { isLanguageReady } from '@/shared/lib/code/highlighter-lazy';
|
|
36
|
+
import { highlightCode, highlightLines, type HighlightLinesOptions } from '@/shared/lib/code/syntax-highlight';
|
|
37
|
+
|
|
38
|
+
/** Per language: a transcript page's worth of distinct blocks. */
|
|
39
|
+
const MAX_BLOCKS_PER_LANGUAGE = 24;
|
|
40
|
+
/** Across every language, so a long session cannot grow without bound. */
|
|
41
|
+
const MAX_CACHED_CHARS = 512 * 1024;
|
|
42
|
+
|
|
43
|
+
const blocks = new Map<string, Map<string, string>>();
|
|
44
|
+
let cachedChars = 0;
|
|
45
|
+
|
|
46
|
+
function bucket(language: string): Map<string, string> {
|
|
47
|
+
const existing = blocks.get(language);
|
|
48
|
+
if (existing) return existing;
|
|
49
|
+
const created = new Map<string, string>();
|
|
50
|
+
blocks.set(language, created);
|
|
51
|
+
return created;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Drop the oldest entry of the first language that has one. */
|
|
55
|
+
function evictOne(): boolean {
|
|
56
|
+
for (const entries of blocks.values()) {
|
|
57
|
+
const oldest = entries.keys().next();
|
|
58
|
+
if (oldest.done) continue;
|
|
59
|
+
const value = entries.get(oldest.value) ?? '';
|
|
60
|
+
entries.delete(oldest.value);
|
|
61
|
+
cachedChars -= value.length;
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function remember(language: string, code: string, html: string): void {
|
|
68
|
+
const entries = bucket(language);
|
|
69
|
+
const previous = entries.get(code);
|
|
70
|
+
if (previous !== undefined) {
|
|
71
|
+
entries.delete(code);
|
|
72
|
+
cachedChars -= previous.length;
|
|
73
|
+
}
|
|
74
|
+
entries.set(code, html);
|
|
75
|
+
cachedChars += html.length;
|
|
76
|
+
while (entries.size > MAX_BLOCKS_PER_LANGUAGE) {
|
|
77
|
+
const oldest = entries.keys().next();
|
|
78
|
+
if (oldest.done) break;
|
|
79
|
+
const value = entries.get(oldest.value) ?? '';
|
|
80
|
+
entries.delete(oldest.value);
|
|
81
|
+
cachedChars -= value.length;
|
|
82
|
+
}
|
|
83
|
+
while (cachedChars > MAX_CACHED_CHARS && evictOne()) {
|
|
84
|
+
// Keep dropping until the budget holds or nothing is left to drop.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The cached markup for `(language, code)`, or `undefined`. */
|
|
89
|
+
export function cachedHighlightBlock(code: string, language: string): string | undefined {
|
|
90
|
+
const entries = blocks.get(language);
|
|
91
|
+
if (!entries) return undefined;
|
|
92
|
+
const hit = entries.get(code);
|
|
93
|
+
if (hit === undefined) return undefined;
|
|
94
|
+
// Reading is a use: move it to the most-recently-used end.
|
|
95
|
+
entries.delete(code);
|
|
96
|
+
entries.set(code, hit);
|
|
97
|
+
return hit;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The cached per-line markup, or `undefined`. */
|
|
101
|
+
export function cachedHighlightBlockLines(code: string, language: string): string[] | undefined {
|
|
102
|
+
const hit = cachedHighlightBlock(code, language);
|
|
103
|
+
return hit === undefined ? undefined : hit.split('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function cacheable(language: string, options: HighlightLinesOptions): boolean {
|
|
107
|
+
// A find-in-progress needs the match markup, which only the line path emits —
|
|
108
|
+
// and which changes per query, so those results are never shared.
|
|
109
|
+
if (options.marks && options.marks.length > 0) return false;
|
|
110
|
+
return isLanguageReady(language);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** `highlightCode` for one block, memoized per `(language, code)`. */
|
|
114
|
+
export function highlightBlock(code: string, language: string, options: HighlightLinesOptions = {}): string {
|
|
115
|
+
if (!code) return '';
|
|
116
|
+
const cached = options.marks?.length ? undefined : cachedHighlightBlock(code, language);
|
|
117
|
+
if (cached !== undefined) return cached;
|
|
118
|
+
const html = highlightCode(code, language, options);
|
|
119
|
+
if (cacheable(language, options)) remember(language, code, html);
|
|
120
|
+
return html;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** `highlightLines` for one block, memoized per `(language, code)`. */
|
|
124
|
+
export function highlightBlockLines(
|
|
125
|
+
code: string,
|
|
126
|
+
language: string,
|
|
127
|
+
options: HighlightLinesOptions = {},
|
|
128
|
+
): string[] {
|
|
129
|
+
if (!code) return [''];
|
|
130
|
+
const cached = options.marks?.length ? undefined : cachedHighlightBlockLines(code, language);
|
|
131
|
+
if (cached !== undefined) return cached;
|
|
132
|
+
const lines = highlightLines(code, language, options);
|
|
133
|
+
if (cacheable(language, options)) remember(language, code, lines.join('\n'));
|
|
134
|
+
return lines;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Drop every entry. For tests and for a highlighter restart (grammar reload). */
|
|
138
|
+
export function clearHighlightMemo(): void {
|
|
139
|
+
blocks.clear();
|
|
140
|
+
cachedChars = 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** How many blocks are held — a hit and a recompute return equal strings, so
|
|
144
|
+
* the map is the only observable. */
|
|
145
|
+
export function highlightMemoSize(): number {
|
|
146
|
+
let total = 0;
|
|
147
|
+
for (const entries of blocks.values()) total += entries.size;
|
|
148
|
+
return total;
|
|
149
|
+
}
|