@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,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The deferred-highlight hook, and the idle scheduler under it.
|
|
8
|
+
*
|
|
9
|
+
* The property that matters is the one the user feels: the FIRST render after a
|
|
10
|
+
* tap must not run the tokenizer. A card that appeared only after 220–300 ms of
|
|
11
|
+
* grammar work (measured at 4× CPU; 730–1484 ms at 20×) is the delay this
|
|
12
|
+
* exists to remove, and a hook that highlighted synchronously "sometimes" would
|
|
13
|
+
* pass a markup assertion and fail the point.
|
|
14
|
+
*
|
|
15
|
+
* So the scheduler is injectable here — the same seam the warm-up and the
|
|
16
|
+
* grammar loader use — and the assertions are about WHEN the compute ran, not
|
|
17
|
+
* only about what it produced. No wall-clock waits: the fallback path is driven
|
|
18
|
+
* by fake timers.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from 'bun:test';
|
|
22
|
+
import { Window } from 'happy-dom';
|
|
23
|
+
import { h, render } from 'preact';
|
|
24
|
+
import { act } from 'preact/test-utils';
|
|
25
|
+
|
|
26
|
+
import { useDeferredHighlight } from '@/client/hooks/code/deferred-highlight';
|
|
27
|
+
import { scheduleIdle } from '@/shared/lib/code/idle';
|
|
28
|
+
import { clearHighlightMemo, highlightMemoSize } from '@/shared/lib/code/highlight-memo';
|
|
29
|
+
import { resetTokenizerWarmup, warmSyntaxTokenizers } from '@/shared/lib/code/highlighter-warmup';
|
|
30
|
+
|
|
31
|
+
const DOM_GLOBALS = ['window', 'document', 'navigator', 'Node', 'Element', 'HTMLElement', 'Event', 'MouseEvent'] as const;
|
|
32
|
+
const nativeGlobals: Partial<Record<(typeof DOM_GLOBALS)[number], unknown>> = {};
|
|
33
|
+
let container: HTMLElement;
|
|
34
|
+
|
|
35
|
+
beforeAll(() => {
|
|
36
|
+
const win = new Window({ url: 'http://localhost' });
|
|
37
|
+
const target = globalThis as unknown as Record<string, unknown>;
|
|
38
|
+
for (const key of DOM_GLOBALS) {
|
|
39
|
+
nativeGlobals[key] = target[key];
|
|
40
|
+
target[key] = (win as unknown as Record<string, unknown>)[key];
|
|
41
|
+
}
|
|
42
|
+
container = document.createElement('div');
|
|
43
|
+
document.body.appendChild(container);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterAll(() => {
|
|
47
|
+
const target = globalThis as unknown as Record<string, unknown>;
|
|
48
|
+
for (const key of DOM_GLOBALS) {
|
|
49
|
+
if (nativeGlobals[key] === undefined) delete target[key];
|
|
50
|
+
else target[key] = nativeGlobals[key];
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
clearHighlightMemo();
|
|
56
|
+
resetTokenizerWarmup();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('scheduleIdle', () => {
|
|
60
|
+
test('a runtime without requestIdleCallback still runs the work', () => {
|
|
61
|
+
const host = globalThis as unknown as Record<string, unknown>;
|
|
62
|
+
const saved = host.requestIdleCallback;
|
|
63
|
+
delete host.requestIdleCallback;
|
|
64
|
+
vi.useFakeTimers();
|
|
65
|
+
try {
|
|
66
|
+
let runs = 0;
|
|
67
|
+
scheduleIdle(() => { runs += 1; });
|
|
68
|
+
vi.advanceTimersByTime(1);
|
|
69
|
+
expect(runs).toBe(1);
|
|
70
|
+
} finally {
|
|
71
|
+
vi.useRealTimers();
|
|
72
|
+
if (saved !== undefined) host.requestIdleCallback = saved;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('cancelling before the slice fires skips the work', () => {
|
|
77
|
+
const host = globalThis as unknown as Record<string, unknown>;
|
|
78
|
+
const saved = host.requestIdleCallback;
|
|
79
|
+
delete host.requestIdleCallback;
|
|
80
|
+
vi.useFakeTimers();
|
|
81
|
+
try {
|
|
82
|
+
let runs = 0;
|
|
83
|
+
const cancel = scheduleIdle(() => { runs += 1; });
|
|
84
|
+
cancel();
|
|
85
|
+
vi.advanceTimersByTime(1);
|
|
86
|
+
expect(runs).toBe(0);
|
|
87
|
+
} finally {
|
|
88
|
+
vi.useRealTimers();
|
|
89
|
+
if (saved !== undefined) host.requestIdleCallback = saved;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('warmSyntaxTokenizers', () => {
|
|
95
|
+
test('tokenizes one sample per language, one slice at a time', () => {
|
|
96
|
+
const calls: Array<{ language: string; code: string }> = [];
|
|
97
|
+
const queue: Array<() => void> = [];
|
|
98
|
+
warmSyntaxTokenizers({
|
|
99
|
+
languages: ['typescript', 'bash'],
|
|
100
|
+
ready: () => true,
|
|
101
|
+
highlight: (code, language) => calls.push({ language, code }),
|
|
102
|
+
schedule: (run) => {
|
|
103
|
+
queue.push(run);
|
|
104
|
+
return () => {};
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// One slice is armed, not the whole set: a slow grammar must not become the
|
|
109
|
+
// long task this exists to prevent.
|
|
110
|
+
expect(queue).toHaveLength(1);
|
|
111
|
+
act(() => queue.shift()!());
|
|
112
|
+
expect(calls.map((c) => c.language)).toEqual(['typescript']);
|
|
113
|
+
expect(queue).toHaveLength(1);
|
|
114
|
+
act(() => queue.shift()!());
|
|
115
|
+
expect(calls.map((c) => c.language)).toEqual(['typescript', 'bash']);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('is a no-op the second time, so a re-evaluated entry cannot re-warm', () => {
|
|
119
|
+
let calls = 0;
|
|
120
|
+
const options = {
|
|
121
|
+
languages: ['typescript'],
|
|
122
|
+
ready: () => true,
|
|
123
|
+
highlight: () => { calls += 1; },
|
|
124
|
+
schedule: (run: () => void) => { run(); return () => {}; },
|
|
125
|
+
};
|
|
126
|
+
warmSyntaxTokenizers(options);
|
|
127
|
+
warmSyntaxTokenizers(options);
|
|
128
|
+
expect(calls).toBe(1);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('a grammar that has not landed is retried rather than skipped', () => {
|
|
132
|
+
const calls: string[] = [];
|
|
133
|
+
let ready = false;
|
|
134
|
+
const queue: Array<() => void> = [];
|
|
135
|
+
warmSyntaxTokenizers({
|
|
136
|
+
languages: ['typescript'],
|
|
137
|
+
ready: () => ready,
|
|
138
|
+
highlight: (_code, language) => calls.push(language),
|
|
139
|
+
schedule: (run) => {
|
|
140
|
+
queue.push(run);
|
|
141
|
+
return () => {};
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
act(() => queue.shift()!());
|
|
146
|
+
expect(calls).toEqual([]);
|
|
147
|
+
ready = true;
|
|
148
|
+
act(() => queue.shift()!());
|
|
149
|
+
expect(calls).toEqual(['typescript']);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('the deferred render', () => {
|
|
154
|
+
test('draws the escaped source without tokenizing on the mounting tick', async () => {
|
|
155
|
+
let renders = 0;
|
|
156
|
+
function Card({ code }: { code: string }) {
|
|
157
|
+
const html = useDeferredHighlight(code, 'typescript');
|
|
158
|
+
renders += 1;
|
|
159
|
+
return h('pre', { dangerouslySetInnerHTML: { __html: html } });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await act(async () => {
|
|
163
|
+
render(h(Card, { code: 'const value = 1;' }), container);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Nothing has been tokenized on this tick — the tap's own task is clean.
|
|
167
|
+
expect(highlightMemoSize()).toBe(0);
|
|
168
|
+
expect(renders).toBeGreaterThan(0);
|
|
169
|
+
// What it DID draw is the escaped source, not an empty box.
|
|
170
|
+
expect(container.textContent).toContain('const value = 1;');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('empty input renders nothing and caches nothing', async () => {
|
|
174
|
+
function Card() {
|
|
175
|
+
return h('pre', { dangerouslySetInnerHTML: { __html: useDeferredHighlight('', 'typescript') } });
|
|
176
|
+
}
|
|
177
|
+
await act(async () => {
|
|
178
|
+
render(h(Card, {}), container);
|
|
179
|
+
});
|
|
180
|
+
expect(highlightMemoSize()).toBe(0);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Syntax-highlighted markup that never blocks the interaction that asked for it.
|
|
8
|
+
*
|
|
9
|
+
* A tool card renders its code the moment it is expanded, and the tokenizer used
|
|
10
|
+
* to run inside that render: measured on a throttled phone (4× CPU), a `read`
|
|
11
|
+
* card's expand was a 220–300 ms long task of which ~75% was the Shiki grammar
|
|
12
|
+
* engine, and at 20× the same card blocked for 730–1.484 ms. The card was on
|
|
13
|
+
* screen the whole time — it just could not paint until the last line had been
|
|
14
|
+
* tokenized.
|
|
15
|
+
*
|
|
16
|
+
* So the first render draws the text, escaped and correct, and the tokenized
|
|
17
|
+
* markup replaces it once the main thread is free (`scheduleIdle`). The two
|
|
18
|
+
* carry the same characters, so the swap costs colour and no layout.
|
|
19
|
+
*
|
|
20
|
+
* Three properties make that honest:
|
|
21
|
+
*
|
|
22
|
+
* - **A cached block renders highlighted on the FIRST paint.** `highlight-memo`
|
|
23
|
+
* is checked during render, so reopening a card the reader already opened —
|
|
24
|
+
* the common case while paging a transcript — has no plain-text frame at all.
|
|
25
|
+
* - **A stale block is never shown.** The value is tagged with the input it was
|
|
26
|
+
* computed from; a filter keystroke in the search panel therefore shows the
|
|
27
|
+
* new text, never the previous query's markup.
|
|
28
|
+
* - **Nothing is computed for empty input.** Call sites that gate a branch pass
|
|
29
|
+
* `''`, which is how `Edit` stops tokenizing the two panels it is not drawing.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { useEffect, useMemo, useState } from 'preact/hooks';
|
|
33
|
+
import { escapeCode } from '@/shared/lib/code/highlighter';
|
|
34
|
+
import {
|
|
35
|
+
cachedHighlightBlock,
|
|
36
|
+
cachedHighlightBlockLines,
|
|
37
|
+
highlightBlock,
|
|
38
|
+
highlightBlockLines,
|
|
39
|
+
} from '@/shared/lib/code/highlight-memo';
|
|
40
|
+
import { scheduleIdle } from '@/shared/lib/code/idle';
|
|
41
|
+
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
42
|
+
|
|
43
|
+
interface Deferred<T> {
|
|
44
|
+
/** Identity of the input; a value computed for another input is not shown. */
|
|
45
|
+
key: object;
|
|
46
|
+
hasContent: boolean;
|
|
47
|
+
cached: T | undefined;
|
|
48
|
+
plain: T;
|
|
49
|
+
compute: () => T;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function useDeferred<T>(spec: Deferred<T>): T {
|
|
53
|
+
// A grammar arriving is what turns a plain fallback into a real highlight, so
|
|
54
|
+
// the effect re-runs on it even though the input did not move.
|
|
55
|
+
const syntaxReady = useSyntaxReady();
|
|
56
|
+
const [state, setState] = useState<{ key: object; value: T }>(() => ({
|
|
57
|
+
key: spec.key,
|
|
58
|
+
value: spec.cached ?? spec.plain,
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
/** Adopt a value only when it is a change, so a cached block does not cost an
|
|
62
|
+
* extra render on every mount. */
|
|
63
|
+
const adopt = (key: object, value: T): void => {
|
|
64
|
+
setState((previous) => (previous.key === key && previous.value === value ? previous : { key, value }));
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!spec.hasContent) {
|
|
69
|
+
adopt(spec.key, spec.plain);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (spec.cached !== undefined) {
|
|
73
|
+
adopt(spec.key, spec.cached);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
let cancelled = false;
|
|
77
|
+
const cancel = scheduleIdle(() => {
|
|
78
|
+
if (cancelled) return;
|
|
79
|
+
let value: T;
|
|
80
|
+
try {
|
|
81
|
+
value = spec.compute();
|
|
82
|
+
} catch {
|
|
83
|
+
// A grammar that cannot tokenize this input leaves the escaped text,
|
|
84
|
+
// which is what the first paint already drew.
|
|
85
|
+
value = spec.plain;
|
|
86
|
+
}
|
|
87
|
+
if (!cancelled) adopt(spec.key, value);
|
|
88
|
+
});
|
|
89
|
+
return () => {
|
|
90
|
+
cancelled = true;
|
|
91
|
+
cancel();
|
|
92
|
+
};
|
|
93
|
+
// `spec` is rebuilt every render; its KEY (the input) and the grammar's
|
|
94
|
+
// readiness are what decide the work.
|
|
95
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
96
|
+
}, [spec.key, syntaxReady]);
|
|
97
|
+
|
|
98
|
+
if (state.key === spec.key) return state.value;
|
|
99
|
+
return spec.cached ?? spec.plain;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Tokenized markup for one block, deferred off the render that asked for it. */
|
|
103
|
+
export function useDeferredHighlight(code: string, language: string): string {
|
|
104
|
+
const key = useMemo(() => ({ code, language }), [code, language]);
|
|
105
|
+
const plain = useMemo(() => escapeCode(code), [code]);
|
|
106
|
+
return useDeferred<string>({
|
|
107
|
+
key,
|
|
108
|
+
hasContent: Boolean(code),
|
|
109
|
+
cached: code ? cachedHighlightBlock(code, language) : undefined,
|
|
110
|
+
plain,
|
|
111
|
+
compute: () => highlightBlock(code, language),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Tokenized markup per line, deferred the same way. */
|
|
116
|
+
export function useDeferredHighlightLines(code: string, language: string): string[] {
|
|
117
|
+
const key = useMemo(() => ({ code, language }), [code, language]);
|
|
118
|
+
const plain = useMemo(
|
|
119
|
+
() => (code ? code.split('\n').map(escapeCode) : []),
|
|
120
|
+
[code],
|
|
121
|
+
);
|
|
122
|
+
return useDeferred<string[]>({
|
|
123
|
+
key,
|
|
124
|
+
hasContent: Boolean(code),
|
|
125
|
+
cached: code ? cachedHighlightBlockLines(code, language) : undefined,
|
|
126
|
+
plain,
|
|
127
|
+
compute: () => highlightBlockLines(code, language),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type { EditorCommand } from '@/shared/lib/code/editor/keymap';
|
|
17
|
-
import type { FindOptions } from '@/shared/lib/code/editor/find';
|
|
17
|
+
import type { FindOptions } from '@/shared/lib/code/editor/find-scan';
|
|
18
18
|
|
|
19
19
|
export interface UseEditorFindOptions {
|
|
20
20
|
/** The buffer on screen. */
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The editor-theme setting, and the two rules that make it safe to offer.
|
|
8
|
+
*
|
|
9
|
+
* A theme is a MODULE that is fetched on selection, so there are two failure
|
|
10
|
+
* modes a unit test can pin without a browser:
|
|
11
|
+
*
|
|
12
|
+
* 1. **The module has to be registered before Shiki can tokenize with it.**
|
|
13
|
+
* `loadTheme` is idempotent, and a second surface selecting the same theme
|
|
14
|
+
* must share the first fetch rather than start a second one.
|
|
15
|
+
* 2. **`auto` must not register anything**, and an id this build does not ship
|
|
16
|
+
* must resolve to no theme rather than leave the editor half-painted.
|
|
17
|
+
*
|
|
18
|
+
* Driven through happy-dom because the hook subscribes to a window event — the
|
|
19
|
+
* same shape the editor FONT uses, and the reason an already-open editor hears
|
|
20
|
+
* about a change at all (`appSettings` is a boot-time prop).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
|
|
24
|
+
import { Window } from 'happy-dom';
|
|
25
|
+
import { h, render } from 'preact';
|
|
26
|
+
import { act } from 'preact/test-utils';
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
EDITOR_THEME_CHANGED_EVENT,
|
|
30
|
+
currentEditorThemeId,
|
|
31
|
+
useEditorTheme,
|
|
32
|
+
} from '@/client/hooks/editor/theme';
|
|
33
|
+
import {
|
|
34
|
+
isEditorThemeReady,
|
|
35
|
+
loadEditorTheme,
|
|
36
|
+
onEditorThemeReady,
|
|
37
|
+
resetEditorThemeLoader,
|
|
38
|
+
} from '@/shared/lib/code/editor/theme-loader';
|
|
39
|
+
import { EDITOR_AUTO_THEME_ID, resolveEditorTheme } from '@/shared/lib/code/editor/themes';
|
|
40
|
+
import { bootSyntax, onLanguageReady, requestLanguage } from '@/shared/lib/code/highlighter';
|
|
41
|
+
import { primeKabmoSettings } from '@/shared/lib/settings/client';
|
|
42
|
+
|
|
43
|
+
const DOM_GLOBALS = [
|
|
44
|
+
'window', 'document', 'navigator', 'HTMLElement', 'Element', 'Node', 'Event', 'CustomEvent',
|
|
45
|
+
'getComputedStyle', 'MutationObserver', 'requestAnimationFrame', 'cancelAnimationFrame',
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
/** The runner's own globals, put back once this file's DOM work is done. */
|
|
49
|
+
const native: Partial<Record<(typeof DOM_GLOBALS)[number], unknown>> = {};
|
|
50
|
+
|
|
51
|
+
/** happy-dom's `CustomEvent`, so a dispatch is an instance its EventTarget accepts. */
|
|
52
|
+
let Custom: typeof CustomEvent;
|
|
53
|
+
|
|
54
|
+
async function bootTokenizer(): Promise<void> {
|
|
55
|
+
// The highlighter only boots where `window` exists, and the Bun runner has
|
|
56
|
+
// none. Stub it for the boot alone, then hand the global to happy-dom — a
|
|
57
|
+
// `window` left pointing at `globalThis` would make the hook's
|
|
58
|
+
// `dispatchEvent` the runner's, which rejects a happy-dom CustomEvent.
|
|
59
|
+
const target = globalThis as unknown as Record<string, unknown>;
|
|
60
|
+
const had = 'window' in target ? target.window : undefined;
|
|
61
|
+
target.window = globalThis;
|
|
62
|
+
await bootSyntax();
|
|
63
|
+
if (had === undefined) delete target.window;
|
|
64
|
+
else target.window = had;
|
|
65
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
66
|
+
const settle = () => {
|
|
67
|
+
if (requestLanguage('typescript')) {
|
|
68
|
+
off();
|
|
69
|
+
resolve();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const off = onLanguageReady(settle);
|
|
73
|
+
settle();
|
|
74
|
+
await promise;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
beforeAll(async () => {
|
|
78
|
+
const win = new Window({ url: 'http://localhost' });
|
|
79
|
+
const target = globalThis as unknown as Record<string, unknown>;
|
|
80
|
+
for (const key of DOM_GLOBALS) {
|
|
81
|
+
if (!(key in native)) native[key] = target[key];
|
|
82
|
+
const value = (win as unknown as Record<string, unknown>)[key];
|
|
83
|
+
if (value !== undefined) target[key] = value;
|
|
84
|
+
}
|
|
85
|
+
Custom = win.CustomEvent as unknown as typeof CustomEvent;
|
|
86
|
+
await bootTokenizer();
|
|
87
|
+
resetEditorThemeLoader();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
primeKabmoSettings({});
|
|
92
|
+
document.body.innerHTML = '';
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
afterAll(() => {
|
|
96
|
+
const target = globalThis as unknown as Record<string, unknown>;
|
|
97
|
+
for (const key of DOM_GLOBALS) {
|
|
98
|
+
if (native[key] === undefined) delete target[key];
|
|
99
|
+
else target[key] = native[key];
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/** A probe that renders exactly what the hook says the surface should paint. */
|
|
104
|
+
function ThemeProbe() {
|
|
105
|
+
const theme = useEditorTheme();
|
|
106
|
+
return h(
|
|
107
|
+
'div',
|
|
108
|
+
{ id: 'probe' },
|
|
109
|
+
`${theme.theme?.id ?? 'auto'}|${theme.vars['--editor-bg'] ?? 'none'}|${theme.themes.light}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function mountProbe(): Promise<HTMLElement> {
|
|
114
|
+
const host = document.createElement('div');
|
|
115
|
+
document.body.appendChild(host);
|
|
116
|
+
await act(async () => {
|
|
117
|
+
render(h(ThemeProbe, {}), host);
|
|
118
|
+
});
|
|
119
|
+
return host.querySelector('#probe') as HTMLElement;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
describe('currentEditorThemeId', () => {
|
|
123
|
+
test('reads the stored value and falls back to auto', () => {
|
|
124
|
+
primeKabmoSettings({});
|
|
125
|
+
expect(currentEditorThemeId()).toBe(EDITOR_AUTO_THEME_ID);
|
|
126
|
+
|
|
127
|
+
primeKabmoSettings({ kabmo_settings: { editorTheme: 'nord' } });
|
|
128
|
+
expect(currentEditorThemeId()).toBe('nord');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('theme module loading', () => {
|
|
133
|
+
test('auto needs no module, and an unknown id is a no-op', async () => {
|
|
134
|
+
resetEditorThemeLoader();
|
|
135
|
+
await loadEditorTheme(EDITOR_AUTO_THEME_ID);
|
|
136
|
+
expect(isEditorThemeReady(EDITOR_AUTO_THEME_ID)).toBe(true);
|
|
137
|
+
|
|
138
|
+
await loadEditorTheme('no-such-theme');
|
|
139
|
+
expect(isEditorThemeReady('no-such-theme')).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('a shipped theme registers with the highlighter and announces itself', async () => {
|
|
143
|
+
resetEditorThemeLoader();
|
|
144
|
+
const seen: string[] = [];
|
|
145
|
+
const off = onEditorThemeReady((id) => seen.push(id));
|
|
146
|
+
try {
|
|
147
|
+
expect(isEditorThemeReady('nord')).toBe(false);
|
|
148
|
+
await loadEditorTheme('nord');
|
|
149
|
+
expect(isEditorThemeReady('nord')).toBe(true);
|
|
150
|
+
expect(seen).toEqual(['nord']);
|
|
151
|
+
} finally {
|
|
152
|
+
off();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('two requests for one theme share a single registration', async () => {
|
|
157
|
+
resetEditorThemeLoader();
|
|
158
|
+
let announcements = 0;
|
|
159
|
+
const off = onEditorThemeReady(() => announcements++);
|
|
160
|
+
try {
|
|
161
|
+
await Promise.all([loadEditorTheme('dracula'), loadEditorTheme('dracula')]);
|
|
162
|
+
expect(announcements).toBe(1);
|
|
163
|
+
// Already registered: a third call resolves without announcing again.
|
|
164
|
+
await loadEditorTheme('dracula');
|
|
165
|
+
expect(announcements).toBe(1);
|
|
166
|
+
} finally {
|
|
167
|
+
off();
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('useEditorTheme', () => {
|
|
173
|
+
test('auto renders no variables and the app’s dual pair', async () => {
|
|
174
|
+
primeKabmoSettings({});
|
|
175
|
+
const probe = await mountProbe();
|
|
176
|
+
expect(probe.textContent).toBe('auto|none|one-light');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('an explicit theme renders its own surface and names itself in both slots', async () => {
|
|
180
|
+
primeKabmoSettings({ kabmo_settings: { editorTheme: 'dracula' } });
|
|
181
|
+
const probe = await mountProbe();
|
|
182
|
+
const theme = resolveEditorTheme('dracula');
|
|
183
|
+
expect(theme).not.toBeNull();
|
|
184
|
+
expect(probe.textContent).toBe(`dracula|${theme!.bg}|dracula`);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('a write reaches an ALREADY-MOUNTED editor without a reload', async () => {
|
|
188
|
+
primeKabmoSettings({});
|
|
189
|
+
const probe = await mountProbe();
|
|
190
|
+
expect(probe.textContent).toBe('auto|none|one-light');
|
|
191
|
+
|
|
192
|
+
// Exactly what the settings modal dispatches on a change.
|
|
193
|
+
await act(async () => {
|
|
194
|
+
window.dispatchEvent(new Custom(EDITOR_THEME_CHANGED_EVENT, { detail: 'nord' }));
|
|
195
|
+
});
|
|
196
|
+
const nord = resolveEditorTheme('nord');
|
|
197
|
+
expect(probe.textContent).toBe(`nord|${nord!.bg}|nord`);
|
|
198
|
+
|
|
199
|
+
// And back to auto, which must clear the variables again rather than leave
|
|
200
|
+
// the previous theme's surface behind.
|
|
201
|
+
await act(async () => {
|
|
202
|
+
window.dispatchEvent(new Custom(EDITOR_THEME_CHANGED_EVENT, { detail: EDITOR_AUTO_THEME_ID }));
|
|
203
|
+
});
|
|
204
|
+
expect(probe.textContent).toBe('auto|none|one-light');
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Which editor theme is live, and how a change reaches an open editor.
|
|
8
|
+
*
|
|
9
|
+
* The setting is visual, so it has to apply to the editor on screen — not on the
|
|
10
|
+
* next reload. `appSettings` is a prop captured at boot, so reading it once
|
|
11
|
+
* would leave an already-mounted editor on the previous palette; this follows
|
|
12
|
+
* the editor FONT's shape (`hooks/editor/font.ts`): the picker writes the
|
|
13
|
+
* setting and announces it, and every editor subscribes.
|
|
14
|
+
*
|
|
15
|
+
* A theme's module is fetched the first time it is SELECTED (`theme-loader.ts`),
|
|
16
|
+
* so an install that never opens the picker downloads none of them.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useEffect, useState } from 'preact/hooks';
|
|
20
|
+
import { readKabmoSetting } from '@/shared/lib/settings/client';
|
|
21
|
+
import {
|
|
22
|
+
EDITOR_AUTO_THEME_ID,
|
|
23
|
+
EDITOR_DEFAULT_THEME_ID,
|
|
24
|
+
editorShikiThemes,
|
|
25
|
+
editorThemeVars,
|
|
26
|
+
resolveEditorTheme,
|
|
27
|
+
type EditorTheme,
|
|
28
|
+
} from '@/shared/lib/code/editor/themes';
|
|
29
|
+
import { loadEditorTheme } from '@/shared/lib/code/editor/theme-loader';
|
|
30
|
+
|
|
31
|
+
/** Broadcast on every editor-theme write. Detail is the new theme id. */
|
|
32
|
+
export const EDITOR_THEME_CHANGED_EVENT = 'omp:editor-theme-changed';
|
|
33
|
+
|
|
34
|
+
/** The theme id currently stored, falling back to `auto`. */
|
|
35
|
+
export function currentEditorThemeId(): string {
|
|
36
|
+
if (typeof window === 'undefined') return EDITOR_DEFAULT_THEME_ID;
|
|
37
|
+
return readKabmoSetting<string>('editorTheme') ?? EDITOR_DEFAULT_THEME_ID;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Everything a code surface needs to paint with the live editor theme. */
|
|
41
|
+
export interface EditorThemeState {
|
|
42
|
+
/** The theme itself, or `null` for `auto` (the app palette). */
|
|
43
|
+
theme: EditorTheme | null;
|
|
44
|
+
/** CSS custom properties for the surface host; empty for `auto`. */
|
|
45
|
+
vars: Record<string, string>;
|
|
46
|
+
/** The two Shiki theme names to tokenize with. */
|
|
47
|
+
themes: { light: string; dark: string };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const AUTO_STATE: EditorThemeState = { theme: null, vars: {}, themes: editorShikiThemes(null) };
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The live editor theme, kept in step with writes.
|
|
54
|
+
*
|
|
55
|
+
* One hook rather than three, because a surface needs all three values and
|
|
56
|
+
* three subscriptions to one event would be three re-render paths that can
|
|
57
|
+
* disagree for a frame.
|
|
58
|
+
*/
|
|
59
|
+
export function useEditorTheme(): EditorThemeState {
|
|
60
|
+
const [id, setId] = useState<string>(currentEditorThemeId);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const onChange = (event: Event) => {
|
|
64
|
+
const detail = (event as CustomEvent<string>).detail;
|
|
65
|
+
setId(detail || currentEditorThemeId());
|
|
66
|
+
};
|
|
67
|
+
window.addEventListener(EDITOR_THEME_CHANGED_EVENT, onChange);
|
|
68
|
+
return () => window.removeEventListener(EDITOR_THEME_CHANGED_EVENT, onChange);
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
// Fetch the module the first time this theme is live, so the tokenizer can
|
|
72
|
+
// colour with it. A no-op for `auto` and for an id already registered.
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
void loadEditorTheme(id);
|
|
75
|
+
}, [id]);
|
|
76
|
+
|
|
77
|
+
// `auto` is the shipped state, and it allocates nothing new per render.
|
|
78
|
+
if (id === EDITOR_AUTO_THEME_ID) return AUTO_STATE;
|
|
79
|
+
|
|
80
|
+
const theme = resolveEditorTheme(id);
|
|
81
|
+
return { theme, vars: editorThemeVars(theme), themes: editorShikiThemes(id) };
|
|
82
|
+
}
|