@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,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Pay the tokenizer's start-up cost before the reader taps anything.
|
|
8
|
+
*
|
|
9
|
+
* `EAGER_LANGS` are loaded in the background at boot, but loading a grammar is
|
|
10
|
+
* not compiling it: the grammar's regexes are built and JIT-ed on the first
|
|
11
|
+
* tokenize, which is why the FIRST expand of a card in a given language measured
|
|
12
|
+
* 220–300 ms at 4× CPU while the same card re-expanded in 33 ms. That first
|
|
13
|
+
* expand is the one a reader actually experiences — a transcript opens with
|
|
14
|
+
* `read` and `bash` cards.
|
|
15
|
+
*
|
|
16
|
+
* So each eager grammar is tokenized once, on a tiny sample, while the main
|
|
17
|
+
* thread is idle. The sample's markup is discarded; the work being paid for is
|
|
18
|
+
* the compile behind it. One language per idle slice, so a slow grammar cannot
|
|
19
|
+
* become the long task this exists to prevent — and a grammar that has not
|
|
20
|
+
* landed yet is retried rather than skipped, because tokenizing before it
|
|
21
|
+
* arrives compiles nothing.
|
|
22
|
+
*
|
|
23
|
+
* Booted from `main.tsx` rather than from `highlighter.ts`: the warm-up needs
|
|
24
|
+
* the memo, which needs the highlighter, and an import in the other direction
|
|
25
|
+
* would close that cycle.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { isLanguageReady } from '@/shared/lib/code/highlighter-lazy';
|
|
29
|
+
import { highlightBlock } from '@/shared/lib/code/highlight-memo';
|
|
30
|
+
import { scheduleIdle } from '@/shared/lib/code/idle';
|
|
31
|
+
import { EAGER_LANGS } from '@/shared/lib/code/shiki-langs';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A few lines that make the grammar walk its own rule set: a keyword, a string,
|
|
35
|
+
* a comment, a call and a block. The exact text does not matter — the compile
|
|
36
|
+
* does — so these are deliberately tiny.
|
|
37
|
+
*/
|
|
38
|
+
const SAMPLES: Record<string, string> = {
|
|
39
|
+
typescript: '// warm\nconst value: number = 1;\nfunction read(input: string) {\n return input;\n}\n',
|
|
40
|
+
tsx: '// warm\nexport const View = ({ title }: { title: string }) => (\n <div className="row">{title}</div>\n);\n',
|
|
41
|
+
javascript: '// warm\nconst value = 1;\nfunction read(input) {\n return input;\n}\n',
|
|
42
|
+
jsx: '// warm\nexport const View = ({ title }) => (\n <div className="row">{title}</div>\n);\n',
|
|
43
|
+
json: '{\n "name": "warm",\n "items": [1, 2, 3],\n "ok": true\n}\n',
|
|
44
|
+
bash: '#!/bin/sh\n# warm\nset -e\nfor file in *.ts; do\n echo "$file"\ndone\n',
|
|
45
|
+
python: '# warm\ndef read(value: str) -> str:\n return value.strip()\n',
|
|
46
|
+
markdown: '# warm\n\nA paragraph with **bold** text and a `code` span.\n\n- item\n',
|
|
47
|
+
yaml: 'name: warm\nitems:\n - one\n - two\nenabled: true\n',
|
|
48
|
+
diff: '--- a/file.ts\n+++ b/file.ts\n@@ -1,2 +1,2 @@\n-old line\n+new line\n context\n',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const DEFAULT_SAMPLE = '// warm\nconst value = 1;\nfunction read(input) {\n return input;\n}\n';
|
|
52
|
+
|
|
53
|
+
/** How many idle slices a not-yet-loaded grammar is retried for. */
|
|
54
|
+
const MAX_WAIT_SLICES = 120;
|
|
55
|
+
|
|
56
|
+
export interface WarmOptions {
|
|
57
|
+
/** Languages to warm; defaults to the eager set. */
|
|
58
|
+
languages?: readonly string[];
|
|
59
|
+
/** The tokenizer; injected so a test can observe the schedule. */
|
|
60
|
+
highlight?: (code: string, language: string) => void;
|
|
61
|
+
/** Grammar readiness; injected for the same reason. */
|
|
62
|
+
ready?: (language: string) => boolean;
|
|
63
|
+
/** The idle scheduler; injected so a test can drive it synchronously. */
|
|
64
|
+
schedule?: (run: () => void) => () => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let started = false;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Warm every eager grammar once per page load, one per idle slice.
|
|
71
|
+
*
|
|
72
|
+
* Idempotent: a second call is a no-op, so a re-evaluated client entry cannot
|
|
73
|
+
* re-tokenize every language.
|
|
74
|
+
*/
|
|
75
|
+
export function warmSyntaxTokenizers(options: WarmOptions = {}): void {
|
|
76
|
+
if (started) return;
|
|
77
|
+
if (typeof window === 'undefined') return;
|
|
78
|
+
started = true;
|
|
79
|
+
|
|
80
|
+
const languages = options.languages ?? EAGER_LANGS;
|
|
81
|
+
const highlight = options.highlight ?? ((code: string, language: string) => { highlightBlock(code, language); });
|
|
82
|
+
const ready = options.ready ?? isLanguageReady;
|
|
83
|
+
const schedule = options.schedule ?? ((run: () => void) => scheduleIdle(run));
|
|
84
|
+
|
|
85
|
+
const queue = [...languages];
|
|
86
|
+
let waiting = 0;
|
|
87
|
+
const step = () => {
|
|
88
|
+
const language = queue.shift();
|
|
89
|
+
if (language === undefined) return;
|
|
90
|
+
if (!ready(language)) {
|
|
91
|
+
// The grammar is still being fetched. Warming it now would compile
|
|
92
|
+
// nothing and the sample would not even be cacheable.
|
|
93
|
+
if (++waiting <= MAX_WAIT_SLICES) queue.push(language);
|
|
94
|
+
schedule(step);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
waiting = 0;
|
|
98
|
+
try {
|
|
99
|
+
highlight(SAMPLES[language] ?? DEFAULT_SAMPLE, language);
|
|
100
|
+
} catch {
|
|
101
|
+
// A grammar that cannot warm is not an error: the first real highlight
|
|
102
|
+
// falls back to escaped text and retries when the grammar lands.
|
|
103
|
+
}
|
|
104
|
+
schedule(step);
|
|
105
|
+
};
|
|
106
|
+
schedule(step);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Test/restart seam: forget that the warm-up already ran. */
|
|
110
|
+
export function resetTokenizerWarmup(): void {
|
|
111
|
+
started = false;
|
|
112
|
+
}
|
|
@@ -69,7 +69,9 @@ async function boot(): Promise<HighlighterCore | null> {
|
|
|
69
69
|
notify();
|
|
70
70
|
// Grammars arrive on demand (`ensureLanguage`); the eager set is only a
|
|
71
71
|
// head start for the languages a chat session hits immediately, and boot
|
|
72
|
-
// does not wait on it.
|
|
72
|
+
// does not wait on it. `main.tsx` pays the tokenizer's compile cost once
|
|
73
|
+
// the eager set has landed — loading a grammar is not compiling it, and the
|
|
74
|
+
// first card expand used to pay that compile inside the tap.
|
|
73
75
|
void ensureLanguages(highlighter, EAGER_LANGS);
|
|
74
76
|
return highlighter;
|
|
75
77
|
} catch (error) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Run work when the main thread is next free.
|
|
8
|
+
*
|
|
9
|
+
* Tokenizing a tool card's code is the expensive half of expanding it — measured
|
|
10
|
+
* at 730–1484 ms on a throttled phone for a `read`/`grep` card — and running it
|
|
11
|
+
* inside the render that a tap scheduled put all of that in the tap's own task:
|
|
12
|
+
* the card appeared only after the grammar had walked every line. Deferring it
|
|
13
|
+
* costs one plain-text frame and moves the work out of the interaction.
|
|
14
|
+
*
|
|
15
|
+
* `requestIdleCallback` is the browser's own "nothing else is waiting" signal;
|
|
16
|
+
* the timeout is a FLOOR on latency rather than a budget, so a busy page still
|
|
17
|
+
* paints the colour within a quarter second. Runtimes without it (happy-dom,
|
|
18
|
+
* older Safari) fall back to the next macrotask.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const IDLE_TIMEOUT_MS = 250;
|
|
22
|
+
|
|
23
|
+
type IdleHost = {
|
|
24
|
+
requestIdleCallback?: (callback: () => void, options?: { timeout: number }) => number;
|
|
25
|
+
cancelIdleCallback?: (handle: number) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Schedule `run`; the returned function cancels it. */
|
|
29
|
+
export function scheduleIdle(run: () => void, timeoutMs: number = IDLE_TIMEOUT_MS): () => void {
|
|
30
|
+
const host = globalThis as unknown as IdleHost;
|
|
31
|
+
if (typeof host.requestIdleCallback === 'function') {
|
|
32
|
+
const handle = host.requestIdleCallback(run, { timeout: timeoutMs });
|
|
33
|
+
return () => host.cancelIdleCallback?.(handle);
|
|
34
|
+
}
|
|
35
|
+
const handle = setTimeout(run, 0);
|
|
36
|
+
return () => clearTimeout(handle);
|
|
37
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The outline's two bounds.
|
|
8
|
+
*
|
|
9
|
+
* Both exist because a large file's outline is not a long list, it is a DOM:
|
|
10
|
+
* measured on a 10 MB source with 63 618 declarations, the panel mounted
|
|
11
|
+
* 446 308 nodes and a keystroke cost 953 ms (against the editor's own 34 ms).
|
|
12
|
+
* These cases pin the arithmetic that keeps that from happening, and the
|
|
13
|
+
* sentences that stop a truncated list from reading as a complete one.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, expect, test } from 'bun:test';
|
|
17
|
+
import {
|
|
18
|
+
MAX_OUTLINE_ROWS,
|
|
19
|
+
MAX_OUTLINE_SCAN_CHARS,
|
|
20
|
+
outlineTooLargeNotice,
|
|
21
|
+
outlineTruncatedNotice,
|
|
22
|
+
outlineWindow,
|
|
23
|
+
} from '@/shared/lib/code/outline/rows';
|
|
24
|
+
import type { OutlineSymbol } from '@/shared/lib/code/outline/types';
|
|
25
|
+
|
|
26
|
+
function symbols(count: number): OutlineSymbol[] {
|
|
27
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
28
|
+
name: `fn${i}`,
|
|
29
|
+
kind: 'function' as const,
|
|
30
|
+
line: i + 1,
|
|
31
|
+
column: 0,
|
|
32
|
+
depth: 0,
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('outlineWindow', () => {
|
|
37
|
+
test('a list under the cap is returned whole, in source order', () => {
|
|
38
|
+
const all = symbols(5);
|
|
39
|
+
const window = outlineWindow(all);
|
|
40
|
+
|
|
41
|
+
expect(window.rows).toEqual(all);
|
|
42
|
+
expect(window.total).toBe(5);
|
|
43
|
+
expect(window.truncated).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('exactly the cap is not truncated', () => {
|
|
47
|
+
const window = outlineWindow(symbols(MAX_OUTLINE_ROWS));
|
|
48
|
+
|
|
49
|
+
expect(window.rows).toHaveLength(MAX_OUTLINE_ROWS);
|
|
50
|
+
expect(window.truncated).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('past the cap the list is the FIRST rows, and it says how many there are', () => {
|
|
54
|
+
const window = outlineWindow(symbols(MAX_OUTLINE_ROWS + 120));
|
|
55
|
+
|
|
56
|
+
expect(window.rows).toHaveLength(MAX_OUTLINE_ROWS);
|
|
57
|
+
expect(window.total).toBe(MAX_OUTLINE_ROWS + 120);
|
|
58
|
+
expect(window.truncated).toBe(true);
|
|
59
|
+
// Source order, not a sample: the outline is a map of the document, so the
|
|
60
|
+
// top of the file is what a window shows.
|
|
61
|
+
expect(window.rows[0].name).toBe('fn0');
|
|
62
|
+
expect(window.rows[MAX_OUTLINE_ROWS - 1].name).toBe(`fn${MAX_OUTLINE_ROWS - 1}`);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('an empty list is not truncated', () => {
|
|
66
|
+
const window = outlineWindow([]);
|
|
67
|
+
|
|
68
|
+
expect(window.rows).toEqual([]);
|
|
69
|
+
expect(window.total).toBe(0);
|
|
70
|
+
expect(window.truncated).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('outlineTooLargeNotice', () => {
|
|
75
|
+
test('a buffer at the scan ceiling is still outlined', () => {
|
|
76
|
+
expect(outlineTooLargeNotice(MAX_OUTLINE_SCAN_CHARS)).toBeNull();
|
|
77
|
+
expect(outlineTooLargeNotice(1024)).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('past it the sentence names the size, so the empty panel is explained', () => {
|
|
81
|
+
const notice = outlineTooLargeNotice(MAX_OUTLINE_SCAN_CHARS + 1);
|
|
82
|
+
|
|
83
|
+
expect(notice).not.toBeNull();
|
|
84
|
+
expect(notice).toContain('8.0 MB');
|
|
85
|
+
expect(notice).toContain('too large to outline');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('outlineTruncatedNotice', () => {
|
|
90
|
+
test('it names both the shown count and the total, thousands separated', () => {
|
|
91
|
+
const notice = outlineTruncatedNotice(63618);
|
|
92
|
+
|
|
93
|
+
expect(notice).toContain('500');
|
|
94
|
+
expect(notice).toContain('63,618');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* How much of an outline a panel is allowed to draw, and when it is not drawn
|
|
8
|
+
* at all.
|
|
9
|
+
*
|
|
10
|
+
* Both bounds exist because a large file's outline is not a long list — it is a
|
|
11
|
+
* DOM. Measured in the browser on a 10 MB source file with 63 618 declarations:
|
|
12
|
+
* **446 308 DOM nodes, 415 MB of JS heap, and 953 ms per keystroke** while the
|
|
13
|
+
* panel was on screen (the editor's own cost is 34 ms). Expanding the section
|
|
14
|
+
* took 2.7 s. The scan is not the problem — it is 366 ms in Bun, once per pause
|
|
15
|
+
* — the ROWS are.
|
|
16
|
+
*
|
|
17
|
+
* Two answers, and they are different questions:
|
|
18
|
+
*
|
|
19
|
+
* - **`MAX_OUTLINE_ROWS` caps the list.** An outline is a map you scan by eye or
|
|
20
|
+
* jump from; nobody reads 63 000 rows, and the panel says how many it is not
|
|
21
|
+
* showing rather than silently truncating. Same shape as the search panel's
|
|
22
|
+
* `MAX_SEARCH_RESULTS`.
|
|
23
|
+
* - **`MAX_OUTLINE_SCAN_CHARS` skips the scan above a size where it costs more
|
|
24
|
+
* than the list is worth.** At 10 MB a scan is ~366 ms of blocked main thread
|
|
25
|
+
* every time the buffer settles, which is a stutter per typing pause. The
|
|
26
|
+
* sentence says so instead of leaving the panel empty, which would read as
|
|
27
|
+
* "this file has no declarations".
|
|
28
|
+
*
|
|
29
|
+
* The size is counted in CODE UNITS (`content.length`), not bytes: measuring
|
|
30
|
+
* UTF-8 bytes would mean encoding the whole buffer to answer a guard, and for
|
|
31
|
+
* the ASCII that dominates source files the two numbers are the same. A file of
|
|
32
|
+
* multibyte characters therefore trips the guard slightly early — the safe
|
|
33
|
+
* direction.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import type { OutlineSymbol } from '@/shared/lib/code/outline/types';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Rows a panel renders. Past this the list is truncated with a count, not
|
|
40
|
+
* silently shortened.
|
|
41
|
+
*/
|
|
42
|
+
export const MAX_OUTLINE_ROWS = 500;
|
|
43
|
+
|
|
44
|
+
/** Buffer size above which the outline is not scanned at all. */
|
|
45
|
+
/**
|
|
46
|
+
* Buffer size above which the outline is not scanned at all.
|
|
47
|
+
*
|
|
48
|
+
* The scan is one blocked stretch of main thread after each typing pause, and it
|
|
49
|
+
* measured 38 ms at 1 MB, 160 ms at 5 MB, 262 ms at 10 MB and 519 ms at 20 MB —
|
|
50
|
+
* linear in the buffer. 8 MiB keeps that pause under ~210 ms, which is less than
|
|
51
|
+
* the debounce that schedules it and therefore reads as "the list settled"
|
|
52
|
+
* rather than as a stutter.
|
|
53
|
+
*
|
|
54
|
+
* Past it the panel says the file is too large to outline instead of scanning
|
|
55
|
+
* it: a declaration list for a 30 MB file is not worth a half-second freeze on
|
|
56
|
+
* every pause, and an empty list would read as "no declarations here".
|
|
57
|
+
*/
|
|
58
|
+
export const MAX_OUTLINE_SCAN_CHARS = 8 * 1024 * 1024;
|
|
59
|
+
|
|
60
|
+
export interface OutlineWindow {
|
|
61
|
+
/** The rows to render — at most `MAX_OUTLINE_ROWS`. */
|
|
62
|
+
rows: OutlineSymbol[];
|
|
63
|
+
/** Symbols the scan actually found, including the ones not rendered. */
|
|
64
|
+
total: number;
|
|
65
|
+
/** True when `rows` is shorter than `total`. */
|
|
66
|
+
truncated: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The rows a panel should render, and whether it is showing all of them.
|
|
71
|
+
*
|
|
72
|
+
* Source order is preserved: an outline is a map of a document, so the first N
|
|
73
|
+
* declarations are the top of the file rather than an arbitrary sample.
|
|
74
|
+
*/
|
|
75
|
+
export function outlineWindow(symbols: readonly OutlineSymbol[]): OutlineWindow {
|
|
76
|
+
const total = symbols.length;
|
|
77
|
+
if (total <= MAX_OUTLINE_ROWS) return { rows: symbols as OutlineSymbol[], total, truncated: false };
|
|
78
|
+
return { rows: symbols.slice(0, MAX_OUTLINE_ROWS), total, truncated: true };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The sentence printed under a truncated list. */
|
|
82
|
+
export function outlineTruncatedNotice(total: number): string {
|
|
83
|
+
return `Showing the first ${MAX_OUTLINE_ROWS.toLocaleString()} of ${total.toLocaleString()} symbols.`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The sentence a panel prints instead of rows when the buffer is past the scan
|
|
88
|
+
* ceiling, or null when it is not.
|
|
89
|
+
*/
|
|
90
|
+
export function outlineTooLargeNotice(contentLength: number): string | null {
|
|
91
|
+
if (contentLength <= MAX_OUTLINE_SCAN_CHARS) return null;
|
|
92
|
+
const mb = contentLength / (1024 * 1024);
|
|
93
|
+
return `This file is ${mb.toFixed(1)} MB — too large to outline.`;
|
|
94
|
+
}
|
|
@@ -6,10 +6,9 @@
|
|
|
6
6
|
import { beforeAll, describe, expect, test } from 'bun:test';
|
|
7
7
|
|
|
8
8
|
import { bootSyntax, onLanguageReady, requestLanguage } from '@/shared/lib/code/highlighter';
|
|
9
|
-
import { findMatches } from '@/shared/lib/code/editor/find';
|
|
9
|
+
import { findMatches } from '@/shared/lib/code/editor/find-scan';
|
|
10
10
|
import { getLanguageFromPath } from '@/shared/lib/code/language';
|
|
11
11
|
import { highlightCode, highlightLines } from '@/shared/lib/code/syntax-highlight';
|
|
12
|
-
import { highlightCodeWindow } from '@/shared/lib/code/windowed-highlight';
|
|
13
12
|
|
|
14
13
|
/** Shiki emits `<span class="shiki">…` with dual-theme CSS variables per token. */
|
|
15
14
|
const SHIKI_RE = /<span class="shiki">/;
|
|
@@ -115,54 +114,6 @@ describe('highlightCode', () => {
|
|
|
115
114
|
});
|
|
116
115
|
});
|
|
117
116
|
|
|
118
|
-
describe('highlightCodeWindow', () => {
|
|
119
|
-
const source = Array.from({ length: 120 }, (_, index) =>
|
|
120
|
-
index === 0 ? 'export const first = 1;' : `export const value${index} = ${index}; // line ${index}`,
|
|
121
|
-
).join('\n');
|
|
122
|
-
|
|
123
|
-
/** The wrapper the app's `.shiki span` colour rule depends on. */
|
|
124
|
-
const unwrap = (html: string) => html.replace(/^<span class="shiki">/, '').replace(/<\/span>$/, '');
|
|
125
|
-
|
|
126
|
-
test('wraps the window in the class the token colours are scoped to', () => {
|
|
127
|
-
const window = highlightCodeWindow(source, 'typescript', { start: 0, end: 3 });
|
|
128
|
-
|
|
129
|
-
// Without this class no `.shiki span` rule matches, so the tokens would
|
|
130
|
-
// render in the plain ink colour.
|
|
131
|
-
expect(window).toMatch(SHIKI_RE);
|
|
132
|
-
expect(window.endsWith('</span>')).toBe(true);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test('returns only the requested lines, joined as the editor renders them', () => {
|
|
136
|
-
const window = highlightCodeWindow(source, 'typescript', { start: 10, end: 13 });
|
|
137
|
-
const rows = highlightLines(source, 'typescript');
|
|
138
|
-
|
|
139
|
-
expect(unwrap(window).split('\n')).toEqual(rows.slice(10, 13));
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test('keeps multi-line grammar state from the lines above the window', () => {
|
|
143
|
-
const commented = ['/*', ' * a block comment', ' * spanning several lines', ' */', 'const after = 1;'].join('\n');
|
|
144
|
-
const rows = highlightLines(commented, 'typescript');
|
|
145
|
-
// Line 2 sits inside the comment, whose opening line is outside the window.
|
|
146
|
-
const window = highlightCodeWindow(commented, 'typescript', { start: 2, end: 4 });
|
|
147
|
-
|
|
148
|
-
expect(unwrap(window).split('\n')).toEqual(rows.slice(2, 4));
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test('renders an over-long line as plain text instead of tokenizing it', () => {
|
|
152
|
-
const minified = `const bundled = "${'x'.repeat(200)}";\nconst next = 1;`;
|
|
153
|
-
const window = highlightCodeWindow(minified, 'javascript', { start: 0, end: 2 }, { maxLineLength: 50 });
|
|
154
|
-
|
|
155
|
-
expect(window).toContain('const bundled');
|
|
156
|
-
expect(unwrap(window).split('\n')[0]).not.toContain('--shiki-light:');
|
|
157
|
-
expect(unwrap(window).split('\n')[1]).toContain('--shiki-light:');
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test('returns nothing for a window outside the document', () => {
|
|
161
|
-
expect(highlightCodeWindow('const a = 1;', 'typescript', { start: 5, end: 9 })).toBe('');
|
|
162
|
-
expect(highlightCodeWindow('const a = 1;', 'typescript', { start: 0, end: 0 })).toBe('');
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
117
|
describe('find marks', () => {
|
|
167
118
|
const code = 'const alpha = 1;\nconst beta = alpha + 2;\n';
|
|
168
119
|
const PLAIN = { matchCase: false, wholeWord: false, isRegex: false };
|
|
@@ -206,35 +157,6 @@ describe('find marks', () => {
|
|
|
206
157
|
expect(plain).not.toContain('<mark');
|
|
207
158
|
});
|
|
208
159
|
|
|
209
|
-
test('keeps a windowed match on its own line, re-based to the slice', () => {
|
|
210
|
-
const marks = findMatches(code, 'alpha', PLAIN).matches;
|
|
211
|
-
// The window renders line 1 only. The match on line 0 must not reappear on
|
|
212
|
-
// line 1's text, and the current index (line 0's match) must not mark
|
|
213
|
-
// anything here.
|
|
214
|
-
const window = highlightCodeWindow(code, 'typescript', { start: 1, end: 2 }, { marks, currentMark: 0 });
|
|
215
|
-
|
|
216
|
-
expect(marked(window).join('')).toBe('alpha');
|
|
217
|
-
expect(window).not.toContain('data-find-current');
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
test('re-bases the current index onto the marks inside the window', () => {
|
|
221
|
-
const marks = findMatches(code, 'alpha', PLAIN).matches;
|
|
222
|
-
const window = highlightCodeWindow(code, 'typescript', { start: 1, end: 2 }, { marks, currentMark: 1 });
|
|
223
|
-
|
|
224
|
-
expect(window.match(/data-find-current/g)?.length).toBe(1);
|
|
225
|
-
expect(marked(window).join('')).toBe('alpha');
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
test('a mark reaching past the window is clipped to it', () => {
|
|
229
|
-
// The last line has no trailing newline, so a mark that runs to the end of
|
|
230
|
-
// the slice would be clamped by the rebase.
|
|
231
|
-
const window = highlightCodeWindow(code, 'typescript', { start: 1, end: 3 }, {
|
|
232
|
-
marks: [{ start: 30, end: 999 }],
|
|
233
|
-
currentMark: 0,
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
expect(marked(window).join('')).toBe('alpha + 2;');
|
|
237
|
-
});
|
|
238
160
|
});
|
|
239
161
|
|
|
240
162
|
describe('getLanguageFromPath', () => {
|
|
@@ -168,12 +168,11 @@ export function highlightLines(
|
|
|
168
168
|
const rawLines = code.split('\n');
|
|
169
169
|
const marks = options.marks;
|
|
170
170
|
const currentMark = options.currentMark ?? -1;
|
|
171
|
-
const occurrenceFrom = options.occurrenceFrom ?? -1;
|
|
172
171
|
// A grammar that has not arrived yet, or a highlighter that failed to boot,
|
|
173
172
|
// still gets the find marks: the widget's feedback must not depend on Shiki.
|
|
174
173
|
const plain = (lineIndex: number): string => {
|
|
175
174
|
if (!marks || marks.length === 0) return escapeCode(rawLines[lineIndex]);
|
|
176
|
-
return tokenMarkup(rawLines[lineIndex], lineStartOffset(code, lineIndex), marks, currentMark
|
|
175
|
+
return tokenMarkup(rawLines[lineIndex], lineStartOffset(code, lineIndex), marks, currentMark);
|
|
177
176
|
};
|
|
178
177
|
if (!code) return rawLines;
|
|
179
178
|
const hl = getHighlighterSync();
|
|
@@ -191,7 +190,7 @@ export function highlightLines(
|
|
|
191
190
|
let offset = lineStartOffset(code, lineIndex);
|
|
192
191
|
return lineTokens
|
|
193
192
|
.map((tok) => {
|
|
194
|
-
const content = tokenMarkup(tok.content, offset, marks, currentMark
|
|
193
|
+
const content = tokenMarkup(tok.content, offset, marks, currentMark);
|
|
195
194
|
offset += tok.content.length;
|
|
196
195
|
if (!tok.htmlStyle) return content;
|
|
197
196
|
const style = Object.entries(tok.htmlStyle)
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
PDF_MAX_FILE_BYTES,
|
|
29
29
|
unsupportedEditorMessage,
|
|
30
30
|
} from '@/shared/lib/fs/file-kind';
|
|
31
|
+
import { formatBytes } from '@/shared/lib/format/number';
|
|
31
32
|
|
|
32
33
|
const bytes = (...values: number[]): Uint8Array => new Uint8Array(values);
|
|
33
34
|
|
|
@@ -96,8 +97,11 @@ describe('classifyEditorFile', () => {
|
|
|
96
97
|
|
|
97
98
|
// The document ceiling does not reach plain text: a 6 MB source file is still
|
|
98
99
|
// refused, because a textarea holds that buffer for the life of the tab.
|
|
99
|
-
test('plain text keeps the text ceiling', () => {
|
|
100
|
-
|
|
100
|
+
test('plain text keeps the text ceiling, which a real source file clears', () => {
|
|
101
|
+
// 6 MB was over the old 5 MiB ceiling and is a normal generated file, not a
|
|
102
|
+
// refusal. The ceiling moved; this case is what keeps it from moving back.
|
|
103
|
+
expect(classifyEditorFile({ path: 'big.ts', size: 6 * 1024 * 1024 })).toBe('text');
|
|
104
|
+
expect(classifyEditorFile({ path: 'big.log', size: EDITOR_MAX_FILE_BYTES })).toBe('text');
|
|
101
105
|
expect(classifyEditorFile({ path: 'big.log', size: EDITOR_MAX_FILE_BYTES + 1 })).toBe('too-large');
|
|
102
106
|
});
|
|
103
107
|
|
|
@@ -120,24 +124,34 @@ describe('unsupportedEditorMessage', () => {
|
|
|
120
124
|
});
|
|
121
125
|
|
|
122
126
|
test('a size refusal names both the actual size and the ceiling', () => {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
// Derived, never spelled out: the point of the sentence is that its number
|
|
128
|
+
// is the one the classifier applied, so a literal here would pass while the
|
|
129
|
+
// two disagreed.
|
|
130
|
+
const message = unsupportedEditorMessage('too-large', 40 * 1024 * 1024);
|
|
131
|
+
expect(message).toContain('40.00 MB');
|
|
132
|
+
expect(message).toContain(formatBytes(EDITOR_MAX_FILE_BYTES));
|
|
126
133
|
});
|
|
127
134
|
|
|
128
135
|
test('a size refusal without a measured size names only the ceiling', () => {
|
|
129
136
|
const message = unsupportedEditorMessage('too-large');
|
|
130
|
-
expect(message).toContain(
|
|
137
|
+
expect(message).toContain(formatBytes(EDITOR_MAX_FILE_BYTES));
|
|
131
138
|
expect(message).not.toContain('undefined');
|
|
132
139
|
});
|
|
133
140
|
|
|
141
|
+
test('the text ceiling leaves room for a real source file and stops short of the knee', () => {
|
|
142
|
+
// 32 MiB: measured, the last size before a tab's heap crosses a gigabyte and
|
|
143
|
+
// a 64 MB file stops opening at all (see the constant's own table).
|
|
144
|
+
expect(EDITOR_MAX_FILE_BYTES).toBe(32 * 1024 * 1024);
|
|
145
|
+
expect(EDITOR_MAX_FILE_BYTES).toBeGreaterThan(DOCUMENT_MAX_FILE_BYTES);
|
|
146
|
+
});
|
|
147
|
+
|
|
134
148
|
// The sentence names the number the classifier actually applied: a PDF is
|
|
135
|
-
// refused against
|
|
136
|
-
// the file was never measured against.
|
|
137
|
-
test('a PDF size refusal names the PDF ceiling', () => {
|
|
149
|
+
// refused against its own ceiling, so naming the text one would describe a
|
|
150
|
+
// limit the file was never measured against.
|
|
151
|
+
test('a PDF size refusal names the PDF ceiling, not the text one', () => {
|
|
138
152
|
const message = unsupportedEditorMessage('too-large', 31 * 1024 * 1024, 'docs/report.pdf');
|
|
139
153
|
expect(message).toContain('31.00 MB');
|
|
140
|
-
expect(message).toContain(
|
|
141
|
-
expect(message).not.toContain(
|
|
154
|
+
expect(message).toContain(formatBytes(PDF_MAX_FILE_BYTES));
|
|
155
|
+
expect(message).not.toContain(formatBytes(EDITOR_MAX_FILE_BYTES));
|
|
142
156
|
});
|
|
143
157
|
});
|
|
@@ -143,16 +143,43 @@ export function getImageMimeFromBytes(bytes: Uint8Array): string | null {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
/**
|
|
146
|
-
* The largest file the editor will open when its bytes reach the page
|
|
146
|
+
* The largest text file the editor will open when its bytes reach the page
|
|
147
|
+
* WHOLE.
|
|
147
148
|
*
|
|
148
|
-
* Not a policy knob: the editor holds the whole file in one string
|
|
149
|
-
*
|
|
150
|
-
*
|
|
149
|
+
* Not a policy knob: the editor holds the whole file in one string, CodeMirror
|
|
150
|
+
* holds a second representation of it, and the whole thing is posted back on
|
|
151
|
+
* every save — so a ceiling has to exist.
|
|
151
152
|
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
153
|
+
* **32 MiB, measured.** The number used to be 5 MiB, and its justification died
|
|
154
|
+
* with the `<textarea>`: that limit existed because a textarea stops responding,
|
|
155
|
+
* and the surface is CodeMirror 6 now. Re-measured on the assembled app (a
|
|
156
|
+
* generated source file, Files panel open, Chromium):
|
|
157
|
+
*
|
|
158
|
+
* | File | Open | Keystroke | Save | JS heap |
|
|
159
|
+
* |---|---|---|---|---|
|
|
160
|
+
* | 5 MB | 99 ms | 17 ms | 289 ms | 73 MB |
|
|
161
|
+
* | 10 MB | 147 ms | 32 ms | 494 ms | 73 MB |
|
|
162
|
+
* | 20 MB | 212 ms | 34 ms | 969 ms | 73 MB |
|
|
163
|
+
* | 30 MB | 323 ms | 39 ms | 2 292 ms | — |
|
|
164
|
+
* | 40 MB | 283 ms | 45 ms | 1 793 ms | — |
|
|
165
|
+
* | 50 MB | 372 ms | 72 ms | 2 228 ms | **1 078 MB** |
|
|
166
|
+
* | 64 MB | never finished loading | — | — | — |
|
|
167
|
+
*
|
|
168
|
+
* The curve is flat through 40 MB and breaks at 50: the heap goes from a few
|
|
169
|
+
* hundred megabytes to over a gigabyte for ONE tab, and a 64 MB file does not
|
|
170
|
+
* open at all. 32 MiB is the last size with a comfortable margin below that
|
|
171
|
+
* knee — ten times any source file, and the point past which a tab is a risk to
|
|
172
|
+
* every other tab in the browser rather than a convenience.
|
|
173
|
+
*
|
|
174
|
+
* Two costs had to be removed before this number was reachable, and both were
|
|
175
|
+
* linear-in-file-size work paid per keystroke: the outline's scan-and-render of
|
|
176
|
+
* every declaration (`outline/rows.ts`), and a full `doc.toString()` on every
|
|
177
|
+
* render (`code-surface/index.tsx`).
|
|
178
|
+
*
|
|
179
|
+
* A DOCX and an XLSX are parsed in the page too, but they get their own number —
|
|
180
|
+
* see `DOCUMENT_MAX_FILE_BYTES`.
|
|
154
181
|
*/
|
|
155
|
-
export const EDITOR_MAX_FILE_BYTES =
|
|
182
|
+
export const EDITOR_MAX_FILE_BYTES = 32 * 1024 * 1024;
|
|
156
183
|
|
|
157
184
|
/**
|
|
158
185
|
* The largest Word document or workbook the editor opens.
|
|
@@ -160,9 +187,10 @@ export const EDITOR_MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
|
160
187
|
* `mammoth` and `read-excel-file` parse the fetched `ArrayBuffer` in the page,
|
|
161
188
|
* so the file does sit in memory — but it is unzipped and converted once and
|
|
162
189
|
* then released, where a text buffer is held for as long as the tab is open and
|
|
163
|
-
* re-posted on every save. That is why these get
|
|
164
|
-
*
|
|
165
|
-
*
|
|
190
|
+
* re-posted on every save. That is why these get a ceiling of their own rather
|
|
191
|
+
* than the text one: the reason the text limit exists (a buffer that must stay
|
|
192
|
+
* editable, and a save that re-posts it) does not apply to a read-only
|
|
193
|
+
* conversion.
|
|
166
194
|
*
|
|
167
195
|
* Measured on a 15 MB workbook: the fetch, the unzip and the grid render all
|
|
168
196
|
* complete, and the panel stays responsive.
|
|
@@ -170,8 +198,13 @@ export const EDITOR_MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
|
170
198
|
export const DOCUMENT_MAX_FILE_BYTES = 20 * 1024 * 1024;
|
|
171
199
|
|
|
172
200
|
/**
|
|
173
|
-
* The largest PDF the editor opens
|
|
174
|
-
*
|
|
201
|
+
* The largest PDF the editor opens.
|
|
202
|
+
*
|
|
203
|
+
* Its own number rather than the text ceiling, because a PDF is the one document
|
|
204
|
+
* the page never holds — and the only one whose limit is not about a buffer at
|
|
205
|
+
* all (see below). It is no longer the largest of the three: the TEXT ceiling
|
|
206
|
+
* moved past it, which is fine because the two numbers answer different
|
|
207
|
+
* questions rather than ranking a shared budget.
|
|
175
208
|
*
|
|
176
209
|
* `PdfView` points an `<iframe>` at `/api/fs/raw`, so the bytes go straight to
|
|
177
210
|
* Chrome's own viewer, which fetches them with range requests (verified: a
|
|
@@ -180,7 +213,8 @@ export const DOCUMENT_MAX_FILE_BYTES = 20 * 1024 * 1024;
|
|
|
180
213
|
* Nothing in Kabmo buffers the file, so the text ceiling has no reason to
|
|
181
214
|
* apply to it — and applying it was the bug: a 6 MB PDF dragged from Finder was
|
|
182
215
|
* refused as "larger than the 5.00 MB the editor opens" before the document
|
|
183
|
-
* branch was ever reached, because the size test runs first.
|
|
216
|
+
* branch was ever reached, because the size test runs first. The 30 MB here is
|
|
217
|
+
* Chrome's own plugin's comfort zone, not Kabmo's memory.
|
|
184
218
|
*/
|
|
185
219
|
export const PDF_MAX_FILE_BYTES = 30 * 1024 * 1024;
|
|
186
220
|
|