@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,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The multi-selection rules.
|
|
8
|
-
*
|
|
9
|
-
* ⌘D's two-stage behaviour is the one users notice immediately: the first press
|
|
10
|
-
* selects the word, the second adds the next occurrence. Getting that wrong
|
|
11
|
-
* leaves the word selected once and its neighbour selected twice, which reads
|
|
12
|
-
* as a duplicated cursor.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { describe, expect, test } from 'bun:test';
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
growToNextOccurrence,
|
|
19
|
-
offsetBelow,
|
|
20
|
-
replicateAtRanges,
|
|
21
|
-
selectAllOccurrences,
|
|
22
|
-
} from '@/shared/lib/code/editor/occurrences';
|
|
23
|
-
|
|
24
|
-
describe('growToNextOccurrence', () => {
|
|
25
|
-
const text = 'alpha beta alpha gamma alpha';
|
|
26
|
-
|
|
27
|
-
test('the first press on a bare caret selects the word and adds nothing', () => {
|
|
28
|
-
expect(growToNextOccurrence(text, { start: 2, end: 2 }, [])).toEqual({
|
|
29
|
-
primary: { start: 0, end: 5 },
|
|
30
|
-
extras: [],
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test('the second press adds the next occurrence', () => {
|
|
35
|
-
const grown = growToNextOccurrence(text, { start: 0, end: 5 }, []);
|
|
36
|
-
expect(grown?.primary).toEqual({ start: 0, end: 5 });
|
|
37
|
-
expect(grown?.extras).toEqual([{ start: 11, end: 16 }]);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test('each further press adds the one after it, never a duplicate', () => {
|
|
41
|
-
const grown = growToNextOccurrence(text, { start: 0, end: 5 }, [{ start: 11, end: 16 }]);
|
|
42
|
-
expect(grown?.extras).toEqual([
|
|
43
|
-
{ start: 11, end: 16 },
|
|
44
|
-
{ start: 23, end: 28 },
|
|
45
|
-
]);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test('reports nothing when every occurrence is already selected', () => {
|
|
49
|
-
const extras = [
|
|
50
|
-
{ start: 11, end: 16 },
|
|
51
|
-
{ start: 23, end: 28 },
|
|
52
|
-
];
|
|
53
|
-
expect(growToNextOccurrence(text, { start: 0, end: 5 }, extras)).toBeNull();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('reports nothing on whitespace with no word beside it', () => {
|
|
57
|
-
expect(growToNextOccurrence(' ', { start: 1, end: 1 }, [])).toBeNull();
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe('selectAllOccurrences', () => {
|
|
62
|
-
test('the first occurrence is the primary, the rest are extras', () => {
|
|
63
|
-
const all = selectAllOccurrences('alpha beta alpha', { start: 0, end: 5 });
|
|
64
|
-
|
|
65
|
-
expect(all?.primary).toEqual({ start: 0, end: 5 });
|
|
66
|
-
expect(all?.extras).toEqual([{ start: 11, end: 16 }]);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test('a bare caret takes the word under it', () => {
|
|
70
|
-
const all = selectAllOccurrences('alpha beta alpha', { start: 2, end: 2 });
|
|
71
|
-
expect(all?.primary).toEqual({ start: 0, end: 5 });
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test('a one-off word yields the selection alone', () => {
|
|
75
|
-
const all = selectAllOccurrences('alpha beta', { start: 0, end: 5 });
|
|
76
|
-
expect(all?.primary).toEqual({ start: 0, end: 5 });
|
|
77
|
-
expect(all?.extras).toEqual([]);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe('offsetBelow', () => {
|
|
82
|
-
test('keeps the column', () => {
|
|
83
|
-
expect(offsetBelow('one\ntwo\nthree', 1)).toBe(5);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('clamps to the end of a shorter next line', () => {
|
|
87
|
-
// Column 6 does not exist on `ab`; the caret lands at its end rather than
|
|
88
|
-
// on a character the line never had.
|
|
89
|
-
expect(offsetBelow('longer line\nab\nnext', 6)).toBe(14);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test('reports nothing on the last line', () => {
|
|
93
|
-
expect(offsetBelow('one\ntwo', 5)).toBeNull();
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe('replicateAtRanges', () => {
|
|
98
|
-
test('reports nothing when there are no extra ranges', () => {
|
|
99
|
-
expect(replicateAtRanges('abc', 'abXc', { start: 2, end: 2 }, [])).toBeNull();
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test('replays one insertion at every range and collapses them to carets', () => {
|
|
103
|
-
const applied = replicateAtRanges('alpha alpha', 'Xalpha alpha', { start: 0, end: 0 }, [{ start: 6, end: 6 }]);
|
|
104
|
-
|
|
105
|
-
expect(applied?.value).toBe('Xalpha Xalpha');
|
|
106
|
-
expect(applied?.caret).toBe(1);
|
|
107
|
-
expect(applied?.rest).toEqual([{ start: 8, end: 8 }]);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test('replays a replacement over the selected text', () => {
|
|
111
|
-
const applied = replicateAtRanges('alpha beta alpha', 'Z beta alpha', { start: 0, end: 5 }, [{ start: 11, end: 16 }]);
|
|
112
|
-
|
|
113
|
-
expect(applied?.value).toBe('Z beta Z');
|
|
114
|
-
// The caret sits after the inserted `Z`, which now ends the buffer.
|
|
115
|
-
expect(applied?.rest).toEqual([{ start: 8, end: 8 }]);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The multi-selection rules: what ⌘D, ⌘⇧L and ⌘⇧D do to a buffer, and how one
|
|
8
|
-
* browser edit is replayed at every range.
|
|
9
|
-
*
|
|
10
|
-
* Pure on purpose. The hook that owns the ranges is a ref and a notify call;
|
|
11
|
-
* every decision about WHERE a range goes and what the buffer becomes lives
|
|
12
|
-
* here, so it is testable without a DOM and cannot drift between the keyboard
|
|
13
|
-
* path and the palette path (both call the same functions).
|
|
14
|
-
*
|
|
15
|
-
* The replication rule is the load-bearing one. The browser applies a keystroke
|
|
16
|
-
* at the primary range and reports the whole new buffer; diffing that against
|
|
17
|
-
* the old one recovers "what was typed", and the same replacement is replayed
|
|
18
|
-
* at every other range in one pass. Typing, pasting, deleting and IME
|
|
19
|
-
* composition therefore need no branch per case.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
diffEdit,
|
|
24
|
-
nextOccurrence,
|
|
25
|
-
occurrenceRanges,
|
|
26
|
-
replicateEdit,
|
|
27
|
-
wordRangeAt,
|
|
28
|
-
type TextRange,
|
|
29
|
-
} from '@/shared/lib/code/editor/commands';
|
|
30
|
-
|
|
31
|
-
/** What a selection command did: the primary range plus the extra ones. */
|
|
32
|
-
export interface OccurrenceSelection {
|
|
33
|
-
primary: TextRange;
|
|
34
|
-
extras: TextRange[];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* ⌘D. With a bare caret the first press selects the word under it and stops —
|
|
39
|
-
* "select this word" and "and the next one too" are two different asks, and
|
|
40
|
-
* adding a range on the first press would leave the word selected once and its
|
|
41
|
-
* neighbour selected twice. With a selection, the next unselected occurrence is
|
|
42
|
-
* added.
|
|
43
|
-
*
|
|
44
|
-
* Null when there is no word and no selection, or when every occurrence is
|
|
45
|
-
* already selected: the caller then leaves the selection alone rather than
|
|
46
|
-
* cycling silently.
|
|
47
|
-
*/
|
|
48
|
-
export function growToNextOccurrence(
|
|
49
|
-
text: string,
|
|
50
|
-
primary: TextRange,
|
|
51
|
-
extras: readonly TextRange[],
|
|
52
|
-
): OccurrenceSelection | null {
|
|
53
|
-
const hasSelection = primary.end > primary.start;
|
|
54
|
-
const seed = hasSelection ? primary : wordRangeAt(text, primary.start);
|
|
55
|
-
if (!seed) return null;
|
|
56
|
-
const word = text.slice(seed.start, seed.end);
|
|
57
|
-
if (!word) return null;
|
|
58
|
-
|
|
59
|
-
if (!hasSelection) return { primary: seed, extras: [...extras] };
|
|
60
|
-
|
|
61
|
-
const next = nextOccurrence(text, word, [primary, ...extras]);
|
|
62
|
-
if (!next) return null;
|
|
63
|
-
return { primary, extras: [...extras, next] };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* ⌘⇧L. Every occurrence becomes a range: the first is the primary (the textarea
|
|
68
|
-
* can only hold one selection) and the rest are extras. Null when there is
|
|
69
|
-
* nothing to select.
|
|
70
|
-
*/
|
|
71
|
-
export function selectAllOccurrences(text: string, primary: TextRange): OccurrenceSelection | null {
|
|
72
|
-
const seed = primary.end > primary.start ? primary : wordRangeAt(text, primary.start);
|
|
73
|
-
if (!seed) return null;
|
|
74
|
-
const word = text.slice(seed.start, seed.end);
|
|
75
|
-
if (!word) return null;
|
|
76
|
-
const [first, ...rest] = occurrenceRanges(text, word);
|
|
77
|
-
if (!first) return null;
|
|
78
|
-
return { primary: first, extras: rest };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* ⌘⇧D. The offset directly below `offset`, keeping the column — and clamped to
|
|
83
|
-
* that line's end, so a caret at the end of a short line lands at the end of the
|
|
84
|
-
* next one rather than on a character it never had. Null on the last line.
|
|
85
|
-
*/
|
|
86
|
-
export function offsetBelow(text: string, offset: number): number | null {
|
|
87
|
-
const lineEnd = text.indexOf('\n', offset);
|
|
88
|
-
if (lineEnd === -1) return null;
|
|
89
|
-
const column = offset - (text.lastIndexOf('\n', offset - 1) + 1);
|
|
90
|
-
const nextLineStart = lineEnd + 1;
|
|
91
|
-
const nextLineEnd = text.indexOf('\n', nextLineStart);
|
|
92
|
-
return Math.min(nextLineStart + column, nextLineEnd === -1 ? text.length : nextLineEnd);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Replay the browser's edit (`before` → `after`, applied at `primaryBefore`) at
|
|
97
|
-
* every extra range. Null when there are no extras, which is the caller's cue to
|
|
98
|
-
* keep the ordinary single-selection path.
|
|
99
|
-
*/
|
|
100
|
-
export function replicateAtRanges(
|
|
101
|
-
before: string,
|
|
102
|
-
after: string,
|
|
103
|
-
primaryBefore: TextRange,
|
|
104
|
-
extras: readonly TextRange[],
|
|
105
|
-
): { value: string; caret: number; rest: TextRange[] } | null {
|
|
106
|
-
if (extras.length === 0) return null;
|
|
107
|
-
const diff = diffEdit(before, after, primaryBefore);
|
|
108
|
-
const applied = replicateEdit(before, [{ start: diff.start, end: diff.end }, ...extras], diff.inserted);
|
|
109
|
-
const [caret, ...rest] = applied.carets;
|
|
110
|
-
return { value: applied.value, caret, rest: rest.map((offset) => ({ start: offset, end: offset })) };
|
|
111
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The editor's reset stylesheet.
|
|
8
|
-
*
|
|
9
|
-
* The rule is what makes the editor work at all: the `<textarea>` must paint the
|
|
10
|
-
* caret and the selection while its glyphs stay invisible. A regression here is
|
|
11
|
-
* silent in review (it is a string) and immediately visible in the app, so it is
|
|
12
|
-
* asserted directly — the two selectors and the one deliberate exception.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { describe, expect, test } from 'bun:test';
|
|
16
|
-
|
|
17
|
-
import { CODE_EDITOR_RESET_CSS } from '@/shared/lib/code/editor/reset-css';
|
|
18
|
-
|
|
19
|
-
describe('CODE_EDITOR_RESET_CSS', () => {
|
|
20
|
-
test('hides the textarea’s own glyphs', () => {
|
|
21
|
-
expect(CODE_EDITOR_RESET_CSS).toContain('-webkit-text-fill-color: transparent');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test('keeps the placeholder visible, since no <pre> sits behind it', () => {
|
|
25
|
-
// Without this exception an empty editor's hint inherits the transparent
|
|
26
|
-
// fill and the field looks blank rather than empty.
|
|
27
|
-
expect(CODE_EDITOR_RESET_CSS).toContain('.code-editor-native-textarea::placeholder');
|
|
28
|
-
expect(CODE_EDITOR_RESET_CSS).toContain('var(--theme-ink)');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test('is scoped to the editor’s own class, not to every textarea', () => {
|
|
32
|
-
expect(CODE_EDITOR_RESET_CSS).not.toMatch(/^\s*textarea\s*\{/m);
|
|
33
|
-
expect(CODE_EDITOR_RESET_CSS).toContain('.code-editor-native-textarea {');
|
|
34
|
-
});
|
|
35
|
-
});
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The stylesheet the editor's two layers depend on.
|
|
8
|
-
*
|
|
9
|
-
* `CODE_EDITOR_RESET_CSS` is a rule, not a component: the transparent
|
|
10
|
-
* `<textarea>` paints the caret and the selection while its glyphs must stay
|
|
11
|
-
* invisible (the highlighted `<pre>` underneath supplies them), and the
|
|
12
|
-
* placeholder is the one thing that must remain visible. Keeping the rule in a
|
|
13
|
-
* `.ts` module rather than beside a JSX `<style>` element means a test can
|
|
14
|
-
* assert it without a DOM — and there is nothing to render but this string.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* The reset rule, scoped to the textarea's own class.
|
|
19
|
-
*
|
|
20
|
-
* `::placeholder` is deliberately excluded: an empty editor's hint has no
|
|
21
|
-
* `<pre>` behind it, so it keeps the theme's ink and a readable opacity
|
|
22
|
-
* instead of inheriting the transparent fill.
|
|
23
|
-
*/
|
|
24
|
-
export const CODE_EDITOR_RESET_CSS = `
|
|
25
|
-
.code-editor-native-textarea {
|
|
26
|
-
-webkit-text-fill-color: transparent;
|
|
27
|
-
}
|
|
28
|
-
.code-editor-native-textarea::placeholder {
|
|
29
|
-
-webkit-text-fill-color: var(--theme-ink);
|
|
30
|
-
opacity: 0.45;
|
|
31
|
-
}
|
|
32
|
-
`;
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The text-command dispatch.
|
|
8
|
-
*
|
|
9
|
-
* `runTextCommand` is the single entry point both the keyboard and the command
|
|
10
|
-
* palette go through, so what it asserts is the ROUTING: which commands it
|
|
11
|
-
* claims, which it leaves to the component (Escape, undo, Tab) and which it
|
|
12
|
-
* hands back to the panel. A command that returns `false` silently does nothing
|
|
13
|
-
* when a chord reaches it, which is why the membership table is asserted
|
|
14
|
-
* directly rather than only through the commands themselves.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { describe, expect, test } from 'bun:test';
|
|
18
|
-
|
|
19
|
-
import { isTextCommand, runTextCommand, type CommandContext } from '@/shared/lib/code/editor/text-commands';
|
|
20
|
-
import type { EditorCommand } from '@/shared/lib/code/editor/keymap';
|
|
21
|
-
|
|
22
|
-
interface Harness {
|
|
23
|
-
ctx: CommandContext;
|
|
24
|
-
committed: Array<{ value: string; selectionStart: number; selectionEnd: number }>;
|
|
25
|
-
carets: number[];
|
|
26
|
-
selections: Array<[number, number]>;
|
|
27
|
-
edges: string[];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function harness(options: { value: string; start: number; end?: number; extend?: boolean; language?: string }): Harness {
|
|
31
|
-
const committed: Harness['committed'] = [];
|
|
32
|
-
const carets: number[] = [];
|
|
33
|
-
const selections: Array<[number, number]> = [];
|
|
34
|
-
const edges: string[] = [];
|
|
35
|
-
return {
|
|
36
|
-
committed,
|
|
37
|
-
carets,
|
|
38
|
-
selections,
|
|
39
|
-
edges,
|
|
40
|
-
ctx: {
|
|
41
|
-
value: options.value,
|
|
42
|
-
selectionStart: options.start,
|
|
43
|
-
selectionEnd: options.end ?? options.start,
|
|
44
|
-
extend: options.extend ?? false,
|
|
45
|
-
language: options.language ?? 'typescript',
|
|
46
|
-
commit: (edit) => committed.push(edit),
|
|
47
|
-
setCaret: (offset) => carets.push(offset),
|
|
48
|
-
selectRange: (start, end) => selections.push([start, end]),
|
|
49
|
-
setDocumentEdge: (edge) => edges.push(edge),
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
describe('isTextCommand', () => {
|
|
55
|
-
test('claims the line and caret commands', () => {
|
|
56
|
-
for (const command of ['moveLineUp', 'copyLineDown', 'deleteLine', 'toggleComment', 'indent', 'lineStart'] as const) {
|
|
57
|
-
expect(isTextCommand(command)).toBe(true);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test('leaves the DOM-owned and panel-owned commands alone', () => {
|
|
62
|
-
// Undo/redo step the history, Escape is about focus, and the find-bar chords
|
|
63
|
-
// belong to the panel — none of them is buffer arithmetic.
|
|
64
|
-
for (const command of ['undo', 'redo', 'escape', 'find', 'selectNextOccurrence', 'toggleWordWrap'] as const) {
|
|
65
|
-
expect(isTextCommand(command)).toBe(false);
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
describe('runTextCommand', () => {
|
|
71
|
-
test('reports an unowned command rather than pretending to run it', () => {
|
|
72
|
-
const h = harness({ value: 'one', start: 0 });
|
|
73
|
-
expect(runTextCommand('find', h.ctx)).toBe(false);
|
|
74
|
-
expect(h.committed).toEqual([]);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('moves a line through the commit path, not by writing the field', () => {
|
|
78
|
-
const h = harness({ value: 'one\ntwo', start: 4, end: 7 });
|
|
79
|
-
expect(runTextCommand('moveLineUp', h.ctx)).toBe(true);
|
|
80
|
-
|
|
81
|
-
expect(h.committed[0].value).toBe('two\none');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test('indents the caret’s whole line, not the text at the caret', () => {
|
|
85
|
-
// ⌘] is a line command: on a bare caret it widens to that line first, which
|
|
86
|
-
// is what keeps it from inserting spaces mid-line.
|
|
87
|
-
const h = harness({ value: 'const a = 1;', start: 10 });
|
|
88
|
-
runTextCommand('indent', h.ctx);
|
|
89
|
-
|
|
90
|
-
expect(h.committed[0].value).toBe(' const a = 1;');
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test('indents every line of a selection', () => {
|
|
94
|
-
const h = harness({ value: 'one\ntwo', start: 0, end: 7 });
|
|
95
|
-
runTextCommand('indent', h.ctx);
|
|
96
|
-
|
|
97
|
-
expect(h.committed[0].value).toBe(' one\n two');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test('outdenting a line that has no indent commits nothing', () => {
|
|
101
|
-
const h = harness({ value: 'one', start: 1 });
|
|
102
|
-
runTextCommand('outdent', h.ctx);
|
|
103
|
-
|
|
104
|
-
expect(h.committed).toEqual([]);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('comments with the language’s own token', () => {
|
|
108
|
-
const h = harness({ value: 'key: value', start: 0, language: 'yaml' });
|
|
109
|
-
runTextCommand('toggleComment', h.ctx);
|
|
110
|
-
|
|
111
|
-
expect(h.committed[0].value).toBe('# key: value');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test('caret moves collapse the selection', () => {
|
|
115
|
-
const h = harness({ value: 'one\ntwo', start: 5 });
|
|
116
|
-
runTextCommand('lineStart', h.ctx);
|
|
117
|
-
|
|
118
|
-
expect(h.carets).toEqual([4]);
|
|
119
|
-
expect(h.selections).toEqual([]);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test('shifted caret moves extend instead', () => {
|
|
123
|
-
const h = harness({ value: 'one\ntwo', start: 5, extend: true });
|
|
124
|
-
runTextCommand('lineEnd', h.ctx);
|
|
125
|
-
|
|
126
|
-
expect(h.selections).toEqual([[5, 7]]);
|
|
127
|
-
expect(h.carets).toEqual([]);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test('file edges go through the document-edge path', () => {
|
|
131
|
-
const h = harness({ value: 'one\ntwo', start: 5 });
|
|
132
|
-
runTextCommand('documentStart', h.ctx);
|
|
133
|
-
runTextCommand('documentEnd', h.ctx);
|
|
134
|
-
|
|
135
|
-
expect(h.edges).toEqual(['start', 'end']);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test('a shifted document move selects instead of jumping', () => {
|
|
139
|
-
const h = harness({ value: 'one\ntwo', start: 5, extend: true });
|
|
140
|
-
runTextCommand('documentStart', h.ctx);
|
|
141
|
-
|
|
142
|
-
expect(h.selections).toEqual([[0, 5]]);
|
|
143
|
-
expect(h.edges).toEqual([]);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test('the palette reaches the same commands with no event at all', () => {
|
|
147
|
-
// This is the path the command palette uses; a command reachable by chord
|
|
148
|
-
// must be reachable this way too.
|
|
149
|
-
const h = harness({ value: 'one\ntwo', start: 0, end: 3 });
|
|
150
|
-
runTextCommand('copyLineDown', h.ctx);
|
|
151
|
-
|
|
152
|
-
expect(h.committed[0].value).toBe('one\none\ntwo');
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
describe('command coverage', () => {
|
|
157
|
-
test('every text command is one the editor keymap can produce', () => {
|
|
158
|
-
// A command in the table that no key or palette entry names would be dead
|
|
159
|
-
// code; this is the check that keeps the table honest.
|
|
160
|
-
const reachable: EditorCommand[] = [
|
|
161
|
-
'moveLineUp',
|
|
162
|
-
'moveLineDown',
|
|
163
|
-
'copyLineUp',
|
|
164
|
-
'copyLineDown',
|
|
165
|
-
'deleteLine',
|
|
166
|
-
'toggleComment',
|
|
167
|
-
'insertLineAbove',
|
|
168
|
-
'insertLineBelow',
|
|
169
|
-
'indent',
|
|
170
|
-
'outdent',
|
|
171
|
-
'lineStart',
|
|
172
|
-
'lineEnd',
|
|
173
|
-
'documentStart',
|
|
174
|
-
'documentEnd',
|
|
175
|
-
];
|
|
176
|
-
for (const command of reachable) expect(isTextCommand(command)).toBe(true);
|
|
177
|
-
});
|
|
178
|
-
});
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The editor's text commands, as a dispatch table.
|
|
8
|
-
*
|
|
9
|
-
* Extracted from the component because it is dispatch rather than rendering,
|
|
10
|
-
* and because it keeps the component under the repository's line ceiling. The
|
|
11
|
-
* component still owns the keys whose behaviour is about the DOM node rather
|
|
12
|
-
* than the buffer — Escape, Tab, Backspace, Enter, undo/redo — and calls in
|
|
13
|
-
* here for everything that is pure string arithmetic.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
commentSyntaxFor,
|
|
18
|
-
deleteLines,
|
|
19
|
-
duplicateLines,
|
|
20
|
-
insertLine,
|
|
21
|
-
lineEdges,
|
|
22
|
-
moveLines,
|
|
23
|
-
toggleLineComment,
|
|
24
|
-
type TextRange,
|
|
25
|
-
} from '@/shared/lib/code/editor/commands';
|
|
26
|
-
import { editForTab } from '@/shared/lib/code/editor/edits';
|
|
27
|
-
import { lineRangeAt } from '@/shared/lib/code/editor/lines';
|
|
28
|
-
import type { EditorCommand } from '@/shared/lib/code/editor/keymap';
|
|
29
|
-
|
|
30
|
-
/** A whole-buffer edit plus where the caret lands. */
|
|
31
|
-
export interface CommittedEdit {
|
|
32
|
-
value: string;
|
|
33
|
-
selectionStart: number;
|
|
34
|
-
selectionEnd: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface CommandContext {
|
|
38
|
-
/** Current buffer and selection, read from the live textarea. */
|
|
39
|
-
value: string;
|
|
40
|
-
selectionStart: number;
|
|
41
|
-
selectionEnd: number;
|
|
42
|
-
/** True while the event carried Shift (⌘↑ with Shift extends instead of moving). */
|
|
43
|
-
extend: boolean;
|
|
44
|
-
/** Shiki language id, which picks the comment syntax. */
|
|
45
|
-
language: string;
|
|
46
|
-
/** Commit a whole-buffer edit as ONE undoable step. */
|
|
47
|
-
commit: (edit: CommittedEdit) => void;
|
|
48
|
-
/** Move the caret, collapsing any selection. */
|
|
49
|
-
setCaret: (offset: number) => void;
|
|
50
|
-
/** Move the caret while keeping the selection's other end anchored. */
|
|
51
|
-
selectRange: (start: number, end: number) => void;
|
|
52
|
-
/** Jump to the buffer's first or last offset. */
|
|
53
|
-
setDocumentEdge: (edge: 'start' | 'end') => void;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Commands that edit or move within the buffer. Everything else (the find bar's
|
|
58
|
-
* chords, word wrap, save) belongs to the panel and is delegated by the caller.
|
|
59
|
-
*/
|
|
60
|
-
const TEXT_COMMANDS: Record<string, true> = {
|
|
61
|
-
moveLineUp: true,
|
|
62
|
-
moveLineDown: true,
|
|
63
|
-
copyLineUp: true,
|
|
64
|
-
copyLineDown: true,
|
|
65
|
-
deleteLine: true,
|
|
66
|
-
toggleComment: true,
|
|
67
|
-
insertLineAbove: true,
|
|
68
|
-
insertLineBelow: true,
|
|
69
|
-
indent: true,
|
|
70
|
-
outdent: true,
|
|
71
|
-
lineStart: true,
|
|
72
|
-
lineEnd: true,
|
|
73
|
-
documentStart: true,
|
|
74
|
-
documentEnd: true,
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export function isTextCommand(command: EditorCommand): boolean {
|
|
78
|
-
return TEXT_COMMANDS[command] === true;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Run one text command. Returns true when the key was consumed.
|
|
83
|
-
*
|
|
84
|
-
* The buffer-changing branches all commit through `ctx.commit`, which is the
|
|
85
|
-
* component's single write path: one undo entry per command, and the highlight
|
|
86
|
-
* layer follows because the commit goes through the controlled value.
|
|
87
|
-
*/
|
|
88
|
-
export function runTextCommand(command: EditorCommand, ctx: CommandContext): boolean {
|
|
89
|
-
const selection: TextRange = { start: ctx.selectionStart, end: ctx.selectionEnd };
|
|
90
|
-
|
|
91
|
-
switch (command) {
|
|
92
|
-
case 'moveLineUp':
|
|
93
|
-
ctx.commit(moveLines(ctx.value, selection, -1));
|
|
94
|
-
return true;
|
|
95
|
-
case 'moveLineDown':
|
|
96
|
-
ctx.commit(moveLines(ctx.value, selection, 1));
|
|
97
|
-
return true;
|
|
98
|
-
case 'copyLineUp':
|
|
99
|
-
ctx.commit(duplicateLines(ctx.value, selection, -1));
|
|
100
|
-
return true;
|
|
101
|
-
case 'copyLineDown':
|
|
102
|
-
ctx.commit(duplicateLines(ctx.value, selection, 1));
|
|
103
|
-
return true;
|
|
104
|
-
case 'deleteLine':
|
|
105
|
-
ctx.commit(deleteLines(ctx.value, selection));
|
|
106
|
-
return true;
|
|
107
|
-
case 'toggleComment':
|
|
108
|
-
ctx.commit(toggleLineComment(ctx.value, selection, commentSyntaxFor(ctx.language)));
|
|
109
|
-
return true;
|
|
110
|
-
case 'insertLineAbove':
|
|
111
|
-
ctx.commit(insertLine(ctx.value, ctx.selectionStart, 'above'));
|
|
112
|
-
return true;
|
|
113
|
-
case 'insertLineBelow':
|
|
114
|
-
ctx.commit(insertLine(ctx.value, ctx.selectionStart, 'below'));
|
|
115
|
-
return true;
|
|
116
|
-
|
|
117
|
-
case 'indent':
|
|
118
|
-
case 'outdent': {
|
|
119
|
-
// ⌘] / ⌘[ act on the LINES the selection touches — on a bare caret that
|
|
120
|
-
// is the caret's own line, which is why the range is widened first. Tab
|
|
121
|
-
// itself still indents at the caret (that is what Tab means in an
|
|
122
|
-
// editor), but a line command that shifted text mid-line would not be
|
|
123
|
-
// indenting anything.
|
|
124
|
-
const range = ctx.selectionStart === ctx.selectionEnd ? lineRangeAt(ctx.value, ctx.selectionStart) : selection;
|
|
125
|
-
const edit = editForTab(ctx.value, range.start, range.end, command === 'outdent');
|
|
126
|
-
if (edit.value !== ctx.value) ctx.commit(edit);
|
|
127
|
-
return true;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
case 'lineStart': {
|
|
131
|
-
const edges = lineEdges(ctx.value, ctx.selectionStart);
|
|
132
|
-
if (ctx.extend) ctx.selectRange(edges.start, ctx.selectionEnd);
|
|
133
|
-
else ctx.setCaret(edges.start);
|
|
134
|
-
return true;
|
|
135
|
-
}
|
|
136
|
-
case 'lineEnd': {
|
|
137
|
-
const edges = lineEdges(ctx.value, ctx.selectionStart);
|
|
138
|
-
if (ctx.extend) ctx.selectRange(ctx.selectionStart, edges.end);
|
|
139
|
-
else ctx.setCaret(edges.end);
|
|
140
|
-
return true;
|
|
141
|
-
}
|
|
142
|
-
case 'documentStart':
|
|
143
|
-
if (ctx.extend) ctx.selectRange(0, ctx.selectionEnd);
|
|
144
|
-
else ctx.setDocumentEdge('start');
|
|
145
|
-
return true;
|
|
146
|
-
case 'documentEnd':
|
|
147
|
-
if (ctx.extend) ctx.selectRange(ctx.selectionStart, ctx.value.length);
|
|
148
|
-
else ctx.setDocumentEdge('end');
|
|
149
|
-
return true;
|
|
150
|
-
|
|
151
|
-
default:
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
154
|
-
}
|