@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,216 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The editor's undo stack.
|
|
8
|
-
*
|
|
9
|
-
* The rule under test is the one that was wrong: the re-seed exists for a value
|
|
10
|
-
* replaced from the OUTSIDE (a file switch, a refresh), but it also fired for
|
|
11
|
-
* the component's own edits — every keystroke pushed a second, identical entry,
|
|
12
|
-
* so the first ⌘Z restored the buffer to the state it was already in and undo
|
|
13
|
-
* looked broken until the second press. `markApplied` is what separates the two,
|
|
14
|
-
* and these assertions are what keeps it separating them.
|
|
15
|
-
*
|
|
16
|
-
* Runs against a real DOM (happy-dom) because it is a Preact hook holding a
|
|
17
|
-
* `<textarea>`; the modules below are imported dynamically, after those globals
|
|
18
|
-
* exist (Preact binds its environment at evaluation time).
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
|
|
22
|
-
import { Window } from 'happy-dom';
|
|
23
|
-
import type { h as PreactH, render as PreactRender } from 'preact';
|
|
24
|
-
import type { act as PreactAct } from 'preact/test-utils';
|
|
25
|
-
import type { EditorHistory, useEditorHistory as UseEditorHistoryHook } from '@/client/hooks/editor/use-editor-history';
|
|
26
|
-
|
|
27
|
-
let useEditorHistory: typeof UseEditorHistoryHook;
|
|
28
|
-
let h: typeof PreactH;
|
|
29
|
-
let render: typeof PreactRender;
|
|
30
|
-
let act: typeof PreactAct;
|
|
31
|
-
|
|
32
|
-
let api: EditorHistory | null = null;
|
|
33
|
-
let container: HTMLElement;
|
|
34
|
-
let textarea: HTMLTextAreaElement;
|
|
35
|
-
|
|
36
|
-
/** The buffer the hook is told is on screen, changed from the outside by a test. */
|
|
37
|
-
let externalValue = 'one';
|
|
38
|
-
|
|
39
|
-
function Harness() {
|
|
40
|
-
api = useEditorHistory({
|
|
41
|
-
value: externalValue,
|
|
42
|
-
inputRef: { current: textarea },
|
|
43
|
-
onValueChange: (next) => {
|
|
44
|
-
textarea.value = next;
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Flush the effect that watches `value`, with no timers: it resolves on a render. */
|
|
51
|
-
async function flush() {
|
|
52
|
-
for (let i = 0; i < 3; i++) {
|
|
53
|
-
await act(async () => {
|
|
54
|
-
for (let turn = 0; turn < 4; turn++) await Promise.resolve();
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function domGlobals(win: Window): Record<string, unknown> {
|
|
60
|
-
return {
|
|
61
|
-
window: win,
|
|
62
|
-
document: win.document,
|
|
63
|
-
navigator: win.navigator,
|
|
64
|
-
Node: win.Node,
|
|
65
|
-
Element: win.Element,
|
|
66
|
-
HTMLElement: win.HTMLElement,
|
|
67
|
-
HTMLTextAreaElement: win.HTMLTextAreaElement,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const installed: Record<string, unknown> = {};
|
|
72
|
-
const displaced: Record<string, unknown> = {};
|
|
73
|
-
|
|
74
|
-
beforeAll(async () => {
|
|
75
|
-
const win = new Window({ url: 'http://localhost' });
|
|
76
|
-
for (const [key, value] of Object.entries(domGlobals(win))) {
|
|
77
|
-
displaced[key] = (globalThis as Record<string, unknown>)[key];
|
|
78
|
-
installed[key] = value;
|
|
79
|
-
(globalThis as Record<string, unknown>)[key] = value;
|
|
80
|
-
}
|
|
81
|
-
({ useEditorHistory } = await import('@/client/hooks/editor/use-editor-history'));
|
|
82
|
-
({ h, render } = await import('preact'));
|
|
83
|
-
({ act } = await import('preact/test-utils'));
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
afterAll(() => {
|
|
87
|
-
for (const key of Object.keys(installed)) {
|
|
88
|
-
if (displaced[key] === undefined) delete (globalThis as Record<string, unknown>)[key];
|
|
89
|
-
else (globalThis as Record<string, unknown>)[key] = displaced[key];
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
beforeEach(async () => {
|
|
94
|
-
api = null;
|
|
95
|
-
externalValue = 'one';
|
|
96
|
-
container = document.createElement('div');
|
|
97
|
-
document.body.appendChild(container);
|
|
98
|
-
textarea = document.createElement('textarea');
|
|
99
|
-
textarea.value = 'one';
|
|
100
|
-
container.appendChild(textarea);
|
|
101
|
-
await act(async () => {
|
|
102
|
-
render(h(Harness, {}), container);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
afterEach(() => {
|
|
107
|
-
if (container) render(null, container);
|
|
108
|
-
container?.remove();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe('useEditorHistory', () => {
|
|
112
|
-
test('commit writes the buffer, the caret and the caller’s state', async () => {
|
|
113
|
-
await act(async () => {
|
|
114
|
-
api?.commit({ value: 'two', selectionStart: 2, selectionEnd: 2 });
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
expect(textarea.value).toBe('two');
|
|
118
|
-
expect(textarea.selectionStart).toBe(2);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test('an external value replacement is recorded, so undo reaches the old file', async () => {
|
|
122
|
-
// A file switch: the hook did not write this, so it becomes a history entry
|
|
123
|
-
// rather than silently replacing the stack.
|
|
124
|
-
externalValue = 'switched';
|
|
125
|
-
await act(async () => {
|
|
126
|
-
render(h(Harness, {}), container);
|
|
127
|
-
});
|
|
128
|
-
await flush();
|
|
129
|
-
|
|
130
|
-
await act(async () => {
|
|
131
|
-
api?.step('undo');
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
expect(textarea.value).toBe('one');
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test('a value the hook itself wrote is NOT re-recorded', async () => {
|
|
138
|
-
// The bug this guards: an extra entry per keystroke made the first ⌘Z a
|
|
139
|
-
// no-op, because it restored the buffer to what was already on screen.
|
|
140
|
-
await act(async () => {
|
|
141
|
-
api?.commit({ value: 'two', selectionStart: 2, selectionEnd: 2 });
|
|
142
|
-
});
|
|
143
|
-
externalValue = 'two';
|
|
144
|
-
await act(async () => {
|
|
145
|
-
render(h(Harness, {}), container);
|
|
146
|
-
});
|
|
147
|
-
await flush();
|
|
148
|
-
|
|
149
|
-
await act(async () => {
|
|
150
|
-
api?.step('undo');
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
// One press reaches the ORIGINAL buffer; a duplicate entry would have
|
|
154
|
-
// restored 'two' again.
|
|
155
|
-
expect(textarea.value).toBe('one');
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test('markApplied declares a browser edit as already recorded', async () => {
|
|
159
|
-
await act(async () => {
|
|
160
|
-
api?.markApplied('typed');
|
|
161
|
-
});
|
|
162
|
-
externalValue = 'typed';
|
|
163
|
-
await act(async () => {
|
|
164
|
-
render(h(Harness, {}), container);
|
|
165
|
-
});
|
|
166
|
-
await flush();
|
|
167
|
-
|
|
168
|
-
await act(async () => {
|
|
169
|
-
api?.step('undo');
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// Nothing was pushed for `typed`, so there is nothing to step back to: the
|
|
173
|
-
// stack still holds only the initial record.
|
|
174
|
-
expect(textarea.value).toBe('one');
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test('step at the ends of the stack is a no-op', async () => {
|
|
178
|
-
await act(async () => {
|
|
179
|
-
api?.step('undo');
|
|
180
|
-
});
|
|
181
|
-
const afterUndo = textarea.value;
|
|
182
|
-
await act(async () => {
|
|
183
|
-
api?.step('redo');
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
expect(afterUndo).toBe('one');
|
|
187
|
-
expect(textarea.value).toBe('one');
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
test('commitEdit supersedes the top entry, keeping its caret for the undo', async () => {
|
|
191
|
-
// A line command replaces a whole range at once; undo must return the caret
|
|
192
|
-
// to the selection the command replaced, not to where the edit ended.
|
|
193
|
-
textarea.selectionStart = 1;
|
|
194
|
-
textarea.selectionEnd = 3;
|
|
195
|
-
await act(async () => {
|
|
196
|
-
api?.commitEdit({ value: 'one-changed', selectionStart: 11, selectionEnd: 11 });
|
|
197
|
-
});
|
|
198
|
-
await act(async () => {
|
|
199
|
-
api?.step('undo');
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
expect(textarea.value).toBe('one');
|
|
203
|
-
expect(textarea.selectionStart).toBe(1);
|
|
204
|
-
expect(textarea.selectionEnd).toBe(3);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
test('the exposed buffer is never a render behind', async () => {
|
|
208
|
-
// `handleChange` diffs against this; a value that lagged a keystroke read a
|
|
209
|
-
// one-character edit as a whole-word replacement.
|
|
210
|
-
await act(async () => {
|
|
211
|
-
api?.commit({ value: 'fresh', selectionStart: 5, selectionEnd: 5 });
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
expect(api?.buffer).toBe('fresh');
|
|
215
|
-
});
|
|
216
|
-
});
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The editor's undo stack, wired to the DOM node.
|
|
8
|
-
*
|
|
9
|
-
* `history.ts` is the pure bookkeeping (push, merge, step, cap); this is the
|
|
10
|
-
* part that has to touch the `<textarea>`: committing an edit as one entry,
|
|
11
|
-
* restoring a stepped record, and re-seeding the stack when the value is
|
|
12
|
-
* replaced from the OUTSIDE.
|
|
13
|
-
*
|
|
14
|
-
* The re-seed is the subtle one. It exists for a file switch or a refresh, but
|
|
15
|
-
* a naive `[value]` effect also fires for the component's own edits — every
|
|
16
|
-
* keystroke then pushed a second, identical entry, so the first ⌘Z restored the
|
|
17
|
-
* buffer to the state it was already in and undo looked broken until the second
|
|
18
|
-
* press (and the no-op `onValueChange` truncated the redo stack). The `applied`
|
|
19
|
-
* ref is what distinguishes "someone else replaced the value" from "we did".
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { useEffect, useRef } from 'preact/hooks';
|
|
23
|
-
|
|
24
|
-
import { createHistory, pushEdit, pushRecord, stepHistory, type History, type HistoryRecord } from '@/shared/lib/code/editor/history';
|
|
25
|
-
|
|
26
|
-
/** The fields every write path needs: a buffer and where the caret goes. */
|
|
27
|
-
export type HistoryEdit = Pick<HistoryRecord, 'value' | 'selectionStart' | 'selectionEnd'>;
|
|
28
|
-
|
|
29
|
-
export interface EditorHistory {
|
|
30
|
-
/** The stack itself, for the typing-burst merge in the change handler. */
|
|
31
|
-
history: History;
|
|
32
|
-
/**
|
|
33
|
-
* The buffer as this component last knew it — its own writes included, and
|
|
34
|
-
* never a render behind. Diffing a browser edit has to start from here: the
|
|
35
|
-
* `value` prop lags a keystroke, and a stale `before` reads a one-character
|
|
36
|
-
* edit as a whole-word replacement.
|
|
37
|
-
*/
|
|
38
|
-
readonly buffer: string;
|
|
39
|
-
/** Commit an edit as ONE undoable entry. */
|
|
40
|
-
commit: (edit: HistoryEdit) => void;
|
|
41
|
-
/**
|
|
42
|
-
* Commit an edit that SUPERSEDES the entry on top, keeping that entry's
|
|
43
|
-
* caret so undo returns to the selection the edit replaced.
|
|
44
|
-
*/
|
|
45
|
-
commitEdit: (edit: HistoryEdit) => void;
|
|
46
|
-
/** Step the stack and restore the record; no-op at either end. */
|
|
47
|
-
step: (direction: 'undo' | 'redo') => void;
|
|
48
|
-
/**
|
|
49
|
-
* Declare a value the BROWSER produced as already applied, so the re-seed
|
|
50
|
-
* effect below does not record it a second time. The change handler calls
|
|
51
|
-
* this for every keystroke and every replicated multi-cursor edit.
|
|
52
|
-
*/
|
|
53
|
-
markApplied: (value: string) => void;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface UseEditorHistoryOptions {
|
|
57
|
-
/** The controlled buffer, used to detect an external replacement. */
|
|
58
|
-
value: string;
|
|
59
|
-
/** The live `<textarea>`, so an external replacement records its own caret. */
|
|
60
|
-
inputRef: { current: HTMLTextAreaElement | null };
|
|
61
|
-
/** Push the committed buffer to the caller. */
|
|
62
|
-
onValueChange: (value: string) => void;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function useEditorHistory({ value, inputRef, onValueChange }: UseEditorHistoryOptions): EditorHistory {
|
|
66
|
-
const historyRef = useRef<History>(createHistory());
|
|
67
|
-
/** The value this component itself last wrote — see the module note. */
|
|
68
|
-
const appliedRef = useRef<string | null>(null);
|
|
69
|
-
|
|
70
|
-
// Re-record when the controlled value is replaced from the outside
|
|
71
|
-
// (file switch, refresh) so undo does not jump between documents.
|
|
72
|
-
useEffect(() => {
|
|
73
|
-
const input = inputRef.current;
|
|
74
|
-
if (!input || appliedRef.current === value) return;
|
|
75
|
-
appliedRef.current = value;
|
|
76
|
-
pushRecord(historyRef.current, {
|
|
77
|
-
value: input.value,
|
|
78
|
-
selectionStart: input.selectionStart,
|
|
79
|
-
selectionEnd: input.selectionEnd,
|
|
80
|
-
timestamp: Date.now(),
|
|
81
|
-
});
|
|
82
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
83
|
-
}, [value]);
|
|
84
|
-
|
|
85
|
-
/** Put a buffer on screen without touching the stack. */
|
|
86
|
-
const write = (edit: HistoryEdit) => {
|
|
87
|
-
appliedRef.current = edit.value;
|
|
88
|
-
onValueChange(edit.value);
|
|
89
|
-
const input = inputRef.current;
|
|
90
|
-
if (!input) return;
|
|
91
|
-
input.value = edit.value;
|
|
92
|
-
input.selectionStart = edit.selectionStart;
|
|
93
|
-
input.selectionEnd = edit.selectionEnd;
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
history: historyRef.current,
|
|
98
|
-
|
|
99
|
-
get buffer() {
|
|
100
|
-
return appliedRef.current ?? value;
|
|
101
|
-
},
|
|
102
|
-
|
|
103
|
-
commit: (edit) => {
|
|
104
|
-
pushEdit(historyRef.current, { ...edit, timestamp: Date.now() });
|
|
105
|
-
write(edit);
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
commitEdit: (edit) => {
|
|
109
|
-
const history = historyRef.current;
|
|
110
|
-
const last = history.stack[history.offset];
|
|
111
|
-
const input = inputRef.current;
|
|
112
|
-
if (last && input) {
|
|
113
|
-
// The entry on top is about to be superseded: keep where its caret was,
|
|
114
|
-
// so undo returns to the selection this edit replaced.
|
|
115
|
-
history.stack[history.offset] = {
|
|
116
|
-
...last,
|
|
117
|
-
selectionStart: input.selectionStart,
|
|
118
|
-
selectionEnd: input.selectionEnd,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
pushEdit(history, { ...edit, timestamp: Date.now() });
|
|
122
|
-
write(edit);
|
|
123
|
-
},
|
|
124
|
-
|
|
125
|
-
step: (direction) => {
|
|
126
|
-
const record = stepHistory(historyRef.current, direction);
|
|
127
|
-
if (record) write(record);
|
|
128
|
-
},
|
|
129
|
-
|
|
130
|
-
markApplied: (next) => {
|
|
131
|
-
appliedRef.current = next;
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
}
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* `useOccurrences` — the ranges behind ⌘D, ⌘⇧L and ⌘⇧D.
|
|
8
|
-
*
|
|
9
|
-
* The rules live in `shared/lib/code/editor/occurrences` and are tested there;
|
|
10
|
-
* what this file covers is the hook's own contract: the caller's copy wins
|
|
11
|
-
* (clearing the ranges in the panel must clear them here), every change is
|
|
12
|
-
* announced so the surface repaints, and the textarea is read rather than
|
|
13
|
-
* assumed.
|
|
14
|
-
*
|
|
15
|
-
* Runs against a real DOM (happy-dom) because it is a Preact hook; the modules
|
|
16
|
-
* below are imported dynamically, after those globals exist.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
|
|
20
|
-
import { Window } from 'happy-dom';
|
|
21
|
-
import type { h as PreactH, render as PreactRender } from 'preact';
|
|
22
|
-
import type { act as PreactAct } from 'preact/test-utils';
|
|
23
|
-
import type { OccurrencesApi, useOccurrences as UseOccurrencesHook } from '@/client/hooks/editor/use-occurrences';
|
|
24
|
-
import type { TextRange } from '@/shared/lib/code/editor/commands';
|
|
25
|
-
|
|
26
|
-
let useOccurrences: typeof UseOccurrencesHook;
|
|
27
|
-
let h: typeof PreactH;
|
|
28
|
-
let render: typeof PreactRender;
|
|
29
|
-
let act: typeof PreactAct;
|
|
30
|
-
|
|
31
|
-
let api: OccurrencesApi | null = null;
|
|
32
|
-
let container: HTMLElement;
|
|
33
|
-
let textarea: HTMLTextAreaElement;
|
|
34
|
-
/** What the caller passes in as its own copy, changed per test. */
|
|
35
|
-
let controlled: TextRange[] | undefined;
|
|
36
|
-
const announced: Array<readonly TextRange[]> = [];
|
|
37
|
-
const selections: Array<[number, number]> = [];
|
|
38
|
-
|
|
39
|
-
function Harness() {
|
|
40
|
-
api = useOccurrences({
|
|
41
|
-
controlled,
|
|
42
|
-
onChange: (ranges) => announced.push(ranges),
|
|
43
|
-
});
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function flush() {
|
|
48
|
-
for (let i = 0; i < 3; i++) {
|
|
49
|
-
await act(async () => {
|
|
50
|
-
for (let turn = 0; turn < 4; turn++) await Promise.resolve();
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function rerender() {
|
|
56
|
-
await act(async () => {
|
|
57
|
-
render(h(Harness, {}), container);
|
|
58
|
-
});
|
|
59
|
-
await flush();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function select(start: number, end = start) {
|
|
63
|
-
textarea.selectionStart = start;
|
|
64
|
-
textarea.selectionEnd = end;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Stands in for the editor's own `select`: it MOVES the textarea's selection and
|
|
69
|
-
* records it. The hook reads the textarea, so a callback that only recorded the
|
|
70
|
-
* range would leave every later press looking at the original caret.
|
|
71
|
-
*/
|
|
72
|
-
function selectingSurface() {
|
|
73
|
-
return (start: number, end: number) => {
|
|
74
|
-
selections.push([start, end]);
|
|
75
|
-
select(start, end);
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function domGlobals(win: Window): Record<string, unknown> {
|
|
80
|
-
return {
|
|
81
|
-
window: win,
|
|
82
|
-
document: win.document,
|
|
83
|
-
navigator: win.navigator,
|
|
84
|
-
Node: win.Node,
|
|
85
|
-
Element: win.Element,
|
|
86
|
-
HTMLElement: win.HTMLElement,
|
|
87
|
-
HTMLTextAreaElement: win.HTMLTextAreaElement,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const installed: Record<string, unknown> = {};
|
|
92
|
-
const displaced: Record<string, unknown> = {};
|
|
93
|
-
|
|
94
|
-
beforeAll(async () => {
|
|
95
|
-
const win = new Window({ url: 'http://localhost' });
|
|
96
|
-
for (const [key, value] of Object.entries(domGlobals(win))) {
|
|
97
|
-
displaced[key] = (globalThis as Record<string, unknown>)[key];
|
|
98
|
-
installed[key] = value;
|
|
99
|
-
(globalThis as Record<string, unknown>)[key] = value;
|
|
100
|
-
}
|
|
101
|
-
({ useOccurrences } = await import('@/client/hooks/editor/use-occurrences'));
|
|
102
|
-
({ h, render } = await import('preact'));
|
|
103
|
-
({ act } = await import('preact/test-utils'));
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
afterAll(() => {
|
|
107
|
-
for (const key of Object.keys(installed)) {
|
|
108
|
-
if (displaced[key] === undefined) delete (globalThis as Record<string, unknown>)[key];
|
|
109
|
-
else (globalThis as Record<string, unknown>)[key] = displaced[key];
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
beforeEach(async () => {
|
|
114
|
-
api = null;
|
|
115
|
-
controlled = undefined;
|
|
116
|
-
announced.length = 0;
|
|
117
|
-
selections.length = 0;
|
|
118
|
-
container = document.createElement('div');
|
|
119
|
-
document.body.appendChild(container);
|
|
120
|
-
textarea = document.createElement('textarea');
|
|
121
|
-
textarea.value = 'alpha beta alpha gamma alpha';
|
|
122
|
-
container.appendChild(textarea);
|
|
123
|
-
await rerender();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
afterEach(() => {
|
|
127
|
-
if (container) render(null, container);
|
|
128
|
-
container?.remove();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
describe('useOccurrences', () => {
|
|
132
|
-
test('starts with no extra ranges', () => {
|
|
133
|
-
expect(api?.ranges).toEqual([]);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test('⌘D selects the word first, then grows one occurrence per press', async () => {
|
|
137
|
-
select(2);
|
|
138
|
-
await act(async () => {
|
|
139
|
-
api?.selectNext(textarea, selectingSurface());
|
|
140
|
-
});
|
|
141
|
-
expect(selections.at(-1)).toEqual([0, 5]);
|
|
142
|
-
expect(api?.ranges).toEqual([]);
|
|
143
|
-
|
|
144
|
-
await act(async () => {
|
|
145
|
-
api?.selectNext(textarea, selectingSurface());
|
|
146
|
-
});
|
|
147
|
-
expect(api?.ranges).toEqual([{ start: 11, end: 16 }]);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test('⌘⇧L takes every occurrence, primary first', async () => {
|
|
151
|
-
select(0, 5);
|
|
152
|
-
await act(async () => {
|
|
153
|
-
api?.selectAll(textarea, selectingSurface());
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
expect(selections.at(-1)).toEqual([0, 5]);
|
|
157
|
-
expect(api?.ranges).toEqual([
|
|
158
|
-
{ start: 11, end: 16 },
|
|
159
|
-
{ start: 23, end: 28 },
|
|
160
|
-
]);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test('every change is announced so the surface can repaint', async () => {
|
|
164
|
-
select(0, 5);
|
|
165
|
-
await act(async () => {
|
|
166
|
-
api?.selectAll(textarea, () => {});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
expect(announced.at(-1)?.length).toBe(2);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test('clear drops every range and says so', async () => {
|
|
173
|
-
select(0, 5);
|
|
174
|
-
await act(async () => {
|
|
175
|
-
api?.selectAll(textarea, () => {});
|
|
176
|
-
});
|
|
177
|
-
await act(async () => {
|
|
178
|
-
api?.clear();
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
expect(api?.ranges).toEqual([]);
|
|
182
|
-
expect(announced.at(-1)).toEqual([]);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test('the caller’s copy is authoritative', async () => {
|
|
186
|
-
// The panel can clear the ranges without the editor asking (a file switch,
|
|
187
|
-
// a save); the hook has to follow, not keep its own stale list.
|
|
188
|
-
controlled = [{ start: 1, end: 3 }];
|
|
189
|
-
await rerender();
|
|
190
|
-
|
|
191
|
-
expect(api?.ranges).toEqual([{ start: 1, end: 3 }]);
|
|
192
|
-
|
|
193
|
-
controlled = [];
|
|
194
|
-
await rerender();
|
|
195
|
-
|
|
196
|
-
expect(api?.ranges).toEqual([]);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
test('⌘⇧D adds a caret below on the same column', async () => {
|
|
200
|
-
textarea.value = 'one\ntwo\nthree';
|
|
201
|
-
select(1);
|
|
202
|
-
await act(async () => {
|
|
203
|
-
api?.insertCursorBelow(textarea);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
expect(api?.ranges).toEqual([{ start: 5, end: 5 }]);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test('⌘⇧D on the last line adds nothing', async () => {
|
|
210
|
-
textarea.value = 'one\ntwo';
|
|
211
|
-
select(5);
|
|
212
|
-
await act(async () => {
|
|
213
|
-
api?.insertCursorBelow(textarea);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
expect(api?.ranges).toEqual([]);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
test('replicate reports nothing without extra ranges', () => {
|
|
220
|
-
expect(api?.replicate('abc', 'abXc', { start: 2, end: 2 })).toBeNull();
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
test('replicate replays the browser’s edit at every extra range', async () => {
|
|
224
|
-
select(0, 5);
|
|
225
|
-
await act(async () => {
|
|
226
|
-
api?.selectAll(textarea, () => {});
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
// The browser replaced `alpha` at 0..5 with `Z`; the other two follow.
|
|
230
|
-
const applied = api?.replicate('alpha beta alpha gamma alpha', 'Z beta alpha gamma alpha', { start: 0, end: 5 });
|
|
231
|
-
|
|
232
|
-
expect(applied?.value).toBe('Z beta Z gamma Z');
|
|
233
|
-
expect(applied?.caret).toBe(1);
|
|
234
|
-
expect(applied?.rest.length).toBe(2);
|
|
235
|
-
});
|
|
236
|
-
});
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The hook that owns the editor's extra selection ranges.
|
|
8
|
-
*
|
|
9
|
-
* It is a thin shell over `shared/lib/code/editor/occurrences`: the rules live
|
|
10
|
-
* there as pure functions, and this holds the ranges in a ref (the keydown and
|
|
11
|
-
* `input` handlers read them synchronously, inside one event) and tells the
|
|
12
|
-
* caller through `onChange` so the surface can paint them.
|
|
13
|
-
*
|
|
14
|
-
* The split is deliberate: a hook that decides where a range goes cannot be
|
|
15
|
-
* tested without a DOM, and every rule here is one a user notices when it is
|
|
16
|
-
* wrong.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { useRef } from 'preact/hooks';
|
|
20
|
-
|
|
21
|
-
import {
|
|
22
|
-
growToNextOccurrence,
|
|
23
|
-
offsetBelow,
|
|
24
|
-
replicateAtRanges,
|
|
25
|
-
selectAllOccurrences,
|
|
26
|
-
} from '@/shared/lib/code/editor/occurrences';
|
|
27
|
-
import type { TextRange } from '@/shared/lib/code/editor/commands';
|
|
28
|
-
|
|
29
|
-
export interface UseOccurrencesOptions {
|
|
30
|
-
/** Called with the extra ranges whenever they change, so the surface repaints. */
|
|
31
|
-
onChange: (ranges: readonly TextRange[]) => void;
|
|
32
|
-
/** The caller's copy of the ranges — clearing them there must clear them here. */
|
|
33
|
-
controlled?: readonly TextRange[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface OccurrencesApi {
|
|
37
|
-
/** The extra ranges, excluding the textarea's own (primary) selection. */
|
|
38
|
-
readonly ranges: readonly TextRange[];
|
|
39
|
-
/** Replace the ranges and notify the surface. */
|
|
40
|
-
set: (ranges: readonly TextRange[]) => void;
|
|
41
|
-
/** Drop every extra range. */
|
|
42
|
-
clear: () => void;
|
|
43
|
-
/** ⌘D — grow the primary selection to the next occurrence of what it covers. */
|
|
44
|
-
selectNext: (input: HTMLTextAreaElement, select: (start: number, end: number) => void) => void;
|
|
45
|
-
/** ⌘⇧L — select every occurrence of the selection/word at the caret. */
|
|
46
|
-
selectAll: (input: HTMLTextAreaElement, select: (start: number, end: number) => void) => void;
|
|
47
|
-
/** ⌘⇧D — add an empty caret on the line below. */
|
|
48
|
-
insertCursorBelow: (input: HTMLTextAreaElement) => void;
|
|
49
|
-
/**
|
|
50
|
-
* Replay the browser's edit at every extra range. Returns the buffer and
|
|
51
|
-
* caret the caller should commit, or null when there is nothing to replicate.
|
|
52
|
-
*/
|
|
53
|
-
replicate: (before: string, after: string, primaryBefore: TextRange) => { value: string; caret: number; rest: TextRange[] } | null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function useOccurrences({ onChange, controlled }: UseOccurrencesOptions): OccurrencesApi {
|
|
57
|
-
const ref = useRef<readonly TextRange[]>([]);
|
|
58
|
-
// The caller may clear the ranges without the editor asking (a file switch, a
|
|
59
|
-
// save), so its copy is mirrored into the ref the handlers read.
|
|
60
|
-
if (controlled && controlled !== ref.current) ref.current = controlled;
|
|
61
|
-
|
|
62
|
-
const set = (ranges: readonly TextRange[]) => {
|
|
63
|
-
ref.current = ranges;
|
|
64
|
-
onChange(ranges);
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
get ranges() {
|
|
69
|
-
return ref.current;
|
|
70
|
-
},
|
|
71
|
-
set,
|
|
72
|
-
clear: () => set([]),
|
|
73
|
-
|
|
74
|
-
selectNext: (input, select) => {
|
|
75
|
-
const current: TextRange = { start: input.selectionStart, end: input.selectionEnd };
|
|
76
|
-
const next = growToNextOccurrence(input.value, current, ref.current);
|
|
77
|
-
if (!next) return;
|
|
78
|
-
select(next.primary.start, next.primary.end);
|
|
79
|
-
set(next.extras);
|
|
80
|
-
},
|
|
81
|
-
|
|
82
|
-
selectAll: (input, select) => {
|
|
83
|
-
const current: TextRange = { start: input.selectionStart, end: input.selectionEnd };
|
|
84
|
-
const all = selectAllOccurrences(input.value, current);
|
|
85
|
-
if (!all) return;
|
|
86
|
-
select(all.primary.start, all.primary.end);
|
|
87
|
-
set(all.extras);
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
insertCursorBelow: (input) => {
|
|
91
|
-
const target = offsetBelow(input.value, input.selectionStart);
|
|
92
|
-
if (target === null) return;
|
|
93
|
-
set([...ref.current, { start: target, end: target }]);
|
|
94
|
-
},
|
|
95
|
-
|
|
96
|
-
replicate: (before, after, primaryBefore) => replicateAtRanges(before, after, primaryBefore, ref.current),
|
|
97
|
-
};
|
|
98
|
-
}
|