@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
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import { useMemo } from 'preact/hooks';
|
|
2
|
-
import type { SplitDiffRow } from '@/shared/lib/fs/diff-parser';
|
|
3
|
-
import { highlightLines } from '@/shared/lib/code/syntax-highlight';
|
|
4
|
-
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
5
|
-
import { useScrollRestore } from '@/client/hooks/editor/use-scroll-restore';
|
|
6
|
-
|
|
7
|
-
interface SplitViewProps {
|
|
8
|
-
rows: SplitDiffRow[];
|
|
9
|
-
language: string;
|
|
10
|
-
/** Wrap long lines instead of scrolling horizontally. */
|
|
11
|
-
wordWrap: boolean;
|
|
12
|
-
/**
|
|
13
|
-
* The diff tab these rows belong to, so its scroll offset is remembered — or
|
|
14
|
-
* null for a caller with no tab of its own (a diff inside a chat tool card,
|
|
15
|
-
* where the timeline owns the scrolling).
|
|
16
|
-
*/
|
|
17
|
-
tabKey: string | null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function SplitView({ rows, language, wordWrap, tabKey }: SplitViewProps) {
|
|
21
|
-
const syntaxReady = useSyntaxReady();
|
|
22
|
-
// Its own offset: split rows and unified lines are different heights, and
|
|
23
|
-
// switching shape must not carry one view's position onto the other's rows.
|
|
24
|
-
const scroll = useScrollRestore({ tabKey, surface: 'split' });
|
|
25
|
-
|
|
26
|
-
const htmlByIndex = useMemo(() => {
|
|
27
|
-
const left = new Array<string>(rows.length).fill('');
|
|
28
|
-
const right = new Array<string>(rows.length).fill('');
|
|
29
|
-
let start = -1;
|
|
30
|
-
const flush = (end: number) => {
|
|
31
|
-
if (start < 0) return;
|
|
32
|
-
const group = rows.slice(start, end);
|
|
33
|
-
const leftHtml = highlightLines(group.map((r) => r.left?.text ?? '').join('\n'), language);
|
|
34
|
-
const rightHtml = highlightLines(group.map((r) => r.right?.text ?? '').join('\n'), language);
|
|
35
|
-
for (let i = start; i < end; i++) {
|
|
36
|
-
left[i] = leftHtml[i - start] ?? '';
|
|
37
|
-
right[i] = rightHtml[i - start] ?? '';
|
|
38
|
-
}
|
|
39
|
-
start = -1;
|
|
40
|
-
};
|
|
41
|
-
rows.forEach((row, idx) => {
|
|
42
|
-
if (row.isMeta) flush(idx);
|
|
43
|
-
else if (start < 0) start = idx;
|
|
44
|
-
});
|
|
45
|
-
flush(rows.length);
|
|
46
|
-
return { left, right };
|
|
47
|
-
}, [rows, language, syntaxReady]);
|
|
48
|
-
|
|
49
|
-
if (rows.length === 0) {
|
|
50
|
-
return (
|
|
51
|
-
<div className="flex-1 flex items-center justify-center text-ink/40 font-mono text-xs p-8">
|
|
52
|
-
No differences between working file and target.
|
|
53
|
-
</div>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<div
|
|
59
|
-
ref={scroll.ref}
|
|
60
|
-
onScroll={scroll.onScroll}
|
|
61
|
-
data-testid="diff-scroller"
|
|
62
|
-
className="w-full h-full overflow-auto font-mono text-xs select-text bg-paper text-ink code-surface"
|
|
63
|
-
>
|
|
64
|
-
{/* The room under the last row belongs to the content, not the scroller:
|
|
65
|
-
a scroll container's own bottom padding is not part of its scrollable
|
|
66
|
-
overflow, so the padding has to sit inside it to have any effect. */}
|
|
67
|
-
<div className={`pb-4 ${wordWrap ? 'w-full' : 'w-max min-w-full min-w-[700px]'}`}>
|
|
68
|
-
<div className="grid grid-cols-2 sticky top-0 z-10 bg-canvas border-b border-ink/10 text-[11px] text-ink/70 font-sans select-none shadow-xs">
|
|
69
|
-
<div className="px-3 py-1 font-medium border-r border-ink/10 flex items-center justify-between">
|
|
70
|
-
<span>Original (HEAD / Base)</span>
|
|
71
|
-
</div>
|
|
72
|
-
<div className="px-3 py-1 font-medium flex items-center justify-between">
|
|
73
|
-
<span>Modified (Working Tree)</span>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
|
|
77
|
-
{/* Wrap needs `table-fixed`: auto layout sizes a column by its widest
|
|
78
|
-
cell's UNAMBIGUOUS minimum (`overflow-wrap: break-word` does not
|
|
79
|
-
lower it), so a long line would widen the table and scroll instead of
|
|
80
|
-
wrapping. Fixed layout takes its widths from the FIRST row — which
|
|
81
|
-
here is the `colSpan={4}` meta row, so every column came out equal
|
|
82
|
-
(measured 99px) and the line-number gutters grew to the width of a
|
|
83
|
-
code column. The colgroup states them instead: two 48px gutters and
|
|
84
|
-
the two content columns sharing the rest. */}
|
|
85
|
-
<table className={`border-collapse ${wordWrap ? 'table-fixed w-full' : 'w-full'}`}>
|
|
86
|
-
{wordWrap && (
|
|
87
|
-
<colgroup>
|
|
88
|
-
<col className="w-12" />
|
|
89
|
-
<col />
|
|
90
|
-
<col className="w-12" />
|
|
91
|
-
<col />
|
|
92
|
-
</colgroup>
|
|
93
|
-
)}
|
|
94
|
-
<tbody>
|
|
95
|
-
{rows.map((row, idx) => {
|
|
96
|
-
if (row.isMeta) {
|
|
97
|
-
return (
|
|
98
|
-
<tr key={idx} className="bg-canvas border-y border-ink/10 text-meta select-none">
|
|
99
|
-
<td colSpan={4} className="py-1 px-3 text-[11px] font-semibold text-meta text-center font-mono">
|
|
100
|
-
{row.metaText}
|
|
101
|
-
</td>
|
|
102
|
-
</tr>
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const left = row.left;
|
|
107
|
-
const right = row.right;
|
|
108
|
-
|
|
109
|
-
const isLeftDel = left?.type === 'del';
|
|
110
|
-
const isRightAdd = right?.type === 'add';
|
|
111
|
-
|
|
112
|
-
return (
|
|
113
|
-
<tr key={idx} className="border-b border-ink/[0.04]">
|
|
114
|
-
{/* Left Line Number */}
|
|
115
|
-
<td
|
|
116
|
-
className={`w-12 py-0.5 px-2 text-right text-[10px] select-none border-r border-ink/10 font-mono ${
|
|
117
|
-
wordWrap ? 'align-top' : ''
|
|
118
|
-
} ${
|
|
119
|
-
isLeftDel
|
|
120
|
-
? 'bg-error/15 text-error font-semibold'
|
|
121
|
-
: left
|
|
122
|
-
? 'bg-canvas/50 text-ink/40'
|
|
123
|
-
: 'bg-canvas/80 text-transparent'
|
|
124
|
-
}`}
|
|
125
|
-
>
|
|
126
|
-
{left?.lineNumber || ''}
|
|
127
|
-
</td>
|
|
128
|
-
|
|
129
|
-
{/* Left Content (Original) */}
|
|
130
|
-
<td
|
|
131
|
-
className={`py-0.5 px-2 border-r border-ink/10 text-ink ${
|
|
132
|
-
wordWrap ? 'whitespace-pre-wrap break-words align-top' : 'min-w-[320px] whitespace-pre'
|
|
133
|
-
} ${
|
|
134
|
-
isLeftDel
|
|
135
|
-
? 'bg-error/10'
|
|
136
|
-
: left
|
|
137
|
-
? 'hover:bg-ink/[0.02]'
|
|
138
|
-
: 'bg-canvas/40 select-none'
|
|
139
|
-
}`}
|
|
140
|
-
>
|
|
141
|
-
{left ? (
|
|
142
|
-
<>
|
|
143
|
-
<span className={`inline-block w-4 text-center select-none font-bold ${isLeftDel ? 'text-error' : 'opacity-0'}`}>
|
|
144
|
-
{isLeftDel ? '-' : ' '}
|
|
145
|
-
</span>
|
|
146
|
-
<span
|
|
147
|
-
className="shiki"
|
|
148
|
-
dangerouslySetInnerHTML={{
|
|
149
|
-
__html: left.text ? htmlByIndex.left[idx] || ' ' : ' ',
|
|
150
|
-
}}
|
|
151
|
-
/>
|
|
152
|
-
</>
|
|
153
|
-
) : (
|
|
154
|
-
<span className="opacity-0"> </span>
|
|
155
|
-
)}
|
|
156
|
-
</td>
|
|
157
|
-
|
|
158
|
-
{/* Right Line Number */}
|
|
159
|
-
<td
|
|
160
|
-
className={`w-12 py-0.5 px-2 text-right text-[10px] select-none border-r border-ink/10 font-mono ${
|
|
161
|
-
wordWrap ? 'align-top' : ''
|
|
162
|
-
} ${
|
|
163
|
-
isRightAdd
|
|
164
|
-
? 'bg-success/15 text-success font-semibold'
|
|
165
|
-
: right
|
|
166
|
-
? 'bg-canvas/50 text-ink/40'
|
|
167
|
-
: 'bg-canvas/80 text-transparent'
|
|
168
|
-
}`}
|
|
169
|
-
>
|
|
170
|
-
{right?.lineNumber || ''}
|
|
171
|
-
</td>
|
|
172
|
-
|
|
173
|
-
{/* Right Content (Modified) */}
|
|
174
|
-
<td
|
|
175
|
-
className={`py-0.5 px-2 text-ink ${
|
|
176
|
-
wordWrap ? 'whitespace-pre-wrap break-words align-top' : 'min-w-[320px] whitespace-pre'
|
|
177
|
-
} ${
|
|
178
|
-
isRightAdd
|
|
179
|
-
? 'bg-success/10'
|
|
180
|
-
: right
|
|
181
|
-
? 'hover:bg-ink/[0.02]'
|
|
182
|
-
: 'bg-canvas/40 select-none'
|
|
183
|
-
}`}
|
|
184
|
-
>
|
|
185
|
-
{right ? (
|
|
186
|
-
<>
|
|
187
|
-
<span className={`inline-block w-4 text-center select-none font-bold ${isRightAdd ? 'text-success' : 'opacity-0'}`}>
|
|
188
|
-
{isRightAdd ? '+' : ' '}
|
|
189
|
-
</span>
|
|
190
|
-
<span
|
|
191
|
-
className="shiki"
|
|
192
|
-
dangerouslySetInnerHTML={{
|
|
193
|
-
__html: right.text ? htmlByIndex.right[idx] || ' ' : ' ',
|
|
194
|
-
}}
|
|
195
|
-
/>
|
|
196
|
-
</>
|
|
197
|
-
) : (
|
|
198
|
-
<span className="opacity-0"> </span>
|
|
199
|
-
)}
|
|
200
|
-
</td>
|
|
201
|
-
</tr>
|
|
202
|
-
);
|
|
203
|
-
})}
|
|
204
|
-
</tbody>
|
|
205
|
-
</table>
|
|
206
|
-
</div>
|
|
207
|
-
</div>
|
|
208
|
-
);
|
|
209
|
-
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { useMemo } from 'preact/hooks';
|
|
2
|
-
import type { DiffLine } from '@/shared/lib/fs/diff-parser';
|
|
3
|
-
import { highlightLines } from '@/shared/lib/code/syntax-highlight';
|
|
4
|
-
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
5
|
-
import { useScrollRestore } from '@/client/hooks/editor/use-scroll-restore';
|
|
6
|
-
|
|
7
|
-
interface UnifiedViewProps {
|
|
8
|
-
lines: DiffLine[];
|
|
9
|
-
language: string;
|
|
10
|
-
/** Wrap long lines instead of scrolling horizontally. */
|
|
11
|
-
wordWrap: boolean;
|
|
12
|
-
/**
|
|
13
|
-
* The diff tab these rows belong to, so its scroll offset is remembered — or
|
|
14
|
-
* null for a caller with no tab of its own (a diff inside a chat tool card,
|
|
15
|
-
* where the timeline owns the scrolling).
|
|
16
|
-
*/
|
|
17
|
-
tabKey: string | null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function UnifiedView({ lines, language, wordWrap, tabKey }: UnifiedViewProps) {
|
|
21
|
-
const syntaxReady = useSyntaxReady();
|
|
22
|
-
// The unified and split shapes are different row counts and heights for the
|
|
23
|
-
// same diff, so each keeps its own offset (see `ScrollSurface`).
|
|
24
|
-
const scroll = useScrollRestore({ tabKey, surface: 'unified' });
|
|
25
|
-
|
|
26
|
-
const htmlByIndex = useMemo(() => {
|
|
27
|
-
const out = new Array<string>(lines.length).fill('');
|
|
28
|
-
let start = -1;
|
|
29
|
-
const flush = (end: number) => {
|
|
30
|
-
if (start < 0) return;
|
|
31
|
-
const html = highlightLines(lines.slice(start, end).map((l) => l.text).join('\n'), language);
|
|
32
|
-
for (let i = start; i < end; i++) out[i] = html[i - start] ?? '';
|
|
33
|
-
start = -1;
|
|
34
|
-
};
|
|
35
|
-
lines.forEach((line, idx) => {
|
|
36
|
-
if (line.type === 'meta') flush(idx);
|
|
37
|
-
else if (start < 0) start = idx;
|
|
38
|
-
});
|
|
39
|
-
flush(lines.length);
|
|
40
|
-
return out;
|
|
41
|
-
}, [lines, language, syntaxReady]);
|
|
42
|
-
|
|
43
|
-
if (lines.length === 0) {
|
|
44
|
-
return (
|
|
45
|
-
<div className="flex-1 flex items-center justify-center text-ink/40 font-mono text-xs p-8">
|
|
46
|
-
No differences between working file and target.
|
|
47
|
-
</div>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<div
|
|
53
|
-
ref={scroll.ref}
|
|
54
|
-
onScroll={scroll.onScroll}
|
|
55
|
-
data-testid="diff-scroller"
|
|
56
|
-
className="w-full h-full overflow-auto font-mono text-xs select-text bg-paper text-ink code-surface"
|
|
57
|
-
>
|
|
58
|
-
{/* The room under the last row belongs to the content, not the scroller:
|
|
59
|
-
a scroll container's own bottom padding is not part of its scrollable
|
|
60
|
-
overflow, so the padding has to sit inside it to have any effect. */}
|
|
61
|
-
<div className={`pb-4 ${wordWrap ? 'w-full' : 'w-max min-w-full'}`}>
|
|
62
|
-
{/* Wrap needs `table-fixed`: auto layout sizes a column by its widest
|
|
63
|
-
cell's UNAMBIGUOUS minimum (`overflow-wrap: break-word` does not
|
|
64
|
-
lower it), so a long line would still widen the table and scroll
|
|
65
|
-
instead of wrapping. Fixed layout takes the width from the row's
|
|
66
|
-
own box, which is what makes the cell's wrap rule effective. */}
|
|
67
|
-
<table className={`border-collapse ${wordWrap ? 'table-fixed w-full' : 'w-max min-w-full'}`}>
|
|
68
|
-
<tbody>
|
|
69
|
-
{lines.map((line, idx) => {
|
|
70
|
-
if (line.type === 'meta') {
|
|
71
|
-
return (
|
|
72
|
-
<tr key={idx} className="bg-canvas border-y border-ink/10 text-meta select-none">
|
|
73
|
-
<td className="w-12 text-center py-1 px-2 text-[10px] opacity-60 border-r border-ink/10 font-mono">...</td>
|
|
74
|
-
<td className="w-12 text-center py-1 px-2 text-[10px] opacity-60 border-r border-ink/10 font-mono">...</td>
|
|
75
|
-
<td className="py-1 px-3 text-[11px] font-semibold text-meta font-mono tracking-tight">{line.text}</td>
|
|
76
|
-
</tr>
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const isAdd = line.type === 'add';
|
|
81
|
-
const isDel = line.type === 'del';
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<tr
|
|
85
|
-
key={idx}
|
|
86
|
-
className={`transition-colors group ${
|
|
87
|
-
isAdd
|
|
88
|
-
? 'bg-success/10 hover:bg-success/15'
|
|
89
|
-
: isDel
|
|
90
|
-
? 'bg-error/10 hover:bg-error/15'
|
|
91
|
-
: 'hover:bg-ink/[0.03]'
|
|
92
|
-
}`}
|
|
93
|
-
>
|
|
94
|
-
{/* Old line number. Top-aligned when the row wraps, or the
|
|
95
|
-
number would float to the middle of a multi-line cell
|
|
96
|
-
instead of naming its first line. */}
|
|
97
|
-
<td
|
|
98
|
-
className={`w-12 py-0.5 px-2 text-right text-[10px] select-none border-r border-ink/10 font-mono ${
|
|
99
|
-
wordWrap ? 'align-top' : ''
|
|
100
|
-
} ${
|
|
101
|
-
isDel
|
|
102
|
-
? 'bg-error/15 text-error font-semibold'
|
|
103
|
-
: isAdd
|
|
104
|
-
? 'bg-success/15 text-success/70'
|
|
105
|
-
: 'bg-canvas/50 text-ink/40'
|
|
106
|
-
}`}
|
|
107
|
-
>
|
|
108
|
-
{line.oldLineNumber || ''}
|
|
109
|
-
</td>
|
|
110
|
-
|
|
111
|
-
{/* New line number */}
|
|
112
|
-
<td
|
|
113
|
-
className={`w-12 py-0.5 px-2 text-right text-[10px] select-none border-r border-ink/10 font-mono ${
|
|
114
|
-
wordWrap ? 'align-top' : ''
|
|
115
|
-
} ${
|
|
116
|
-
isAdd
|
|
117
|
-
? 'bg-success/15 text-success font-semibold'
|
|
118
|
-
: isDel
|
|
119
|
-
? 'bg-error/15 text-error/70'
|
|
120
|
-
: 'bg-canvas/50 text-ink/40'
|
|
121
|
-
}`}
|
|
122
|
-
>
|
|
123
|
-
{line.newLineNumber || ''}
|
|
124
|
-
</td>
|
|
125
|
-
|
|
126
|
-
{/* Change prefix & code text */}
|
|
127
|
-
<td className={`py-0.5 px-3 text-ink pr-6 ${wordWrap ? 'whitespace-pre-wrap break-words align-top' : 'whitespace-pre'}`}>
|
|
128
|
-
<span
|
|
129
|
-
className={`inline-block w-4 text-center select-none font-bold ${
|
|
130
|
-
isAdd ? 'text-success' : isDel ? 'text-error' : 'opacity-0'
|
|
131
|
-
}`}
|
|
132
|
-
>
|
|
133
|
-
{isAdd ? '+' : isDel ? '-' : ' '}
|
|
134
|
-
</span>
|
|
135
|
-
<span
|
|
136
|
-
className="shiki"
|
|
137
|
-
dangerouslySetInnerHTML={{
|
|
138
|
-
__html: line.text ? htmlByIndex[idx] || ' ' : ' ',
|
|
139
|
-
}}
|
|
140
|
-
/>
|
|
141
|
-
</td>
|
|
142
|
-
</tr>
|
|
143
|
-
);
|
|
144
|
-
})}
|
|
145
|
-
</tbody>
|
|
146
|
-
</table>
|
|
147
|
-
</div>
|
|
148
|
-
</div>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The scroll-container walk, shared by the editor's lazy window and the find
|
|
8
|
-
* widget's reveal.
|
|
9
|
-
*
|
|
10
|
-
* The two must agree: one resolves which lines to draw from the visible band,
|
|
11
|
-
* the other moves that band. Two copies of this walk would let the widget scroll
|
|
12
|
-
* a container the window never reads — the match would be marked somewhere
|
|
13
|
-
* nobody can see. Exercised against a real DOM (happy-dom) because the answer is
|
|
14
|
-
* a computed style.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
18
|
-
import { Window } from 'happy-dom';
|
|
19
|
-
|
|
20
|
-
import { findScrollContainer } from '@/client/hooks/editor/scroll-container';
|
|
21
|
-
|
|
22
|
-
const installed: Record<string, unknown> = {};
|
|
23
|
-
const displaced: Record<string, unknown> = {};
|
|
24
|
-
|
|
25
|
-
beforeAll(() => {
|
|
26
|
-
const win = new Window({ url: 'http://localhost' });
|
|
27
|
-
for (const key of ['window', 'document', 'Node', 'Element', 'HTMLElement', 'getComputedStyle']) {
|
|
28
|
-
displaced[key] = (globalThis as Record<string, unknown>)[key];
|
|
29
|
-
installed[key] = (win as unknown as Record<string, unknown>)[key];
|
|
30
|
-
(globalThis as Record<string, unknown>)[key] = installed[key];
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
afterAll(() => {
|
|
35
|
-
for (const key of Object.keys(installed)) {
|
|
36
|
-
if (displaced[key] === undefined) delete (globalThis as Record<string, unknown>)[key];
|
|
37
|
-
else (globalThis as Record<string, unknown>)[key] = displaced[key];
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
/** An element whose computed style reports `overflow-y`, with a scrollable height. */
|
|
42
|
-
function makeElement(overflowY: string, scrollable: boolean): HTMLElement {
|
|
43
|
-
const element = document.createElement('div');
|
|
44
|
-
element.style.overflowY = overflowY;
|
|
45
|
-
if (scrollable) {
|
|
46
|
-
// happy-dom reports clientHeight 0, so the walk's `scrollHeight > clientHeight`
|
|
47
|
-
// test needs a stubbed pair to describe a container that can actually scroll.
|
|
48
|
-
Object.defineProperty(element, 'scrollHeight', { value: 400, configurable: true });
|
|
49
|
-
Object.defineProperty(element, 'clientHeight', { value: 100, configurable: true });
|
|
50
|
-
}
|
|
51
|
-
return element;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
describe('findScrollContainer', () => {
|
|
55
|
-
test('finds the nearest ancestor that scrolls', () => {
|
|
56
|
-
const outer = makeElement('auto', true);
|
|
57
|
-
const inner = makeElement('visible', false);
|
|
58
|
-
const surface = document.createElement('div');
|
|
59
|
-
inner.appendChild(surface);
|
|
60
|
-
outer.appendChild(inner);
|
|
61
|
-
document.body.appendChild(outer);
|
|
62
|
-
|
|
63
|
-
expect(findScrollContainer(surface)).toBe(outer);
|
|
64
|
-
outer.remove();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('skips a container that cannot actually scroll', () => {
|
|
68
|
-
// `overflow: auto` on a box whose content fits is not a scroller: returning
|
|
69
|
-
// it would make `revealOffset` write scrollTop onto a box that ignores it.
|
|
70
|
-
const outer = makeElement('auto', true);
|
|
71
|
-
const fitted = makeElement('auto', false);
|
|
72
|
-
const surface = document.createElement('div');
|
|
73
|
-
fitted.appendChild(surface);
|
|
74
|
-
outer.appendChild(fitted);
|
|
75
|
-
document.body.appendChild(outer);
|
|
76
|
-
|
|
77
|
-
expect(findScrollContainer(surface)).toBe(outer);
|
|
78
|
-
outer.remove();
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test('reports nothing when no ancestor scrolls', () => {
|
|
82
|
-
const plain = document.createElement('div');
|
|
83
|
-
const surface = document.createElement('div');
|
|
84
|
-
plain.appendChild(surface);
|
|
85
|
-
document.body.appendChild(plain);
|
|
86
|
-
|
|
87
|
-
expect(findScrollContainer(surface)).toBeNull();
|
|
88
|
-
plain.remove();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test('prefers the nearest scroller, so a nested panel scrolls itself', () => {
|
|
92
|
-
const outer = makeElement('auto', true);
|
|
93
|
-
const inner = makeElement('scroll', true);
|
|
94
|
-
const surface = document.createElement('div');
|
|
95
|
-
inner.appendChild(surface);
|
|
96
|
-
outer.appendChild(inner);
|
|
97
|
-
document.body.appendChild(outer);
|
|
98
|
-
|
|
99
|
-
expect(findScrollContainer(surface)).toBe(inner);
|
|
100
|
-
outer.remove();
|
|
101
|
-
});
|
|
102
|
-
});
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The ancestor a code surface actually scrolls inside.
|
|
8
|
-
*
|
|
9
|
-
* The lazy editor window and the find widget's "scroll to this match" both need
|
|
10
|
-
* this answer, and they must agree: one resolves which lines to draw from the
|
|
11
|
-
* visible band, the other moves that band. Two copies of the walk would let the
|
|
12
|
-
* widget scroll a container the window never reads.
|
|
13
|
-
*
|
|
14
|
-
* Walks up rather than taking a ref: the surface is mounted under different
|
|
15
|
-
* scrollers (the desktop editor panel, the phone's full-screen editor) and is
|
|
16
|
-
* also mounted inside a page that scrolls itself, which is the `null` case.
|
|
17
|
-
*/
|
|
18
|
-
export function findScrollContainer(element: HTMLElement): HTMLElement | null {
|
|
19
|
-
for (let node = element.parentElement; node; node = node.parentElement) {
|
|
20
|
-
const overflowY = getComputedStyle(node).overflowY;
|
|
21
|
-
if ((overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') && node.scrollHeight > node.clientHeight) {
|
|
22
|
-
return node;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
|
2
|
-
import type { RefObject } from 'preact';
|
|
3
|
-
|
|
4
|
-
import { codeWindow, firstWindow, resolveWindow, sameWindow, type CodeWindow, type LineGeometry, type LineWindow } from '@/shared/lib/code/lazy-window';
|
|
5
|
-
import { findScrollContainer } from '@/client/hooks/editor/scroll-container';
|
|
6
|
-
|
|
7
|
-
export interface UseCodeWindowOptions {
|
|
8
|
-
/** The surface's root box: the frame every line offset in `geometry` is measured from. */
|
|
9
|
-
rootRef: RefObject<HTMLElement | null>;
|
|
10
|
-
/** Lines in the document on screen. */
|
|
11
|
-
lineCount: number;
|
|
12
|
-
/** Document geometry, or null until the wrap/row measurement lands. */
|
|
13
|
-
geometry: LineGeometry | null;
|
|
14
|
-
/** False for short documents (they render whole) and when geometry cannot be measured. */
|
|
15
|
-
enabled: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* The lines visible inside the scroll container `root` lives in.
|
|
20
|
-
*
|
|
21
|
-
* Both rectangles are read in viewport coordinates, so this needs no knowledge
|
|
22
|
-
* of scroll offsets: the part of the editor's box the container actually shows
|
|
23
|
-
* is exactly the band between the two.
|
|
24
|
-
*/
|
|
25
|
-
function visibleWindow(root: HTMLElement, geometry: LineGeometry): LineWindow {
|
|
26
|
-
const frameTop = root.getBoundingClientRect().top;
|
|
27
|
-
const container = findScrollContainer(root);
|
|
28
|
-
const viewTop = container ? container.getBoundingClientRect().top : 0;
|
|
29
|
-
const viewBottom = container
|
|
30
|
-
? container.getBoundingClientRect().bottom
|
|
31
|
-
: globalThis.innerHeight || geometry.rowHeight;
|
|
32
|
-
return resolveWindow(geometry, viewTop - frameTop, viewBottom - viewTop);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Rendering window for the lazy editor surface.
|
|
37
|
-
*
|
|
38
|
-
* A long document is highlighted, gutters and all, only where it can be seen;
|
|
39
|
-
* this hook owns which lines those are and re-resolves them on scroll, on
|
|
40
|
-
* resize, and whenever the geometry moves (a keystroke re-wraps a line). The
|
|
41
|
-
* scroll listener is a capturing one on the window: scroll events do not
|
|
42
|
-
* bubble, but they do reach a capturing listener, so the surface follows
|
|
43
|
-
* whichever ancestor scrolls — the desktop editor panel, the phone's
|
|
44
|
-
* full-screen editor, or the page.
|
|
45
|
-
*
|
|
46
|
-
* Returns null for a document that renders whole, which is the caller's cue to
|
|
47
|
-
* keep the plain, unwindowed surface.
|
|
48
|
-
*/
|
|
49
|
-
export function useCodeWindow({ rootRef, lineCount, geometry, enabled }: UseCodeWindowOptions): CodeWindow | null {
|
|
50
|
-
const [lineWindow, setLineWindow] = useState<LineWindow | null>(null);
|
|
51
|
-
|
|
52
|
-
const compute = useCallback(() => {
|
|
53
|
-
const root = rootRef.current;
|
|
54
|
-
if (!enabled || !root) return;
|
|
55
|
-
const next = geometry ? visibleWindow(root, geometry) : firstWindow(lineCount);
|
|
56
|
-
setLineWindow((previous) => (sameWindow(previous, next) ? previous : next));
|
|
57
|
-
}, [enabled, geometry, lineCount, rootRef]);
|
|
58
|
-
|
|
59
|
-
// Layout effect: the first window has to be resolved before the browser
|
|
60
|
-
// paints, or a long document flashes its untouched opening lines.
|
|
61
|
-
useLayoutEffect(() => {
|
|
62
|
-
compute();
|
|
63
|
-
}, [compute]);
|
|
64
|
-
|
|
65
|
-
// The listeners follow `enabled`, not `compute`: re-resolving on every
|
|
66
|
-
// keystroke must not tear down and rebuild the observer each time.
|
|
67
|
-
const computeRef = useRef(compute);
|
|
68
|
-
computeRef.current = compute;
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
if (!enabled) return;
|
|
71
|
-
let frame = 0;
|
|
72
|
-
const schedule = () => {
|
|
73
|
-
if (frame) return;
|
|
74
|
-
frame = requestAnimationFrame(() => {
|
|
75
|
-
frame = 0;
|
|
76
|
-
computeRef.current();
|
|
77
|
-
});
|
|
78
|
-
};
|
|
79
|
-
window.addEventListener('scroll', schedule, { passive: true, capture: true });
|
|
80
|
-
window.addEventListener('resize', schedule);
|
|
81
|
-
const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(schedule);
|
|
82
|
-
const root = rootRef.current;
|
|
83
|
-
if (root) observer?.observe(root);
|
|
84
|
-
return () => {
|
|
85
|
-
if (frame) cancelAnimationFrame(frame);
|
|
86
|
-
window.removeEventListener('scroll', schedule, { capture: true });
|
|
87
|
-
window.removeEventListener('resize', schedule);
|
|
88
|
-
observer?.disconnect();
|
|
89
|
-
};
|
|
90
|
-
}, [enabled, rootRef]);
|
|
91
|
-
|
|
92
|
-
if (!enabled) return null;
|
|
93
|
-
if (!geometry) {
|
|
94
|
-
// First commit: the layout effect above lands the real window in the same
|
|
95
|
-
// frame, so this only bounds how much the opening render is allowed to draw.
|
|
96
|
-
const opening = firstWindow(lineCount);
|
|
97
|
-
return { ...opening, top: 0, bottom: 0, breakAtEnd: opening.end >= lineCount };
|
|
98
|
-
}
|
|
99
|
-
return codeWindow(geometry, lineWindow ?? firstWindow(geometry.lineCount));
|
|
100
|
-
}
|