@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,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState } from 'preact/hooks';
|
|
2
2
|
import { useSessionUiState } from '@/client/hooks/workspace/session-state';
|
|
3
|
-
import {
|
|
4
|
-
import { isUnmergedCode, type ConflictMarkers } from '@/shared/lib/fs/git-unmerged';
|
|
3
|
+
import { isUnmergedCode } from '@/shared/lib/fs/git-unmerged';
|
|
5
4
|
import { useFileDiff } from '@/client/hooks/workspace/diff-file';
|
|
5
|
+
import { useDiffGitActions } from '@/client/hooks/workspace/diff-git-actions';
|
|
6
6
|
import { getLanguageFromPath } from '@/shared/lib/code/language';
|
|
7
7
|
import { DiffToolbar, type DiffContentMode, type DiffViewMode } from '@/client/components/workspace/diff-panel/Toolbar';
|
|
8
8
|
import { ConflictBanner } from '@/client/components/workspace/diff-panel/ConflictBanner';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { ThemedMergeDiffView } from '@/client/components/workspace/diff-panel/ThemedMergeDiffView';
|
|
10
|
+
import { useEditorFontStack } from '@/client/hooks/editor/font';
|
|
11
|
+
import { EDITOR_DEFAULT_FONT_SIZE } from '@/shared/lib/code/editor/typography';
|
|
11
12
|
import { usePluginDiffView } from '@/client/components/workspace/editor/plugin-file-view';
|
|
12
13
|
import { PluginIdProvider } from '@kabmo-labs/kabmo-ui';
|
|
13
14
|
import { AlertCircle, RefreshCw } from 'lucide-preact';
|
|
@@ -26,6 +27,12 @@ interface DiffPanelProps {
|
|
|
26
27
|
* per tab (and per view shape). Null for a host with no tab of its own.
|
|
27
28
|
*/
|
|
28
29
|
tabKey?: string | null;
|
|
30
|
+
/**
|
|
31
|
+
* Offer the two width-mode buttons. On by default (the editor column is where
|
|
32
|
+
* they belong); the phone's full-screen overlay turns them off, because it
|
|
33
|
+
* already covers the chat and the sidebar.
|
|
34
|
+
*/
|
|
35
|
+
viewModeButtons?: boolean;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
export function DiffPanel({
|
|
@@ -38,6 +45,7 @@ export function DiffPanel({
|
|
|
38
45
|
onFileSaved,
|
|
39
46
|
className = '',
|
|
40
47
|
tabKey = null,
|
|
48
|
+
viewModeButtons = true,
|
|
41
49
|
}: DiffPanelProps) {
|
|
42
50
|
// Read and persist view mode & whitespace preference in session_ui_state
|
|
43
51
|
const [viewMode, setViewMode] = useSessionUiState<DiffViewMode>('diff.viewMode', 'unified');
|
|
@@ -45,42 +53,26 @@ export function DiffPanel({
|
|
|
45
53
|
const [wordWrap, setWordWrap] = useSessionUiState<boolean>('diff.wordWrap', true);
|
|
46
54
|
const [ignoreWhitespace, setIgnoreWhitespace] = useSessionUiState<boolean>('diff.ignoreWhitespace', false);
|
|
47
55
|
|
|
48
|
-
const [
|
|
49
|
-
const [actionError, setActionError] = useState<string | null>(null);
|
|
56
|
+
const [clipboardError, setClipboardError] = useState<string | null>(null);
|
|
50
57
|
const [isCopied, setIsCopied] = useState<boolean>(false);
|
|
51
58
|
const [showDiscardConfirm, setShowDiscardConfirm] = useState<boolean>(false);
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
* Read only while the file IS conflicted, and only when the status says so —
|
|
56
|
-
* the read is the whole file, and `status` is what the caller already knows.
|
|
57
|
-
* A failed read leaves `{}`, which falls back to the words `ours`/`theirs`:
|
|
58
|
-
* the actions still work, only the naming is less precise.
|
|
59
|
-
*/
|
|
60
|
-
const [markers, setMarkers] = useState<ConflictMarkers>({});
|
|
59
|
+
/** Additions/deletions of the pair on screen, reported by the merge view. */
|
|
60
|
+
const [counts, setCounts] = useState<{ additions: number; deletions: number }>({ additions: 0, deletions: 0 });
|
|
61
|
+
const editorFontFamily = useEditorFontStack();
|
|
61
62
|
|
|
62
63
|
const {
|
|
63
64
|
rawDiff,
|
|
65
|
+
oldContent,
|
|
66
|
+
newContent,
|
|
64
67
|
currentStatus,
|
|
65
68
|
currentStaged,
|
|
66
69
|
isLoading,
|
|
67
70
|
errorMessage,
|
|
68
71
|
fetchDiff,
|
|
69
72
|
adoptState,
|
|
70
|
-
} = useFileDiff({ filePath, status, isStaged, repo, root
|
|
73
|
+
} = useFileDiff({ filePath, status, isStaged, repo, root });
|
|
71
74
|
const conflicted = isUnmergedCode(currentStatus);
|
|
72
75
|
|
|
73
|
-
// Full-code mode parses the whole file, not the hunks, so the parse is keyed
|
|
74
|
-
// to its inputs: every render used to re-walk the diff (measured 21 ms on an
|
|
75
|
-
// 80k-line file), for a result that only changes when the diff or the
|
|
76
|
-
// whitespace toggle does.
|
|
77
|
-
// Only the rendered view is built: `views` keeps the shared flush (so the
|
|
78
|
-
// whitespace decision and the counts cannot diverge) while skipping the
|
|
79
|
-
// object graph nobody draws.
|
|
80
|
-
const parsed = useMemo(
|
|
81
|
-
() => parseUnifiedDiff(rawDiff, { ignoreWhitespace, views: viewMode === 'split' ? 'split' : 'unified' }),
|
|
82
|
-
[rawDiff, ignoreWhitespace, viewMode],
|
|
83
|
-
);
|
|
84
76
|
const language = getLanguageFromPath(filePath);
|
|
85
77
|
// A contributed renderer replaces the built-in views below, never the toolbar.
|
|
86
78
|
const pluginDiffView = usePluginDiffView({ path: filePath, diff: rawDiff });
|
|
@@ -108,117 +100,31 @@ export function DiffPanel({
|
|
|
108
100
|
setIsCopied(true);
|
|
109
101
|
setTimeout(() => setIsCopied(false), 2000);
|
|
110
102
|
},
|
|
111
|
-
() =>
|
|
103
|
+
() => setClipboardError('Clipboard write was refused by the browser'),
|
|
112
104
|
);
|
|
113
105
|
};
|
|
114
106
|
|
|
115
107
|
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
108
|
+
* The git writes, their busy flag, their refusals, and the conflict-marker
|
|
109
|
+
* read behind the banner — see `diff-git-actions.ts`. The one exception is
|
|
110
|
+
* the clipboard, which is not a write: it reads the buffer already on screen
|
|
111
|
+
* and must stay available while a write is in flight.
|
|
120
112
|
*/
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
} catch (err: unknown) {
|
|
137
|
-
setActionError(err instanceof Error ? err.message : 'Request failed');
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
},
|
|
141
|
-
[repo, root],
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
const handleStageUnstage = async () => {
|
|
145
|
-
if (isBusy) return;
|
|
146
|
-
const nextStaged = !currentStaged;
|
|
147
|
-
setIsBusy(true);
|
|
148
|
-
try {
|
|
149
|
-
const ok = await runGitAction({
|
|
150
|
-
actionType: currentStaged ? 'unstage' : 'stage',
|
|
151
|
-
file: filePath,
|
|
152
|
-
});
|
|
153
|
-
if (!ok) return;
|
|
154
|
-
adoptState({ staged: nextStaged });
|
|
155
|
-
onFileSaved?.();
|
|
156
|
-
await fetchDiff({ staged: nextStaged });
|
|
157
|
-
} finally {
|
|
158
|
-
setIsBusy(false);
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
const handleDiscard = () => {
|
|
163
|
-
setShowDiscardConfirm(true);
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
// The marker labels are fetched when the file turns out to be conflicted, and
|
|
167
|
-
// re-fetched when the path changes. Keyed on the path and the status so a
|
|
168
|
-
// resolve (which changes the status away from `UU`) clears the banner.
|
|
169
|
-
useEffect(() => {
|
|
170
|
-
if (!conflicted) {
|
|
171
|
-
setMarkers({});
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
let cancelled = false;
|
|
175
|
-
const params = new URLSearchParams({ conflictMarkers: '1', file: filePath, repo: repo || '.' });
|
|
176
|
-
if (root) params.set('root', root);
|
|
177
|
-
void fetch(`/api/fs/git?${params.toString()}`)
|
|
178
|
-
.then((res) => res.json() as Promise<{ markers?: ConflictMarkers }>)
|
|
179
|
-
.then((body) => {
|
|
180
|
-
if (!cancelled) setMarkers(body.markers ?? {});
|
|
181
|
-
})
|
|
182
|
-
.catch(() => {
|
|
183
|
-
if (!cancelled) setMarkers({});
|
|
184
|
-
});
|
|
185
|
-
return () => { cancelled = true; };
|
|
186
|
-
}, [conflicted, filePath, repo, root]);
|
|
187
|
-
|
|
188
|
-
/** Resolve this file: `ours`/`theirs` write a stage, `''` only stages. */
|
|
189
|
-
const handleResolve = async (side: 'ours' | 'theirs' | '') => {
|
|
190
|
-
if (isBusy) return;
|
|
191
|
-
setIsBusy(true);
|
|
192
|
-
try {
|
|
193
|
-
const ok = await runGitAction({
|
|
194
|
-
actionType: 'resolve_conflict',
|
|
195
|
-
file: filePath,
|
|
196
|
-
...(side ? { side } : {}),
|
|
197
|
-
});
|
|
198
|
-
if (!ok) return;
|
|
199
|
-
onFileSaved?.();
|
|
200
|
-
await fetchDiff();
|
|
201
|
-
} finally {
|
|
202
|
-
setIsBusy(false);
|
|
203
|
-
}
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
const confirmDiscard = async () => {
|
|
207
|
-
setShowDiscardConfirm(false);
|
|
208
|
-
setIsBusy(true);
|
|
209
|
-
try {
|
|
210
|
-
// `revert` is the server's own name for "restore this file from the index",
|
|
211
|
-
// and it is the one that handles the untracked case (there the file is
|
|
212
|
-
// removed). Sending `discard` — which no handler matched — returned
|
|
213
|
-
// `{success:true}` for doing nothing at all.
|
|
214
|
-
const ok = await runGitAction({ actionType: 'revert', file: filePath });
|
|
215
|
-
if (!ok) return;
|
|
216
|
-
onFileSaved?.();
|
|
217
|
-
await fetchDiff();
|
|
218
|
-
} finally {
|
|
219
|
-
setIsBusy(false);
|
|
220
|
-
}
|
|
221
|
-
};
|
|
113
|
+
const gitActions = useDiffGitActions({
|
|
114
|
+
filePath,
|
|
115
|
+
repo,
|
|
116
|
+
root,
|
|
117
|
+
status: currentStatus,
|
|
118
|
+
staged: currentStaged,
|
|
119
|
+
conflicted,
|
|
120
|
+
adoptState,
|
|
121
|
+
fetchDiff,
|
|
122
|
+
onFileSaved,
|
|
123
|
+
});
|
|
124
|
+
const { isBusy, markers } = gitActions;
|
|
125
|
+
// Two refusal surfaces with one source: the clipboard's own failure is local
|
|
126
|
+
// to this panel, while a git refusal comes from the server.
|
|
127
|
+
const actionError = clipboardError ?? gitActions.error;
|
|
222
128
|
|
|
223
129
|
return (
|
|
224
130
|
// The panel fills, it does not size itself: the caller owns the height
|
|
@@ -238,17 +144,18 @@ export function DiffPanel({
|
|
|
238
144
|
contentMode={contentMode}
|
|
239
145
|
wordWrap={wordWrap}
|
|
240
146
|
ignoreWhitespace={ignoreWhitespace}
|
|
241
|
-
additions={
|
|
242
|
-
deletions={
|
|
147
|
+
additions={counts.additions}
|
|
148
|
+
deletions={counts.deletions}
|
|
243
149
|
isLoading={isLoading}
|
|
244
150
|
isCopied={isCopied}
|
|
245
151
|
isBusy={isBusy}
|
|
152
|
+
viewModeButtons={viewModeButtons}
|
|
246
153
|
onSetViewMode={handleSetViewMode}
|
|
247
154
|
onSetContentMode={handleSetContentMode}
|
|
248
155
|
onToggleWordWrap={handleToggleWordWrap}
|
|
249
156
|
onToggleWhitespace={handleToggleWhitespace}
|
|
250
|
-
onStageUnstage={
|
|
251
|
-
onDiscard={
|
|
157
|
+
onStageUnstage={() => void gitActions.stageUnstage()}
|
|
158
|
+
onDiscard={() => setShowDiscardConfirm(true)}
|
|
252
159
|
onOpenInEditor={onOpenInEditor}
|
|
253
160
|
onRefresh={() => void fetchDiff()}
|
|
254
161
|
onCopyDiff={handleCopyDiff}
|
|
@@ -259,8 +166,11 @@ export function DiffPanel({
|
|
|
259
166
|
markers={markers}
|
|
260
167
|
isBusy={isBusy}
|
|
261
168
|
error={actionError}
|
|
262
|
-
onResolve={
|
|
263
|
-
onDismissError={() =>
|
|
169
|
+
onResolve={(side) => void gitActions.resolve(side)}
|
|
170
|
+
onDismissError={() => {
|
|
171
|
+
gitActions.dismissError();
|
|
172
|
+
setClipboardError(null);
|
|
173
|
+
}}
|
|
264
174
|
/>
|
|
265
175
|
)}
|
|
266
176
|
|
|
@@ -270,7 +180,10 @@ export function DiffPanel({
|
|
|
270
180
|
<span className="min-w-0 flex-1 truncate" title={actionError}>{actionError}</span>
|
|
271
181
|
<button
|
|
272
182
|
type="button"
|
|
273
|
-
onClick={() =>
|
|
183
|
+
onClick={() => {
|
|
184
|
+
gitActions.dismissError();
|
|
185
|
+
setClipboardError(null);
|
|
186
|
+
}}
|
|
274
187
|
className="px-1.5 rounded hover:bg-error/15 cursor-pointer"
|
|
275
188
|
title="Dismiss"
|
|
276
189
|
aria-label="Dismiss"
|
|
@@ -302,10 +215,25 @@ export function DiffPanel({
|
|
|
302
215
|
<PluginIdProvider pluginId={pluginDiffView.pluginId}>
|
|
303
216
|
<pluginDiffView.Component {...pluginDiffView.props} />
|
|
304
217
|
</PluginIdProvider>
|
|
305
|
-
) : viewMode === 'split' ? (
|
|
306
|
-
<SplitView rows={parsed.splitRows} language={language} wordWrap={wordWrap} tabKey={tabKey} />
|
|
307
218
|
) : (
|
|
308
|
-
|
|
219
|
+
// Both shapes are the SAME component: `@codemirror/merge` owns the
|
|
220
|
+
// diff and the rows, and a shape switch is a re-mount of a different
|
|
221
|
+
// object (an `EditorView` vs a `MergeView`), which is why it is a prop
|
|
222
|
+
// rather than a second component.
|
|
223
|
+
<ThemedMergeDiffView
|
|
224
|
+
original={oldContent}
|
|
225
|
+
next={newContent}
|
|
226
|
+
language={language}
|
|
227
|
+
viewMode={viewMode}
|
|
228
|
+
contentMode={contentMode}
|
|
229
|
+
wordWrap={wordWrap}
|
|
230
|
+
ignoreWhitespace={ignoreWhitespace}
|
|
231
|
+
tabKey={tabKey}
|
|
232
|
+
onCounts={setCounts}
|
|
233
|
+
empty={!rawDiff || !rawDiff.trim()}
|
|
234
|
+
fontFamily={editorFontFamily}
|
|
235
|
+
fontSize={EDITOR_DEFAULT_FONT_SIZE}
|
|
236
|
+
/>
|
|
309
237
|
)}
|
|
310
238
|
</div>
|
|
311
239
|
|
|
@@ -327,7 +255,10 @@ export function DiffPanel({
|
|
|
327
255
|
</button>
|
|
328
256
|
<button
|
|
329
257
|
type="button"
|
|
330
|
-
onClick={() =>
|
|
258
|
+
onClick={() => {
|
|
259
|
+
setShowDiscardConfirm(false);
|
|
260
|
+
void gitActions.revert();
|
|
261
|
+
}}
|
|
331
262
|
className="px-3 py-1.5 text-xs bg-error text-white rounded hover:bg-error/90 font-medium transition-colors cursor-pointer"
|
|
332
263
|
>
|
|
333
264
|
Discard Changes
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The diff panel's CodeMirror side: the two view shapes, their theme, and the
|
|
8
|
+
* counts the toolbar prints.
|
|
9
|
+
*
|
|
10
|
+
* What this replaced: a unified-diff PARSER feeding two hand-built tables
|
|
11
|
+
* (1 066 lines across `UnifiedView` / `SplitView` plus the diff half of
|
|
12
|
+
* `diff-parser`), with a Shiki pass per row and its own wrapping rules.
|
|
13
|
+
* `@codemirror/merge` does the diff itself and draws the rows, so a chunk is a
|
|
14
|
+
* real editor range: the gutter markers, the deleted-chunk widgets and the
|
|
15
|
+
* collapsed stretches are the extension's, not ours.
|
|
16
|
+
*
|
|
17
|
+
* Three facts about this API are load-bearing and none is guessable:
|
|
18
|
+
*
|
|
19
|
+
* - `getChunks` is a FUNCTION, not a field: `state.field(getChunks)` throws
|
|
20
|
+
* "Field is not present in this state". The counts do not need it either —
|
|
21
|
+
* `Chunk.build(a, b)` is the same line-level chunking without a view, which
|
|
22
|
+
* is what lets a badge be computed before anything is mounted.
|
|
23
|
+
* - `collapseUnchanged` is CONFIG, so switching *Diff Only* ↔ *Full Code* is a
|
|
24
|
+
* compartment reconfigure. Re-mounting would drop the reader's scroll.
|
|
25
|
+
* - The merge package's own colours are hardcoded hex. They are overridden with
|
|
26
|
+
* the app's palette variables, or a diff would be the one surface that ignores
|
|
27
|
+
* the theme.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { Compartment, EditorState, Text, type Extension } from '@codemirror/state';
|
|
31
|
+
import { EditorView, lineNumbers } from '@codemirror/view';
|
|
32
|
+
import { Chunk, MergeView, diff, unifiedMergeView } from '@codemirror/merge';
|
|
33
|
+
|
|
34
|
+
import { shikiHighlight, type ShikiThemeNames } from '@/client/components/common/code-surface/shiki';
|
|
35
|
+
import { surfaceTheme } from '@/client/components/common/code-surface/theme';
|
|
36
|
+
|
|
37
|
+
/** What the toolbar shows; `diff` collapses the unchanged stretches. */
|
|
38
|
+
export type CollapseMode = 'diff' | 'full';
|
|
39
|
+
|
|
40
|
+
/** The collapsed-stretch settings: git's own `-U3` shape, plus a minimum. */
|
|
41
|
+
const COLLAPSE = { margin: 3, minSize: 4 } as const;
|
|
42
|
+
|
|
43
|
+
/** Both sides of a diff are a READER here: the toolbar writes, not the editor. */
|
|
44
|
+
const READ_ONLY = [EditorState.readOnly.of(true), EditorView.editable.of(false)];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The app's palette, applied to the merge package's own classes.
|
|
48
|
+
*
|
|
49
|
+
* `&.cm-merge-a` / `&.cm-merge-b` are spelled out because the extension's base
|
|
50
|
+
* theme uses the same shape — an equally specific rule from a `theme` facet wins
|
|
51
|
+
* on order, and a weaker one loses.
|
|
52
|
+
*/
|
|
53
|
+
function diffTheme(): Extension {
|
|
54
|
+
return EditorView.theme({
|
|
55
|
+
'&': {
|
|
56
|
+
height: '100%',
|
|
57
|
+
backgroundColor: 'var(--editor-bg, var(--theme-paper))',
|
|
58
|
+
color: 'var(--editor-fg, var(--theme-ink))',
|
|
59
|
+
fontSize: 'inherit',
|
|
60
|
+
fontFamily: 'inherit',
|
|
61
|
+
lineHeight: 'inherit',
|
|
62
|
+
},
|
|
63
|
+
'&.cm-focused': { outline: 'none' },
|
|
64
|
+
'.cm-scroller': { fontFamily: 'inherit', fontSize: 'inherit', lineHeight: 'inherit' },
|
|
65
|
+
'.cm-gutters': {
|
|
66
|
+
backgroundColor: 'var(--editor-bg, var(--theme-canvas))',
|
|
67
|
+
color: 'var(--editor-gutter-fg, color-mix(in srgb, var(--theme-ink) 30%, transparent))',
|
|
68
|
+
border: 'none',
|
|
69
|
+
borderRight: '1px solid var(--editor-border, color-mix(in srgb, var(--theme-ink) 10%, transparent))',
|
|
70
|
+
fontFamily: 'inherit',
|
|
71
|
+
fontSize: 'inherit',
|
|
72
|
+
lineHeight: 'inherit',
|
|
73
|
+
},
|
|
74
|
+
// The diff bands follow the editor theme when one is chosen (its own
|
|
75
|
+
// inserted/removed colours) and the app's signal pair otherwise.
|
|
76
|
+
'&.cm-merge-a .cm-changedLine, .cm-deletedChunk': {
|
|
77
|
+
backgroundColor: 'var(--editor-removed, color-mix(in srgb, var(--theme-error) 10%, transparent))',
|
|
78
|
+
},
|
|
79
|
+
'&.cm-merge-b .cm-changedLine, .cm-inlineChangedLine': {
|
|
80
|
+
backgroundColor: 'var(--editor-added, color-mix(in srgb, var(--theme-success) 10%, transparent))',
|
|
81
|
+
},
|
|
82
|
+
// The per-token underline is deliberately NOT drawn, and it CANNOT be
|
|
83
|
+
// removed from here: `@codemirror/merge`'s base theme paints a 2px gradient
|
|
84
|
+
// along the bottom of every `.cm-changedText` / `.cm-deletedText` fragment,
|
|
85
|
+
// and its rules are spelled with the `&light`/`&dark` prefixes that only
|
|
86
|
+
// `EditorView.baseTheme` accepts — `EditorView.theme` throws
|
|
87
|
+
// `RangeError: Unsupported selector: &light` (measured; it took the whole
|
|
88
|
+
// merge view down with an empty host). `tailwind.css` therefore owns the
|
|
89
|
+
// suppression, under the same `.cm-changedText` class. See the block there.
|
|
90
|
+
'&.cm-merge-a .cm-changedLineGutter, .cm-deletedLineGutter': {
|
|
91
|
+
background: 'var(--editor-removed-gutter, var(--theme-error))',
|
|
92
|
+
},
|
|
93
|
+
'&.cm-merge-b .cm-changedLineGutter': {
|
|
94
|
+
background: 'var(--editor-added-gutter, var(--theme-success))',
|
|
95
|
+
},
|
|
96
|
+
'.cm-collapsedLines': {
|
|
97
|
+
color: 'color-mix(in srgb, var(--editor-fg, var(--theme-ink)) 45%, transparent)',
|
|
98
|
+
background: 'var(--editor-bg, var(--theme-canvas))',
|
|
99
|
+
fontSize: '11px',
|
|
100
|
+
},
|
|
101
|
+
'.cm-changeGutter': { width: '3px', paddingLeft: '1px' },
|
|
102
|
+
'.cm-mergeView': { height: '100%' },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface DiffViewOptions {
|
|
107
|
+
/** The document before the change (HEAD for a staged diff, the index otherwise). */
|
|
108
|
+
original: string;
|
|
109
|
+
/** The document after it. */
|
|
110
|
+
next: string;
|
|
111
|
+
language: string;
|
|
112
|
+
wordWrap: boolean;
|
|
113
|
+
collapse: CollapseMode;
|
|
114
|
+
/** The two Shiki theme names; absent keeps the app's dual pair. */
|
|
115
|
+
themes?: ShikiThemeNames;
|
|
116
|
+
/** `data-testid` for the scroller, so the e2e layer can address it. */
|
|
117
|
+
scrollTestId: string;
|
|
118
|
+
/** The element that scrolls, reported on mount and cleared on unmount. */
|
|
119
|
+
onScroller: (element: HTMLElement | null) => void;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface DiffViewHandle {
|
|
123
|
+
/** Swap the documents (a refresh, a stage, a discard). */
|
|
124
|
+
setDocuments(original: string, next: string): void;
|
|
125
|
+
/** Re-collapse or expand the unchanged stretches without re-mounting. */
|
|
126
|
+
setCollapse(collapse: CollapseMode): void;
|
|
127
|
+
setWordWrap(wordWrap: boolean): void;
|
|
128
|
+
/** The element that actually scrolls, for scroll restore. */
|
|
129
|
+
scroller(): HTMLElement | null;
|
|
130
|
+
destroy(): void;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** What `collapseUnchanged` is given, or `undefined` for the full file. */
|
|
134
|
+
function collapseConfig(collapse: CollapseMode): { margin: number; minSize: number } | undefined {
|
|
135
|
+
return collapse === 'diff' ? COLLAPSE : undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The extension stack both shapes share. */
|
|
139
|
+
function baseExtensions(options: DiffViewOptions, wrap: Compartment): Extension[] {
|
|
140
|
+
return [
|
|
141
|
+
...READ_ONLY,
|
|
142
|
+
lineNumbers(),
|
|
143
|
+
shikiHighlight(options.language, options.themes),
|
|
144
|
+
surfaceTheme(),
|
|
145
|
+
diffTheme(),
|
|
146
|
+
wrap.of(options.wordWrap ? EditorView.lineWrapping : []),
|
|
147
|
+
// The browser's spellchecker underlines every identifier in a diff, and
|
|
148
|
+
// autocorrect would be offered on code.
|
|
149
|
+
EditorView.contentAttributes.of({ spellcheck: 'false', autocorrect: 'off', autocapitalize: 'off' }),
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Replace a document wholesale — the diff is a reader, so there is no history to keep. */
|
|
154
|
+
function replaceDoc(view: EditorView, text: string): void {
|
|
155
|
+
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: text } });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The Unified shape: one editor whose content IS the new document. */
|
|
159
|
+
export function mountUnifiedDiff(parent: HTMLElement, options: DiffViewOptions): DiffViewHandle {
|
|
160
|
+
const collapse = new Compartment();
|
|
161
|
+
const wrap = new Compartment();
|
|
162
|
+
/** The pair the LIVE view is comparing; re-stated whenever collapse changes. */
|
|
163
|
+
let original = options.original;
|
|
164
|
+
|
|
165
|
+
const unified = (source: string): Extension =>
|
|
166
|
+
unifiedMergeView({
|
|
167
|
+
original: source,
|
|
168
|
+
collapseUnchanged: collapseConfig(options.collapse),
|
|
169
|
+
// The panel's toolbar owns stage/unstage/discard; per-chunk accept/reject
|
|
170
|
+
// would be a second, weaker write path into the working tree.
|
|
171
|
+
mergeControls: false,
|
|
172
|
+
gutter: true,
|
|
173
|
+
// Deleted lines are widgets, so the Shiki decorations cannot reach them;
|
|
174
|
+
// leaving this on would colour them from a grammar the editor does not
|
|
175
|
+
// have installed.
|
|
176
|
+
syntaxHighlightDeletions: false,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const view = new EditorView({
|
|
180
|
+
parent,
|
|
181
|
+
state: EditorState.create({
|
|
182
|
+
doc: options.next,
|
|
183
|
+
extensions: [...baseExtensions(options, wrap), collapse.of(unified(original))],
|
|
184
|
+
}),
|
|
185
|
+
});
|
|
186
|
+
if (options.scrollTestId) view.scrollDOM.dataset.testid = options.scrollTestId;
|
|
187
|
+
options.onScroller(view.scrollDOM);
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
setDocuments(nextOriginal, next) {
|
|
191
|
+
original = nextOriginal;
|
|
192
|
+
view.dispatch({
|
|
193
|
+
changes: { from: 0, to: view.state.doc.length, insert: next },
|
|
194
|
+
// The merge extension reads `original` from its own configuration, so a
|
|
195
|
+
// new pair means reconfiguring it — a document-only dispatch would keep
|
|
196
|
+
// comparing against the previous side.
|
|
197
|
+
effects: collapse.reconfigure(unified(original)),
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
setCollapse(mode) {
|
|
201
|
+
options.collapse = mode;
|
|
202
|
+
view.dispatch({ effects: collapse.reconfigure(unified(original)) });
|
|
203
|
+
},
|
|
204
|
+
setWordWrap(wordWrap) {
|
|
205
|
+
options.wordWrap = wordWrap;
|
|
206
|
+
view.dispatch({ effects: wrap.reconfigure(wordWrap ? EditorView.lineWrapping : []) });
|
|
207
|
+
},
|
|
208
|
+
scroller: () => view.scrollDOM,
|
|
209
|
+
destroy: () => {
|
|
210
|
+
options.onScroller(null);
|
|
211
|
+
view.destroy();
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** The Split shape: two editors, aligned by the merge view. */
|
|
217
|
+
export function mountSplitDiff(parent: HTMLElement, options: DiffViewOptions): DiffViewHandle {
|
|
218
|
+
const wrap = new Compartment();
|
|
219
|
+
const merge = new MergeView({
|
|
220
|
+
parent,
|
|
221
|
+
a: { doc: options.original, extensions: baseExtensions(options, wrap) },
|
|
222
|
+
b: { doc: options.next, extensions: baseExtensions(options, wrap) },
|
|
223
|
+
collapseUnchanged: collapseConfig(options.collapse),
|
|
224
|
+
highlightChanges: true,
|
|
225
|
+
gutter: true,
|
|
226
|
+
orientation: 'a-b',
|
|
227
|
+
});
|
|
228
|
+
// The merge view's own element is the scroller: it styles `.cm-mergeView` with
|
|
229
|
+
// a height and `overflow: auto`, and the two editors inside it do not scroll.
|
|
230
|
+
const scroller = merge.dom;
|
|
231
|
+
if (options.scrollTestId) scroller.dataset.testid = options.scrollTestId;
|
|
232
|
+
options.onScroller(scroller);
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
setDocuments(original, next) {
|
|
236
|
+
replaceDoc(merge.a, original);
|
|
237
|
+
replaceDoc(merge.b, next);
|
|
238
|
+
// `chunks` is recomputed from the editors' states, so this re-runs the
|
|
239
|
+
// diff against the new pair.
|
|
240
|
+
merge.reconfigure({ collapseUnchanged: collapseConfig(options.collapse) });
|
|
241
|
+
},
|
|
242
|
+
setCollapse(mode) {
|
|
243
|
+
options.collapse = mode;
|
|
244
|
+
merge.reconfigure({ collapseUnchanged: collapseConfig(mode) });
|
|
245
|
+
},
|
|
246
|
+
setWordWrap(wordWrap) {
|
|
247
|
+
options.wordWrap = wordWrap;
|
|
248
|
+
const effects = wrap.reconfigure(wordWrap ? EditorView.lineWrapping : []);
|
|
249
|
+
merge.a.dispatch({ effects });
|
|
250
|
+
merge.b.dispatch({ effects });
|
|
251
|
+
},
|
|
252
|
+
scroller: () => scroller,
|
|
253
|
+
destroy: () => {
|
|
254
|
+
options.onScroller(null);
|
|
255
|
+
merge.destroy();
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Additions and deletions, counted from the SAME line-level diff the view draws.
|
|
262
|
+
*
|
|
263
|
+
* `Chunk.build` is the merge view's own chunking — a line-level diff, which is
|
|
264
|
+
* what its rows are. `diff()` would be the wrong source here: it is a
|
|
265
|
+
* CHARACTER diff, so changing `line 5` into `line 5 changed` reports one
|
|
266
|
+
* insertion of `" changed"` and no deletion at all, and the badge would claim
|
|
267
|
+
* two additions for two edited lines.
|
|
268
|
+
*
|
|
269
|
+
* Counting from the view's own chunks is also what makes the whitespace toggle
|
|
270
|
+
* honest: a change folded out of the comparison is not a changed row on screen,
|
|
271
|
+
* and it is not counted.
|
|
272
|
+
*/
|
|
273
|
+
export function countChanges(original: string, next: string): { additions: number; deletions: number } {
|
|
274
|
+
const a = Text.of(original.split('\n'));
|
|
275
|
+
const b = Text.of(next.split('\n'));
|
|
276
|
+
let additions = 0;
|
|
277
|
+
let deletions = 0;
|
|
278
|
+
for (const chunk of Chunk.build(a, b)) {
|
|
279
|
+
// `endA`/`endB` are the end of the chunk's last line, and equal the start
|
|
280
|
+
// when the chunk covers no lines on that side (a pure insertion/deletion).
|
|
281
|
+
if (chunk.endA > chunk.fromA) deletions += a.lineAt(chunk.endA).number - a.lineAt(chunk.fromA).number + 1;
|
|
282
|
+
if (chunk.endB > chunk.fromB) additions += b.lineAt(chunk.endB).number - b.lineAt(chunk.fromB).number + 1;
|
|
283
|
+
}
|
|
284
|
+
return { additions, deletions };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* `original` with every whitespace-only change replaced by the new text.
|
|
289
|
+
*
|
|
290
|
+
* The toolbar's whitespace toggle means "do not show me formatting noise", and
|
|
291
|
+
* the merge view compares DOCUMENTS — so the way to honour it is to make the two
|
|
292
|
+
* sides agree there. The old table parser folded a `-`/`+` pair whose trimmed
|
|
293
|
+
* text matched back into a context row; this does the same at the source, which
|
|
294
|
+
* is why the toggle changes the CHUNKS rather than only their colour.
|
|
295
|
+
*
|
|
296
|
+
* Only line-for-line changes are folded: an inserted or deleted line is a real
|
|
297
|
+
* change even when it is blank, and hiding it would make the file's line count
|
|
298
|
+
* disagree with what is on screen.
|
|
299
|
+
*/
|
|
300
|
+
export function foldWhitespaceOnly(original: string, next: string): string {
|
|
301
|
+
const changes = diff(original, next);
|
|
302
|
+
let folded = original;
|
|
303
|
+
// Back to front: an earlier splice would move every later offset.
|
|
304
|
+
for (let index = changes.length - 1; index >= 0; index--) {
|
|
305
|
+
const change = changes[index];
|
|
306
|
+
const before = original.slice(change.fromA, change.toA);
|
|
307
|
+
const after = next.slice(change.fromB, change.toB);
|
|
308
|
+
const beforeLines = before.split('\n');
|
|
309
|
+
const afterLines = after.split('\n');
|
|
310
|
+
if (beforeLines.length !== afterLines.length) continue;
|
|
311
|
+
if (!beforeLines.every((line, lineIndex) => line.trim() === afterLines[lineIndex].trim())) continue;
|
|
312
|
+
folded = folded.slice(0, change.fromA) + after + folded.slice(change.toA);
|
|
313
|
+
}
|
|
314
|
+
return folded;
|
|
315
|
+
}
|