@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,4 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
AlertCircle,
|
|
3
|
+
Check,
|
|
4
|
+
Copy,
|
|
5
|
+
Download,
|
|
6
|
+
Eye,
|
|
7
|
+
EyeOff,
|
|
8
|
+
Maximize,
|
|
9
|
+
Minimize,
|
|
10
|
+
Save,
|
|
11
|
+
Search,
|
|
12
|
+
WrapText,
|
|
13
|
+
ZoomIn,
|
|
14
|
+
ZoomOut,
|
|
15
|
+
} from 'lucide-preact';
|
|
16
|
+
import { EditorViewButtons, useEditorViewActions } from '@/client/components/common/editor-view-buttons';
|
|
17
|
+
import { ToolbarRow, type ToolbarCluster } from '@/client/components/common/toolbar-row';
|
|
2
18
|
|
|
3
19
|
export type EditorSaveStatus = 'idle' | 'saving' | 'saved' | 'error' | 'needs-permission';
|
|
4
20
|
|
|
@@ -94,146 +110,227 @@ export function EditorToolbar({
|
|
|
94
110
|
? 'Save failed — the file on disk was not changed'
|
|
95
111
|
: 'Save file (Ctrl+S)';
|
|
96
112
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
{
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
113
|
+
const saveButton = (
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
onClick={onSave}
|
|
117
|
+
disabled={saveDisabled || saveStatus === 'saving'}
|
|
118
|
+
className={`${ACTION_CLASS} hover:bg-ink/5`}
|
|
119
|
+
title={saveTitle}
|
|
120
|
+
aria-label={saveTitle}
|
|
121
|
+
>
|
|
122
|
+
{saveStatus === 'saving' ? (
|
|
123
|
+
<Save size={14} className="text-amber-500 animate-pulse" />
|
|
124
|
+
) : saveStatus === 'saved' ? (
|
|
125
|
+
<Check size={14} className="text-success" />
|
|
126
|
+
) : saveStatus === 'error' ? (
|
|
127
|
+
<AlertCircle size={14} className="text-error" />
|
|
128
|
+
) : saveStatus === 'needs-permission' ? (
|
|
129
|
+
// Not an error: the buffer is fine and one click grants access.
|
|
130
|
+
<Save size={14} className="text-warning" />
|
|
131
|
+
) : (
|
|
132
|
+
<Save size={14} className="hover:text-ink" />
|
|
133
|
+
)}
|
|
134
|
+
</button>
|
|
135
|
+
);
|
|
108
136
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
aria-label={saveTitle}
|
|
122
|
-
>
|
|
123
|
-
{saveStatus === 'saving' ? (
|
|
124
|
-
<Save size={14} className="text-amber-500 animate-pulse" />
|
|
125
|
-
) : saveStatus === 'saved' ? (
|
|
126
|
-
<Check size={14} className="text-success" />
|
|
127
|
-
) : saveStatus === 'error' ? (
|
|
128
|
-
<AlertCircle size={14} className="text-error" />
|
|
129
|
-
) : saveStatus === 'needs-permission' ? (
|
|
130
|
-
// Not an error: the buffer is fine and one click grants access.
|
|
131
|
-
<Save size={14} className="text-warning" />
|
|
132
|
-
) : (
|
|
133
|
-
<Save size={14} className="hover:text-ink" />
|
|
134
|
-
)}
|
|
135
|
-
</button>
|
|
136
|
-
</div>
|
|
137
|
-
)}
|
|
137
|
+
const copyLabel = copied ? 'Copied' : byteViewer ? 'Copy image' : 'Copy content';
|
|
138
|
+
const copyButton = (
|
|
139
|
+
<button
|
|
140
|
+
type="button"
|
|
141
|
+
onClick={onCopy}
|
|
142
|
+
className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
|
|
143
|
+
title={copyLabel}
|
|
144
|
+
aria-label={copyLabel}
|
|
145
|
+
>
|
|
146
|
+
{copied ? <Check size={14} className="text-success" /> : <Copy size={14} />}
|
|
147
|
+
</button>
|
|
148
|
+
);
|
|
138
149
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
</button>
|
|
151
|
-
</div>
|
|
152
|
-
)}
|
|
150
|
+
const downloadButton = (
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
onClick={onDownload}
|
|
154
|
+
className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
|
|
155
|
+
title="Download file"
|
|
156
|
+
aria-label="Download file"
|
|
157
|
+
>
|
|
158
|
+
<Download size={14} />
|
|
159
|
+
</button>
|
|
160
|
+
);
|
|
153
161
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
</div>
|
|
162
|
+
const maximizeLabel = isMaximized ? 'Exit full screen' : 'Full screen';
|
|
163
|
+
const maximizeButton = (
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
onClick={onToggleMaximize}
|
|
167
|
+
className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink ${isMaximized ? 'text-ink' : ''}`}
|
|
168
|
+
title={maximizeLabel}
|
|
169
|
+
aria-label={maximizeLabel}
|
|
170
|
+
aria-pressed={isMaximized}
|
|
171
|
+
>
|
|
172
|
+
{isMaximized ? <Minimize size={14} /> : <Maximize size={14} />}
|
|
173
|
+
</button>
|
|
174
|
+
);
|
|
168
175
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
className={`${ACTION_CLASS} hover:bg-ink/5 ${wordWrap ? 'text-ink' : 'hover:text-ink'}`}
|
|
174
|
-
title={wordWrap ? 'Word wrap on' : 'Word wrap off'}
|
|
175
|
-
aria-label={wordWrap ? 'Word wrap on' : 'Word wrap off'}
|
|
176
|
-
aria-pressed={wordWrap}
|
|
177
|
-
>
|
|
178
|
-
<WrapText size={14} />
|
|
179
|
-
</button>
|
|
180
|
-
</div>
|
|
176
|
+
const viewModeActions = useEditorViewActions();
|
|
177
|
+
const findLabel = 'Find and replace';
|
|
178
|
+
const wordWrapLabel = wordWrap ? 'Word wrap on' : 'Word wrap off';
|
|
179
|
+
const previewLabel = isPreview ? 'Show source' : 'Show preview';
|
|
181
180
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
type="button"
|
|
194
|
-
onClick={onZoomIn}
|
|
195
|
-
className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
|
|
196
|
-
title="Increase font size"
|
|
197
|
-
aria-label="Increase font size"
|
|
198
|
-
>
|
|
199
|
-
<ZoomIn size={14} />
|
|
200
|
-
</button>
|
|
201
|
-
</div>
|
|
202
|
-
</>
|
|
203
|
-
)}
|
|
181
|
+
/**
|
|
182
|
+
* The controls, as clusters that hide as a unit.
|
|
183
|
+
*
|
|
184
|
+
* ORDER is the hiding order, most important first: the row keeps the width
|
|
185
|
+
* modes and full screen (the controls this panel is FOR), then copy/download,
|
|
186
|
+
* and the reading aids (find, wrap, zoom, preview) move into the `⋯` menu on a
|
|
187
|
+
* narrow panel. Grouping is the existing one — zoom is a pair, copy/download is
|
|
188
|
+
* a pair, the modes are a pair — because splitting a pair leaves half of it on
|
|
189
|
+
* the row.
|
|
190
|
+
*/
|
|
191
|
+
const clusters: ToolbarCluster[] = [];
|
|
204
192
|
|
|
205
|
-
|
|
193
|
+
if (!byteViewer) {
|
|
194
|
+
clusters.push({
|
|
195
|
+
id: 'save',
|
|
196
|
+
// The primary action of the panel: a menu row must never be the only way
|
|
197
|
+
// to save a file.
|
|
198
|
+
pinned: true,
|
|
199
|
+
actions: [{ label: 'Save file', icon: Save, onSelect: onSave, disabled: saveDisabled }],
|
|
200
|
+
children: saveButton,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
if (hasPreview) {
|
|
204
|
+
clusters.push({
|
|
205
|
+
id: 'preview',
|
|
206
|
+
actions: [{ label: previewLabel, icon: isPreview ? EyeOff : Eye, onSelect: onTogglePreview, active: isPreview }],
|
|
207
|
+
children: (
|
|
208
|
+
<button
|
|
209
|
+
type="button"
|
|
210
|
+
onClick={onTogglePreview}
|
|
211
|
+
className={`${ACTION_CLASS} hover:bg-ink/5 ${isPreview ? 'text-ink' : 'hover:text-ink'}`}
|
|
212
|
+
title={previewLabel}
|
|
213
|
+
aria-label={previewLabel}
|
|
214
|
+
aria-pressed={isPreview}
|
|
215
|
+
>
|
|
216
|
+
{isPreview ? <EyeOff size={14} /> : <Eye size={14} />}
|
|
217
|
+
</button>
|
|
218
|
+
),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (!byteViewer) {
|
|
222
|
+
clusters.push({
|
|
223
|
+
id: 'find',
|
|
224
|
+
actions: [{ label: findLabel, icon: Search, onSelect: onToggleFind, active: findOpen }],
|
|
225
|
+
children: (
|
|
226
|
+
<button
|
|
227
|
+
type="button"
|
|
228
|
+
onClick={onToggleFind}
|
|
229
|
+
className={`${ACTION_CLASS} hover:bg-ink/5 ${findOpen ? 'text-ink' : 'hover:text-ink'}`}
|
|
230
|
+
title={`${findLabel} (Ctrl+F)`}
|
|
231
|
+
aria-label={findLabel}
|
|
232
|
+
aria-pressed={findOpen}
|
|
233
|
+
>
|
|
234
|
+
<Search size={14} />
|
|
235
|
+
</button>
|
|
236
|
+
),
|
|
237
|
+
});
|
|
238
|
+
clusters.push({
|
|
239
|
+
id: 'wrap',
|
|
240
|
+
actions: [{ label: wordWrapLabel, icon: WrapText, onSelect: onToggleWordWrap, active: wordWrap }],
|
|
241
|
+
children: (
|
|
242
|
+
<button
|
|
243
|
+
type="button"
|
|
244
|
+
onClick={onToggleWordWrap}
|
|
245
|
+
className={`${ACTION_CLASS} hover:bg-ink/5 ${wordWrap ? 'text-ink' : 'hover:text-ink'}`}
|
|
246
|
+
title={wordWrapLabel}
|
|
247
|
+
aria-label={wordWrapLabel}
|
|
248
|
+
aria-pressed={wordWrap}
|
|
249
|
+
>
|
|
250
|
+
<WrapText size={14} />
|
|
251
|
+
</button>
|
|
252
|
+
),
|
|
253
|
+
});
|
|
254
|
+
clusters.push({
|
|
255
|
+
id: 'zoom',
|
|
256
|
+
actions: [
|
|
257
|
+
{ label: 'Decrease font size', icon: ZoomOut, onSelect: onZoomOut },
|
|
258
|
+
{ label: 'Increase font size', icon: ZoomIn, onSelect: onZoomIn },
|
|
259
|
+
],
|
|
260
|
+
children: (
|
|
261
|
+
<>
|
|
206
262
|
<button
|
|
207
263
|
type="button"
|
|
208
|
-
onClick={
|
|
264
|
+
onClick={onZoomOut}
|
|
209
265
|
className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
|
|
210
|
-
title=
|
|
211
|
-
aria-label=
|
|
266
|
+
title="Decrease font size"
|
|
267
|
+
aria-label="Decrease font size"
|
|
212
268
|
>
|
|
213
|
-
|
|
269
|
+
<ZoomOut size={14} />
|
|
214
270
|
</button>
|
|
215
271
|
<button
|
|
216
272
|
type="button"
|
|
217
|
-
onClick={
|
|
273
|
+
onClick={onZoomIn}
|
|
218
274
|
className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
|
|
219
|
-
title="
|
|
220
|
-
aria-label="
|
|
275
|
+
title="Increase font size"
|
|
276
|
+
aria-label="Increase font size"
|
|
221
277
|
>
|
|
222
|
-
<
|
|
278
|
+
<ZoomIn size={14} />
|
|
223
279
|
</button>
|
|
224
|
-
|
|
280
|
+
</>
|
|
281
|
+
),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
225
284
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
285
|
+
clusters.push({
|
|
286
|
+
id: 'copy',
|
|
287
|
+
actions: [
|
|
288
|
+
{ label: copyLabel, icon: Copy, onSelect: onCopy },
|
|
289
|
+
{ label: 'Download file', icon: Download, onSelect: onDownload },
|
|
290
|
+
],
|
|
291
|
+
children: (
|
|
292
|
+
<>
|
|
293
|
+
{copyButton}
|
|
294
|
+
{downloadButton}
|
|
295
|
+
</>
|
|
296
|
+
),
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// The width modes and full SCREEN are the controls this panel exists for, so
|
|
300
|
+
// they are the LAST clusters — the tail is what the menu takes. Three controls
|
|
301
|
+
// about one axis, three different glyphs: compact collapses the chat (a
|
|
302
|
+
// crossed-out message), full collapses the chat and the sidebar (a closing
|
|
303
|
+
// panel edge), and full screen covers the right panel too (a maximise frame).
|
|
304
|
+
clusters.push({
|
|
305
|
+
id: 'view-modes',
|
|
306
|
+
// The two width modes are why this toolbar grew these buttons, and a mode
|
|
307
|
+
// reachable only from a menu is a mode half the readers never find.
|
|
308
|
+
pinned: true,
|
|
309
|
+
actions: viewModeActions,
|
|
310
|
+
children: <EditorViewButtons size={14} />,
|
|
311
|
+
});
|
|
312
|
+
clusters.push({
|
|
313
|
+
id: 'maximize',
|
|
314
|
+
pinned: true,
|
|
315
|
+
actions: [{ label: maximizeLabel, icon: isMaximized ? Minimize : Maximize, onSelect: onToggleMaximize, active: isMaximized }],
|
|
316
|
+
children: maximizeButton,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
// `@container`: the path below hides on a NARROW PANEL, not a narrow
|
|
321
|
+
// viewport. At the editor panel's 320px floor the path would be a truncated
|
|
322
|
+
// fragment of a filename — not information, and the tab above already
|
|
323
|
+
// carries it.
|
|
324
|
+
<div className="@container flex items-center justify-between gap-2 px-3 py-2 border-b border-ink/10 bg-paper flex-shrink-0">
|
|
325
|
+
{/* Left status */}
|
|
326
|
+
<div className="hidden @[420px]:flex items-center space-x-2 text-xs font-mono text-ink/40 min-w-0">
|
|
327
|
+
<span className="truncate max-w-[300px]" title={path}>{path}</span>
|
|
236
328
|
</div>
|
|
329
|
+
|
|
330
|
+
{/* One row, never two: what does not fit moves into `⋯` (see
|
|
331
|
+
`ToolbarRow`). A wrapped toolbar pushed the document down by its own
|
|
332
|
+
height and read as a second toolbar. */}
|
|
333
|
+
<ToolbarRow clusters={clusters} className="text-ink/40 ml-auto" />
|
|
237
334
|
</div>
|
|
238
335
|
);
|
|
239
336
|
}
|
|
@@ -5,7 +5,7 @@ import { EditorTabs } from '@/client/components/workspace/editor/Tabs';
|
|
|
5
5
|
import { EDITOR_KEY_BINDINGS, isFindBarCommand, type EditorCommand } from '@/shared/lib/code/editor/keymap';
|
|
6
6
|
import { EDITOR_DEFAULT_FONT_SIZE } from '@/shared/lib/code/editor/typography';
|
|
7
7
|
import { useEditorFontStack } from '@/client/hooks/editor/font';
|
|
8
|
-
import
|
|
8
|
+
import { useEditorTheme } from '@/client/hooks/editor/theme';
|
|
9
9
|
import type { CodeSurfaceHandle } from '@/client/components/common/code-surface';
|
|
10
10
|
import { resolveBinding } from '@/shared/lib/ui/key-binding';
|
|
11
11
|
import { useActiveEditorPanel } from '@/client/components/workspace/plugin-panel/resolve';
|
|
@@ -62,12 +62,11 @@ export function Editor({
|
|
|
62
62
|
const [zoomLevel, setZoomLevel] = useSessionState<number>('editor.zoomLevel', EDITOR_DEFAULT_FONT_SIZE);
|
|
63
63
|
const [isMaximized, setIsMaximized] = useState(false);
|
|
64
64
|
const [wordWrap, setWordWrap] = useSessionState<boolean>('editor.wordWrap', true);
|
|
65
|
-
/** Extra ⌘D ranges; the surface paints them and the editor edits them by replication. */
|
|
66
|
-
const [occurrences, setOccurrences] = useState<readonly TextRange[]>([]);
|
|
67
65
|
const [paletteOpen, setPaletteOpen] = useState(false);
|
|
68
66
|
const { sessionId } = useSessionStateContext();
|
|
69
67
|
const surfaceRef = useRef<CodeSurfaceHandle | null>(null);
|
|
70
68
|
const editorFontFamily = useEditorFontStack();
|
|
69
|
+
const editorTheme = useEditorTheme();
|
|
71
70
|
|
|
72
71
|
// A deleted file keeps its tab (its buffer is still on screen), but the tab
|
|
73
72
|
// strip must say so. Only a plain FILE tab is probed: a diff tab's path names
|
|
@@ -216,10 +215,10 @@ export function Editor({
|
|
|
216
215
|
* Editor shortcuts, on the CAPTURE phase of the panel.
|
|
217
216
|
*
|
|
218
217
|
* Capture is what makes them work while focus is inside the document: the
|
|
219
|
-
*
|
|
220
|
-
* otherwise see these first. Only the
|
|
221
|
-
* here — the editor's own keymap runs in
|
|
222
|
-
* buffer and the caret in hand.
|
|
218
|
+
* contenteditable has CodeMirror's own handlers (the generated keymap, and
|
|
219
|
+
* `defaultKeymap` behind it) and would otherwise see these first. Only the
|
|
220
|
+
* commands the PANEL owns are consumed here — the editor's own keymap runs in
|
|
221
|
+
* the document, where it has the buffer and the caret in hand.
|
|
223
222
|
*
|
|
224
223
|
* The panel is the scope, not the window: another panel's text field keeps
|
|
225
224
|
* its own ⌘F, and the composer is untouched.
|
|
@@ -312,12 +311,11 @@ export function Editor({
|
|
|
312
311
|
setIsMaximized={setIsMaximized}
|
|
313
312
|
zoomLevel={zoomLevel}
|
|
314
313
|
setZoomLevel={setZoomLevel}
|
|
315
|
-
occurrences={occurrences}
|
|
316
|
-
setOccurrences={setOccurrences}
|
|
317
314
|
paletteOpen={paletteOpen}
|
|
318
315
|
setPaletteOpen={setPaletteOpen}
|
|
319
316
|
runCommand={runCommand}
|
|
320
317
|
editorFontFamily={editorFontFamily}
|
|
318
|
+
editorTheme={editorTheme}
|
|
321
319
|
togglePreview={togglePreview}
|
|
322
320
|
revealActive={Boolean(searchReveal)}
|
|
323
321
|
missing={activeFileMissing}
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
import { createPortal } from 'preact/compat';
|
|
31
31
|
import type { ReactNode, RefObject } from 'preact/compat';
|
|
32
|
-
import { ClipboardPaste, Copy, CopyPlus, Edit2, ExternalLink, Eye, FilePlus, GitCompare, History, Scissors, Trash2 } from 'lucide-preact';
|
|
32
|
+
import { ClipboardPaste, Copy, CopyPlus, Edit2, ExternalLink, Eye, FilePlus, FolderPlus, GitCompare, History, Scissors, Trash2 } from 'lucide-preact';
|
|
33
33
|
|
|
34
34
|
interface ContextMenuProps {
|
|
35
35
|
x: number;
|
|
@@ -61,6 +61,7 @@ function buildRows(options: { isFolder: boolean; hasGitStatus?: boolean; pasteLa
|
|
|
61
61
|
|
|
62
62
|
if (isFolder) {
|
|
63
63
|
entries.push({ kind: 'item', id: 'new_file', label: 'New File', icon: <FilePlus size={13} className="text-ink/60" /> });
|
|
64
|
+
entries.push({ kind: 'item', id: 'new_folder', label: 'New Folder', icon: <FolderPlus size={13} className="text-ink/60" /> });
|
|
64
65
|
entries.push({ kind: 'separator' });
|
|
65
66
|
} else {
|
|
66
67
|
entries.push({ kind: 'item', id: 'view', label: 'View', icon: <Eye size={13} className="text-ink/60" /> });
|
|
@@ -4,10 +4,12 @@ import { History, RefreshCw, X } from 'lucide-preact';
|
|
|
4
4
|
import type { FsActionResponse } from '@/shared/types';
|
|
5
5
|
|
|
6
6
|
interface NewFileModalProps {
|
|
7
|
-
/** Folder the
|
|
7
|
+
/** Folder the entry will be created in, relative to the listed root. */
|
|
8
8
|
folderPath: string;
|
|
9
|
+
/** What the dialog creates — a file or a folder. */
|
|
10
|
+
kind: 'file' | 'folder';
|
|
9
11
|
value: string;
|
|
10
|
-
/** Server's refusal (a name that already exists, a bad
|
|
12
|
+
/** Server's refusal (a name that already exists, a bad path), shown inline. */
|
|
11
13
|
error?: string | null;
|
|
12
14
|
isLoading: boolean;
|
|
13
15
|
onChange: (value: string) => void;
|
|
@@ -16,21 +18,26 @@ interface NewFileModalProps {
|
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
|
-
* The "New File" dialog.
|
|
21
|
+
* The "New File" / "New Folder" dialog.
|
|
22
|
+
*
|
|
23
|
+
* The folder is the one that was right-clicked, shown as a fixed prefix so the
|
|
24
|
+
* field's meaning is unambiguous — and the name may carry SEGMENTS
|
|
25
|
+
* (`lib/parse.ts`, `src/nested/x.ts`), which creates every directory on the way
|
|
26
|
+
* in one step. The grammar that keeps that inside the named folder is the
|
|
27
|
+
* server's, so the field does not restate it; it only says what it accepts.
|
|
20
28
|
*
|
|
21
|
-
* Only a name is asked for — the folder is the one that was right-clicked, and
|
|
22
|
-
* showing it as a fixed prefix is what makes the field's meaning unambiguous.
|
|
23
29
|
* The input starts empty (no `.txt` default): a name is what the user has in
|
|
24
30
|
* mind, and a prefilled suffix is a character to delete before every keystroke
|
|
25
31
|
* lands. The server's refusal is rendered INLINE rather than as a toast: the
|
|
26
32
|
* field is still open, and "already exists" is a message about what to type
|
|
27
33
|
* next, not a notification about something that happened.
|
|
28
34
|
*/
|
|
29
|
-
export function FileCreateModal({ folderPath, value, error, isLoading, onChange, onCancel, onSubmit }: NewFileModalProps) {
|
|
35
|
+
export function FileCreateModal({ folderPath, kind, value, error, isLoading, onChange, onCancel, onSubmit }: NewFileModalProps) {
|
|
36
|
+
const label = kind === 'folder' ? 'Folder' : 'File';
|
|
30
37
|
return createPortal(
|
|
31
38
|
<div className="fixed inset-0 bg-ink/20 z-[100] flex items-center justify-center p-4 backdrop-blur-sm">
|
|
32
39
|
<form onSubmit={onSubmit} className="bg-paper rounded shadow-xl p-4 w-full max-w-sm border border-ink/10 font-sans">
|
|
33
|
-
<h3 className="font-semibold text-ink mb-3 text-sm">New
|
|
40
|
+
<h3 className="font-semibold text-ink mb-3 text-sm">New {label}</h3>
|
|
34
41
|
<div className="flex items-center gap-1 mb-1 text-xs font-mono text-ink/50 min-w-0">
|
|
35
42
|
<span className="truncate" title={folderPath}>{folderPath || '.'}/</span>
|
|
36
43
|
</div>
|
|
@@ -39,12 +46,15 @@ export function FileCreateModal({ folderPath, value, error, isLoading, onChange,
|
|
|
39
46
|
type="text"
|
|
40
47
|
value={value}
|
|
41
48
|
onChange={e => onChange(e.currentTarget.value)}
|
|
42
|
-
placeholder=
|
|
43
|
-
aria-label=
|
|
44
|
-
className={`w-full bg-paper border rounded px-2 py-1.5 text-xs font-mono focus:outline-none mb-
|
|
49
|
+
placeholder={kind === 'folder' ? 'folder name or nested/path' : 'file name or nested/path'}
|
|
50
|
+
aria-label={`New ${label.toLowerCase()} name`}
|
|
51
|
+
className={`w-full bg-paper border rounded px-2 py-1.5 text-xs font-mono focus:outline-none mb-1 ${
|
|
45
52
|
error ? 'border-error' : 'border-ink/20 focus:border-ink'
|
|
46
53
|
}`}
|
|
47
54
|
/>
|
|
55
|
+
<p className="text-[10px] text-ink/40 mb-2">
|
|
56
|
+
Nested paths create the folders they need — <span className="font-mono">lib/parse.ts</span>.
|
|
57
|
+
</p>
|
|
48
58
|
{error && <p className="text-[11px] text-error mb-2">{error}</p>}
|
|
49
59
|
<div className="flex justify-end space-x-2 mt-2">
|
|
50
60
|
<button
|
|
@@ -109,13 +119,24 @@ interface RenameModalProps {
|
|
|
109
119
|
isFolder: boolean;
|
|
110
120
|
renameValue: string;
|
|
111
121
|
originalPath: string;
|
|
122
|
+
/** The server's refusal (an existing destination, a bad path), shown inline. */
|
|
123
|
+
error?: string | null;
|
|
112
124
|
isLoading: boolean;
|
|
113
125
|
onChange: (value: string) => void;
|
|
114
126
|
onCancel: () => void;
|
|
115
127
|
onSubmit: (e: FormEvent) => void;
|
|
116
128
|
}
|
|
117
129
|
|
|
118
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Rename, with the WHOLE path in the field.
|
|
132
|
+
*
|
|
133
|
+
* The field holds the path relative to the listed root, not just the leaf name,
|
|
134
|
+
* because that is what makes a nested destination reachable: `src/a.ts` →
|
|
135
|
+
* `lib/deep/a.ts` both renames the file and moves it, and its parents are
|
|
136
|
+
* created on the way. A leaf-only field would have made that a create-then-
|
|
137
|
+
* delete, which is not a rename.
|
|
138
|
+
*/
|
|
139
|
+
export function FileRenameModal({ isFolder, renameValue, originalPath, error, isLoading, onChange, onCancel, onSubmit }: RenameModalProps) {
|
|
119
140
|
return createPortal(
|
|
120
141
|
<div className="fixed inset-0 bg-ink/20 z-[100] flex items-center justify-center p-4 backdrop-blur-sm">
|
|
121
142
|
<form onSubmit={onSubmit} className="bg-paper rounded shadow-xl p-4 w-full max-w-sm border border-ink/10 font-sans">
|
|
@@ -125,8 +146,15 @@ export function FileRenameModal({ isFolder, renameValue, originalPath, isLoading
|
|
|
125
146
|
type="text"
|
|
126
147
|
value={renameValue}
|
|
127
148
|
onChange={e => onChange(e.currentTarget.value)}
|
|
128
|
-
|
|
149
|
+
aria-label={`Rename ${isFolder ? 'folder' : 'file'} path`}
|
|
150
|
+
className={`w-full bg-paper border rounded px-2 py-1.5 text-xs font-mono focus:outline-none mb-1 ${
|
|
151
|
+
error ? 'border-error' : 'border-ink/20 focus:border-ink'
|
|
152
|
+
}`}
|
|
129
153
|
/>
|
|
154
|
+
<p className="text-[10px] text-ink/40 mb-2">
|
|
155
|
+
Paths are relative to the tree — <span className="font-mono">lib/deep/name.ts</span> moves it too.
|
|
156
|
+
</p>
|
|
157
|
+
{error && <p className="text-[11px] text-error mb-2">{error}</p>}
|
|
130
158
|
<div className="flex justify-end space-x-2">
|
|
131
159
|
<button
|
|
132
160
|
type="button"
|
|
@@ -32,6 +32,7 @@ import { SessionStateProvider } from '@/client/components/common/session-state-p
|
|
|
32
32
|
import { forgetSession } from '@/shared/lib/workspace/session-state/store';
|
|
33
33
|
import { installDomGlobals, restoreDomGlobals } from '@/test-support/pristine-globals';
|
|
34
34
|
import { publishEditorDocument, resetEditorDocument, type EditorDocument } from '@/client/lib/editor-document';
|
|
35
|
+
import { MAX_OUTLINE_ROWS } from '@/shared/lib/code/outline/rows';
|
|
35
36
|
import { getLineReveal, setLineReveal } from '@/client/lib/line-reveal';
|
|
36
37
|
|
|
37
38
|
const SESSION = 'new-outline-section';
|
|
@@ -215,4 +216,21 @@ describe('OutlineSection', () => {
|
|
|
215
216
|
expect(reveal().getAttribute('data-expanded')).toBe('true');
|
|
216
217
|
expect(toggle().getAttribute('aria-expanded')).toBe('true');
|
|
217
218
|
});
|
|
219
|
+
|
|
220
|
+
// The cap is what keeps a large file's outline from becoming a DOM: 63 618
|
|
221
|
+
// declarations mounted 446 308 nodes and charged 953 ms to every keystroke
|
|
222
|
+
// (measured), where the editor's own cost is 34 ms.
|
|
223
|
+
test('a large outline renders only the first rows, and says how many it is not showing', async () => {
|
|
224
|
+
owner = {};
|
|
225
|
+
const many = Array.from({ length: MAX_OUTLINE_ROWS + 200 }, (_, i) => `export function fn${i}() {}`).join('\n');
|
|
226
|
+
publishEditorDocument(owner, { ...READY, path: 'src/many.ts', content: many });
|
|
227
|
+
await mount();
|
|
228
|
+
await expand();
|
|
229
|
+
|
|
230
|
+
expect(rowLabels()).toHaveLength(MAX_OUTLINE_ROWS);
|
|
231
|
+
// The count badge shows the window against the total, so the number on the
|
|
232
|
+
// toggle cannot be read as "this file has 500 symbols".
|
|
233
|
+
expect(toggle().textContent).toContain(`${MAX_OUTLINE_ROWS}/700`);
|
|
234
|
+
expect(container.textContent).toContain('Showing the first 500 of 700 symbols.');
|
|
235
|
+
});
|
|
218
236
|
});
|
|
@@ -35,6 +35,7 @@ import { setLineReveal } from '@/client/lib/line-reveal';
|
|
|
35
35
|
import { useSessionState } from '@/client/hooks/workspace/session-state';
|
|
36
36
|
import { useOutlineView } from '@/client/hooks/editor/use-outline-view';
|
|
37
37
|
import type { OutlineSymbol } from '@/shared/lib/code/outline/types';
|
|
38
|
+
import { outlineTruncatedNotice } from '@/shared/lib/code/outline/rows';
|
|
38
39
|
|
|
39
40
|
export function OutlineSection() {
|
|
40
41
|
const [expanded, setExpanded] = useSessionState<boolean>('files.outlineExpanded', false);
|
|
@@ -44,11 +45,12 @@ export function OutlineSection() {
|
|
|
44
45
|
// Recomputed from the LIVE buffer, so an edit moves the rows with it. The
|
|
45
46
|
// scan is a depth-aware line walk (see `outline/extract.ts`): measured at
|
|
46
47
|
// 0.1–2.2 ms on this repository's largest sources.
|
|
47
|
-
const { symbols, notice } = useOutlineView({
|
|
48
|
+
const { symbols, total, truncated, notice } = useOutlineView({
|
|
48
49
|
content: current.content,
|
|
49
50
|
language: current.language,
|
|
50
51
|
state: current.state,
|
|
51
52
|
note: current.note,
|
|
53
|
+
path: current.path,
|
|
52
54
|
});
|
|
53
55
|
|
|
54
56
|
// A diff tab HIDES the section rather than explaining that it has none. The
|
|
@@ -91,9 +93,9 @@ export function OutlineSection() {
|
|
|
91
93
|
<span className="w-3 text-center font-mono" aria-hidden="true">{expanded ? '▾' : '▸'}</span>
|
|
92
94
|
<ListTree size={12} />
|
|
93
95
|
<span>Outline</span>
|
|
94
|
-
{
|
|
96
|
+
{total > 0 && (
|
|
95
97
|
<span className="font-normal ml-1 border border-ink/20 rounded-full px-1.5 text-[9px] text-ink/50">
|
|
96
|
-
{symbols.length}
|
|
98
|
+
{truncated ? `${symbols.length}/${total.toLocaleString()}` : total}
|
|
97
99
|
</span>
|
|
98
100
|
)}
|
|
99
101
|
</button>
|
|
@@ -125,6 +127,11 @@ export function OutlineSection() {
|
|
|
125
127
|
<span className="flex-shrink-0 font-mono text-[9px] text-ink/35">{symbol.line}</span>
|
|
126
128
|
</button>
|
|
127
129
|
))}
|
|
130
|
+
{truncated && (
|
|
131
|
+
<div className="px-1 py-1 text-[10px] italic text-ink/40">
|
|
132
|
+
{outlineTruncatedNotice(total)}
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
128
135
|
</div>
|
|
129
136
|
)}
|
|
130
137
|
</div>
|