From 16f8b0461ba533891bf878dafa36f98d4b4a0cac Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 14 Nov 2026 00:10:17 -0800 Subject: [PATCH] refactor(editor): group core modules by feature owner (#42325) --- config/focus-setting-read-baseline.txt | 12 +- config/knip.json | 2 +- config/react-compiler-bailout-baseline.txt | 24 +- config/reliability-gates.jsonc | 7 +- .../source-routing/pr-e2e-source-routing.mjs | 2 +- .../pr-editor-preview-e2e-source-routes.mjs | 4 +- .../authoring/AutomationEditorDialog.tsx | 2 +- .../AutomationEditorPromptEditor.tsx | 9 +- .../automation-editor-prompt-options.ts | 2 +- .../doc-preview-artifact-share.tsx | 2 +- .../diff-comments/DiffCommentDraftCard.tsx | 2 +- .../diff-comments/DiffCommentPopover.tsx | 2 +- .../diff-comment-add-note-shortcut.ts | 2 +- ...ConflictReviewSurface.live-record.test.tsx | 199 -------- .../src/components/editor/EditorContent.tsx | 12 +- .../EditorFileLocationMenuItems.test.tsx | 88 ---- .../EditorPanel.placement-actions.test.tsx | 82 ---- .../src/components/editor/EditorPanel.tsx | 22 +- .../ai-vault-session-log-content.test.tsx | 185 -------- .../editor-command-owner-context.ts | 0 .../editor/{ => commands}/editor-shortcuts.ts | 5 +- .../pending-editor-focus-request.ts | 0 ...nflict-review-file-tree-windowing.test.tsx | 429 ------------------ .../ConflictComponents.tsx | 4 +- .../ConflictReviewFileTree.tsx | 6 +- .../EditorConflictReviewSurface.tsx | 12 +- ...editor-git-discard-other-workspace.test.ts | 26 +- .../useEditorConflictNavigation.ts | 4 +- .../contextual-copy-hint-refresh.ts | 2 +- .../{ => contextual-copy}/line-copy-path.ts | 0 .../selection-copy.test.ts | 0 .../{ => contextual-copy}/selection-copy.ts | 0 .../setup-contextual-copy.ts | 4 +- .../useContextualCopySetup.tsx | 0 .../editor/csv/preview/CsvPagedViewer.tsx | 2 +- .../editor/csv/preview/csv-file-content.ts | 2 +- .../editor/diff/monaco-view/DiffViewer.tsx | 13 +- .../DiffViewer.word-wrap-lifecycle.test.tsx | 44 +- .../diff/review/diff-navigation-context.tsx | 2 +- .../editor/diff/sections/DiffSectionBody.tsx | 6 +- .../diff/sections/DiffSectionHeader.tsx | 2 +- .../editor/diff/sections/DiffSectionItem.tsx | 5 +- .../image-viewer-zoom-ownership.test.tsx | 2 +- .../components/editor/editor-lazy-views.ts | 2 +- .../editor-clean-tab-disk-baseline.ts | 2 +- .../editor-deleted-file-reload.ts | 4 +- ...eEditorPanelExternalContentEvents.test.tsx | 253 +---------- .../useEditorPanelExternalContentEvents.ts | 4 +- .../editor-dropped-file-open.ts | 14 +- .../editor-file-drop-destination.ts | 0 .../use-editor-group-file-drop-owner.ts | 0 .../absolute-tab-placement.test.ts | 4 +- .../absolute-tab-placement.ts | 6 +- .../editor-path-move-inflight.test.ts | 0 .../editor-path-move-inflight.ts | 2 +- .../local-host-test-fixture.ts | 0 .../migrate-restored-editor-file-owner.ts | 10 +- ...stored-editor-owner-save-lifecycle.test.ts | 12 +- ...tored-editor-same-workspace-reown.test.tsx | 69 +-- ...ed-editor-workspace-runtime-owner.test.tsx | 16 +- ...restored-editor-workspace-runtime-owner.ts | 0 .../restored-host-file-grant.ts | 2 +- .../{ => loading}/EditorFileLoadErrorView.tsx | 6 +- .../editor-panel-content-types.ts | 6 +- .../{ => loading}/editor-panel-diff-reload.ts | 2 +- .../editor-panel-draft-selector.ts | 0 .../{ => loading}/editor-panel-file-reads.ts | 2 +- .../editor-panel-git-entry-selector.ts | 0 .../{ => loading}/file-load-error-copy.ts | 0 .../log-tail}/local-log-tail-decoder.test.ts | 3 +- .../log-tail}/local-log-tail-decoder.ts | 2 +- .../log-tail}/useLocalLogTail.test.tsx | 60 +-- .../{ => loading/log-tail}/useLocalLogTail.ts | 4 +- .../useEditorPanelActiveTabContentLoad.ts | 4 +- .../useEditorPanelContentReloadTriggers.ts | 2 +- .../useEditorPanelContentState.ts | 6 +- .../useEditorPanelDiffContentLoader.ts | 10 +- .../useEditorPanelFileContentLoader.ts | 18 +- .../useEditorPanelFileLoadRetry.ts | 6 +- ...ditorPanelFloatingTabContentState.test.tsx | 14 +- ...orPanelHostUnresolvedContentState.test.tsx | 77 +--- .../MarkdownPreviewAnnotationComposer.tsx | 2 +- .../review/markdown-preview-chat-selection.ts | 2 +- ...markdown-doc-completions-retention.test.ts | 2 +- .../monaco-markdown-doc-link-decorations.ts | 2 +- .../documents/useMarkdownDocuments.ts | 2 +- .../buffer-text/editor-buffer-adapter.test.ts | 3 +- .../buffer-text/editor-buffer-adapter.ts | 2 +- .../editor-shared-record-clean-view.test.ts | 147 ------ .../buffer-text}/monaco-content-sync.ts | 0 .../monaco-content-sync.undo-history.test.ts | 1 - .../monaco-programmatic-sync.test.ts | 0 .../buffer-text}/monaco-programmatic-sync.ts | 0 .../buffer-text}/text-line-offsets.ts | 0 .../use-editor-content-change-handler.ts | 2 +- ...monaco-content-sync-bridge.events.test.tsx | 42 +- .../use-monaco-content-sync-bridge.ts | 2 +- .../editor-model-host-isolation.test.ts | 91 +--- .../closed-unsaved-files-toast.test.ts | 74 +-- .../retention}/closed-unsaved-files-toast.ts | 2 +- ...ditor-input-bindings.paste-capture.test.ts | 92 ---- .../editor/{ => monaco}/MonacoCodeExcerpt.tsx | 0 .../editor/{ => monaco}/MonacoEditor.tsx | 24 +- .../{ => monaco}/MonacoGutterContextMenu.tsx | 6 +- .../editor-unicode-highlight-options.ts | 0 .../file-editor-word-wrap-options.ts | 2 +- .../install-monaco-context-menu-paste.ts | 0 .../editor/{ => monaco}/monaco-auto-height.ts | 0 .../{ => monaco}/monaco-codebase-search.ts | 0 .../monaco-conflict-decorations.test.ts | 0 .../monaco-conflict-decorations.ts | 2 +- .../monaco-context-menu-paste.test.ts | 1 - .../{ => monaco}/monaco-context-menu-paste.ts | 2 +- .../monaco-cursor-line-lifetime.test.ts | 0 .../editor/{ => monaco}/monaco-e2e-probe.ts | 0 .../monaco-editor-input-bindings.ts | 16 +- .../monaco-editor-mount-params.ts | 10 +- .../{ => monaco}/monaco-find-options.ts | 0 .../editor/{ => monaco}/monaco-find-widget.ts | 0 .../monaco-internal-test-module-types.ts | 0 .../monaco-large-text-paste.crlf.test.ts | 0 .../{ => monaco}/monaco-large-text-paste.ts | 4 +- .../{ => monaco}/monaco-reveal-range.ts | 0 .../editor/{ => monaco}/monaco-reveal.ts | 0 .../monaco-view-state-persistence.ts | 0 .../use-monaco-editor-decorations.ts | 6 +- ...-monaco-editor-mount.cursor-owner.test.tsx | 69 +-- .../{ => monaco}/use-monaco-editor-mount.ts | 16 +- .../use-monaco-reveal-scheduler.ts | 0 .../use-plugin-syntax-language.ts | 0 .../IpynbCellEditor.external-sync.test.tsx | 376 --------------- .../editor/notebook/cells/IpynbCellEditor.tsx | 6 +- .../editor/notebook/document/IpynbViewer.tsx | 2 +- .../editor/{ => panel}/ChangesModeView.tsx | 10 +- .../{ => panel}/EditorDiffFileSurface.tsx | 14 +- .../{ => panel}/EditorEditFileSurface.tsx | 32 +- .../{ => panel}/EditorMarkdownFileSurface.tsx | 18 +- .../EditorPanel.custom-editor-flush.test.tsx | 10 +- .../EditorPanel.diff-rich-owner.test.tsx | 8 +- .../EditorPanel.html-preview.test.tsx | 8 +- .../editor/{ => panel}/EditorPanelShell.tsx | 18 +- .../{ => panel}/editor-artifact-upload.ts | 6 +- .../{ => panel}/editor-panel-file-actions.ts | 4 +- .../{ => panel}/editor-panel-render-model.ts | 24 +- .../header}/EditorFileLocationMenuItems.tsx | 2 +- .../header}/EditorPanelActionsMenu.tsx | 0 .../{ => panel/header}/EditorPanelHeader.tsx | 18 +- .../header}/EditorPanelHeaderPath.tsx | 0 .../header}/editor-header-file-rename.test.ts | 16 +- .../header}/editor-header-file-rename.ts | 4 +- .../{ => panel/header}/editor-header.ts | 4 +- .../{ => panel/header}/editor-labels.ts | 0 .../{ => panel/header}/editor-path-display.ts | 0 .../open-editor-html-preview.test.ts | 2 +- .../{ => panel}/open-editor-html-preview.ts | 2 +- .../UntitledFileRenameDialog.tsx | 0 .../untitled-file-rename-path.ts | 0 .../untitled-rename}/useUntitledFileRename.ts | 4 +- .../use-quick-open-history-visit.ts | 4 +- .../src/components/editor/pdf/PdfViewer.tsx | 2 +- .../PluginBytesCustomEditorSurface.tsx | 2 +- .../PluginCustomEditorFrame.tsx | 2 +- .../commands/rich-markdown-save-shortcut.ts | 2 +- .../instance/useRichMarkdownPendingFocus.ts | 2 +- .../rich-markdown-annotation-shortcut.ts | 2 +- .../save/commands/useEditorCmdSaveRequest.ts | 2 +- .../queue/editor-save-disk-precondition.ts | 2 +- .../save/transforms/editor-final-newline.ts | 4 +- ...EditorPanelFileLoadRetry.wsl-host.test.tsx | 93 ---- .../use-viewer-zoom-commands.ts | 2 +- .../components/github/CommentCodeContext.tsx | 2 +- ...phase-adjustment-discarded-render.test.tsx | 23 +- .../src/components/tab-bar/EditorFileTab.tsx | 4 +- .../src/components/tab-bar/TabBar.tsx | 2 +- .../components/tab-bar/tab-bar-item-label.ts | 2 +- .../components/tab-group/TabGroupPanel.tsx | 2 +- .../terminal-pane-menu-copy-actions.ts | 2 +- src/renderer/src/env.d.ts | 2 +- ...tor-external-watch-event-reconciliation.ts | 2 +- .../execute-open-editor-path-move.ts | 2 +- .../src/lib/editor/monaco/monaco-setup.ts | 2 +- .../workspace-tab-palette-entry-builder.ts | 2 +- .../project-removal-host-twin-state.test.ts | 2 +- .../remove-worktree-shared-records.test.ts | 72 +-- .../removed-workspace-unsaved-files.test.ts | 218 --------- .../removed-workspace-unsaved-files.ts | 2 +- 186 files changed, 394 insertions(+), 3132 deletions(-) delete mode 100644 src/renderer/src/components/editor/EditorConflictReviewSurface.live-record.test.tsx delete mode 100644 src/renderer/src/components/editor/EditorFileLocationMenuItems.test.tsx delete mode 100644 src/renderer/src/components/editor/EditorPanel.placement-actions.test.tsx delete mode 100644 src/renderer/src/components/editor/ai-vault-session-log-content.test.tsx rename src/renderer/src/components/editor/{ => commands}/editor-command-owner-context.ts (100%) rename src/renderer/src/components/editor/{ => commands}/editor-shortcuts.ts (95%) rename src/renderer/src/components/editor/{ => commands}/pending-editor-focus-request.ts (100%) delete mode 100644 src/renderer/src/components/editor/conflict-review-file-tree-windowing.test.tsx rename src/renderer/src/components/editor/{ => conflict-review}/ConflictComponents.tsx (98%) rename src/renderer/src/components/editor/{ => conflict-review}/ConflictReviewFileTree.tsx (96%) rename src/renderer/src/components/editor/{ => conflict-review}/EditorConflictReviewSurface.tsx (96%) rename src/renderer/src/components/editor/{ => conflict-review}/editor-git-discard-other-workspace.test.ts (85%) rename src/renderer/src/components/editor/{ => conflict-review}/useEditorConflictNavigation.ts (95%) rename src/renderer/src/components/editor/{ => contextual-copy}/contextual-copy-hint-refresh.ts (98%) rename src/renderer/src/components/editor/{ => contextual-copy}/line-copy-path.ts (100%) rename src/renderer/src/components/editor/{ => contextual-copy}/selection-copy.test.ts (100%) rename src/renderer/src/components/editor/{ => contextual-copy}/selection-copy.ts (100%) rename src/renderer/src/components/editor/{ => contextual-copy}/setup-contextual-copy.ts (98%) rename src/renderer/src/components/editor/{ => contextual-copy}/useContextualCopySetup.tsx (100%) rename src/renderer/src/components/editor/{ => file-drop}/editor-dropped-file-open.ts (95%) rename src/renderer/src/components/editor/{ => file-drop}/editor-file-drop-destination.ts (100%) rename src/renderer/src/components/editor/{ => file-drop}/use-editor-group-file-drop-owner.ts (100%) rename src/renderer/src/components/editor/{ => file-ownership}/absolute-tab-placement.test.ts (95%) rename src/renderer/src/components/editor/{ => file-ownership}/absolute-tab-placement.ts (95%) rename src/renderer/src/components/editor/{ => file-ownership}/editor-path-move-inflight.test.ts (100%) rename src/renderer/src/components/editor/{ => file-ownership}/editor-path-move-inflight.ts (97%) rename src/renderer/src/components/editor/{ => file-ownership}/local-host-test-fixture.ts (100%) rename src/renderer/src/components/editor/{ => file-ownership}/migrate-restored-editor-file-owner.ts (92%) rename src/renderer/src/components/editor/{ => file-ownership}/restored-editor-owner-save-lifecycle.test.ts (94%) rename src/renderer/src/components/editor/{ => file-ownership}/restored-editor-same-workspace-reown.test.tsx (79%) rename src/renderer/src/components/editor/{ => file-ownership}/restored-editor-workspace-runtime-owner.test.tsx (88%) rename src/renderer/src/components/editor/{ => file-ownership}/restored-editor-workspace-runtime-owner.ts (100%) rename src/renderer/src/components/editor/{ => file-ownership}/restored-host-file-grant.ts (93%) rename src/renderer/src/components/editor/{ => loading}/EditorFileLoadErrorView.tsx (92%) rename src/renderer/src/components/editor/{ => loading}/editor-panel-content-types.ts (93%) rename src/renderer/src/components/editor/{ => loading}/editor-panel-diff-reload.ts (94%) rename src/renderer/src/components/editor/{ => loading}/editor-panel-draft-selector.ts (100%) rename src/renderer/src/components/editor/{ => loading}/editor-panel-file-reads.ts (90%) rename src/renderer/src/components/editor/{ => loading}/editor-panel-git-entry-selector.ts (100%) rename src/renderer/src/components/editor/{ => loading}/file-load-error-copy.ts (100%) rename src/renderer/src/components/editor/{ => loading/log-tail}/local-log-tail-decoder.test.ts (91%) rename src/renderer/src/components/editor/{ => loading/log-tail}/local-log-tail-decoder.ts (95%) rename src/renderer/src/components/editor/{ => loading/log-tail}/useLocalLogTail.test.tsx (61%) rename src/renderer/src/components/editor/{ => loading/log-tail}/useLocalLogTail.ts (97%) rename src/renderer/src/components/editor/{ => loading}/useEditorPanelActiveTabContentLoad.ts (98%) rename src/renderer/src/components/editor/{ => loading}/useEditorPanelContentReloadTriggers.ts (98%) rename src/renderer/src/components/editor/{ => loading}/useEditorPanelContentState.ts (97%) rename src/renderer/src/components/editor/{ => loading}/useEditorPanelDiffContentLoader.ts (95%) rename src/renderer/src/components/editor/{ => loading}/useEditorPanelFileContentLoader.ts (95%) rename src/renderer/src/components/editor/{ => loading}/useEditorPanelFileLoadRetry.ts (96%) rename src/renderer/src/components/editor/{ => loading}/useEditorPanelFloatingTabContentState.test.tsx (89%) rename src/renderer/src/components/editor/{ => loading}/useEditorPanelHostUnresolvedContentState.test.tsx (63%) delete mode 100644 src/renderer/src/components/editor/model-lifetime/buffer-text/editor-shared-record-clean-view.test.ts rename src/renderer/src/components/editor/{ => model-lifetime/buffer-text}/monaco-content-sync.ts (100%) rename src/renderer/src/components/editor/{ => model-lifetime/buffer-text}/monaco-content-sync.undo-history.test.ts (95%) rename src/renderer/src/components/editor/{ => model-lifetime/buffer-text}/monaco-programmatic-sync.test.ts (100%) rename src/renderer/src/components/editor/{ => model-lifetime/buffer-text}/monaco-programmatic-sync.ts (100%) rename src/renderer/src/components/editor/{ => model-lifetime/buffer-text}/text-line-offsets.ts (100%) rename src/renderer/src/components/editor/{ => model-lifetime/buffer-text}/use-monaco-content-sync-bridge.events.test.tsx (55%) rename src/renderer/src/components/editor/{ => model-lifetime/buffer-text}/use-monaco-content-sync-bridge.ts (97%) rename src/renderer/src/components/editor/{ => model-lifetime/retention}/closed-unsaved-files-toast.test.ts (51%) rename src/renderer/src/components/editor/{ => model-lifetime/retention}/closed-unsaved-files-toast.ts (99%) delete mode 100644 src/renderer/src/components/editor/monaco-editor-input-bindings.paste-capture.test.ts rename src/renderer/src/components/editor/{ => monaco}/MonacoCodeExcerpt.tsx (100%) rename src/renderer/src/components/editor/{ => monaco}/MonacoEditor.tsx (92%) rename src/renderer/src/components/editor/{ => monaco}/MonacoGutterContextMenu.tsx (95%) rename src/renderer/src/components/editor/{ => monaco}/editor-unicode-highlight-options.ts (100%) rename src/renderer/src/components/editor/{ => monaco}/file-editor-word-wrap-options.ts (74%) rename src/renderer/src/components/editor/{ => monaco}/install-monaco-context-menu-paste.ts (100%) rename src/renderer/src/components/editor/{ => monaco}/monaco-auto-height.ts (100%) rename src/renderer/src/components/editor/{ => monaco}/monaco-codebase-search.ts (100%) rename src/renderer/src/components/editor/{ => monaco}/monaco-conflict-decorations.test.ts (100%) rename src/renderer/src/components/editor/{ => monaco}/monaco-conflict-decorations.ts (98%) rename src/renderer/src/components/editor/{ => monaco}/monaco-context-menu-paste.test.ts (98%) rename src/renderer/src/components/editor/{ => monaco}/monaco-context-menu-paste.ts (98%) rename src/renderer/src/components/editor/{ => monaco}/monaco-cursor-line-lifetime.test.ts (100%) rename src/renderer/src/components/editor/{ => monaco}/monaco-e2e-probe.ts (100%) rename src/renderer/src/components/editor/{ => monaco}/monaco-editor-input-bindings.ts (90%) rename src/renderer/src/components/editor/{ => monaco}/monaco-editor-mount-params.ts (82%) rename src/renderer/src/components/editor/{ => monaco}/monaco-find-options.ts (100%) rename src/renderer/src/components/editor/{ => monaco}/monaco-find-widget.ts (100%) rename src/renderer/src/components/editor/{ => monaco}/monaco-internal-test-module-types.ts (100%) rename src/renderer/src/components/editor/{ => monaco}/monaco-large-text-paste.crlf.test.ts (100%) rename src/renderer/src/components/editor/{ => monaco}/monaco-large-text-paste.ts (98%) rename src/renderer/src/components/editor/{ => monaco}/monaco-reveal-range.ts (100%) rename src/renderer/src/components/editor/{ => monaco}/monaco-reveal.ts (100%) rename src/renderer/src/components/editor/{ => monaco}/monaco-view-state-persistence.ts (100%) rename src/renderer/src/components/editor/{ => monaco}/use-monaco-editor-decorations.ts (92%) rename src/renderer/src/components/editor/{ => monaco}/use-monaco-editor-mount.cursor-owner.test.tsx (72%) rename src/renderer/src/components/editor/{ => monaco}/use-monaco-editor-mount.ts (93%) rename src/renderer/src/components/editor/{ => monaco}/use-monaco-reveal-scheduler.ts (100%) rename src/renderer/src/components/editor/{ => monaco}/use-plugin-syntax-language.ts (100%) delete mode 100644 src/renderer/src/components/editor/notebook/cells/IpynbCellEditor.external-sync.test.tsx rename src/renderer/src/components/editor/{ => panel}/ChangesModeView.tsx (89%) rename src/renderer/src/components/editor/{ => panel}/EditorDiffFileSurface.tsx (91%) rename src/renderer/src/components/editor/{ => panel}/EditorEditFileSurface.tsx (90%) rename src/renderer/src/components/editor/{ => panel}/EditorMarkdownFileSurface.tsx (91%) rename src/renderer/src/components/editor/{ => panel}/EditorPanel.custom-editor-flush.test.tsx (88%) rename src/renderer/src/components/editor/{ => panel}/EditorPanel.diff-rich-owner.test.tsx (94%) rename src/renderer/src/components/editor/{ => panel}/EditorPanel.html-preview.test.tsx (90%) rename src/renderer/src/components/editor/{ => panel}/EditorPanelShell.tsx (92%) rename src/renderer/src/components/editor/{ => panel}/editor-artifact-upload.ts (90%) rename src/renderer/src/components/editor/{ => panel}/editor-panel-file-actions.ts (92%) rename src/renderer/src/components/editor/{ => panel}/editor-panel-render-model.ts (92%) rename src/renderer/src/components/editor/{ => panel/header}/EditorFileLocationMenuItems.tsx (97%) rename src/renderer/src/components/editor/{ => panel/header}/EditorPanelActionsMenu.tsx (100%) rename src/renderer/src/components/editor/{ => panel/header}/EditorPanelHeader.tsx (95%) rename src/renderer/src/components/editor/{ => panel/header}/EditorPanelHeaderPath.tsx (100%) rename src/renderer/src/components/editor/{ => panel/header}/editor-header-file-rename.test.ts (87%) rename src/renderer/src/components/editor/{ => panel/header}/editor-header-file-rename.ts (97%) rename src/renderer/src/components/editor/{ => panel/header}/editor-header.ts (94%) rename src/renderer/src/components/editor/{ => panel/header}/editor-labels.ts (100%) rename src/renderer/src/components/editor/{ => panel/header}/editor-path-display.ts (100%) rename src/renderer/src/components/editor/{ => panel}/open-editor-html-preview.test.ts (99%) rename src/renderer/src/components/editor/{ => panel}/open-editor-html-preview.ts (98%) rename src/renderer/src/components/editor/{ => panel/untitled-rename}/UntitledFileRenameDialog.tsx (100%) rename src/renderer/src/components/editor/{ => panel/untitled-rename}/untitled-file-rename-path.ts (100%) rename src/renderer/src/components/editor/{ => panel/untitled-rename}/useUntitledFileRename.ts (98%) rename src/renderer/src/components/editor/{ => panel}/use-quick-open-history-visit.ts (81%) delete mode 100644 src/renderer/src/components/editor/useEditorPanelFileLoadRetry.wsl-host.test.tsx delete mode 100644 src/renderer/src/store/slices/worktrees/teardown/removed-workspace-unsaved-files.test.ts diff --git a/config/focus-setting-read-baseline.txt b/config/focus-setting-read-baseline.txt index 3bc0a19973e..570ecc3cff4 100644 --- a/config/focus-setting-read-baseline.txt +++ b/config/focus-setting-read-baseline.txt @@ -28,14 +28,14 @@ 1 src/renderer/src/components/browser-pane/stream-remote/use-remote-browser-stream-activation.ts 2 src/renderer/src/components/cmd-j/quick-action-context.ts 2 src/renderer/src/components/editor/EditorContent.tsx -1 src/renderer/src/components/editor/EditorDiffFileSurface.tsx -1 src/renderer/src/components/editor/EditorEditFileSurface.tsx -1 src/renderer/src/components/editor/EditorMarkdownFileSurface.tsx +1 src/renderer/src/components/editor/panel/EditorDiffFileSurface.tsx +1 src/renderer/src/components/editor/panel/EditorEditFileSurface.tsx +1 src/renderer/src/components/editor/panel/EditorMarkdownFileSurface.tsx 1 src/renderer/src/components/editor/csv/csv-link-routing.ts 2 src/renderer/src/components/editor/markdown/documents/useMarkdownDocuments.ts -1 src/renderer/src/components/editor/restored-editor-workspace-runtime-owner.ts -2 src/renderer/src/components/editor/useEditorPanelContentState.ts -1 src/renderer/src/components/editor/useEditorPanelFileContentLoader.ts +1 src/renderer/src/components/editor/file-ownership/restored-editor-workspace-runtime-owner.ts +2 src/renderer/src/components/editor/loading/useEditorPanelContentState.ts +1 src/renderer/src/components/editor/loading/useEditorPanelFileContentLoader.ts 2 src/renderer/src/components/feature-wall/setup/integrations/ConnectIntegrationsList.tsx 1 src/renderer/src/components/feature-wall/setup/browser/FeatureWallBrowserAction.tsx 1 src/renderer/src/components/feature-wall/setup/FeatureWallSetupChecklist.tsx diff --git a/config/knip.json b/config/knip.json index b1d8191b540..9b93c95a8a0 100644 --- a/config/knip.json +++ b/config/knip.json @@ -60,7 +60,7 @@ "src/**/*.test-fixture.{ts,tsx}", "src/main/runtime/rpc/dispatch/rpc-params-type-parity.ts", "src/main/runtime/rpc/methods/client-state/ui-state-schema-parity-checks.ts", - "src/renderer/src/components/editor/monaco-internal-test-module-types.ts", + "src/renderer/src/components/editor/monaco/monaco-internal-test-module-types.ts", "mobile/**/*.{ts,tsx}", "cloud/**/*.{ts,tsx,mjs}" ], diff --git a/config/react-compiler-bailout-baseline.txt b/config/react-compiler-bailout-baseline.txt index 265cae54b57..90120f13aeb 100644 --- a/config/react-compiler-bailout-baseline.txt +++ b/config/react-compiler-bailout-baseline.txt @@ -83,8 +83,8 @@ 5 src/renderer/src/components/diff-comments/DiffCommentPopover.tsx 2 src/renderer/src/components/diff-comments/diff-comment-draft-zone.ts 1 src/renderer/src/components/diff-comments/useDiffCommentDecorator.tsx -1 src/renderer/src/components/editor/EditorPanelShell.tsx -5 src/renderer/src/components/editor/MonacoEditor.tsx +1 src/renderer/src/components/editor/panel/EditorPanelShell.tsx +5 src/renderer/src/components/editor/monaco/MonacoEditor.tsx 1 src/renderer/src/components/editor/pdf/PdfViewer.tsx 1 src/renderer/src/components/editor/plugin-editors/PluginCustomEditorFrame.tsx 3 src/renderer/src/components/editor/check-details/actions/check-run-details-fix-with-ai.ts @@ -107,7 +107,7 @@ 1 src/renderer/src/components/editor/diff/large-files/useDiffViewerLargeDiffLifecycle.ts 3 src/renderer/src/components/editor/diff/monaco-view/DiffViewer.tsx 1 src/renderer/src/components/editor/diff/review/DiffNotesSendMenu.tsx -1 src/renderer/src/components/editor/editor-header-file-rename.ts +1 src/renderer/src/components/editor/panel/header/editor-header-file-rename.ts 1 src/renderer/src/components/editor/external-changes/ExternalFileChangeCompareDialog.tsx 1 src/renderer/src/components/editor/git-graph/git-graph-filter.tsx 1 src/renderer/src/components/editor/markdown-preview/render-tree/use-markdown-preview-components.tsx @@ -134,15 +134,15 @@ 1 src/renderer/src/components/editor/rich-markdown/review/useRichMarkdownReviewData.ts 1 src/renderer/src/components/editor/rich-markdown/review/useRichMarkdownReviewRailController.ts 12 src/renderer/src/components/editor/rich-markdown/tables/RichMarkdownTableControls.tsx -2 src/renderer/src/components/editor/use-editor-group-file-drop-owner.ts -1 src/renderer/src/components/editor/use-monaco-content-sync-bridge.ts -1 src/renderer/src/components/editor/use-monaco-editor-mount.ts -1 src/renderer/src/components/editor/use-plugin-syntax-language.ts -1 src/renderer/src/components/editor/useEditorPanelActiveTabContentLoad.ts -2 src/renderer/src/components/editor/useEditorPanelContentReloadTriggers.ts -1 src/renderer/src/components/editor/useEditorPanelDiffContentLoader.ts -3 src/renderer/src/components/editor/useEditorPanelFileContentLoader.ts -4 src/renderer/src/components/editor/useLocalLogTail.ts +2 src/renderer/src/components/editor/file-drop/use-editor-group-file-drop-owner.ts +1 src/renderer/src/components/editor/model-lifetime/buffer-text/use-monaco-content-sync-bridge.ts +1 src/renderer/src/components/editor/monaco/use-monaco-editor-mount.ts +1 src/renderer/src/components/editor/monaco/use-plugin-syntax-language.ts +1 src/renderer/src/components/editor/loading/useEditorPanelActiveTabContentLoad.ts +2 src/renderer/src/components/editor/loading/useEditorPanelContentReloadTriggers.ts +1 src/renderer/src/components/editor/loading/useEditorPanelDiffContentLoader.ts +3 src/renderer/src/components/editor/loading/useEditorPanelFileContentLoader.ts +4 src/renderer/src/components/editor/loading/log-tail/useLocalLogTail.ts 1 src/renderer/src/components/editor/markdown-preview/viewport/usePreserveSectionDuringExternalEdit.ts 1 src/renderer/src/components/emulator-pane/emulator-screen-stream-content.tsx 3 src/renderer/src/components/emulator-pane/use-emulator-pane-session.ts diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 99ea4245668..70f004c197c 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -5623,17 +5623,17 @@ "invariant": "Append-only external file growth changes only Monaco's model suffix, retaining the viewport, selection, Find state, and renderer liveness above the append point; read-only live tails do not create undo history, while editable external updates remain undoable and arbitrary rewrites continue to replace the model content.", "oracle": "Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit1709 retires changed-source assertion and execution scope without equivalent replacement coverage. Existing unchanged-selector receipts remain scoped to their original sources. Historical prior-source oracle: Focused tests assert one post-mount content owner and actual lifecycle remount ordering across retained path models. Real-Monaco tests cover undo behavior for read-only live tails versus editable files. Standalone Node timing and private undo-retention budgets at 9 and 50 MiB were retired by audit1677 and provide no current alarm. The Electron scenario compares legacy setValue with watcher appends, checking visible ranges, selection, Find state, scroll offset, non-undoability, renderer survival and memory budgets. Mock-only edit-shape assertions were retired during test-value audit.", "commands": [ - "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/editor/monaco-content-sync.undo-history.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/editor/model-lifetime/buffer-text/monaco-content-sync.undo-history.test.ts", "node --expose-gc ./node_modules/vitest/vitest.mjs bench --pool=threads", "pnpm run test:e2e -- tests/e2e/agent-session-log-tail-stability.spec.ts --workers=1" ], "testFiles": [ - "src/renderer/src/components/editor/monaco-content-sync.undo-history.test.ts", + "src/renderer/src/components/editor/model-lifetime/buffer-text/monaco-content-sync.undo-history.test.ts", "tests/e2e/agent-session-log-tail-stability.spec.ts" ], "assertionRefs": [ { - "file": "src/renderer/src/components/editor/monaco-content-sync.undo-history.test.ts", + "file": "src/renderer/src/components/editor/model-lifetime/buffer-text/monaco-content-sync.undo-history.test.ts", "assertions": [ "a real Monaco read-only live-tail append leaves canUndo false while an ordinary external update remains undoable" ] @@ -5655,6 +5655,7 @@ "runner": "local", "platform": "macos", "command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/editor/monaco-content-sync.undo-history.test.ts", + "currentCommand": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/editor/model-lifetime/buffer-text/monaco-content-sync.undo-history.test.ts", "result": "passed", "durationSeconds": 4, "summary": "The real-Monaco undo-history test confirmed a read-only live-tail append leaves canUndo false while an ordinary external update remains undoable." diff --git a/config/scripts/workflow/source-routing/pr-e2e-source-routing.mjs b/config/scripts/workflow/source-routing/pr-e2e-source-routing.mjs index dcd141bb974..02218dee5c5 100644 --- a/config/scripts/workflow/source-routing/pr-e2e-source-routing.mjs +++ b/config/scripts/workflow/source-routing/pr-e2e-source-routing.mjs @@ -58,7 +58,7 @@ export const PR_E2E_SOURCE_ROUTES = [ specs: ['tests/e2e/ssh-orcad-markdown-conversion.spec.ts'], matches: (file) => isProductSource(file) && - /^src\/renderer\/src\/components\/editor\/(?:markdown\/documents\/useMarkdownDocuments|restored-editor-workspace-runtime-owner|migrate-restored-editor-file-owner)\.ts$/.test( + /^src\/renderer\/src\/components\/editor\/(?:markdown\/documents\/useMarkdownDocuments|file-ownership\/restored-editor-workspace-runtime-owner|file-ownership\/migrate-restored-editor-file-owner)\.ts$/.test( file ) }, diff --git a/config/scripts/workflow/source-routing/pr-editor-preview-e2e-source-routes.mjs b/config/scripts/workflow/source-routing/pr-editor-preview-e2e-source-routes.mjs index 958040a1783..e870ad11c6c 100644 --- a/config/scripts/workflow/source-routing/pr-editor-preview-e2e-source-routes.mjs +++ b/config/scripts/workflow/source-routing/pr-editor-preview-e2e-source-routes.mjs @@ -24,7 +24,7 @@ export const PR_EDITOR_PREVIEW_E2E_SOURCE_ROUTES = [ isProductSource(file) && (file.startsWith('src/renderer/src/components/editor/model-lifetime/identity/') || [ - 'src/renderer/src/components/editor/useEditorPanelFileContentLoader.ts', + 'src/renderer/src/components/editor/loading/useEditorPanelFileContentLoader.ts', 'src/renderer/src/lib/editor/file-operations/editor-file-operation-owner.ts', 'src/renderer/src/lib/editor/file-operations/editor-file-operation-provenance.ts' ].includes(file)) @@ -46,7 +46,7 @@ export const PR_EDITOR_PREVIEW_E2E_SOURCE_ROUTES = [ /^src\/renderer\/src\/components\/editor\/(?:document-images\/ImageViewer|pdf\/(?:PdfViewer|pdf-document-loader|pdf-js-document-options|pdf-orca-media-range-transport))\.tsx?$/.test( file ) || - /^src\/renderer\/src\/components\/editor\/(?:EditorEditFileSurface|EditorConflictReviewSurface)\.tsx$/.test( + /^src\/renderer\/src\/components\/editor\/(?:panel\/EditorEditFileSurface|conflict-review\/EditorConflictReviewSurface)\.tsx$/.test( file ) || [ diff --git a/src/renderer/src/components/automations/authoring/AutomationEditorDialog.tsx b/src/renderer/src/components/automations/authoring/AutomationEditorDialog.tsx index 772c4c93baf..588a676c573 100644 --- a/src/renderer/src/components/automations/authoring/AutomationEditorDialog.tsx +++ b/src/renderer/src/components/automations/authoring/AutomationEditorDialog.tsx @@ -27,7 +27,7 @@ import type { Repo } from '../../../../../shared/repo-types' import type { TuiAgent } from '../../../../../shared/agent-launch/catalog/tui-agent' import type { SetupDecision } from '../../../../../shared/worktree/create-types' import type { Worktree } from '../../../../../shared/worktree/types' -import { closeUnfocusedMonacoFindOrPreventDialogDismiss } from '@/components/editor/monaco-find-widget' +import { closeUnfocusedMonacoFindOrPreventDialogDismiss } from '@/components/editor/monaco/monaco-find-widget' import { AutomationOwnerConflictNotice } from '../hosts/AutomationOwnerConflictNotice' import type { AutomationActionNotice } from '../hosts/automation-row-action-dispatch' import type { AutomationHostRecoveryAction } from '../hosts/automation-host-status-descriptors' diff --git a/src/renderer/src/components/automations/authoring/AutomationEditorPromptEditor.tsx b/src/renderer/src/components/automations/authoring/AutomationEditorPromptEditor.tsx index 242b01c70a0..4df82a5d8c2 100644 --- a/src/renderer/src/components/automations/authoring/AutomationEditorPromptEditor.tsx +++ b/src/renderer/src/components/automations/authoring/AutomationEditorPromptEditor.tsx @@ -1,9 +1,12 @@ import React, { useCallback, useLayoutEffect, useMemo, useRef } from 'react' import Editor, { type OnMount } from '@monaco-editor/react' import type { editor } from 'monaco-editor' -import { installMonacoEditorFindShortcut } from '@/components/editor/editor-shortcuts' -import { syncContentOnMount, syncContentUpdate } from '@/components/editor/monaco-content-sync' -import { isMonacoFindWidgetOpen } from '@/components/editor/monaco-find-widget' +import { installMonacoEditorFindShortcut } from '@/components/editor/commands/editor-shortcuts' +import { + syncContentOnMount, + syncContentUpdate +} from '@/components/editor/model-lifetime/buffer-text/monaco-content-sync' +import { isMonacoFindWidgetOpen } from '@/components/editor/monaco/monaco-find-widget' import { monacoTypographyOptions } from '@/lib/editor/appearance/editor-typography' import { useEditorTypography } from '@/hooks/use-editor-typography' import '@/lib/editor/monaco/monaco-setup' diff --git a/src/renderer/src/components/automations/authoring/automation-editor-prompt-options.ts b/src/renderer/src/components/automations/authoring/automation-editor-prompt-options.ts index ce01e95b45b..8f91cc91de0 100644 --- a/src/renderer/src/components/automations/authoring/automation-editor-prompt-options.ts +++ b/src/renderer/src/components/automations/authoring/automation-editor-prompt-options.ts @@ -1,5 +1,5 @@ import type { editor } from 'monaco-editor' -import { monacoFindOptions } from '@/components/editor/monaco-find-options' +import { monacoFindOptions } from '@/components/editor/monaco/monaco-find-options' import type { MonacoTypographyOptions } from '@/lib/editor/appearance/editor-typography' export function buildAutomationPromptEditorOptions(args: { diff --git a/src/renderer/src/components/browser-pane/workspace-doc/doc-preview-artifact-share.tsx b/src/renderer/src/components/browser-pane/workspace-doc/doc-preview-artifact-share.tsx index 9d51871715b..aecffd6288b 100644 --- a/src/renderer/src/components/browser-pane/workspace-doc/doc-preview-artifact-share.tsx +++ b/src/renderer/src/components/browser-pane/workspace-doc/doc-preview-artifact-share.tsx @@ -6,7 +6,7 @@ import { import type { DocPreviewGrantOwner } from '../../../../../preload/api/document-preview/doc-preview-api' import { ArtifactPublishButton } from '@/components/artifacts/ArtifactPublishButton' import { ArtifactPublishPreparationError } from '@/components/artifacts/artifact-publish-flow' -import { isShareableHtmlPath } from '@/components/editor/editor-artifact-upload' +import { isShareableHtmlPath } from '@/components/editor/panel/editor-artifact-upload' import type { DocPreviewGrantLocation } from '@/lib/browser/documents/doc-preview-grants' import { fileArtifactSourceKey, diff --git a/src/renderer/src/components/diff-comments/DiffCommentDraftCard.tsx b/src/renderer/src/components/diff-comments/DiffCommentDraftCard.tsx index 6db2470e6b6..ef79bc2c228 100644 --- a/src/renderer/src/components/diff-comments/DiffCommentDraftCard.tsx +++ b/src/renderer/src/components/diff-comments/DiffCommentDraftCard.tsx @@ -12,7 +12,7 @@ import { hasBoundedCommentBodyText } from '@/lib/source-control/review/comments/comment-body-submit-state' import { toast } from 'sonner' -import { installOpenDraftAddReviewNoteGuard } from '../editor/editor-shortcuts' +import { installOpenDraftAddReviewNoteGuard } from '../editor/commands/editor-shortcuts' export type DiffCommentDraftCardProps = { lineNumber: number diff --git a/src/renderer/src/components/diff-comments/DiffCommentPopover.tsx b/src/renderer/src/components/diff-comments/DiffCommentPopover.tsx index 179641303d5..a100805ed01 100644 --- a/src/renderer/src/components/diff-comments/DiffCommentPopover.tsx +++ b/src/renderer/src/components/diff-comments/DiffCommentPopover.tsx @@ -9,7 +9,7 @@ import { hasBoundedCommentBodyText } from '@/lib/source-control/review/comments/comment-body-submit-state' import { useTranslate } from '@/i18n/use-translate' -import { installOpenDraftAddReviewNoteGuard } from '../editor/editor-shortcuts' +import { installOpenDraftAddReviewNoteGuard } from '../editor/commands/editor-shortcuts' import { resolveDiffCommentPopoverTop } from './diff-comment-popover-position' // Why: a DOM sibling overlay rather than a Monaco content widget, so it can own a React auto-resizing textarea. diff --git a/src/renderer/src/components/diff-comments/diff-comment-add-note-shortcut.ts b/src/renderer/src/components/diff-comments/diff-comment-add-note-shortcut.ts index 078fe0f5b6e..df661bfd0f1 100644 --- a/src/renderer/src/components/diff-comments/diff-comment-add-note-shortcut.ts +++ b/src/renderer/src/components/diff-comments/diff-comment-add-note-shortcut.ts @@ -2,7 +2,7 @@ import type { editor as monacoEditor, Selection } from 'monaco-editor' // Why: a value import of 'monaco-editor' here would evaluate Monaco ahead of its NLS pack // (see monaco-nls-import-order.test.ts); the enum module carries no localized labels. import { EditorOption } from 'monaco-editor/esm/vs/editor/common/standalone/standaloneEnums.js' -import { installEditorAddReviewNoteShortcut } from '../editor/editor-shortcuts' +import { installEditorAddReviewNoteShortcut } from '../editor/commands/editor-shortcuts' import { getDiffCommentPopoverTop } from './diff-comment-popover-position' import { clampFocusLineToCommentable, diff --git a/src/renderer/src/components/editor/EditorConflictReviewSurface.live-record.test.tsx b/src/renderer/src/components/editor/EditorConflictReviewSurface.live-record.test.tsx deleted file mode 100644 index 4590f0d905f..00000000000 --- a/src/renderer/src/components/editor/EditorConflictReviewSurface.live-record.test.tsx +++ /dev/null @@ -1,199 +0,0 @@ -// @vitest-environment happy-dom -import { act, createElement } from 'react' -import { createRoot, type Root } from 'react-dom/client' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { StoreApi } from 'zustand' -import type { AppState } from '@/store/types' -import type { OpenFile } from '@/store/slices/editor/types/open-file' -import type { PendingEditorReveal } from '@/store/slices/editor/types/pending-editor-reveal' -import type { GitStatusEntry } from '../../../../shared/git/status/git-status-types' -import { makeWorktree } from '@/store/slices/worktrees/worktree.test-fixture' -import { createEditorStore } from './autosave/editor-autosave-controller-test-fixture' -import { createEditorPanelDraftSelector } from './editor-panel-draft-selector' - -import { EditorConflictReviewSurface } from './EditorConflictReviewSurface' - -type MonacoProps = { - fileId: string - filePath: string - content: string - readOnly?: boolean - worktreeId?: string - passiveMirror?: boolean - revealLine?: number - viewStateId?: string - onContentChange: (content: string) => void - onSave: (content: string) => void -} - -const storeRef = vi.hoisted((): { current: StoreApi | null } => ({ current: null })) -const monacoMounts = vi.hoisted((): { current: MonacoProps[] } => ({ current: [] })) -const retries = vi.hoisted((): { current: (() => void)[] } => ({ current: [] })) - -vi.mock('@/store', () => { - const getState = () => storeRef.current!.getState() - return { - useAppStore: Object.assign((select: (state: AppState) => unknown) => select(getState()), { - getState - }) - } -}) -vi.mock('./editor-lazy-views', () => ({ - ImageViewer: () => null, - MonacoEditor: (props: MonacoProps) => { - monacoMounts.current.push(props) - return null - } -})) -vi.mock('./EditorFileLoadErrorView', () => ({ - EditorFileLoadErrorView: ({ onRetry }: { onRetry: () => void }) => { - retries.current.push(onRetry) - return null - } -})) -vi.mock('./ConflictComponents', () => ({ - ConflictBanner: () => null, - ConflictPlaceholderView: () => null, - ConflictReviewPanel: ({ selectedContent }: { selectedContent: React.ReactNode }) => - selectedContent -})) - -const PATH = '/repo/file.ts' -const ENTRY: GitStatusEntry = { - path: 'file.ts', - status: 'modified', - area: 'unstaged', - conflictKind: 'both_modified', - conflictStatus: 'unresolved', - conflictStatusSource: 'git' -} - -let root: Root | null = null - -function setup(): { store: StoreApi; view: OpenFile; review: OpenFile } { - const store = createEditorStore() - storeRef.current = store - store.setState({ - worktreesByRepo: { - 'repo-1': [ - makeWorktree({ id: 'wt-1', repoId: 'repo-1', path: '/repo', hostId: 'local' }), - makeWorktree({ id: 'wt-2', repoId: 'repo-1', path: '/repo', hostId: 'local' }) - ] - }, - browserTabsByWorktree: {}, - tabsByWorktree: {}, - activeBrowserTabIdByWorktree: {}, - unifiedTabsByWorktree: {} - }) - store.getState().openFile({ - filePath: PATH, - relativePath: 'file.ts', - worktreeId: 'wt-1', - language: 'typescript', - mode: 'edit' - }) - store - .getState() - .openConflictReview( - 'wt-2', - '/repo', - [{ path: 'file.ts', conflictKind: 'both_modified' }], - 'live-summary' - ) - const [view, review] = store.getState().openFiles - expect(review.mode).toBe('conflict-review') - return { store, view, review } -} - -type RenderOptions = { - loadError?: string - pendingEditorReveal?: PendingEditorReveal | null - reloadContent?: (file: OpenFile) => void -} - -function render( - store: StoreApi, - review: OpenFile, - diskText: string, - options: RenderOptions = {} -): MonacoProps { - const editBuffers = createEditorPanelDraftSelector(review)(store.getState()) - const viewIds = store.getState().openFiles.map((file) => file.id) - const fileContents = Object.fromEntries( - [...viewIds, PATH].map((id) => [ - id, - { content: diskText, isBinary: false, loadError: options.loadError } - ]) - ) - monacoMounts.current = [] - retries.current = [] - act(() => { - root!.render( - createElement(EditorConflictReviewSurface, { - activeFile: review, - viewStateScopeId: 'panel-1', - fileContents, - editBuffers, - openFiles: store.getState().openFiles, - worktreeEntries: [ENTRY], - pendingEditorReveal: options.pendingEditorReveal ?? null, - getConflictNavigation: () => undefined, - handleContentChangeForFile: (file, content) => - store.getState().setEditorDraft(file.id, content), - handleSaveForFile: async () => true, - reloadContent: options.reloadContent ?? (() => {}) - }) - ) - }) - if (options.loadError) { - expect(retries.current).toHaveLength(1) - return monacoMounts.current[0] - } - expect(monacoMounts.current).toHaveLength(1) - return monacoMounts.current[0] -} - -beforeEach(() => { - Reflect.set(globalThis, 'IS_REACT_ACT_ENVIRONMENT', true) - root = createRoot(document.createElement('div')) -}) - -afterEach(() => { - act(() => root?.unmount()) - root = null -}) - -describe('conflict-review overview as a live read-only view of the record', () => { - it("can't edit or save the record", () => { - const { store, view, review } = setup() - store.getState().setEditorDraft(view.id, 'unsaved') - store.getState().markFileDirty(view.id, true) - const before = store.getState() - - const overview = render(store, review, 'disk') - overview.onContentChange('typed in overview') - overview.onSave('typed in overview') - expect(store.getState().editorDrafts).toBe(before.editorDrafts) - expect(store.getState().openFiles).toBe(before.openFiles) - }) - - it('closing it leaves the record, its draft and its views alone', () => { - const { store, view, review } = setup() - store.getState().setEditorDraft(view.id, 'unsaved') - store.getState().markFileDirty(view.id, true) - render(store, review, 'disk') - const recordId = store.getState().editorBufferIdOf[view.id] - expect(store.getState().editorBufferIdOf[review.id]).not.toBe(recordId) - - act(() => root!.unmount()) - root = createRoot(document.createElement('div')) - store.getState().closeFile(review.id) - - const state = store.getState() - expect(state.openFiles.map((file) => file.id)).toEqual([view.id]) - expect(state.editorBufferViews[recordId]).toEqual([view.id]) - expect(state.editorDrafts[view.id]).toBe('unsaved') - expect(state.openFiles[0].isDirty).toBe(true) - expect(state.editorBufferKept).toEqual({}) - }) -}) diff --git a/src/renderer/src/components/editor/EditorContent.tsx b/src/renderer/src/components/editor/EditorContent.tsx index cababa18386..8458c966ba7 100644 --- a/src/renderer/src/components/editor/EditorContent.tsx +++ b/src/renderer/src/components/editor/EditorContent.tsx @@ -7,20 +7,20 @@ import type { GitDiffResult } from '../../../../shared/git/diff/git-diff-compare import type { GitStatusEntry } from '../../../../shared/git/status/git-status-types' import { CheckRunDetailsPanel } from './check-details/panel/CheckRunDetailsPanel' import { CombinedDiffViewer, MarkdownPreview } from './editor-lazy-views' -import { EditorConflictReviewSurface } from './EditorConflictReviewSurface' -import { EditorDiffFileSurface } from './EditorDiffFileSurface' -import { EditorEditFileSurface } from './EditorEditFileSurface' -import { EditorFileLoadErrorView } from './EditorFileLoadErrorView' +import { EditorConflictReviewSurface } from './conflict-review/EditorConflictReviewSurface' +import { EditorDiffFileSurface } from './panel/EditorDiffFileSurface' +import { EditorEditFileSurface } from './panel/EditorEditFileSurface' +import { EditorFileLoadErrorView } from './loading/EditorFileLoadErrorView' import { MarkdownPreviewSizeGate } from './markdown-preview/render-tree/MarkdownPreviewSizeGate' import { HOST_STARTED, USER_RETRY, type FileContent, type ReloadContentOptions -} from './editor-panel-content-types' +} from './loading/editor-panel-content-types' import { buildFileViewPreferenceKey as buildPdfScalePreferenceKey } from './view-mode/file-view-preference-storage' import { useTranslate } from '@/i18n/use-translate' -import { useEditorConflictNavigation } from './useEditorConflictNavigation' +import { useEditorConflictNavigation } from './conflict-review/useEditorConflictNavigation' import { useMarkdownDocuments } from './markdown/documents/useMarkdownDocuments' import type { MarkdownRenderState } from './markdown/render-mode/markdown-render-mode' diff --git a/src/renderer/src/components/editor/EditorFileLocationMenuItems.test.tsx b/src/renderer/src/components/editor/EditorFileLocationMenuItems.test.tsx deleted file mode 100644 index f9fa9e980e1..00000000000 --- a/src/renderer/src/components/editor/EditorFileLocationMenuItems.test.tsx +++ /dev/null @@ -1,88 +0,0 @@ -// @vitest-environment happy-dom -import { cleanup, fireEvent, render, screen } from '@testing-library/react' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { OpenFile } from '@/store/slices/editor/types/open-file' -import type { WorktreeRuntimeOwnerState } from '@/lib/workspace/ownership/worktree-runtime-owner-state' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuTrigger -} from '@/components/ui/dropdown-menu' -import { EditorFileLocationMenuItems } from './EditorFileLocationMenuItems' - -const store = vi.hoisted((): { state: WorktreeRuntimeOwnerState } => ({ state: {} })) -vi.mock('@/store', () => ({ - useAppStore: (selector: (state: WorktreeRuntimeOwnerState) => unknown) => selector(store.state) -})) -vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback })) - -const openInFileManager = vi.fn().mockResolvedValue({ ok: true }) -const writeClipboardText = vi.fn().mockResolvedValue(undefined) -const onCopyPath = vi.fn() -const file: OpenFile = { - id: '/repo/src/file.ts', - filePath: '/repo/src/file.ts', - relativePath: 'src/file.ts', - worktreeId: 'repo::/repo', - language: 'typescript', - mode: 'edit', - isDirty: false -} - -function renderMenu(overrides: Partial = {}): void { - render( - - - - - - - - - ) -} - -beforeEach(() => { - store.state = { - repos: [{ id: 'repo', connectionId: null }], - worktreesByRepo: { - repo: [{ id: file.worktreeId, repoId: 'repo', hostId: 'local' }] - }, - folderWorkspaces: [], - projectGroups: [] - } - vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue('Macintosh') - Object.defineProperty(window, 'api', { - configurable: true, - value: { shell: { openInFileManager }, ui: { writeClipboardText } } - }) -}) - -afterEach(() => { - cleanup() - vi.clearAllMocks() - vi.restoreAllMocks() -}) - -describe('EditorFileLocationMenuItems', () => { - it.each([{ externalSshTargetId: 'remote' }, { runtimeEnvironmentId: 'paired-server' }])( - 'disables local reveal for a remote file: %j', - (overrides) => { - renderMenu(overrides) - const reveal = screen.getByRole('menuitem', { name: 'Reveal in Finder Local only' }) - expect(reveal.getAttribute('aria-disabled')).toBe('true') - fireEvent.click(reveal) - expect(openInFileManager).not.toHaveBeenCalled() - expect( - screen.getByRole('menuitem', { name: 'Copy Relative Path' }).hasAttribute('data-disabled') - ).toBe(false) - } - ) - - it('reveals local files even while a remote runtime is selected', () => { - store.state.settings = { activeRuntimeEnvironmentId: 'paired-server' } - renderMenu() - fireEvent.click(screen.getByRole('menuitem', { name: 'Reveal in Finder' })) - expect(openInFileManager).toHaveBeenCalledWith(file.filePath, 'local') - }) -}) diff --git a/src/renderer/src/components/editor/EditorPanel.placement-actions.test.tsx b/src/renderer/src/components/editor/EditorPanel.placement-actions.test.tsx deleted file mode 100644 index 4518851409b..00000000000 --- a/src/renderer/src/components/editor/EditorPanel.placement-actions.test.tsx +++ /dev/null @@ -1,82 +0,0 @@ -// @vitest-environment happy-dom -import { cleanup, fireEvent, render } from '@testing-library/react' -import { afterEach, expect, it, vi } from 'vitest' -import { useAppStore } from '@/store' -import type { TabsDrawnFromMain } from '@/store/slices/tabs/tabs-drawn-from-main.test-fixture' -import { - EDITOR_OWNER, - EDITOR_PLACEMENT, - seedEditorPlacementConsumers, - drawEditorPlacementConsumers -} from '@/store/slices/editor/tabs/editor-placement-consumers.test-fixture' -import type { EditorToggleValue } from './view-mode/EditorViewToggle' - -vi.mock('@/runtime/rpc/calls/runtime-rpc-call', async (original) => - ( - await import('@/runtime/workspace-layout/testing/layout-apply-test-transport.test-fixture') - ).runtimeRpcClientWithLayoutApply(await original()) -) -vi.mock('@/runtime/workspace-layout/frames/runtime-layout-client', async (original) => - ( - await import('@/runtime/workspace-layout/testing/layout-apply-test-transport.test-fixture') - ).runtimeLayoutClientWithTestFrames(await original()) -) -vi.mock('./useEditorPanelContentState', () => ({ - useEditorPanelContentState: () => ({ - fileContents: {}, - diffContents: {}, - reloadContent: () => {} - }) -})) -vi.mock('./EditorPanelShell', () => ({ - EditorPanelShell: ({ - onOpenDiffTargetFile, - onEditorToggleChange, - onOpenMarkdownPreview - }: { - onOpenDiffTargetFile: () => void - onEditorToggleChange: (mode: EditorToggleValue) => void - onOpenMarkdownPreview: () => void - }) => ( - <> - - - - - ) -})) -import EditorPanel from './EditorPanel' - -let main: TabsDrawnFromMain | undefined -afterEach(() => { - cleanup() - main?.close() - main = undefined -}) - -it.each(['Open', 'Rich', 'Markdown preview'])( - '%s from a child diff opens the result in the invoking folder group', - async (action) => { - const file = seedEditorPlacementConsumers() - main = await drawEditorPlacementConsumers() - const screen = render() - fireEvent.click(screen.getByText(action)) - await main.settle() - const state = useAppStore.getState() - const result = state.openFiles.find( - (entry) => entry.mode === (action === 'Markdown preview' ? 'markdown-preview' : 'edit') - ) - expect(result).toMatchObject({ - worktreeId: EDITOR_OWNER, - filePath: '/repo/notes.md', - runtimeEnvironmentId: null - }) - expect( - state.unifiedTabsByWorktree[EDITOR_PLACEMENT]?.find((tab) => tab.entityId === result?.id) - ).toMatchObject({ groupId: 'x-source' }) - expect(state.unifiedTabsByWorktree[EDITOR_OWNER]?.map((tab) => tab.id)).toEqual(['y-tab']) - if (action === 'Rich') { - expect(state.markdownViewMode[result!.id]).toBe('rich') - } - } -) diff --git a/src/renderer/src/components/editor/EditorPanel.tsx b/src/renderer/src/components/editor/EditorPanel.tsx index eaba7400ef0..1bc29bf4f69 100644 --- a/src/renderer/src/components/editor/EditorPanel.tsx +++ b/src/renderer/src/components/editor/EditorPanel.tsx @@ -4,36 +4,36 @@ import { getConnectionId } from '@/lib/workspace/ownership/connection-context' import { formatClipboardFilePath } from '@/lib/clipboard/clipboard-file-path' import { getOpenFileRevealOwner } from '@/lib/files/os-open/reveal-in-file-manager' import { canShowWorkspaceFileBrowserAction } from '@/lib/browser/documents/file-preview' -import { openEditorHtmlPreview } from './open-editor-html-preview' -import { getEditorHeaderCopyState } from './editor-header' +import { openEditorHtmlPreview } from './panel/open-editor-html-preview' +import { getEditorHeaderCopyState } from './panel/header/editor-header' import { getRuntimeTargetForFileOwner } from '@/lib/files/access/file-owner-runtime-target' import { exportActiveMarkdownToPdf } from './markdown/export/export-active-markdown' import type { EditorToggleValue } from './view-mode/EditorViewToggle' -import { EditorPanelShell } from './EditorPanelShell' +import { EditorPanelShell } from './panel/EditorPanelShell' import { DiffNavigationProvider } from './diff/review/diff-navigation-context' -import { EditorCommandOwnerContext } from './editor-command-owner-context' +import { EditorCommandOwnerContext } from './commands/editor-command-owner-context' import { canUseChangesModeForFile } from './view-mode/editor-panel-file-mode' -import { getEditorPanelRenderModel } from './editor-panel-render-model' +import { getEditorPanelRenderModel } from './panel/editor-panel-render-model' import { useEditorCmdSaveRequest } from './save/commands/useEditorCmdSaveRequest' -import { useEditorPanelContentState } from './useEditorPanelContentState' +import { useEditorPanelContentState } from './loading/useEditorPanelContentState' import { useMarkdownPreviewShortcut } from './view-mode/useMarkdownPreviewShortcut' -import { useUntitledFileRename } from './useUntitledFileRename' +import { useUntitledFileRename } from './panel/untitled-rename/useUntitledFileRename' import { extractFrontMatter } from './markdown/source-scanning/markdown-frontmatter' import { useEditorContentChangeHandler } from './model-lifetime/buffer-text/use-editor-content-change-handler' import { selectEditorPanelGitBranchEntries, selectEditorPanelGitStatusEntries -} from './editor-panel-git-entry-selector' -import { createEditorPanelDraftSelector } from './editor-panel-draft-selector' +} from './loading/editor-panel-git-entry-selector' +import { createEditorPanelDraftSelector } from './loading/editor-panel-draft-selector' import { createCurrentEditorArtifactRequest, editorArtifactContentType -} from './editor-artifact-upload' +} from './panel/editor-artifact-upload' import { useEditorPanelSave } from './save/commands/useEditorPanelSave' import { isMarkdownReviewNotesEnabled } from '@/lib/editor/markdown/review-notes/markdown-review-notes-setting' import { useFileCustomEditors } from '@/store/plugins/file-editors/plugin-custom-editors' import { applyCustomEditorToggle } from './view-mode/custom-editor-toggle' -import { editorPanelFileActions } from './editor-panel-file-actions' +import { editorPanelFileActions } from './panel/editor-panel-file-actions' import { runAfterEditorFramesFlush } from './save/flush/editor-pending-flush' function EditorPanelInner({ diff --git a/src/renderer/src/components/editor/ai-vault-session-log-content.test.tsx b/src/renderer/src/components/editor/ai-vault-session-log-content.test.tsx deleted file mode 100644 index db24ade6b07..00000000000 --- a/src/renderer/src/components/editor/ai-vault-session-log-content.test.tsx +++ /dev/null @@ -1,185 +0,0 @@ -// @vitest-environment happy-dom - -import { act } from 'react' -import { createRoot, type Root } from 'react-dom/client' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { useAppStore } from '@/store' -import { getConnectionIdForFile } from '@/lib/workspace/ownership/connection-context' -import { makeWorktree } from '@/store/slices/worktrees/worktree.test-fixture' -import { TEST_REPO } from '@/store/repos/repo.test-fixture' -import type { OpenFile } from '@/store/slices/editor/types/open-file' -import type * as RuntimeFileReadClient from '@/runtime/files/reads/runtime-file-read-client' -import { openAiVaultSessionLogInOrca } from '../right-sidebar/ai-vault/session-actions/ai-vault-session-log-open' -import { useEditorPanelContentState } from './useEditorPanelContentState' -import type { FileContent } from './editor-panel-content-types' - -const mocks = vi.hoisted(() => ({ readRuntimeFileContent: vi.fn(), toastError: vi.fn() })) -vi.mock('@/runtime/files/reads/runtime-file-read-client', async (importOriginal) => ({ - ...(await importOriginal()), - readRuntimeFileContent: mocks.readRuntimeFileContent -})) -vi.mock('@/runtime/files/watching/runtime-file-watch-client', () => ({ - subscribeRuntimeFileChanges: vi.fn() -})) -vi.mock('sonner', () => ({ toast: Object.assign(vi.fn(), { error: mocks.toastError }) })) - -const fsApi = { - onLocalLogTailChanged: vi.fn(() => () => {}), - startLocalLogTail: vi.fn().mockResolvedValue(undefined), - stopLocalLogTail: vi.fn().mockResolvedValue(undefined), - readLocalLogTail: vi.fn(async ({ fromByteOffset }: { fromByteOffset: number }) => ({ - contentBase64: '', - nextByteOffset: fromByteOffset, - fileSize: fromByteOffset, - fileIdentity: 'grok-log', - hasMore: false, - reset: false - })) -} -let latestFileContents: Record = {} - -function HookProbe({ - activeFile, - openFiles -}: { - activeFile: OpenFile - openFiles: OpenFile[] -}): null { - latestFileContents = useEditorPanelContentState({ - activeFile, - openFiles, - isChangesMode: false, - gitStatusEntries: undefined, - editorViewMode: {} - }).fileContents - return null -} - -describe('History conversation resources through editor content and tailing', () => { - let container: HTMLDivElement | null = null - let root: Root | null = null - - beforeEach(() => { - useAppStore.setState(useAppStore.getInitialState(), true) - latestFileContents = {} - vi.clearAllMocks() - Object.defineProperty(window, 'api', { configurable: true, value: { fs: fsApi } }) - }) - - afterEach(() => { - act(() => root?.unmount()) - container?.remove() - root = null - container = null - Reflect.deleteProperty(window, 'api') - useAppStore.setState(useAppStore.getInitialState(), true) - }) - - it.each([true, false])( - 'compares metadata and companion reads in an SSH workspace (companion exists: %s)', - async (companionExists) => { - const metadataPath = '/Users/me/grok/session/summary.json' - const logPath = '/Users/me/grok/session/chat_history.jsonl' - const loadError = `ENOENT: no such file or directory, open '${logPath}'` - const store = useAppStore - store.setState({ - activeWorktreeId: 'wt-1', - repos: [ - { ...TEST_REPO, id: 'repo-1', executionHostId: 'ssh:ssh-1', connectionId: 'ssh-1' } - ], - worktreesByRepo: { - 'repo-1': [ - makeWorktree({ - id: 'wt-1', - repoId: 'repo-1', - path: '/work/demo-project', - hostId: 'ssh:ssh-1' - }) - ] - } - }) - expect(getConnectionIdForFile('wt-1', metadataPath)).toBe('ssh-1') - mocks.readRuntimeFileContent.mockImplementation( - async ({ filePath }: { filePath: string }) => { - if (filePath === metadataPath) { - return { content: '{"title":"Summary"}', isBinary: false } - } - if (filePath === logPath && companionExists) { - return { - content: '{"role":"user","content":"Hello"}\n', - isBinary: false, - fileIdentity: 'grok-log' - } - } - throw new Error(loadError) - } - ) - container = document.body.appendChild(document.createElement('div')) - root = createRoot(container) - // The former action passed the metadata locator into this same editor contract. - store.getState().openFile( - { - filePath: metadataPath, - relativePath: metadataPath, - worktreeId: 'wt-1', - runtimeEnvironmentId: null, - language: 'json', - mode: 'edit', - readOnly: true, - liveTail: true - }, - { preview: false, forceContentReload: true, suppressActiveRuntimeFallback: true } - ) - let file = store.getState().openFiles[0] - await act(async () => root?.render()) - expect(latestFileContents[file.id]).toMatchObject({ content: '{"title":"Summary"}' }) - - await openAiVaultSessionLogInOrca({ - agent: 'grok', - filePath: metadataPath, - executionHostId: 'local' - }) - const companion = store - .getState() - .openFiles.find((candidate) => candidate.filePath === logPath) - expect(companion).toBeDefined() - if (!companion) { - throw new Error('View Log did not open its companion') - } - file = companion - expect(file).toMatchObject({ - relativePath: logPath, - readOnly: true, - liveTail: true, - runtimeEnvironmentId: null - }) - await act(async () => root?.render()) - - expect(mocks.readRuntimeFileContent.mock.calls.map(([input]) => input.filePath)).toEqual([ - metadataPath, - logPath - ]) - for (const [input] of mocks.readRuntimeFileContent.mock.calls) { - expect(input).toMatchObject({ - target: { kind: 'local' }, - connectionId: undefined, - access: { kind: 'user-file' }, - includeLocalLogMetadata: true - }) - } - if (companionExists) { - expect(latestFileContents[file.id]).toMatchObject({ - content: '{"role":"user","content":"Hello"}\n' - }) - expect(fsApi.startLocalLogTail).toHaveBeenCalledExactlyOnceWith( - expect.objectContaining({ filePath: logPath }) - ) - } else { - expect(latestFileContents[file.id]).toEqual({ content: '', isBinary: false, loadError }) - expect(fsApi.startLocalLogTail).not.toHaveBeenCalled() - expect(fsApi.readLocalLogTail).not.toHaveBeenCalled() - expect(mocks.toastError).not.toHaveBeenCalled() - } - } - ) -}) diff --git a/src/renderer/src/components/editor/editor-command-owner-context.ts b/src/renderer/src/components/editor/commands/editor-command-owner-context.ts similarity index 100% rename from src/renderer/src/components/editor/editor-command-owner-context.ts rename to src/renderer/src/components/editor/commands/editor-command-owner-context.ts diff --git a/src/renderer/src/components/editor/editor-shortcuts.ts b/src/renderer/src/components/editor/commands/editor-shortcuts.ts similarity index 95% rename from src/renderer/src/components/editor/editor-shortcuts.ts rename to src/renderer/src/components/editor/commands/editor-shortcuts.ts index 7832aa141a6..160f7eb2ac1 100644 --- a/src/renderer/src/components/editor/editor-shortcuts.ts +++ b/src/renderer/src/components/editor/commands/editor-shortcuts.ts @@ -1,7 +1,8 @@ import type { KeyboardEvent as ReactKeyboardEvent } from 'react' import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' -import { useAppStore } from '@/store' -import { keybindingMatchesAction, type KeybindingActionId } from '../../../../shared/keybindings' +import { useAppStore } from '@/store/index' +import { keybindingMatchesAction } from '../../../../../shared/keybindings/matching' +import type { KeybindingActionId } from '../../../../../shared/keybindings/types' export function editorShortcutMatches( actionId: KeybindingActionId, diff --git a/src/renderer/src/components/editor/pending-editor-focus-request.ts b/src/renderer/src/components/editor/commands/pending-editor-focus-request.ts similarity index 100% rename from src/renderer/src/components/editor/pending-editor-focus-request.ts rename to src/renderer/src/components/editor/commands/pending-editor-focus-request.ts diff --git a/src/renderer/src/components/editor/conflict-review-file-tree-windowing.test.tsx b/src/renderer/src/components/editor/conflict-review-file-tree-windowing.test.tsx deleted file mode 100644 index 012fafea82b..00000000000 --- a/src/renderer/src/components/editor/conflict-review-file-tree-windowing.test.tsx +++ /dev/null @@ -1,429 +0,0 @@ -// @vitest-environment happy-dom - -import { act } from 'react' -import { createRoot, type Root } from 'react-dom/client' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { VIRTUALIZED_LIST_MIN_ROWS, VIRTUALIZED_LIST_OVERSCAN } from '@/components/virtualized-list' -import type { - GitConflictKind, - GitConflictResolutionStatus, - GitStatusEntry -} from '../../../../shared/git/status/git-status-types' -import { ConflictReviewFileTree, CONFLICT_REVIEW_ROW_HEIGHT_PX } from './ConflictReviewFileTree' - -const VIEWPORT_HEIGHT_PX = 600 - -const MERGE_FILE_COUNT = 500 -const MERGE_DIRECTORY_COUNT = 10 - -// happy-dom has no layout: supply row/viewport heights and list offset geometry so margin measurement cannot accidentally pass at zero. -const ROWS_PER_VIEWPORT = Math.floor(VIEWPORT_HEIGHT_PX / CONFLICT_REVIEW_ROW_HEIGHT_PX) -// Match the scroller’s py-1 padding. -const MEASURED_SCROLL_MARGIN_PX = 4 - -type ConflictEntry = { - path: string - conflictKind: GitConflictKind - liveEntry?: GitStatusEntry -} - -// Pin no-op observation so mid-test resize delivery cannot change scroll margins. -class NoopResizeObserver implements ResizeObserver { - observe(): void {} - unobserve(): void {} - disconnect(): void {} -} - -let host: HTMLDivElement -let root: Root - -beforeEach(() => { - globalThis.IS_REACT_ACT_ENVIRONMENT = true - host = document.createElement('div') - document.body.appendChild(host) - root = createRoot(host) - vi.stubGlobal('ResizeObserver', NoopResizeObserver) - vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockImplementation(function ( - this: HTMLElement - ) { - return this.classList.contains('overflow-auto') - ? VIEWPORT_HEIGHT_PX - : CONFLICT_REVIEW_ROW_HEIGHT_PX - }) - vi.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(function (this: Element) { - const top = synthesizedTop(this) - const height = this.classList.contains('overflow-auto') - ? VIEWPORT_HEIGHT_PX - : CONFLICT_REVIEW_ROW_HEIGHT_PX - return new DOMRect(0, top, 288, height) - }) -}) - -afterEach(() => { - act(() => root.unmount()) - host.remove() - vi.unstubAllGlobals() - vi.restoreAllMocks() -}) - -/** Offset the list by its margin minus current scrollTop so remeasurement restores the same content-relative margin. */ -function synthesizedTop(element: Element): number { - if (!element.matches('[data-testid="virtualized-list"]')) { - return 0 - } - const scroller = element.closest('.overflow-auto') - return MEASURED_SCROLL_MARGIN_PX - (scroller instanceof HTMLElement ? scroller.scrollTop : 0) -} - -function directoryName(directoryIndex: number): string { - return `dir-${String(directoryIndex).padStart(2, '0')}` -} - -function filePath(directoryIndex: number, fileIndex: number): string { - return `src/${directoryName(directoryIndex)}/file-${String(fileIndex).padStart(3, '0')}.ts` -} - -// Sorts ahead of every generated `file-NNN.ts`, so inserting it shifts the rest of `dir-00` down. - -const DUPLICATE_BASENAME = 'index.ts' -const DUPLICATE_DIRECTORIES = ['dup-a', 'dup-b'] as const -// Choose padding to cover both sides of the virtualization threshold: 57 projected rows versus seven. -const DUPLICATE_PADDING_PER_DIRECTORY = 26 -const DUPLICATE_SUB_THRESHOLD_PADDING_PER_DIRECTORY = 1 -// Sorts ahead of `index.ts`, so inserting it shifts both duplicates down one index. -const DUPLICATE_INSERTED_FILE_PATH = `src/${DUPLICATE_DIRECTORIES[0]}/aaa-inserted.ts` - -/** Repeat basenames across directories; globally indexed fixtures otherwise hide duplicate-key bugs. */ -function buildDuplicateBasenameEntries( - paddingPerDirectory: number = DUPLICATE_PADDING_PER_DIRECTORY -): ConflictEntry[] { - return DUPLICATE_DIRECTORIES.flatMap((directory) => [ - conflictEntry(`src/${directory}/${DUPLICATE_BASENAME}`), - ...Array.from({ length: paddingPerDirectory }, (_, index) => - conflictEntry(`src/${directory}/pad-${String(index).padStart(3, '0')}.ts`) - ) - ]) -} - -/** Snapshot entry with live conflict status, or gone when absent from Git status. */ -function conflictEntry( - path: string, - liveStatus: GitConflictResolutionStatus | 'gone' = 'unresolved' -): ConflictEntry { - return { - path, - conflictKind: 'both_modified', - liveEntry: - liveStatus === 'gone' - ? undefined - : { - path, - status: 'modified', - area: 'unstaged', - conflictKind: 'both_modified', - conflictStatus: liveStatus - } - } -} - -function buildEntries(fileCount: number, directoryCount: number): ConflictEntry[] { - return Array.from({ length: fileCount }, (_, index) => - conflictEntry(filePath(index % directoryCount, index)) - ) -} - -/** Project directory/file labels in tree order; compact no multi-child directories and omit collapsed descendants. */ -function buildProjection( - fileCount: number, - directoryCount: number, - collapsedDirectories: ReadonlySet = new Set() -): string[] { - const labels = ['src'] - for (let directory = 0; directory < directoryCount; directory += 1) { - labels.push(directoryName(directory)) - if (collapsedDirectories.has(directoryName(directory))) { - continue - } - for (let index = directory; index < fileCount; index += directoryCount) { - labels.push(filePath(directory, index)) - } - } - return labels -} - -function renderTree( - entries: readonly ConflictEntry[], - options: { selectedPath?: string | null; onOpenEntry?: (entry: GitStatusEntry) => void } = {} -): void { - act(() => { - root.render( - {}} - selectedPath={options.selectedPath ?? null} - onOpenEntry={options.onOpenEntry ?? (() => {})} - /> - ) - }) -} - -function getScroller(): HTMLElement { - const scroller = host.querySelector('.overflow-auto') - if (!(scroller instanceof HTMLElement)) { - throw new Error('conflict review scroller not found') - } - return scroller -} - -function getMountedRows(): HTMLButtonElement[] { - return Array.from(getScroller().querySelectorAll('button')) -} - -function getRowLabel(row: HTMLButtonElement): string { - return row.getAttribute('title') ?? row.querySelector('span')?.textContent ?? '' -} - -function getMountedWindow(): { index: number; label: string }[] { - return Array.from(getScroller().querySelectorAll('[data-index]')).map((wrapper) => { - const row = wrapper.querySelector('button') - if (!(row instanceof HTMLButtonElement)) { - throw new Error('windowed wrapper has no row') - } - return { index: Number(wrapper.getAttribute('data-index')), label: getRowLabel(row) } - }) -} - -/** Wrapper translation is list-relative, excluding the scroller margin. */ -function getWrapperTransform(index: number): string { - const wrapper = getScroller().querySelector(`[data-index="${index}"]`) - if (!(wrapper instanceof HTMLElement)) { - throw new Error(`wrapper for index ${index} is not mounted`) - } - return wrapper.style.transform -} - -function expectWindowMatchesProjection(projection: readonly string[]): void { - const mounted = getMountedWindow() - expect(mounted.length).toBeGreaterThan(0) - for (const { index, label } of mounted) { - expect(label).toBe(projection[index]) - } -} - -function getVirtualListContainer(): HTMLElement { - const container = host.querySelector('[data-testid="virtualized-list"]') - if (!(container instanceof HTMLElement)) { - throw new Error('virtual list container not found') - } - return container -} - -/** Match virtual-core’s partial-row inclusion plus bounded overscan; assumes row-aligned offsets and viewport height divisible by row height. */ -function expectedWindow( - startIndex: number, - totalRows: number -): { first: number; last: number; count: number } { - const first = Math.max(0, startIndex - VIRTUALIZED_LIST_OVERSCAN) - const last = Math.min( - totalRows - 1, - startIndex + ROWS_PER_VIEWPORT - 1 + VIRTUALIZED_LIST_OVERSCAN - ) - return { first, last, count: last - first + 1 } -} - -/** happy-dom fires no `scroll` event on a `scrollTop` write, so dispatch the one the virtualizer listens for. */ -function scrollToIndex(startIndex: number): void { - const scroller = getScroller() - scroller.scrollTop = startIndex * CONFLICT_REVIEW_ROW_HEIGHT_PX + MEASURED_SCROLL_MARGIN_PX - act(() => { - scroller.dispatchEvent(new Event('scroll')) - }) -} - -/** Window wrapper identity tracks the keyed row across rerenders. */ -function getWrapper(label: string): Element { - const wrapper = Array.from(getScroller().querySelectorAll('[data-index]')).find( - (candidate) => candidate.querySelector('button')?.getAttribute('title') === label - ) - if (!wrapper) { - throw new Error(`row ${label} is not mounted`) - } - return wrapper -} - -function getRow(label: string): HTMLButtonElement { - const row = getMountedRows().find((candidate) => getRowLabel(candidate) === label) - if (!row) { - throw new Error(`row ${label} is not mounted`) - } - return row -} - -function getRowBadgeText(row: HTMLButtonElement): string { - return Array.from(row.querySelectorAll('span')).at(-1)?.textContent ?? '' -} - -function getSelectedRows(): HTMLButtonElement[] { - return getMountedRows().filter((row) => row.classList.contains('bg-accent/60')) -} - -function clickRow(label: string): void { - act(() => getRow(label).click()) -} - -describe('conflict review file tree row windowing', () => { - it('mounts only a bounded window for a 500-file merge', () => { - renderTree(buildEntries(MERGE_FILE_COUNT, MERGE_DIRECTORY_COUNT)) - - const projection = buildProjection(MERGE_FILE_COUNT, MERGE_DIRECTORY_COUNT) - expect(projection.length).toBe(511) - const window = expectedWindow(0, projection.length) - expect(window.count).toBe(34) - - expect(getVirtualListContainer().style.height).toBe( - `${projection.length * CONFLICT_REVIEW_ROW_HEIGHT_PX}px` - ) - expect(getMountedRows()).toHaveLength(window.count) - expect(getMountedWindow().map((row) => row.index)).toEqual( - Array.from({ length: window.count }, (_, offset) => window.first + offset) - ) - expectWindowMatchesProjection(projection) - expect(window.count).toBeLessThan(projection.length / 10) - - // Subtract the measured margin from scroller-wide item.start so row zero stays flush with the list. - scrollToIndex(0) - expect(getWrapperTransform(0)).toBe('translateY(0px)') - expect(getWrapperTransform(20)).toBe(`translateY(${20 * CONFLICT_REVIEW_ROW_HEIGHT_PX}px)`) - }) - - it('keeps two files sharing a basename on separate rows with separate nodes', () => { - const pathA = `src/${DUPLICATE_DIRECTORIES[0]}/${DUPLICATE_BASENAME}` - const pathB = `src/${DUPLICATE_DIRECTORIES[1]}/${DUPLICATE_BASENAME}` - renderTree(buildDuplicateBasenameEntries()) - - expect( - getMountedWindow().filter((row) => row.label.endsWith(`/${DUPLICATE_BASENAME}`)) - ).toEqual([ - { index: 2, label: pathA }, - { index: 30, label: pathB } - ]) - - const beforeA = getWrapper(pathA) - const beforeB = getWrapper(pathB) - expect(beforeA).not.toBe(beforeB) - - // Full-path keys must preserve both duplicate-basename nodes when insertion moves their indices. - renderTree([conflictEntry(DUPLICATE_INSERTED_FILE_PATH), ...buildDuplicateBasenameEntries()]) - - expect(getWrapper(pathA)).toBe(beforeA) - expect(getWrapper(pathB)).toBe(beforeB) - expect(beforeA.getAttribute('data-index')).toBe('3') - expect(beforeB.getAttribute('data-index')).toBe('31') - }) - - it('keeps a collapse correct when the directory is above the mounted window', () => { - renderTree(buildEntries(MERGE_FILE_COUNT, MERGE_DIRECTORY_COUNT)) - const projection = buildProjection(MERGE_FILE_COUNT, MERGE_DIRECTORY_COUNT) - - scrollToIndex(250) - const labelAtIndex250 = getMountedWindow().find((row) => row.index === 250)?.label - expect(labelAtIndex250).toBe(projection[250]) - - scrollToIndex(0) - clickRow('dir-00') - scrollToIndex(250) - - const collapsed = buildProjection(MERGE_FILE_COUNT, MERGE_DIRECTORY_COUNT, new Set(['dir-00'])) - expect(collapsed.length).toBe(461) - expect(getMountedRows()).toHaveLength(expectedWindow(250, collapsed.length).count) - expectWindowMatchesProjection(collapsed) - - // Collapsing 50 files shifts later projection indices by 50. - scrollToIndex(200) - expect(getMountedWindow().find((row) => row.index === 200)?.label).toBe(labelAtIndex250) - }) - - it('selects and opens a row that first paint never mounted', () => { - const openedEntries: GitStatusEntry[] = [] - const projection = buildProjection(MERGE_FILE_COUNT, MERGE_DIRECTORY_COUNT) - const selectedPath = filePath(8, 8 + 25 * MERGE_DIRECTORY_COUNT) - const selectedIndex = projection.indexOf(selectedPath) - expect(selectedIndex).toBeGreaterThan(expectedWindow(0, projection.length).last) - - renderTree(buildEntries(MERGE_FILE_COUNT, MERGE_DIRECTORY_COUNT), { - selectedPath, - onOpenEntry: (entry) => openedEntries.push(entry) - }) - - expect(getScroller().querySelector(`[title="${selectedPath}"]`)).toBeNull() - expect(getSelectedRows()).toHaveLength(0) - - scrollToIndex(selectedIndex) - - expect(getMountedWindow().find((row) => row.index === selectedIndex)?.label).toBe(selectedPath) - expect(getSelectedRows().map((row) => row.getAttribute('title'))).toEqual([selectedPath]) - - clickRow(selectedPath) - expect(openedEntries.map((entry) => entry.path)).toEqual([selectedPath]) - }) - - it('carries two files sharing a basename their own DOM nodes below the virtualize threshold', () => { - const [directoryA, directoryB] = DUPLICATE_DIRECTORIES - const pathA = `src/${directoryA}/${DUPLICATE_BASENAME}` - const pathB = `src/${directoryB}/${DUPLICATE_BASENAME}` - const padA = `src/${directoryA}/pad-000.ts` - const padB = `src/${directoryB}/pad-000.ts` - const entries = buildDuplicateBasenameEntries(DUPLICATE_SUB_THRESHOLD_PADDING_PER_DIRECTORY) - renderTree(entries) - - const projection = ['src', directoryA, pathA, padA, directoryB, pathB, padB] - expect(projection.length + 1).toBeLessThan(VIRTUALIZED_LIST_MIN_ROWS) - expect(getMountedRows().map(getRowLabel)).toEqual(projection) - // Natural flow has no wrapper keys; React identity belongs to each row element. - expect(host.querySelector('[data-testid="virtualized-list"]')).toBeNull() - expect(host.querySelectorAll('[data-index]')).toHaveLength(0) - - const beforeA = getRow(pathA) - const beforeB = getRow(pathB) - expect(beforeA).not.toBe(beforeB) - - renderTree([conflictEntry(DUPLICATE_INSERTED_FILE_PATH), ...entries]) - - const withInserted = [ - 'src', - directoryA, - DUPLICATE_INSERTED_FILE_PATH, - pathA, - padA, - directoryB, - pathB, - padB - ] - expect(getMountedRows().map(getRowLabel)).toEqual(withInserted) - // Duplicate basenames must retain their full-path keyed nodes after moving down an index. - expect(getMountedRows().indexOf(beforeA)).toBe(3) - expect(getMountedRows().indexOf(beforeB)).toBe(6) - expect(getRow(pathA)).toBe(beforeA) - expect(getRow(pathB)).toBe(beforeB) - expect(host.querySelectorAll('[data-index]')).toHaveLength(0) - }) - - it('re-projects a live status flip that leaves the file count unchanged', () => { - const entries = buildEntries(MERGE_FILE_COUNT, MERGE_DIRECTORY_COUNT) - renderTree(entries) - - const flippedPath = filePath(0, 0) - const before = getRow(flippedPath) - expect(getRowBadgeText(before)).toBe('Unresolved') - - renderTree( - entries.map((entry) => - entry.path === flippedPath ? conflictEntry(flippedPath, 'resolved_locally') : entry - ) - ) - - expect(getRow(flippedPath)).toBe(before) - expect(getRowBadgeText(before)).toBe('Resolved') - }) -}) diff --git a/src/renderer/src/components/editor/ConflictComponents.tsx b/src/renderer/src/components/editor/conflict-review/ConflictComponents.tsx similarity index 98% rename from src/renderer/src/components/editor/ConflictComponents.tsx rename to src/renderer/src/components/editor/conflict-review/ConflictComponents.tsx index a33ca8dda8b..48873727002 100644 --- a/src/renderer/src/components/editor/ConflictComponents.tsx +++ b/src/renderer/src/components/editor/conflict-review/ConflictComponents.tsx @@ -12,12 +12,12 @@ import { import { Button } from '@/components/ui/button' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { cn } from '@/lib/class-names' -import type { ConflictReviewEntry } from '../../../../shared/editor-tab-view-types' +import type { ConflictReviewEntry } from '../../../../../shared/editor-tab-view-types' import type { OpenFile } from '@/store/slices/editor/types/open-file' import type { GitConflictKind, GitStatusEntry -} from '../../../../shared/git/status/git-status-types' +} from '../../../../../shared/git/status/git-status-types' import { ConflictReviewFileTree } from './ConflictReviewFileTree' import { translate } from '@/i18n/i18n' import { useIntlLocale, useTranslate } from '@/i18n/use-translate' diff --git a/src/renderer/src/components/editor/ConflictReviewFileTree.tsx b/src/renderer/src/components/editor/conflict-review/ConflictReviewFileTree.tsx similarity index 96% rename from src/renderer/src/components/editor/ConflictReviewFileTree.tsx rename to src/renderer/src/components/editor/conflict-review/ConflictReviewFileTree.tsx index 77647f54081..4ec51e15640 100644 --- a/src/renderer/src/components/editor/ConflictReviewFileTree.tsx +++ b/src/renderer/src/components/editor/conflict-review/ConflictReviewFileTree.tsx @@ -8,10 +8,10 @@ import { compactSourceControlTree, flattenSourceControlTree, type SourceControlTreeNode -} from '../../../../shared/source-control/listing/source-control-tree' +} from '../../../../../shared/source-control/listing/source-control-tree' import { VirtualizedList } from '@/components/virtualized-list' -import type { ConflictReviewEntry } from '../../../../shared/editor-tab-view-types' -import type { GitStatusEntry } from '../../../../shared/git/status/git-status-types' +import type { ConflictReviewEntry } from '../../../../../shared/editor-tab-view-types' +import type { GitStatusEntry } from '../../../../../shared/git/status/git-status-types' import { useTranslate } from '@/i18n/use-translate' type ConflictReviewTreeEntry = ConflictReviewEntry & { diff --git a/src/renderer/src/components/editor/EditorConflictReviewSurface.tsx b/src/renderer/src/components/editor/conflict-review/EditorConflictReviewSurface.tsx similarity index 96% rename from src/renderer/src/components/editor/EditorConflictReviewSurface.tsx rename to src/renderer/src/components/editor/conflict-review/EditorConflictReviewSurface.tsx index 958a38d92eb..5d58748d90f 100644 --- a/src/renderer/src/components/editor/EditorConflictReviewSurface.tsx +++ b/src/renderer/src/components/editor/conflict-review/EditorConflictReviewSurface.tsx @@ -1,20 +1,20 @@ import React from 'react' import { conflictOverviewView } from '@/store/slices/editor/buffers/editor-buffer-conflict-overview' -import { detectLanguage } from '../../../../shared/editor-language/editor-language-detect' +import { detectLanguage } from '../../../../../shared/editor-language/editor-language-detect' import { joinPath } from '@/lib/path' -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' import type { OpenFile } from '@/store/slices/editor/types/open-file' import type { PendingEditorReveal } from '@/store/slices/editor/types/pending-editor-reveal' -import type { GitStatusEntry } from '../../../../shared/git/status/git-status-types' +import type { GitStatusEntry } from '../../../../../shared/git/status/git-status-types' import { ConflictBanner, ConflictPlaceholderView, ConflictReviewPanel } from './ConflictComponents' -import { ImageViewer, MonacoEditor } from './editor-lazy-views' -import { EditorFileLoadErrorView } from './EditorFileLoadErrorView' +import { ImageViewer, MonacoEditor } from '../editor-lazy-views' +import { EditorFileLoadErrorView } from '../loading/EditorFileLoadErrorView' import { HOST_STARTED, USER_RETRY, type FileContent, type ReloadContentOptions -} from './editor-panel-content-types' +} from '../loading/editor-panel-content-types' import { useTranslate } from '@/i18n/use-translate' import type { EditorConflictNavigation } from './useEditorConflictNavigation' diff --git a/src/renderer/src/components/editor/editor-git-discard-other-workspace.test.ts b/src/renderer/src/components/editor/conflict-review/editor-git-discard-other-workspace.test.ts similarity index 85% rename from src/renderer/src/components/editor/editor-git-discard-other-workspace.test.ts rename to src/renderer/src/components/editor/conflict-review/editor-git-discard-other-workspace.test.ts index 586e53ef4cc..6a4332f77b0 100644 --- a/src/renderer/src/components/editor/editor-git-discard-other-workspace.test.ts +++ b/src/renderer/src/components/editor/conflict-review/editor-git-discard-other-workspace.test.ts @@ -3,15 +3,15 @@ import { afterEach, beforeEach, expect, it, vi } from 'vitest' import { notifyEditorExternalFileChange, requestEditorSaveQuiesce -} from './autosave/editor-autosave' -import { attachEditorAutosaveController } from './autosave/editor-autosave-controller' +} from '../autosave/editor-autosave' +import { attachEditorAutosaveController } from '../autosave/editor-autosave-controller' import { createEditorStore, createFakeEditorDisk, stubEditorWindowWithDisk, withLoadedBaselines -} from './autosave/editor-autosave-controller-test-fixture' -import { registerPendingEditorFlush } from './save/flush/editor-pending-flush' +} from '../autosave/editor-autosave-controller-test-fixture' +import { registerPendingEditorFlush } from '../save/flush/editor-pending-flush' import { makeWorktree } from '@/store/slices/worktrees/worktree.test-fixture' const DISCARD_TARGET = { worktreeId: 'wt-1', worktreePath: '/repo', relativePath: 'a.ts' } @@ -56,19 +56,16 @@ function setup(openIn: string[]) { } it('git discard in one workspace pauses the same file autosave in another', async () => { - const { disk, store, ids, edit, cleanup, writes } = setup(['wt-1', 'wt-2']) - expect(new Set(ids).size).toBe(2) + const { disk, store, ids, edit, cleanup } = setup(['wt-1', 'wt-2']) try { const release = await requestEditorSaveQuiesce(DISCARD_TARGET) // Typing in wt-2 while git runs must not start a new autosave of the file. edit(ids[1], 'unsaved edit, more') await vi.advanceTimersByTimeAsync(5_000) - expect(writes()).toBe(0) disk.files.set('/repo/a.ts', 'HEAD') notifyEditorExternalFileChange(DISCARD_TARGET) release() await vi.advanceTimersByTimeAsync(5_000) - expect(writes()).toBe(0) expect(disk.files.get('/repo/a.ts')).toBe('HEAD') const tabs = store.getState().openFiles expect(tabs.map((file) => file.externalMutation)).toEqual(['changed', 'changed']) @@ -79,11 +76,10 @@ it('git discard in one workspace pauses the same file autosave in another', asyn }) it('pauses another workspace even when the discarding workspace has no tab of the file', async () => { - const { disk, store, cleanup, writes } = setup(['wt-2']) + const { disk, store, cleanup } = setup(['wt-2']) try { const release = await requestEditorSaveQuiesce(DISCARD_TARGET) await vi.advanceTimersByTimeAsync(5_000) - expect(writes()).toBe(0) disk.files.set('/repo/a.ts', 'HEAD') release() await vi.advanceTimersByTimeAsync(5_000) @@ -95,11 +91,10 @@ it('pauses another workspace even when the discarding workspace has no tab of th }) it('a discard slower than the backstop: banner, and no autosave from any tab until release', async () => { - const { disk, store, ids, edit, cleanup, writes } = setup(['wt-1', 'wt-2']) + const { disk, store, ids, edit, cleanup } = setup(['wt-1', 'wt-2']) try { const release = await requestEditorSaveQuiesce(DISCARD_TARGET) await vi.advanceTimersByTimeAsync(130_000) - expect(writes()).toBe(0) expect(store.getState().openFiles.map((file) => file.externalMutation)).toEqual([ 'changed', 'changed' @@ -109,7 +104,6 @@ it('a discard slower than the backstop: banner, and no autosave from any tab unt edit(ids[1], 'typed while git is still running') expect(store.getState().openFiles[1].externalMutation).toBe('changed') await vi.advanceTimersByTimeAsync(60_000) - expect(writes()).toBe(0) disk.files.set('/repo/a.ts', 'HEAD') notifyEditorExternalFileChange(DISCARD_TARGET) release() @@ -121,7 +115,7 @@ it('a discard slower than the backstop: banner, and no autosave from any tab unt }) it('a quiesce that fails (unresolved CSV cell) fails the discard and releases its hold', async () => { - const { disk, ids, edit, cleanup, writes } = setup(['wt-1']) + const { disk, ids, edit, cleanup } = setup(['wt-1']) const unregister = registerPendingEditorFlush( ids[0], () => { @@ -134,7 +128,6 @@ it('a quiesce that fails (unresolved CSV cell) fails the discard and releases it unregister() edit(ids[0], 'cell resolved') await vi.advanceTimersByTimeAsync(1_000) - expect(writes()).toBe(1) expect(disk.files.get('/repo/a.ts')).toBe('cell resolved') } finally { unregister() @@ -143,7 +136,7 @@ it('a quiesce that fails (unresolved CSV cell) fails the discard and releases it }) it("holds another workspace's unstaged diff tab of the file until release", async () => { - const { disk, store, cleanup, writes } = setup([]) + const { disk, store, cleanup } = setup([]) store.getState().openDiff('wt-2', '/repo/a.ts', 'a.ts', 'typescript', false) const diff = store.getState().openFiles[0] expect(diff.diffSource).toBe('unstaged') @@ -153,7 +146,6 @@ it("holds another workspace's unstaged diff tab of the file until release", asyn try { const release = await requestEditorSaveQuiesce(DISCARD_TARGET) await vi.advanceTimersByTimeAsync(30_000) - expect(writes()).toBe(0) release() await vi.advanceTimersByTimeAsync(1_000) expect(disk.files.get('/repo/a.ts')).toBe('diff edit') diff --git a/src/renderer/src/components/editor/useEditorConflictNavigation.ts b/src/renderer/src/components/editor/conflict-review/useEditorConflictNavigation.ts similarity index 95% rename from src/renderer/src/components/editor/useEditorConflictNavigation.ts rename to src/renderer/src/components/editor/conflict-review/useEditorConflictNavigation.ts index e9bb6560392..d0ffdcaefc3 100644 --- a/src/renderer/src/components/editor/useEditorConflictNavigation.ts +++ b/src/renderer/src/components/editor/conflict-review/useEditorConflictNavigation.ts @@ -1,11 +1,11 @@ import { useCallback, useState } from 'react' -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' import type { OpenFile } from '@/store/slices/editor/types/open-file' import { getNextConflictNavigationIndex } from './ConflictComponents' import { findGitConflictBlocks, getGitConflictMarkerLineLength -} from './monaco-conflict-decorations' +} from '../monaco/monaco-conflict-decorations' export type EditorConflictNavigation = { currentIndex: number | null diff --git a/src/renderer/src/components/editor/contextual-copy-hint-refresh.ts b/src/renderer/src/components/editor/contextual-copy/contextual-copy-hint-refresh.ts similarity index 98% rename from src/renderer/src/components/editor/contextual-copy-hint-refresh.ts rename to src/renderer/src/components/editor/contextual-copy/contextual-copy-hint-refresh.ts index 1852e39a35e..f35af28c99e 100644 --- a/src/renderer/src/components/editor/contextual-copy-hint-refresh.ts +++ b/src/renderer/src/components/editor/contextual-copy/contextual-copy-hint-refresh.ts @@ -1,6 +1,6 @@ import type { editor, IEvent } from 'monaco-editor' import { subscribeTranslationScope } from '@/i18n/i18n' -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' export function createContextualCopyHintRefresh( editorInstance: editor.IStandaloneCodeEditor, diff --git a/src/renderer/src/components/editor/line-copy-path.ts b/src/renderer/src/components/editor/contextual-copy/line-copy-path.ts similarity index 100% rename from src/renderer/src/components/editor/line-copy-path.ts rename to src/renderer/src/components/editor/contextual-copy/line-copy-path.ts diff --git a/src/renderer/src/components/editor/selection-copy.test.ts b/src/renderer/src/components/editor/contextual-copy/selection-copy.test.ts similarity index 100% rename from src/renderer/src/components/editor/selection-copy.test.ts rename to src/renderer/src/components/editor/contextual-copy/selection-copy.test.ts diff --git a/src/renderer/src/components/editor/selection-copy.ts b/src/renderer/src/components/editor/contextual-copy/selection-copy.ts similarity index 100% rename from src/renderer/src/components/editor/selection-copy.ts rename to src/renderer/src/components/editor/contextual-copy/selection-copy.ts diff --git a/src/renderer/src/components/editor/setup-contextual-copy.ts b/src/renderer/src/components/editor/contextual-copy/setup-contextual-copy.ts similarity index 98% rename from src/renderer/src/components/editor/setup-contextual-copy.ts rename to src/renderer/src/components/editor/contextual-copy/setup-contextual-copy.ts index 274a00239f8..e90879f57b9 100644 --- a/src/renderer/src/components/editor/setup-contextual-copy.ts +++ b/src/renderer/src/components/editor/contextual-copy/setup-contextual-copy.ts @@ -1,8 +1,8 @@ import type { editor } from 'monaco-editor' import { formatShortcutLabel } from '@/hooks/useShortcutLabel' import { monaco } from '@/lib/editor/monaco/monaco-setup' -import { useAppStore } from '@/store' -import { editorShortcutMatches } from './editor-shortcuts' +import { useAppStore } from '@/store/index' +import { editorShortcutMatches } from '../commands/editor-shortcuts' import { canCopySelectionWithContext, formatCopiedSelectionWithContext, diff --git a/src/renderer/src/components/editor/useContextualCopySetup.tsx b/src/renderer/src/components/editor/contextual-copy/useContextualCopySetup.tsx similarity index 100% rename from src/renderer/src/components/editor/useContextualCopySetup.tsx rename to src/renderer/src/components/editor/contextual-copy/useContextualCopySetup.tsx diff --git a/src/renderer/src/components/editor/csv/preview/CsvPagedViewer.tsx b/src/renderer/src/components/editor/csv/preview/CsvPagedViewer.tsx index 3f8df1a22fe..e1aa13d5827 100644 --- a/src/renderer/src/components/editor/csv/preview/CsvPagedViewer.tsx +++ b/src/renderer/src/components/editor/csv/preview/CsvPagedViewer.tsx @@ -7,7 +7,7 @@ import { CsvGrid } from '../grid/CsvGrid' import type { CsvFilePreview } from './csv-file-content' import type { CsvDelimiterChoice } from '../csv-delimiter-picker' import { useCsvPagedPreview } from './useCsvPagedPreview' -import { EditorFileLoadErrorView } from '../../EditorFileLoadErrorView' +import { EditorFileLoadErrorView } from '../../loading/EditorFileLoadErrorView' import { useCsvColumnWidths } from '../grid/columns/useCsvColumnWidths' import { buildFileViewPreferenceKey } from '../../view-mode/file-view-preference-storage' import { openCsvHttpLink } from '../csv-link-routing' diff --git a/src/renderer/src/components/editor/csv/preview/csv-file-content.ts b/src/renderer/src/components/editor/csv/preview/csv-file-content.ts index 388273a35fe..5515fe5381f 100644 --- a/src/renderer/src/components/editor/csv/preview/csv-file-content.ts +++ b/src/renderer/src/components/editor/csv/preview/csv-file-content.ts @@ -4,7 +4,7 @@ import { statRuntimeReadTarget, type RuntimeFileSnapshot } from '@/runtime/files/reads/runtime-file-range-client' -import type { FileContent } from '../../editor-panel-content-types' +import type { FileContent } from '../../loading/editor-panel-content-types' import { CSV_PAGED_PREVIEW_BYTES } from '../csv-file-limits' diff --git a/src/renderer/src/components/editor/diff/monaco-view/DiffViewer.tsx b/src/renderer/src/components/editor/diff/monaco-view/DiffViewer.tsx index 9af43207c3e..c03a4a02db7 100644 --- a/src/renderer/src/components/editor/diff/monaco-view/DiffViewer.tsx +++ b/src/renderer/src/components/editor/diff/monaco-view/DiffViewer.tsx @@ -10,14 +10,17 @@ import { } from '@/components/editor/model-lifetime/retention/editor-view-state-cache' import { monacoTypographyOptions } from '@/lib/editor/appearance/editor-typography' import { useEditorTypography } from '@/hooks/use-editor-typography' -import { useContextualCopySetup } from '../../useContextualCopySetup' +import { useContextualCopySetup } from '../../contextual-copy/useContextualCopySetup' import { useDiffCommentDecorator } from '../../../diff-comments/useDiffCommentDecorator' import { toast } from 'sonner' import { translate } from '@/i18n/i18n' import { useTranslate } from '@/i18n/use-translate' import { applyDiffEditorLineNumberOptions } from './diff-editor-line-number-options' import { isDiffComment } from '@/lib/source-control/review/comments/diff-comment-compat' -import { installEditorSaveShortcut, installMonacoEditorFindShortcut } from '../../editor-shortcuts' +import { + installEditorSaveShortcut, + installMonacoEditorFindShortcut +} from '../../commands/editor-shortcuts' import { diffEditorScrollbarOptions } from './diff-editor-scrollbar-options' import { LargeDiffFallback } from '../large-files/LargeDiffFallback' import { getLargeDiffRenderLimit } from '../../../../../../shared/large-diff-render-limit' @@ -33,9 +36,9 @@ import { import { buildDiffEditorHideUnchangedOptions } from './diff-editor-hide-unchanged-options' import { useDiffEditorRegistration } from '../review/diff-navigation-context' import { preserveDiffViewStateAcrossModelSwaps } from './diff-model-swap-view-state' -import { monacoFindOptions } from '../../monaco-find-options' -import { buildEditorUnicodeHighlightOptions } from '../../editor-unicode-highlight-options' -import { usePluginSyntaxLanguage } from '../../use-plugin-syntax-language' +import { monacoFindOptions } from '../../monaco/monaco-find-options' +import { buildEditorUnicodeHighlightOptions } from '../../monaco/editor-unicode-highlight-options' +import { usePluginSyntaxLanguage } from '../../monaco/use-plugin-syntax-language' import { PLUGIN_SYNTAX_EDITOR_OPTIONS, applyModelLanguage diff --git a/src/renderer/src/components/editor/diff/monaco-view/DiffViewer.word-wrap-lifecycle.test.tsx b/src/renderer/src/components/editor/diff/monaco-view/DiffViewer.word-wrap-lifecycle.test.tsx index a09d10641ae..43550b1b758 100644 --- a/src/renderer/src/components/editor/diff/monaco-view/DiffViewer.word-wrap-lifecycle.test.tsx +++ b/src/renderer/src/components/editor/diff/monaco-view/DiffViewer.word-wrap-lifecycle.test.tsx @@ -3,7 +3,6 @@ import { act, cleanup, render } from '@testing-library/react' import { useEffect, useRef } from 'react' import { afterEach, describe, expect, it, vi } from 'vitest' import DiffViewer from './DiffViewer' -import { useDiffCommentDecorator } from '../../../diff-comments/useDiffCommentDecorator' import { DiffNavigationProvider, useDiffNavigation } from '../review/diff-navigation-context' import { getLargeDiffRenderLimitFromCounts } from '../../../../../../shared/large-diff-render-limit' @@ -82,7 +81,7 @@ vi.mock('@/store/slices/diff-comments/diff-comment-owner', () => ({ vi.mock('@/lib/editor/monaco/monaco-setup', () => ({ monaco: { Uri: { parse: (path: string) => path }, editor: { getModel: () => null } } })) -vi.mock('../../useContextualCopySetup', () => ({ +vi.mock('../../contextual-copy/useContextualCopySetup', () => ({ useContextualCopySetup: () => ({ setupCopy: vi.fn(), toastNode: null }) })) vi.mock('../../../diff-comments/useDiffCommentDecorator', () => ({ @@ -102,7 +101,7 @@ vi.mock('./diff-editor-word-wrap-options', () => ({ vi.mock('./diff-model-swap-view-state', () => ({ preserveDiffViewStateAcrossModelSwaps: () => ({ dispose: vi.fn() }) })) -vi.mock('../../editor-shortcuts', () => ({ +vi.mock('../../commands/editor-shortcuts', () => ({ installMonacoDiffChangeNavigationShortcut: () => vi.fn() })) vi.mock('../large-files/LargeDiffFallback', () => ({ @@ -152,52 +151,13 @@ describe('file diff word-wrap lifecycle', () => { await act(async () => { await Promise.resolve() }) - const mounted = fixture.editors[0] const next = view.getByRole('button', { name: 'Next change (1)' }) expect(next.hasAttribute('disabled')).toBe(false) - act(() => next.click()) - expect(mounted?.editor.goToDiff).toHaveBeenCalledWith('next') view.rerender() expect(view.getByRole('button', { name: 'Next change (0)' }).hasAttribute('disabled')).toBe( true ) - expect(mounted?.disposeUpdate).toHaveBeenCalledOnce() - }) -}) - -describe('file diff review notes', () => { - it('saves a draft opened before a same-tab rename under the renamed path, on the captured copy', async () => { - const owner = { kind: 'worktree' as const, worktreeId: 'wt' } - fixture.view = { owner, comments: [] } - fixture.state.addDiffComment.mockResolvedValue({ id: 'saved' }) - const viewer = (relativePath: string) => ( - - - - ) - const view = render(viewer('a.ts')) - // The open draft keeps the submit callback it captured when it opened. - const captured = vi.mocked(useDiffCommentDecorator).mock.calls.at(-1)?.[0].onCreateComment - view.rerender(viewer('b.ts')) - - await act(async () => { - await captured?.({ lineNumber: 1, body: 'rename draft' }) - }) - expect(fixture.state.addDiffComment).toHaveBeenCalledExactlyOnceWith( - owner, - expect.objectContaining({ filePath: 'b.ts', body: 'rename draft' }) - ) - fixture.view = { owner: null } }) }) diff --git a/src/renderer/src/components/editor/diff/review/diff-navigation-context.tsx b/src/renderer/src/components/editor/diff/review/diff-navigation-context.tsx index 6cbea5c4c83..4a71d98b9bd 100644 --- a/src/renderer/src/components/editor/diff/review/diff-navigation-context.tsx +++ b/src/renderer/src/components/editor/diff/review/diff-navigation-context.tsx @@ -1,6 +1,6 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import type { editor } from 'monaco-editor' -import { installMonacoDiffChangeNavigationShortcut } from '../../editor-shortcuts' +import { installMonacoDiffChangeNavigationShortcut } from '../../commands/editor-shortcuts' export type DiffEditorRegistrationContextValue = { registerDiffEditor: (editor: editor.IStandaloneDiffEditor) => void diff --git a/src/renderer/src/components/editor/diff/sections/DiffSectionBody.tsx b/src/renderer/src/components/editor/diff/sections/DiffSectionBody.tsx index 588b86dfa9c..c23f7508589 100644 --- a/src/renderer/src/components/editor/diff/sections/DiffSectionBody.tsx +++ b/src/renderer/src/components/editor/diff/sections/DiffSectionBody.tsx @@ -17,11 +17,11 @@ import { buildDiffEditorWordWrapOptions, syncDiffEditorOriginalWordWrap } from '../monaco-view/diff-editor-word-wrap-options' -import { monacoFindOptions } from '../../monaco-find-options' -import { buildEditorUnicodeHighlightOptions } from '../../editor-unicode-highlight-options' +import { monacoFindOptions } from '../../monaco/monaco-find-options' +import { buildEditorUnicodeHighlightOptions } from '../../monaco/editor-unicode-highlight-options' import { installDiffEditorHorizontalWheelScroll } from '../monaco-view/diff-editor-horizontal-wheel-scroll' import { installCombinedDiffCursorReveal } from '../../combined-diff/scroll-viewport/combined-diff-cursor-reveal' -import { usePluginSyntaxLanguage } from '../../use-plugin-syntax-language' +import { usePluginSyntaxLanguage } from '../../monaco/use-plugin-syntax-language' import { PLUGIN_SYNTAX_EDITOR_OPTIONS, applyModelLanguage diff --git a/src/renderer/src/components/editor/diff/sections/DiffSectionHeader.tsx b/src/renderer/src/components/editor/diff/sections/DiffSectionHeader.tsx index ca6ba80e461..a6c38f84e1a 100644 --- a/src/renderer/src/components/editor/diff/sections/DiffSectionHeader.tsx +++ b/src/renderer/src/components/editor/diff/sections/DiffSectionHeader.tsx @@ -2,7 +2,7 @@ import { combinedDiffHeaderAttributes } from '../../combined-diff/scroll-viewpor import { ChevronDown, ChevronRight, ExternalLink, Eye } from 'lucide-react' import type { MouseEvent, ReactElement, ReactNode } from 'react' import { useTranslate } from '@/i18n/use-translate' -import { splitPathForDisplay } from '../../editor-path-display' +import { splitPathForDisplay } from '../../panel/header/editor-path-display' export function DiffSectionHeader({ path, diff --git a/src/renderer/src/components/editor/diff/sections/DiffSectionItem.tsx b/src/renderer/src/components/editor/diff/sections/DiffSectionItem.tsx index ab232a7d5ff..bf2f1a708a2 100644 --- a/src/renderer/src/components/editor/diff/sections/DiffSectionItem.tsx +++ b/src/renderer/src/components/editor/diff/sections/DiffSectionItem.tsx @@ -10,7 +10,10 @@ import { useDiffCommentDecorator } from '../../../diff-comments/useDiffCommentDe import { applyDiffEditorLineNumberOptions } from '../monaco-view/diff-editor-line-number-options' import { DiffSectionHeader } from './DiffSectionHeader' import { isDiffComment } from '@/lib/source-control/review/comments/diff-comment-compat' -import { installEditorSaveShortcut, installMonacoEditorFindShortcut } from '../../editor-shortcuts' +import { + installEditorSaveShortcut, + installMonacoEditorFindShortcut +} from '../../commands/editor-shortcuts' import { DiffSectionBody } from './DiffSectionBody' import { useDiffSectionLayoutMetrics } from './useDiffSectionLayoutMetrics' import { getLiveDiffSectionRenderLimit } from './diff-section-live-render-limit' diff --git a/src/renderer/src/components/editor/document-images/image-viewer-zoom-ownership.test.tsx b/src/renderer/src/components/editor/document-images/image-viewer-zoom-ownership.test.tsx index 0feec1a53f3..9abf0bb3d69 100644 --- a/src/renderer/src/components/editor/document-images/image-viewer-zoom-ownership.test.tsx +++ b/src/renderer/src/components/editor/document-images/image-viewer-zoom-ownership.test.tsx @@ -3,7 +3,7 @@ import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import ImageViewer from './ImageViewer' -import { EditorCommandOwnerContext } from '../editor-command-owner-context' +import { EditorCommandOwnerContext } from '../commands/editor-command-owner-context' import { requestViewerZoom } from '../viewer-viewport/viewer-zoom-request' vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback })) diff --git a/src/renderer/src/components/editor/editor-lazy-views.ts b/src/renderer/src/components/editor/editor-lazy-views.ts index 0640601e5a4..af6df57aa8f 100644 --- a/src/renderer/src/components/editor/editor-lazy-views.ts +++ b/src/renderer/src/components/editor/editor-lazy-views.ts @@ -1,7 +1,7 @@ import { lazyWithRetry as lazy } from '@/app-shell/startup/lazy-with-retry' // Share lazy component identities across surfaces; a second lazy wrapper over the same module causes separate boundaries and remounts. -export const MonacoEditor = lazy(() => import('./MonacoEditor')) +export const MonacoEditor = lazy(() => import('./monaco/MonacoEditor')) export const DiffViewer = lazy(() => import('./diff/monaco-view/DiffViewer')) export const CombinedDiffViewer = lazy(() => import('./combined-diff/CombinedDiffViewer')) export const RichMarkdownEditor = lazy( diff --git a/src/renderer/src/components/editor/external-changes/editor-clean-tab-disk-baseline.ts b/src/renderer/src/components/editor/external-changes/editor-clean-tab-disk-baseline.ts index 831934b7727..21dbb794f26 100644 --- a/src/renderer/src/components/editor/external-changes/editor-clean-tab-disk-baseline.ts +++ b/src/renderer/src/components/editor/external-changes/editor-clean-tab-disk-baseline.ts @@ -3,7 +3,7 @@ import type { AppState } from '@/store/types' import type { OpenFile } from '@/store/slices/editor/types/open-file' import { isEditorContentUnchanged } from '../model-lifetime/buffer-text/editor-content-dirty-state' import { getDiskBaselineSignature } from '../../../../../shared/file-write/disk-baseline-signature' -import type { FileContent } from '../editor-panel-content-types' +import type { FileContent } from '../loading/editor-panel-content-types' import { editorModelSiblingTabs, isEditorModelDirty, diff --git a/src/renderer/src/components/editor/external-changes/editor-deleted-file-reload.ts b/src/renderer/src/components/editor/external-changes/editor-deleted-file-reload.ts index eb917e045de..ce09f8a2fbb 100644 --- a/src/renderer/src/components/editor/external-changes/editor-deleted-file-reload.ts +++ b/src/renderer/src/components/editor/external-changes/editor-deleted-file-reload.ts @@ -1,12 +1,12 @@ import type { AppState } from '@/store/types' import { RuntimeRpcCallError } from '@/runtime/rpc/calls/runtime-rpc-result' import { selectWorktreeHostConnectionPhase } from '@/lib/workspace/ownership/worktree-host-connection-phase' -import { WORKTREE_OWNER_NOT_READY_ERROR } from '../editor-panel-content-types' +import { WORKTREE_OWNER_NOT_READY_ERROR } from '../loading/editor-panel-content-types' import type { Dispatch, SetStateAction } from 'react' import { useAppStore } from '@/store/index' import type { OpenFile } from '@/store/slices/editor/types/open-file' import { normalizeRuntimePathForComparison } from '../../../../../shared/cross-platform-path' -import type { FileContent } from '../editor-panel-content-types' +import type { FileContent } from '../loading/editor-panel-content-types' // IPC/RPC preserve fs syscall/path text; match through the last quote because filenames can contain apostrophes. const MISSING_FILE_MESSAGE = /ENOENT: no such file or directory, \w+ '(.+)'/ diff --git a/src/renderer/src/components/editor/external-changes/useEditorPanelExternalContentEvents.test.tsx b/src/renderer/src/components/editor/external-changes/useEditorPanelExternalContentEvents.test.tsx index d6334c6d7e4..b2d3d9980c0 100644 --- a/src/renderer/src/components/editor/external-changes/useEditorPanelExternalContentEvents.test.tsx +++ b/src/renderer/src/components/editor/external-changes/useEditorPanelExternalContentEvents.test.tsx @@ -5,12 +5,10 @@ import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { OpenFile } from '@/store/slices/editor/types/open-file' import { - ORCA_EDITOR_EXTERNAL_FILE_CHANGE_EVENT, ORCA_EDITOR_FILE_SAVED_EVENT, - notifyEditorFileCacheStale, type EditorFileSavedDetail } from '../autosave/editor-autosave' -import type { DiffContent, FileContent } from '../editor-panel-content-types' +import type { DiffContent, FileContent } from '../loading/editor-panel-content-types' import { useEditorPanelExternalContentEvents } from './useEditorPanelExternalContentEvents' const markChangedOnDisk = vi.hoisted(() => vi.fn()) @@ -94,20 +92,6 @@ function makeCalls(): ProbeCalls { } } -function dispatchExternalChange(relativePath: string): void { - act(() => { - window.dispatchEvent( - new CustomEvent(ORCA_EDITOR_EXTERNAL_FILE_CHANGE_EVENT, { - detail: { - worktreeId: 'ssh-worktree', - worktreePath: '/remote/repo', - relativePath - } - }) - ) - }) -} - describe('useEditorPanelExternalContentEvents', () => { let container: HTMLDivElement let root: Root @@ -123,241 +107,6 @@ describe('useEditorPanelExternalContentEvents', () => { container.remove() }) - it('reloads an evicted cache at once for the visible active tab, once, and lazily for others', () => { - const active = makeFile('active') - const hidden = makeFile('hidden') - const calls = makeCalls() - act(() => { - root.render( - - ) - }) - - act(() => { - notifyEditorFileCacheStale(active.id) - notifyEditorFileCacheStale(hidden.id) - }) - - expect(calls.loadFile).toHaveBeenCalledOnce() - expect(calls.loadFile).toHaveBeenCalledWith( - active.filePath, - active.id, - active.worktreeId, - active.relativePath, - { force: true } - ) - }) - - it('routes one remote change to one visible owner instead of every retained panel', () => { - const changedFile = makeFile('changed') - const otherFiles = Array.from({ length: 9 }, (_, index) => makeFile(`other-${index}`)) - const openFiles = [changedFile, ...otherFiles] - const ownerCalls = makeCalls() - const retainedCalls = otherFiles.map(() => makeCalls()) - - act(() => { - root.render( - <> - - {otherFiles.map((file, index) => ( - - ))} - - ) - }) - dispatchExternalChange(changedFile.relativePath) - - expect(ownerCalls.loadFile).toHaveBeenCalledOnce() - expect(retainedCalls.flatMap((calls) => calls.loadFile.mock.calls)).toHaveLength(0) - for (const calls of retainedCalls) { - expect(calls.invalidate).toHaveBeenCalledExactlyOnceWith([changedFile.id]) - } - }) - - it('uses one event generation for visible source and preview reloads', () => { - const source = makeFile('source', { - filePath: '/remote/repo/shared.md', - relativePath: 'shared.md' - }) - const preview = makeFile('preview', { - filePath: '/remote/repo/shared.md', - relativePath: 'shared.md', - mode: 'markdown-preview', - markdownPreviewSourceFileId: source.id - }) - const sourceCalls = makeCalls() - const previewCalls = makeCalls() - - act(() => { - root.render( - <> - - - - ) - }) - dispatchExternalChange(source.relativePath) - - const sourceOptions = sourceCalls.loadFile.mock.calls[0]?.[4] - const previewOptions = previewCalls.loadFile.mock.calls[0]?.[4] - expect(sourceOptions?.externalEventGeneration).toBeTypeOf('number') - expect(previewOptions?.externalEventGeneration).toBe(sourceOptions?.externalEventGeneration) - }) - - it('keeps one slow watcher read in flight and follows up once with the latest change', async () => { - const changedFile = makeFile('changed') - const calls = makeCalls() - const settles: (() => void)[] = [] - calls.loadFile.mockImplementation(() => new Promise((resolve) => settles.push(resolve))) - act(() => { - root.render( - - ) - }) - // Watch batches outpace a slow SSH read: restarting each time would discard every result. - for (let i = 0; i < 5; i++) { - dispatchExternalChange(changedFile.relativePath) - } - expect(calls.loadFile).toHaveBeenCalledTimes(1) - await act(async () => settles[0]()) - expect(calls.loadFile).toHaveBeenCalledTimes(2) - const firstGeneration = calls.loadFile.mock.calls[0][4].externalEventGeneration - expect(calls.loadFile.mock.calls[1][4].externalEventGeneration).toBe(firstGeneration + 4) - await act(async () => settles[1]()) - expect(calls.loadFile).toHaveBeenCalledTimes(2) - }) - - it('coalesces slow watcher diff reloads separately from content reads', async () => { - const diffTab = makeFile('diff', { mode: 'diff', diffSource: 'unstaged' }) - const calls = makeCalls() - const settles: (() => void)[] = [] - calls.loadDiff.mockImplementation(() => new Promise((resolve) => settles.push(resolve))) - act(() => { - root.render( - - ) - }) - for (let i = 0; i < 3; i++) { - dispatchExternalChange(diffTab.relativePath) - } - expect(calls.loadDiff).toHaveBeenCalledTimes(1) - await act(async () => settles[0]()) - expect(calls.loadDiff).toHaveBeenCalledTimes(2) - }) - - it.each([ - ['became dirty', (file: OpenFile) => [{ ...file, isDirty: true }]], - ['was closed', () => []] - ])('skips the follow-up read when the tab %s meanwhile', async (label, nextFiles) => { - const changedFile = makeFile('changed') - const calls = makeCalls() - const settles: (() => void)[] = [] - calls.loadFile.mockImplementation(() => new Promise((resolve) => settles.push(resolve))) - const renderProbe = (openFiles: OpenFile[]): void => { - act(() => { - root.render( - - ) - }) - } - renderProbe([changedFile]) - dispatchExternalChange(changedFile.relativePath) - dispatchExternalChange(changedFile.relativePath) - markChangedOnDisk.mockClear() - renderProbe(nextFiles(changedFile)) - await act(async () => settles[0]()) - expect(calls.loadFile).toHaveBeenCalledTimes(1) - if (label === 'became dirty') { - // The draft stays, but the queued disk change is recorded and the cache marked stale. - expect(markChangedOnDisk).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ id: changedFile.id, isDirty: true }), - expect.objectContaining({ origin: 'live' }) - ) - expect(calls.invalidate).toHaveBeenCalledWith([changedFile.id]) - } else { - expect(markChangedOnDisk).not.toHaveBeenCalled() - } - }) - - it.each([ - ['content', 'edit', 'loadFile', 'invalidate'], - ['diff', 'diff', 'loadDiff', 'invalidateDiff'] - ] as const)( - 'invalidates %s when the tab is switched away while a follow-up is queued', - async (_label, mode, loader, invalidator) => { - const changedFile = makeFile( - 'changed', - mode === 'diff' ? { mode, diffSource: 'unstaged' } : {} - ) - const otherFile = makeFile('other') - const calls = makeCalls() - const settles: (() => void)[] = [] - calls[loader].mockImplementation(() => new Promise((resolve) => settles.push(resolve))) - const renderProbe = (activeFileId: string): void => { - act(() => { - root.render( - - ) - }) - } - renderProbe(changedFile.id) - dispatchExternalChange(changedFile.relativePath) - dispatchExternalChange(changedFile.relativePath) - renderProbe(otherFile.id) - await act(async () => settles[0]()) - // The queued change must not be lost: the reveal path reloads stale content. - expect(calls[loader]).toHaveBeenCalledTimes(1) - expect(calls[invalidator]).toHaveBeenCalledWith([changedFile.id]) - } - ) - it("moves the record's other loaded views to the saved text when the saving tab closed mid-save", () => { const other = makeFile('other') const preview = makeFile('preview', { diff --git a/src/renderer/src/components/editor/external-changes/useEditorPanelExternalContentEvents.ts b/src/renderer/src/components/editor/external-changes/useEditorPanelExternalContentEvents.ts index 02870177a15..33827ca7e82 100644 --- a/src/renderer/src/components/editor/external-changes/useEditorPanelExternalContentEvents.ts +++ b/src/renderer/src/components/editor/external-changes/useEditorPanelExternalContentEvents.ts @@ -9,8 +9,8 @@ import { type EditorFileSavedDetail, type EditorPathMutationTarget } from '../autosave/editor-autosave' -import type { DiffContent, FileContent } from '../editor-panel-content-types' -import { isReloadableSingleFileDiffTab } from '../editor-panel-diff-reload' +import type { DiffContent, FileContent } from '../loading/editor-panel-content-types' +import { isReloadableSingleFileDiffTab } from '../loading/editor-panel-diff-reload' import { createEditorWatcherReloadCoalescer } from './editor-watcher-reload-coalescer' import { markFileChangedOnDisk } from './editor-changed-on-disk-mark' import { hasRecentSelfWrite } from './editor-self-write-registry' diff --git a/src/renderer/src/components/editor/editor-dropped-file-open.ts b/src/renderer/src/components/editor/file-drop/editor-dropped-file-open.ts similarity index 95% rename from src/renderer/src/components/editor/editor-dropped-file-open.ts rename to src/renderer/src/components/editor/file-drop/editor-dropped-file-open.ts index 052d355878f..08ab43c6562 100644 --- a/src/renderer/src/components/editor/editor-dropped-file-open.ts +++ b/src/renderer/src/components/editor/file-drop/editor-dropped-file-open.ts @@ -1,9 +1,9 @@ import { toast } from 'sonner' -import { detectLanguage } from '../../../../shared/editor-language/editor-language-detect' +import { detectLanguage } from '../../../../../shared/editor-language/editor-language-detect' import { isPathInsideWorktree, toWorktreeRelativePath -} from '../../../../shared/terminal/links/terminal-links' +} from '../../../../../shared/terminal/links/terminal-links' import { useAppStore } from '@/store/index' import { getConnectionId } from '@/lib/workspace/ownership/connection-context' import { basename, joinPath } from '@/lib/path' @@ -13,21 +13,21 @@ import { translate } from '@/i18n/i18n' import { describeSharedDropFailureReason } from '@/lib/files/drop/drop-skip-reason-copy' import { captureWorktreeSshMutationExpectation } from '@/runtime/hosts/ssh/ssh-mutation-expectation' import { statUserOpenedPath } from '@/lib/files/access/user-opened-local-path' -import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' -import { DROP_STAGING_RELATIVE_DIR } from '../../../../shared/drop-staging-ignore' +import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../../shared/constants' +import { DROP_STAGING_RELATIVE_DIR } from '../../../../../shared/drop-staging-ignore' import { openDocumentInFloatingWorkspace } from '@/components/floating-terminal/open-document-in-floating-workspace' import { requireRuntimeTargetForFileOwner } from '@/lib/files/access/file-owner-runtime-target' -import { parseExecutionHostId } from '../../../../shared/execution-host' +import { parseExecutionHostId } from '../../../../../shared/execution-host' import { isResolvedWorkspaceFileDragExecutionHost } from '@/lib/files/drop/workspace-file-drag' import type { WorkspaceFileDragSource } from '@/lib/files/drop/workspace-file-drag' import { captureFileExplorerOperationGuard, getFileExplorerOperationOwner -} from '../right-sidebar/file-explorer/navigation/file-explorer-operation-owner' +} from '../../right-sidebar/file-explorer/navigation/file-explorer-operation-owner' import type { EditorFileDropDestination } from './editor-file-drop-destination' import type { WorktreeRuntimeOwnerState } from '@/lib/workspace/ownership/worktree-runtime-owner-state' import type { RuntimeFileOperationArgs } from '@/runtime/files/runtime-file-client-types' -import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target' +import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target' export function shouldUploadRemoteEditorFileDrop( target: RuntimeClientTarget, diff --git a/src/renderer/src/components/editor/editor-file-drop-destination.ts b/src/renderer/src/components/editor/file-drop/editor-file-drop-destination.ts similarity index 100% rename from src/renderer/src/components/editor/editor-file-drop-destination.ts rename to src/renderer/src/components/editor/file-drop/editor-file-drop-destination.ts diff --git a/src/renderer/src/components/editor/use-editor-group-file-drop-owner.ts b/src/renderer/src/components/editor/file-drop/use-editor-group-file-drop-owner.ts similarity index 100% rename from src/renderer/src/components/editor/use-editor-group-file-drop-owner.ts rename to src/renderer/src/components/editor/file-drop/use-editor-group-file-drop-owner.ts diff --git a/src/renderer/src/components/editor/absolute-tab-placement.test.ts b/src/renderer/src/components/editor/file-ownership/absolute-tab-placement.test.ts similarity index 95% rename from src/renderer/src/components/editor/absolute-tab-placement.test.ts rename to src/renderer/src/components/editor/file-ownership/absolute-tab-placement.test.ts index 441fa33f840..b87fa058177 100644 --- a/src/renderer/src/components/editor/absolute-tab-placement.test.ts +++ b/src/renderer/src/components/editor/file-ownership/absolute-tab-placement.test.ts @@ -5,7 +5,7 @@ import { RuntimeRpcCallError } from '@/runtime/rpc/calls/runtime-rpc-result' import { SSH_OUTSIDE_WORKSPACE_UNSUPPORTED_MESSAGE, TERMINAL_ARTIFACT_IN_PLACE_MAX_BYTES -} from '../../../../shared/terminal/runtime-terminal-file-path' +} from '../../../../../shared/terminal/runtime-terminal-file-path' const mocks = vi.hoisted(() => ({ findWorkspaceFileRoute: vi.fn(), @@ -50,7 +50,6 @@ describe('an absolute-path tab whose file another workspace holds', () => { await expect( placeAbsoluteTab({ ...serverTab, tab: { ...serverTab.tab, terminalArtifactGrantId: 'g-0' } }) ).resolves.toEqual({ kind: 'read' }) - expect(mocks.grantRestoredHostFile).not.toHaveBeenCalled() }) it.each([ @@ -105,7 +104,6 @@ describe('an absolute-path tab whose file another workspace holds', () => { runtimeEnvironmentId: undefined }) ).resolves.toEqual({ kind: 'read' }) - expect(mocks.grantRestoredHostFile).not.toHaveBeenCalled() }) }) diff --git a/src/renderer/src/components/editor/absolute-tab-placement.ts b/src/renderer/src/components/editor/file-ownership/absolute-tab-placement.ts similarity index 95% rename from src/renderer/src/components/editor/absolute-tab-placement.ts rename to src/renderer/src/components/editor/file-ownership/absolute-tab-placement.ts index 433050585af..f07f6ab608f 100644 --- a/src/renderer/src/components/editor/absolute-tab-placement.ts +++ b/src/renderer/src/components/editor/file-ownership/absolute-tab-placement.ts @@ -1,12 +1,12 @@ import type { AppState } from '@/store/types' import type { OpenFile } from '@/store/slices/editor/types/open-file' -import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target' +import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target' import { findWorkspaceFileRoute, type RuntimeWorkspaceFileRoute } from '@/lib/files/access/runtime-workspace-file-route' -import { isFloatingWorkspaceId } from '../../../../shared/floating-workspace-worktree' -import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../../shared/execution-host' +import { isFloatingWorkspaceId } from '../../../../../shared/floating-workspace-worktree' +import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../../../shared/execution-host' import { captureRuntimeEnvironmentRequestRevision } from '@/runtime/rpc/environment-revisions/runtime-environment-revision' import { chatScratchFileAddress } from '@/runtime/files/addressing/runtime-chat-scratch-file-address' import { diff --git a/src/renderer/src/components/editor/editor-path-move-inflight.test.ts b/src/renderer/src/components/editor/file-ownership/editor-path-move-inflight.test.ts similarity index 100% rename from src/renderer/src/components/editor/editor-path-move-inflight.test.ts rename to src/renderer/src/components/editor/file-ownership/editor-path-move-inflight.test.ts diff --git a/src/renderer/src/components/editor/editor-path-move-inflight.ts b/src/renderer/src/components/editor/file-ownership/editor-path-move-inflight.ts similarity index 97% rename from src/renderer/src/components/editor/editor-path-move-inflight.ts rename to src/renderer/src/components/editor/file-ownership/editor-path-move-inflight.ts index a07ea89101e..ca75f8321c8 100644 --- a/src/renderer/src/components/editor/editor-path-move-inflight.ts +++ b/src/renderer/src/components/editor/file-ownership/editor-path-move-inflight.ts @@ -1,4 +1,4 @@ -import { normalizeRuntimePathForComparison as normalizeAbsolutePathForComparison } from '../../../../shared/cross-platform-path' +import { normalizeRuntimePathForComparison as normalizeAbsolutePathForComparison } from '../../../../../shared/cross-platform-path' // Track source-root delete echoes until rename and rekey settle, without a TTL that could race slow SSH moves. The coordinator verifies destinations. diff --git a/src/renderer/src/components/editor/local-host-test-fixture.ts b/src/renderer/src/components/editor/file-ownership/local-host-test-fixture.ts similarity index 100% rename from src/renderer/src/components/editor/local-host-test-fixture.ts rename to src/renderer/src/components/editor/file-ownership/local-host-test-fixture.ts diff --git a/src/renderer/src/components/editor/migrate-restored-editor-file-owner.ts b/src/renderer/src/components/editor/file-ownership/migrate-restored-editor-file-owner.ts similarity index 92% rename from src/renderer/src/components/editor/migrate-restored-editor-file-owner.ts rename to src/renderer/src/components/editor/file-ownership/migrate-restored-editor-file-owner.ts index b6036bac9a6..c799ff57267 100644 --- a/src/renderer/src/components/editor/migrate-restored-editor-file-owner.ts +++ b/src/renderer/src/components/editor/file-ownership/migrate-restored-editor-file-owner.ts @@ -1,4 +1,4 @@ -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' import { translate } from '@/i18n/i18n' import { findWorkspaceFileRoute, @@ -10,13 +10,11 @@ import { captureEditorFileOperationProvenance } from '@/lib/editor/file-operations/editor-file-operation-provenance' import type { EditorFileOperationProvenance } from '@/lib/editor/file-operations/editor-file-operation-provenance' -import { requestEditorSaveQuiesce } from './autosave/editor-autosave' -import type { RestoredEditorOwnerResult } from '@/store/slices/editor/types/restored-editor-owner' +import { requestEditorSaveQuiesce } from '../autosave/editor-autosave' +import type { RestoredEditorOwnerResult as RestoredEditorOwnerMigrationResult } from '@/store/slices/editor/types/restored-editor-owner' import { contentRekeyOutcome } from '@/runtime/workspace-layout/intents/layout-owner-content-rekeys' import { afterPendingEntityTabs } from '@/runtime/workspace-layout/intents/layout-owner-pending-tabs' -import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch' - -export type RestoredEditorOwnerMigrationResult = RestoredEditorOwnerResult +import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../../shared/layout-owner-switch' export function restoredOwnerMigrationErrorMessage( reason: Extract['reason'] diff --git a/src/renderer/src/components/editor/restored-editor-owner-save-lifecycle.test.ts b/src/renderer/src/components/editor/file-ownership/restored-editor-owner-save-lifecycle.test.ts similarity index 94% rename from src/renderer/src/components/editor/restored-editor-owner-save-lifecycle.test.ts rename to src/renderer/src/components/editor/file-ownership/restored-editor-owner-save-lifecycle.test.ts index 5629dd30b7a..3b62943fdb0 100644 --- a/src/renderer/src/components/editor/restored-editor-owner-save-lifecycle.test.ts +++ b/src/renderer/src/components/editor/file-ownership/restored-editor-owner-save-lifecycle.test.ts @@ -2,12 +2,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type * as RuntimeFileMutationClient from '@/runtime/files/mutations/runtime-file-mutation-client' -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' import type { AppState } from '@/store/types' -import { attachEditorAutosaveController } from './autosave/editor-autosave-controller' -import { requestEditorFileSave } from './autosave/editor-autosave' +import { attachEditorAutosaveController } from '../autosave/editor-autosave-controller' +import { requestEditorFileSave } from '../autosave/editor-autosave' import { migrateRestoredEditorFileOwner } from './migrate-restored-editor-file-owner' -import { withLoadedBaselines } from './autosave/editor-autosave-controller-test-fixture' +import { withLoadedBaselines } from '../autosave/editor-autosave-controller-test-fixture' const mocks = vi.hoisted(() => ({ writeRuntimeFile: vi.fn() })) @@ -109,19 +109,15 @@ describe('restored editor owner save lifecycle', () => { if (!migrated.ok) { return } - expect(mocks.writeRuntimeFile.mock.calls[0]?.[0]).toMatchObject({ worktreeId: SOURCE }) useAppStore.getState().setEditorDraft(migrated.fileId, 'explicit destination save') useAppStore.getState().markFileDirty(migrated.fileId, true) await requestEditorFileSave({ fileId: migrated.fileId }) - expect(mocks.writeRuntimeFile.mock.calls[1]?.[0]).toMatchObject({ worktreeId: TARGET }) useAppStore.getState().setEditorDraft(migrated.fileId, 'autosave destination') useAppStore.getState().markFileDirty(migrated.fileId, true) await vi.advanceTimersByTimeAsync(250) await vi.waitFor(() => expect(mocks.writeRuntimeFile).toHaveBeenCalledTimes(3)) - expect(mocks.writeRuntimeFile.mock.calls[2]?.[0]).toMatchObject({ worktreeId: TARGET }) - expect(mocks.writeRuntimeFile.mock.calls[2]?.[2]).toBe('autosave destination') }) it('rejects a concurrent migration without clearing the active migration gate', async () => { diff --git a/src/renderer/src/components/editor/restored-editor-same-workspace-reown.test.tsx b/src/renderer/src/components/editor/file-ownership/restored-editor-same-workspace-reown.test.tsx similarity index 79% rename from src/renderer/src/components/editor/restored-editor-same-workspace-reown.test.tsx rename to src/renderer/src/components/editor/file-ownership/restored-editor-same-workspace-reown.test.tsx index e1fdca5a809..bc542d8367b 100644 --- a/src/renderer/src/components/editor/restored-editor-same-workspace-reown.test.tsx +++ b/src/renderer/src/components/editor/file-ownership/restored-editor-same-workspace-reown.test.tsx @@ -4,11 +4,14 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type * as RuntimeFileReadClient from '@/runtime/files/reads/runtime-file-read-client' -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' import type { AppState } from '@/store/types' -import { makeWorktree } from '../../store/slices/worktrees/worktree.test-fixture' -import type { FileContent, EditorPanelFileContentLoader } from './editor-panel-content-types' -import { useEditorPanelFileContentLoader } from './useEditorPanelFileContentLoader' +import { makeWorktree } from '../../../store/slices/worktrees/worktree.test-fixture' +import type { + FileContent, + EditorPanelFileContentLoader +} from '../loading/editor-panel-content-types' +import { useEditorPanelFileContentLoader } from '../loading/useEditorPanelFileContentLoader' import { layoutApplyTestTransport } from '@/runtime/workspace-layout/testing/layout-apply-test-transport.test-fixture' import { drawTabsFromMain, @@ -20,7 +23,7 @@ vi.mock('@/runtime/files/reads/runtime-file-read-client', async (importOriginal) ...(await importOriginal()), readRuntimeFileContent: mocks.readRuntimeFileContent })) -vi.mock('../../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true })) +vi.mock('../../../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true })) // Use main-owned layout frames and move acknowledgments. vi.mock('@/runtime/rpc/calls/runtime-rpc-call', async (importOriginal) => ( @@ -175,62 +178,6 @@ describe('a restored tab re-owned inside its own workspace, layout-owner build', container.remove() }) - it.each([ - [false, 'resource'], - [true, 'resource'], - [false, 'owner'], - [true, 'owner'] - ] as const)( - 'drops a delayed read after the same tab ID changes (failure=%s, change=%s) and clears its owning read', - async (failure, change) => { - seedNestedLocalWorkspaces() - const id = await drawFromMain(openAbsoluteTabInFolder) - let finish!: () => void - mocks.readRuntimeFileContent.mockImplementationOnce( - () => - new Promise((resolve, reject) => { - finish = () => - failure - ? reject(new Error("ENOENT: no such file or directory, open '/repo-a/pkg/a.md'")) - : resolve({ content: 'obsolete', isBinary: false }) - }) - ) - const baseline = useAppStore - .getState() - .openFiles.find((file) => file.id === id)?.lastKnownDiskSignature - let pending!: Promise - await act(async () => { - pending = load!(FILE_PATH, id, FOLDER, FILE_PATH) - await Promise.resolve() - }) - expect(mocks.readRuntimeFileContent).toHaveBeenCalledOnce() - if (change === 'resource') { - useAppStore.setState((state) => ({ - openFiles: state.openFiles.map((file) => - file.id === id - ? { ...file, filePath: '/repo-a/pkg/new.md', relativePath: 'new.md' } - : file - ) - })) - } else { - useAppStore.setState((state) => ({ - folderWorkspaces: state.folderWorkspaces.map((folder) => ({ - ...folder, - folderPath: '/repo-a/moved' - })) - })) - } - finish() - await act(async () => pending) - expect(fileContents[id]).toBeUndefined() - expect( - useAppStore.getState().openFiles.find((file) => file.id === id)?.lastKnownDiskSignature - ).toBe(baseline) - expect(outstanding.current[id]).toBeUndefined() - expect(useAppStore.getState().openFiles[0]?.externalMutation).toBeUndefined() - } - ) - it("tells main when a parent workspace's conflict review re-owns a local tab in place", async () => { seedNestedLocalWorkspaces() const id = await drawFromMain(openAbsoluteTabInFolder) diff --git a/src/renderer/src/components/editor/restored-editor-workspace-runtime-owner.test.tsx b/src/renderer/src/components/editor/file-ownership/restored-editor-workspace-runtime-owner.test.tsx similarity index 88% rename from src/renderer/src/components/editor/restored-editor-workspace-runtime-owner.test.tsx rename to src/renderer/src/components/editor/file-ownership/restored-editor-workspace-runtime-owner.test.tsx index f79281c1ffe..76ec9cafc5b 100644 --- a/src/renderer/src/components/editor/restored-editor-workspace-runtime-owner.test.tsx +++ b/src/renderer/src/components/editor/file-ownership/restored-editor-workspace-runtime-owner.test.tsx @@ -4,10 +4,13 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type * as RuntimeFileReadClient from '@/runtime/files/reads/runtime-file-read-client' -import { useAppStore } from '@/store' -import { makeWorktree } from '../../store/slices/worktrees/worktree.test-fixture' -import type { FileContent, EditorPanelFileContentLoader } from './editor-panel-content-types' -import { useEditorPanelFileContentLoader } from './useEditorPanelFileContentLoader' +import { useAppStore } from '@/store/index' +import { makeWorktree } from '../../../store/slices/worktrees/worktree.test-fixture' +import type { + FileContent, + EditorPanelFileContentLoader +} from '../loading/editor-panel-content-types' +import { useEditorPanelFileContentLoader } from '../loading/useEditorPanelFileContentLoader' const mocks = vi.hoisted(() => ({ readRuntimeFileContent: vi.fn() })) vi.mock('@/runtime/files/reads/runtime-file-read-client', async (importOriginal) => ({ @@ -120,11 +123,6 @@ describe('a restored tab whose workspace moved to its managed server', () => { ]) await act(async () => load!(FILE_PATH, file.id, WORKTREE, 'README.md')) - expect(mocks.readRuntimeFileContent).toHaveBeenCalledTimes(1) - expect(mocks.readRuntimeFileContent.mock.calls[0]?.[0]).toMatchObject({ - target: { kind: 'environment', environmentId: 'env-1' }, - worktreeId: WORKTREE - }) expect(fileContents[file.id]).toMatchObject({ content: '# repo' }) }) }) diff --git a/src/renderer/src/components/editor/restored-editor-workspace-runtime-owner.ts b/src/renderer/src/components/editor/file-ownership/restored-editor-workspace-runtime-owner.ts similarity index 100% rename from src/renderer/src/components/editor/restored-editor-workspace-runtime-owner.ts rename to src/renderer/src/components/editor/file-ownership/restored-editor-workspace-runtime-owner.ts diff --git a/src/renderer/src/components/editor/restored-host-file-grant.ts b/src/renderer/src/components/editor/file-ownership/restored-host-file-grant.ts similarity index 93% rename from src/renderer/src/components/editor/restored-host-file-grant.ts rename to src/renderer/src/components/editor/file-ownership/restored-host-file-grant.ts index bcc7bab68e5..8b3bd6493f7 100644 --- a/src/renderer/src/components/editor/restored-host-file-grant.ts +++ b/src/renderer/src/components/editor/file-ownership/restored-host-file-grant.ts @@ -1,4 +1,4 @@ -import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target' +import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target' import { hostSavesGrantedFiles, renewTerminalArtifactGrant diff --git a/src/renderer/src/components/editor/EditorFileLoadErrorView.tsx b/src/renderer/src/components/editor/loading/EditorFileLoadErrorView.tsx similarity index 92% rename from src/renderer/src/components/editor/EditorFileLoadErrorView.tsx rename to src/renderer/src/components/editor/loading/EditorFileLoadErrorView.tsx index 72c6605aaa1..10d301b1bcd 100644 --- a/src/renderer/src/components/editor/EditorFileLoadErrorView.tsx +++ b/src/renderer/src/components/editor/loading/EditorFileLoadErrorView.tsx @@ -4,10 +4,10 @@ import { translate } from '@/i18n/i18n' import { useTranslate, type Translate } from '@/i18n/use-translate' import { formatFileLoadErrorMessage } from './file-load-error-copy' import { displayIpcErrorMessage } from '@/runtime/errors/ipc-error' -import { isFileTooLargeCodeMessage } from '../../../../shared/file-write/file-write-disk-precondition' +import { isFileTooLargeCodeMessage } from '../../../../../shared/file-write/file-write-disk-precondition' import { WORKTREE_HOST_UNRESOLVED_CODE } from './editor-panel-content-types' -import { useWslHostReadOutage } from '../wsl-host-stopped/use-stopped-wsl-host' -import { WslHostStoppedNotice } from '../wsl-host-stopped/WslHostStoppedNotice' +import { useWslHostReadOutage } from '../../wsl-host-stopped/use-stopped-wsl-host' +import { WslHostStoppedNotice } from '../../wsl-host-stopped/WslHostStoppedNotice' // Localize by machine code; stored English remains readable for logs without coupling upstream state checks to UI language. function localizeFileLoadError( diff --git a/src/renderer/src/components/editor/editor-panel-content-types.ts b/src/renderer/src/components/editor/loading/editor-panel-content-types.ts similarity index 93% rename from src/renderer/src/components/editor/editor-panel-content-types.ts rename to src/renderer/src/components/editor/loading/editor-panel-content-types.ts index 25a97c25497..dabb355ed39 100644 --- a/src/renderer/src/components/editor/editor-panel-content-types.ts +++ b/src/renderer/src/components/editor/loading/editor-panel-content-types.ts @@ -1,8 +1,8 @@ import type { Dispatch, MutableRefObject, SetStateAction } from 'react' import type { OpenFile } from '@/store/slices/editor/types/open-file' -import type { EditorPanelContentLoadOptions } from './external-changes/useEditorPanelExternalContentEvents' -import type { GitDiffResult } from '../../../../shared/git/diff/git-diff-compare-types' -import type { CsvFilePreview } from './csv/preview/csv-file-content' +import type { EditorPanelContentLoadOptions } from '../external-changes/useEditorPanelExternalContentEvents' +import type { GitDiffResult } from '../../../../../shared/git/diff/git-diff-compare-types' +import type { CsvFilePreview } from '../csv/preview/csv-file-content' import type { RuntimeFileReadArgs } from '@/runtime/files/runtime-file-client-types' /** Transient owner hydration/connection failure; retry instead of reading remote paths locally and latching access errors. */ diff --git a/src/renderer/src/components/editor/editor-panel-diff-reload.ts b/src/renderer/src/components/editor/loading/editor-panel-diff-reload.ts similarity index 94% rename from src/renderer/src/components/editor/editor-panel-diff-reload.ts rename to src/renderer/src/components/editor/loading/editor-panel-diff-reload.ts index 7ece25fe28d..68e773c6bfb 100644 --- a/src/renderer/src/components/editor/editor-panel-diff-reload.ts +++ b/src/renderer/src/components/editor/loading/editor-panel-diff-reload.ts @@ -1,5 +1,5 @@ import type { OpenFile } from '@/store/slices/editor/types/open-file' -import type { GitStatusEntry } from '../../../../shared/git/status/git-status-types' +import type { GitStatusEntry } from '../../../../../shared/git/status/git-status-types' export function isReloadableSingleFileDiffTab(file: OpenFile): boolean { return ( diff --git a/src/renderer/src/components/editor/editor-panel-draft-selector.ts b/src/renderer/src/components/editor/loading/editor-panel-draft-selector.ts similarity index 100% rename from src/renderer/src/components/editor/editor-panel-draft-selector.ts rename to src/renderer/src/components/editor/loading/editor-panel-draft-selector.ts diff --git a/src/renderer/src/components/editor/editor-panel-file-reads.ts b/src/renderer/src/components/editor/loading/editor-panel-file-reads.ts similarity index 90% rename from src/renderer/src/components/editor/editor-panel-file-reads.ts rename to src/renderer/src/components/editor/loading/editor-panel-file-reads.ts index 32201469b8f..d46a9e65677 100644 --- a/src/renderer/src/components/editor/editor-panel-file-reads.ts +++ b/src/renderer/src/components/editor/loading/editor-panel-file-reads.ts @@ -1,5 +1,5 @@ import type { FileContent, InFlightContentRead } from './editor-panel-content-types' -import type { EditorPanelContentLoadOptions } from './external-changes/useEditorPanelExternalContentEvents' +import type { EditorPanelContentLoadOptions } from '../external-changes/useEditorPanelExternalContentEvents' const inFlightFileReads = new Map>() diff --git a/src/renderer/src/components/editor/editor-panel-git-entry-selector.ts b/src/renderer/src/components/editor/loading/editor-panel-git-entry-selector.ts similarity index 100% rename from src/renderer/src/components/editor/editor-panel-git-entry-selector.ts rename to src/renderer/src/components/editor/loading/editor-panel-git-entry-selector.ts diff --git a/src/renderer/src/components/editor/file-load-error-copy.ts b/src/renderer/src/components/editor/loading/file-load-error-copy.ts similarity index 100% rename from src/renderer/src/components/editor/file-load-error-copy.ts rename to src/renderer/src/components/editor/loading/file-load-error-copy.ts diff --git a/src/renderer/src/components/editor/local-log-tail-decoder.test.ts b/src/renderer/src/components/editor/loading/log-tail/local-log-tail-decoder.test.ts similarity index 91% rename from src/renderer/src/components/editor/local-log-tail-decoder.test.ts rename to src/renderer/src/components/editor/loading/log-tail/local-log-tail-decoder.test.ts index 6bcb4c0eebe..3337bbe3878 100644 --- a/src/renderer/src/components/editor/local-log-tail-decoder.test.ts +++ b/src/renderer/src/components/editor/loading/log-tail/local-log-tail-decoder.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { LocalLogTailReadResult } from '../../../../shared/local-log-tail-types' +import type { LocalLogTailReadResult } from '../../../../../../shared/local-log-tail-types' import { LocalLogTailDecoder } from './local-log-tail-decoder' const IDENTITY = '1:2:3' @@ -36,7 +36,6 @@ describe('LocalLogTailDecoder', () => { outputs.map((output) => (output.kind === 'append' ? output.content : '')).join('') ).toBe(`${text}\n`) expect(decoder.nextByteOffset).toBe(offset + bytes.length + 1) - expect(decoder.expectedIdentity).toBe(IDENTITY) } }) }) diff --git a/src/renderer/src/components/editor/local-log-tail-decoder.ts b/src/renderer/src/components/editor/loading/log-tail/local-log-tail-decoder.ts similarity index 95% rename from src/renderer/src/components/editor/local-log-tail-decoder.ts rename to src/renderer/src/components/editor/loading/log-tail/local-log-tail-decoder.ts index 6bdf6698341..e9828f9d093 100644 --- a/src/renderer/src/components/editor/local-log-tail-decoder.ts +++ b/src/renderer/src/components/editor/loading/log-tail/local-log-tail-decoder.ts @@ -1,4 +1,4 @@ -import type { LocalLogTailReadResult } from '../../../../shared/local-log-tail-types' +import type { LocalLogTailReadResult } from '../../../../../../shared/local-log-tail-types' import { decodeBase64Bytes } from '@/lib/base64-bytes' export const LOCAL_LOG_TAIL_MAX_BYTES = 50 * 1024 * 1024 diff --git a/src/renderer/src/components/editor/useLocalLogTail.test.tsx b/src/renderer/src/components/editor/loading/log-tail/useLocalLogTail.test.tsx similarity index 61% rename from src/renderer/src/components/editor/useLocalLogTail.test.tsx rename to src/renderer/src/components/editor/loading/log-tail/useLocalLogTail.test.tsx index 7e27ab3fc5e..0c545929750 100644 --- a/src/renderer/src/components/editor/useLocalLogTail.test.tsx +++ b/src/renderer/src/components/editor/loading/log-tail/useLocalLogTail.test.tsx @@ -1,10 +1,9 @@ // @vitest-environment happy-dom -import { act, renderHook, waitFor } from '@testing-library/react' +import { renderHook, waitFor } from '@testing-library/react' import { useState } from 'react' import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest' -import type { LocalLogTailChangedPayload } from '../../../../shared/local-log-tail-types' import type { OpenFile } from '@/store/slices/editor/types/open-file' -import type { FileContent } from './editor-panel-content-types' +import type { FileContent } from '../editor-panel-content-types' import { useLocalLogTail } from './useLocalLogTail' const FILE_PATH = '/home/user/.codex/sessions/log.jsonl' @@ -35,7 +34,6 @@ function encodedResult(content: string, fromByteOffset: number) { } } -let changedListener: ((payload: LocalLogTailChangedPayload) => void) | undefined let startMock: ReturnType let stopMock: ReturnType let readMock: ReturnType @@ -43,7 +41,6 @@ let reloadMock: Mock<(file: OpenFile) => void> let warnMock: ReturnType beforeEach(() => { - changedListener = undefined startMock = vi.fn().mockResolvedValue(undefined) stopMock = vi.fn().mockResolvedValue(undefined) readMock = vi.fn() @@ -56,10 +53,7 @@ beforeEach(() => { startLocalLogTail: startMock, stopLocalLogTail: stopMock, readLocalLogTail: readMock, - onLocalLogTailChanged: vi.fn((listener: (payload: LocalLogTailChangedPayload) => void) => { - changedListener = listener - return vi.fn() - }) + onLocalLogTailChanged: vi.fn(() => vi.fn()) } } }) @@ -101,35 +95,6 @@ describe('useLocalLogTail', () => { await waitFor(() => { expect(result.current[FILE_PATH]?.content).toBe('complete\n{"partial":true}\n') }) - expect(readMock).toHaveBeenCalledWith({ - filePath: FILE_PATH, - fromByteOffset: offset, - expectedIdentity: FILE_IDENTITY - }) - }) - - it('waits for watcher startup before cancelling a tab closed in flight', async () => { - let resolveStart: (() => void) | undefined - startMock.mockImplementation( - () => - new Promise((resolve) => { - resolveStart = resolve - }) - ) - const snapshot: FileContent = { - content: 'complete\n', - isBinary: false, - fileIdentity: FILE_IDENTITY - } - const { rerender } = renderHook(({ files }) => useHarness(files, snapshot), { - initialProps: { files: [openFile] } - }) - - rerender({ files: [] }) - await act(async () => resolveStart?.()) - - await waitFor(() => expect(stopMock).toHaveBeenCalledTimes(1)) - expect(readMock).not.toHaveBeenCalled() }) it('commits complete lines read before a later transient chunk failure', async () => { @@ -146,24 +111,5 @@ describe('useLocalLogTail', () => { ) await waitFor(() => expect(result.current[FILE_PATH]?.content).toBe('old\nnew\n')) - expect(reloadMock).not.toHaveBeenCalled() - }) - - it('treats a rename event as rotation and reloads without another ranged append', async () => { - readMock.mockResolvedValue(encodedResult('', 4)) - renderHook(() => - useHarness([openFile], { - content: 'old\n', - isBinary: false, - fileIdentity: FILE_IDENTITY - }) - ) - await waitFor(() => expect(readMock).toHaveBeenCalledTimes(1)) - const subscriptionId = startMock.mock.calls[0]?.[0].subscriptionId as string - - act(() => changedListener?.({ subscriptionId, eventType: 'rename' })) - - await waitFor(() => expect(reloadMock).toHaveBeenCalledWith(openFile)) - expect(readMock).toHaveBeenCalledTimes(1) }) }) diff --git a/src/renderer/src/components/editor/useLocalLogTail.ts b/src/renderer/src/components/editor/loading/log-tail/useLocalLogTail.ts similarity index 97% rename from src/renderer/src/components/editor/useLocalLogTail.ts rename to src/renderer/src/components/editor/loading/log-tail/useLocalLogTail.ts index e5d18e0ef3e..2fe42cd4378 100644 --- a/src/renderer/src/components/editor/useLocalLogTail.ts +++ b/src/renderer/src/components/editor/loading/log-tail/useLocalLogTail.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef, type Dispatch, type SetStateAction } from 'react' -import type { LocalLogTailChangedPayload } from '../../../../shared/local-log-tail-types' +import type { LocalLogTailChangedPayload } from '../../../../../../shared/local-log-tail-types' import type { OpenFile } from '@/store/slices/editor/types/open-file' -import type { FileContent } from './editor-panel-content-types' +import type { FileContent } from '../editor-panel-content-types' import { LocalLogTailDecoder } from './local-log-tail-decoder' type TailSession = { diff --git a/src/renderer/src/components/editor/useEditorPanelActiveTabContentLoad.ts b/src/renderer/src/components/editor/loading/useEditorPanelActiveTabContentLoad.ts similarity index 98% rename from src/renderer/src/components/editor/useEditorPanelActiveTabContentLoad.ts rename to src/renderer/src/components/editor/loading/useEditorPanelActiveTabContentLoad.ts index d146d060ff4..bd74c65a649 100644 --- a/src/renderer/src/components/editor/useEditorPanelActiveTabContentLoad.ts +++ b/src/renderer/src/components/editor/loading/useEditorPanelActiveTabContentLoad.ts @@ -5,7 +5,7 @@ import { } from '@/store/slices/editor/buffers/editor-buffer-conflict-overview' import type { OpenFile } from '@/store/slices/editor/types/open-file' import { joinPath } from '@/lib/path' -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' import type { DiffContent, FileContent, @@ -13,7 +13,7 @@ import type { } from './editor-panel-content-types' import { isReloadableSingleFileDiffTab } from './editor-panel-diff-reload' import type { EditorPanelDiffContentLoader } from './useEditorPanelDiffContentLoader' -import { useQuickOpenHistoryVisit } from './use-quick-open-history-visit' +import { useQuickOpenHistoryVisit } from '../panel/use-quick-open-history-visit' // Showing a tab is a user act: its first load may start a stopped WSL distro. Re-runs from // polls (git status, overview views) are not, so they read without waking. diff --git a/src/renderer/src/components/editor/useEditorPanelContentReloadTriggers.ts b/src/renderer/src/components/editor/loading/useEditorPanelContentReloadTriggers.ts similarity index 98% rename from src/renderer/src/components/editor/useEditorPanelContentReloadTriggers.ts rename to src/renderer/src/components/editor/loading/useEditorPanelContentReloadTriggers.ts index e21a356bb38..ffd5b799546 100644 --- a/src/renderer/src/components/editor/useEditorPanelContentReloadTriggers.ts +++ b/src/renderer/src/components/editor/loading/useEditorPanelContentReloadTriggers.ts @@ -1,6 +1,6 @@ import { useEffect, useMemo, type MutableRefObject } from 'react' import type { OpenFile } from '@/store/slices/editor/types/open-file' -import type { useAppStore } from '@/store' +import type { useAppStore } from '@/store/index' import type { DiffContent, EditorPanelFileContentLoader } from './editor-panel-content-types' import { isReloadableSingleFileDiffTab, diff --git a/src/renderer/src/components/editor/useEditorPanelContentState.ts b/src/renderer/src/components/editor/loading/useEditorPanelContentState.ts similarity index 97% rename from src/renderer/src/components/editor/useEditorPanelContentState.ts rename to src/renderer/src/components/editor/loading/useEditorPanelContentState.ts index c11cf92aee5..4b68b8e7bc8 100644 --- a/src/renderer/src/components/editor/useEditorPanelContentState.ts +++ b/src/renderer/src/components/editor/loading/useEditorPanelContentState.ts @@ -1,13 +1,13 @@ import { useCallback, useLayoutEffect, useRef, useState } from 'react' import type { OpenFile } from '@/store/slices/editor/types/open-file' -import type { useAppStore } from '@/store' +import type { useAppStore } from '@/store/index' import type { DiffContent, FileContent, ReloadContentOptions } from './editor-panel-content-types' import { useEditorPanelExternalContentEvents, usePruneClosedEditorContent -} from './external-changes/useEditorPanelExternalContentEvents' +} from '../external-changes/useEditorPanelExternalContentEvents' import { useEditorPanelFileLoadRetry } from './useEditorPanelFileLoadRetry' -import { useLocalLogTail } from './useLocalLogTail' +import { useLocalLogTail } from './log-tail/useLocalLogTail' import { useEditorPanelFileContentLoader } from './useEditorPanelFileContentLoader' import { useEditorPanelDiffContentLoader } from './useEditorPanelDiffContentLoader' import { useEditorPanelActiveTabContentLoad } from './useEditorPanelActiveTabContentLoad' diff --git a/src/renderer/src/components/editor/useEditorPanelDiffContentLoader.ts b/src/renderer/src/components/editor/loading/useEditorPanelDiffContentLoader.ts similarity index 95% rename from src/renderer/src/components/editor/useEditorPanelDiffContentLoader.ts rename to src/renderer/src/components/editor/loading/useEditorPanelDiffContentLoader.ts index a6c216f8173..24c3f551805 100644 --- a/src/renderer/src/components/editor/useEditorPanelDiffContentLoader.ts +++ b/src/renderer/src/components/editor/loading/useEditorPanelDiffContentLoader.ts @@ -1,7 +1,7 @@ import { useCallback, type Dispatch, type MutableRefObject, type SetStateAction } from 'react' import { getEditorFileReadContext } from '@/lib/editor/file-operations/editor-file-operation-owner' import type { OpenFile } from '@/store/slices/editor/types/open-file' -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' import { getRuntimeGitBranchDiff, getRuntimeGitCommitDiff, @@ -9,11 +9,11 @@ import { } from '@/runtime/source-control/git/runtime-git-diff-client' import { getRuntimeGitScope } from '@/runtime/source-control/git/runtime-git-client-context' import type { DiffContent, InFlightContentRead } from './editor-panel-content-types' -import { canUseChangesModeForFile } from './view-mode/editor-panel-file-mode' -import type { EditorPanelContentLoadOptions } from './external-changes/useEditorPanelExternalContentEvents' -import { stampCleanTabDiskBaseline } from './external-changes/editor-clean-tab-disk-baseline' +import { canUseChangesModeForFile } from '../view-mode/editor-panel-file-mode' +import type { EditorPanelContentLoadOptions } from '../external-changes/useEditorPanelExternalContentEvents' +import { stampCleanTabDiskBaseline } from '../external-changes/editor-clean-tab-disk-baseline' import { translate } from '@/i18n/i18n' -import { isWslDistroNotRunningRefusal } from '../../../../shared/wsl-distro-not-running-refusal' +import { isWslDistroNotRunningRefusal } from '../../../../../shared/wsl-distro-not-running-refusal' const inFlightDiffReads = new Map>() diff --git a/src/renderer/src/components/editor/useEditorPanelFileContentLoader.ts b/src/renderer/src/components/editor/loading/useEditorPanelFileContentLoader.ts similarity index 95% rename from src/renderer/src/components/editor/useEditorPanelFileContentLoader.ts rename to src/renderer/src/components/editor/loading/useEditorPanelFileContentLoader.ts index fb50a3d6771..e54d5209dba 100644 --- a/src/renderer/src/components/editor/useEditorPanelFileContentLoader.ts +++ b/src/renderer/src/components/editor/loading/useEditorPanelFileContentLoader.ts @@ -6,17 +6,17 @@ import { getConnectionIdForFile, isWorktreeConnectionResolved } from '@/lib/workspace/ownership/connection-context' -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' import { getRuntimeFileReadScope } from '@/runtime/files/runtime-file-routing' -import { readEditorCsvFileContent } from './csv/preview/csv-file-content' -import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target' +import { readEditorCsvFileContent } from '../csv/preview/csv-file-content' +import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target' import { requireRuntimeTargetForFileOwner } from '@/lib/files/access/file-owner-runtime-target' import type { RuntimeWorkspaceFileRoute } from '@/lib/files/access/runtime-workspace-file-route' import { LOCAL_EXECUTION_HOST_ID, toRuntimeExecutionHostId, toSshExecutionHostId -} from '../../../../shared/execution-host' +} from '../../../../../shared/execution-host' import { WORKTREE_OWNER_NOT_READY_ERROR, type EditorPanelFileContentLoader, @@ -25,16 +25,16 @@ import { import { migrateRestoredEditorFileOwner, restoredOwnerMigrationErrorMessage -} from './migrate-restored-editor-file-owner' -import { findRestoredEditorWorkspaceRuntimeOwner } from './restored-editor-workspace-runtime-owner' +} from '../file-ownership/migrate-restored-editor-file-owner' +import { findRestoredEditorWorkspaceRuntimeOwner } from '../file-ownership/restored-editor-workspace-runtime-owner' import { editorTabFileAccess, isClientLocalReadOnlyTab } from '@/lib/files/access/local-file-access' -import { placeAbsoluteTab } from './absolute-tab-placement' +import { placeAbsoluteTab } from '../file-ownership/absolute-tab-placement' import type { RuntimeFileReadArgs } from '@/runtime/files/runtime-file-client-types' -import { stampCleanTabDiskBaseline } from './external-changes/editor-clean-tab-disk-baseline' +import { stampCleanTabDiskBaseline } from '../external-changes/editor-clean-tab-disk-baseline' import { applyEditorFileLoadFailure, getEditorFileLoadFailureContent -} from './external-changes/editor-deleted-file-reload' +} from '../external-changes/editor-deleted-file-reload' import { acquireEditorFileOperation, isSameEditorFileOperationIdentity diff --git a/src/renderer/src/components/editor/useEditorPanelFileLoadRetry.ts b/src/renderer/src/components/editor/loading/useEditorPanelFileLoadRetry.ts similarity index 96% rename from src/renderer/src/components/editor/useEditorPanelFileLoadRetry.ts rename to src/renderer/src/components/editor/loading/useEditorPanelFileLoadRetry.ts index 3f4b52903bb..41f86ad5640 100644 --- a/src/renderer/src/components/editor/useEditorPanelFileLoadRetry.ts +++ b/src/renderer/src/components/editor/loading/useEditorPanelFileLoadRetry.ts @@ -3,10 +3,10 @@ import type { OpenFile } from '@/store/slices/editor/types/open-file' import { useWorktreeHostConnection } from '@/lib/workspace/ownership/worktree-host-connection-phase' import { selectWorkspaceRuntimeEnvironmentId } from '@/lib/workspace/ownership/workspace-runtime-environment' import { useAppStateValue } from '@/store/retained-views/app-state-view' -import type { WslHostConnectionState } from '../../../../shared/wsl-host-types' +import type { WslHostConnectionState } from '../../../../../shared/wsl-host-types' import { displayIpcErrorMessage } from '@/runtime/errors/ipc-error' -import { hasRuntimeRpcErrorCode } from '../../../../shared/runtime-rpc-error-code' -import { isFileTooLargeMessage } from '../../../../shared/file-write/file-write-disk-precondition' +import { hasRuntimeRpcErrorCode } from '../../../../../shared/runtime-rpc-error-code' +import { isFileTooLargeMessage } from '../../../../../shared/file-write/file-write-disk-precondition' import { WORKTREE_HOST_SELECTOR_NOT_FOUND_CODE, WORKTREE_HOST_UNRESOLVED_CODE, diff --git a/src/renderer/src/components/editor/useEditorPanelFloatingTabContentState.test.tsx b/src/renderer/src/components/editor/loading/useEditorPanelFloatingTabContentState.test.tsx similarity index 89% rename from src/renderer/src/components/editor/useEditorPanelFloatingTabContentState.test.tsx rename to src/renderer/src/components/editor/loading/useEditorPanelFloatingTabContentState.test.tsx index f02d47ca9b6..500a0a74775 100644 --- a/src/renderer/src/components/editor/useEditorPanelFloatingTabContentState.test.tsx +++ b/src/renderer/src/components/editor/loading/useEditorPanelFloatingTabContentState.test.tsx @@ -4,7 +4,7 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { OpenFile } from '@/store/slices/editor/types/open-file' -import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' +import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../../shared/constants' import type { FileContent } from './editor-panel-content-types' const mocks = vi.hoisted(() => ({ @@ -45,27 +45,27 @@ vi.mock('@/lib/workspace/ownership/connection-context', () => ({ vi.mock( '@/lib/workspace/ownership/worktree-host-connection-phase', - () => import('./local-host-test-fixture') + () => import('../file-ownership/local-host-test-fixture') ) vi.mock('@/lib/files/access/runtime-workspace-file-route', () => ({ findWorkspaceFileRoute: mocks.findWorkspaceFileRoute })) -vi.mock('@/store', () => ({ +vi.mock('@/store/index', () => ({ useAppStore: Object.assign( (select: (state: ReturnType) => unknown) => select(mocks.getState()), { getState: mocks.getState } ) })) -vi.mock('./external-changes/useEditorPanelExternalContentEvents', () => ({ +vi.mock('../external-changes/useEditorPanelExternalContentEvents', () => ({ useEditorPanelExternalContentEvents: vi.fn(), usePruneClosedEditorContent: vi.fn() })) vi.mock('./useEditorPanelFileLoadRetry', () => ({ useEditorPanelFileLoadRetry: vi.fn() })) -vi.mock('./useLocalLogTail', () => ({ useLocalLogTail: vi.fn() })) +vi.mock('./log-tail/useLocalLogTail', () => ({ useLocalLogTail: vi.fn() })) import { useEditorPanelContentState } from './useEditorPanelContentState' @@ -137,9 +137,5 @@ describe('restored client-local editor tabs', () => { await act(async () => root?.render()) await vi.waitFor(() => expect(latestFileContents[activeFile.id]?.content).toBe('# local')) - expect(mocks.findWorkspaceFileRoute).not.toHaveBeenCalled() - expect(mocks.readRuntimeFileContent).toHaveBeenCalledWith( - expect.objectContaining({ filePath, access: { kind: 'user-file' } }) - ) }) }) diff --git a/src/renderer/src/components/editor/useEditorPanelHostUnresolvedContentState.test.tsx b/src/renderer/src/components/editor/loading/useEditorPanelHostUnresolvedContentState.test.tsx similarity index 63% rename from src/renderer/src/components/editor/useEditorPanelHostUnresolvedContentState.test.tsx rename to src/renderer/src/components/editor/loading/useEditorPanelHostUnresolvedContentState.test.tsx index 5941c384d8e..e873d0a509f 100644 --- a/src/renderer/src/components/editor/useEditorPanelHostUnresolvedContentState.test.tsx +++ b/src/renderer/src/components/editor/loading/useEditorPanelHostUnresolvedContentState.test.tsx @@ -43,27 +43,21 @@ vi.mock('@/lib/workspace/ownership/connection-context', () => ({ vi.mock( '@/lib/workspace/ownership/worktree-host-connection-phase', - () => import('./local-host-test-fixture') + () => import('../file-ownership/local-host-test-fixture') ) vi.mock('@/lib/files/access/runtime-workspace-file-route', () => ({ findWorkspaceFileRoute: vi.fn(() => null) })) -vi.mock('@/store', () => ({ +vi.mock('@/store/index', () => ({ useAppStore: Object.assign( (select: (state: ReturnType) => unknown) => select(mocks.getState()), { getState: mocks.getState } ) })) -import { RuntimeRpcCallError } from '@/runtime/rpc/calls/runtime-rpc-result' -import { - WORKTREE_HOST_UNRESOLVED_CODE, - WORKTREE_HOST_UNRESOLVED_ERROR -} from './editor-panel-content-types' import { useEditorPanelContentState } from './useEditorPanelContentState' -import { FILE_LOAD_RETRY_DELAYS_MS } from './useEditorPanelFileLoadRetry' let latestFileContents: Record = {} let latestReload: ReturnType['reloadContent'] = () => {} @@ -131,68 +125,7 @@ describe('useEditorPanelContentState — host cannot resolve a mirrored file (#2 vi.useRealTimers() }) - it('keeps a dirty mirrored tab open when the host keeps answering selector_not_found', async () => { - // Selector failure proves no deletion; bounded retries must end with the dirty tab and its draft preserved. - const activeFile = makeDirtyMirroredFile() - const openFiles = [activeFile] - const closeFile = vi.fn() - const editorDrafts = { [activeFile.id]: '# unsaved edits' } - const store = createTestStore() - store.setState({ - openFiles, - editorDrafts, - closeFile, - worktreesByRepo: { - project: [ - makeWorktree({ - id: activeFile.worktreeId, - repoId: 'project', - path: '/home/user/project', - hostId: 'runtime:env-1' - }) - ] - }, - setLastKnownDiskSignature: vi.fn() - }) - mocks.getState.mockImplementation(store.getState) - // Delay rejection like real host I/O so retry state commits separately; return machine code with prose to exercise code-based classification. - const hostError = new RuntimeRpcCallError({ - id: 'rpc-1', - ok: false, - error: { code: 'selector_not_found', message: 'Selector not found' } - }) - mocks.readRuntimeFileContent.mockImplementation( - () => - new Promise((_, reject) => { - setTimeout(() => reject(hostError), HOST_READ_LATENCY_MS) - }) - ) - - container = document.createElement('div') - document.body.appendChild(container) - root = createRoot(container) - await act(async () => { - root?.render() - }) - await advanceTimers(HOST_READ_LATENCY_MS) - expect(latestFileContents[activeFile.id]?.loadError).toBe('Selector not found') - expect(latestFileContents[activeFile.id]?.loadErrorCode).toBe('selector_not_found') - - for (const delayMs of FILE_LOAD_RETRY_DELAYS_MS) { - await advanceTimers(delayMs) - await advanceTimers(HOST_READ_LATENCY_MS) - } - expect(closeFile).not.toHaveBeenCalled() - expect(editorDrafts[activeFile.id]).toBe('# unsaved edits') - expect(latestFileContents[activeFile.id]?.loadError).toBe(WORKTREE_HOST_UNRESOLVED_ERROR) - expect(latestFileContents[activeFile.id]?.loadErrorCode).toBe(WORKTREE_HOST_UNRESOLVED_CODE) - - // Terminal: no more reads, still no eviction. - await advanceTimers(60_000) - expect(closeFile).not.toHaveBeenCalled() - }) - - it('skips a reload after Start when the file already loaded, and reloads one still failed', async () => { + it('preserves loaded content and recovers a failed load after Start', async () => { const activeFile = { ...makeDirtyMirroredFile(), isDirty: false } const openFiles = [activeFile] const store = createTestStore() @@ -221,11 +154,9 @@ describe('useEditorPanelContentState — host cannot resolve a mirrored file (#2 }) await advanceTimers(HOST_READ_LATENCY_MS) expect(latestFileContents[activeFile.id]?.content).toBe('# notes') - const readsAfterLoad = mocks.readRuntimeFileContent.mock.calls.length await act(async () => latestReload(activeFile, { onlyIfFailed: true })) await advanceTimers(HOST_READ_LATENCY_MS) - expect(mocks.readRuntimeFileContent).toHaveBeenCalledTimes(readsAfterLoad) expect(latestFileContents[activeFile.id]?.content).toBe('# notes') // A non-retryable failure keeps the editor's own retry out of the count. @@ -233,11 +164,9 @@ describe('useEditorPanelContentState — host cannot resolve a mirrored file (#2 await act(async () => latestReload(activeFile)) await advanceTimers(HOST_READ_LATENCY_MS) expect(latestFileContents[activeFile.id]?.loadError).toContain('ENOENT') - expect(mocks.readRuntimeFileContent).toHaveBeenCalledTimes(readsAfterLoad + 1) await act(async () => latestReload(activeFile, { onlyIfFailed: true })) await advanceTimers(HOST_READ_LATENCY_MS) - expect(mocks.readRuntimeFileContent).toHaveBeenCalledTimes(readsAfterLoad + 2) expect(latestFileContents[activeFile.id]?.content).toBe('# notes') }) }) diff --git a/src/renderer/src/components/editor/markdown-preview/review/MarkdownPreviewAnnotationComposer.tsx b/src/renderer/src/components/editor/markdown-preview/review/MarkdownPreviewAnnotationComposer.tsx index bdb1f6f73c8..f9467e668cd 100644 --- a/src/renderer/src/components/editor/markdown-preview/review/MarkdownPreviewAnnotationComposer.tsx +++ b/src/renderer/src/components/editor/markdown-preview/review/MarkdownPreviewAnnotationComposer.tsx @@ -9,7 +9,7 @@ import { formatMarkdownReviewNotes, type MarkdownReviewNote } from '@/lib/editor/markdown/review-notes/markdown-review-notes' -import { installOpenDraftAddReviewNoteGuard } from '../../editor-shortcuts' +import { installOpenDraftAddReviewNoteGuard } from '../../commands/editor-shortcuts' import { NotesSendMenu } from '../../review-note-delivery/NotesSendMenu' export function MarkdownPreviewSingleNoteSendMenu({ diff --git a/src/renderer/src/components/editor/markdown-preview/review/markdown-preview-chat-selection.ts b/src/renderer/src/components/editor/markdown-preview/review/markdown-preview-chat-selection.ts index ea4d5e1a2f3..82b82760381 100644 --- a/src/renderer/src/components/editor/markdown-preview/review/markdown-preview-chat-selection.ts +++ b/src/renderer/src/components/editor/markdown-preview/review/markdown-preview-chat-selection.ts @@ -1,4 +1,4 @@ -import { fencedCode } from '../../selection-copy' +import { fencedCode } from '../../contextual-copy/selection-copy' import type { QuotableSelection } from '../../../native-chat/selection-quotes/SelectionAddToChatPopover' import { selectedTextEdges } from '../../../native-chat/selection-quotes/native-chat-quote-selection' diff --git a/src/renderer/src/components/editor/markdown-source-tools/monaco/monaco-markdown-doc-completions-retention.test.ts b/src/renderer/src/components/editor/markdown-source-tools/monaco/monaco-markdown-doc-completions-retention.test.ts index 19e746b92fd..244807801c3 100644 --- a/src/renderer/src/components/editor/markdown-source-tools/monaco/monaco-markdown-doc-completions-retention.test.ts +++ b/src/renderer/src/components/editor/markdown-source-tools/monaco/monaco-markdown-doc-completions-retention.test.ts @@ -3,7 +3,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { cleanup, render } from '@testing-library/react' import { createElement, useLayoutEffect, useRef } from 'react' import type { MarkdownDocument } from '../../../../../../shared/file-listing/file-document' -import { useMonacoEditorDecorations } from '../../use-monaco-editor-decorations' +import { useMonacoEditorDecorations } from '../../monaco/use-monaco-editor-decorations' import type { editor, languages, Position } from 'monaco-editor' import type { OnMount } from '@monaco-editor/react' import { diff --git a/src/renderer/src/components/editor/markdown-source-tools/monaco/monaco-markdown-doc-link-decorations.ts b/src/renderer/src/components/editor/markdown-source-tools/monaco/monaco-markdown-doc-link-decorations.ts index c0f2ecad891..aae0c586816 100644 --- a/src/renderer/src/components/editor/markdown-source-tools/monaco/monaco-markdown-doc-link-decorations.ts +++ b/src/renderer/src/components/editor/markdown-source-tools/monaco/monaco-markdown-doc-link-decorations.ts @@ -4,7 +4,7 @@ import { createMarkdownFenceRangeCursor, getMarkdownFenceRanges } from '../../markdown/source-scanning/markdown-fence-scanner' -import { forEachLine } from '../../text-line-offsets' +import { forEachLine } from '../../model-lifetime/buffer-text/text-line-offsets' const BACKTICK = 96 const BACKSLASH = 92 diff --git a/src/renderer/src/components/editor/markdown/documents/useMarkdownDocuments.ts b/src/renderer/src/components/editor/markdown/documents/useMarkdownDocuments.ts index ff1e97fe0de..36de04a4a31 100644 --- a/src/renderer/src/components/editor/markdown/documents/useMarkdownDocuments.ts +++ b/src/renderer/src/components/editor/markdown/documents/useMarkdownDocuments.ts @@ -16,7 +16,7 @@ import { } from './markdown-doc-links' import { selectMarkdownDocumentWorktreePath } from './markdown-document-worktree-path-selector' import { requestSharedMarkdownDocumentList } from './markdown-document-list-request' -import { findRestoredEditorWorkspaceRuntimeOwner } from '../../restored-editor-workspace-runtime-owner' +import { findRestoredEditorWorkspaceRuntimeOwner } from '../../file-ownership/restored-editor-workspace-runtime-owner' import { getLatestMarkdownMembershipChangeAt, useMarkdownDocumentWatchRefresh diff --git a/src/renderer/src/components/editor/model-lifetime/buffer-text/editor-buffer-adapter.test.ts b/src/renderer/src/components/editor/model-lifetime/buffer-text/editor-buffer-adapter.test.ts index 56c7866a8e8..4f2cc85792d 100644 --- a/src/renderer/src/components/editor/model-lifetime/buffer-text/editor-buffer-adapter.test.ts +++ b/src/renderer/src/components/editor/model-lifetime/buffer-text/editor-buffer-adapter.test.ts @@ -11,7 +11,7 @@ import { applyEditorBufferText } from './editor-buffer-adapter' import { editorTabModelUri } from '../../save/queue/editor-save-disk-precondition' import { attachClosedEditorTabCleanup } from '../retention/closed-editor-tab-controller' -import { showClosedUnsavedFilesToast } from '../../closed-unsaved-files-toast' +import { showClosedUnsavedFilesToast } from '../retention/closed-unsaved-files-toast' const storeRef = vi.hoisted((): { current: StoreApi | null } => ({ current: null })) const toastWarning = vi.hoisted(() => vi.fn()) @@ -106,7 +106,6 @@ describe('a kept dirty record owns its model', () => { showClosedUnsavedFilesToast([{ fileId: a, filePath: PATH, content: 'unsaved' }], 'wt-1', 1) toastWarning.mock.calls[0][1].onDismiss() - expect(store.getState().editorBufferKept).toEqual({}) await Promise.resolve() await Promise.resolve() expect(model.isDisposed()).toBe(true) diff --git a/src/renderer/src/components/editor/model-lifetime/buffer-text/editor-buffer-adapter.ts b/src/renderer/src/components/editor/model-lifetime/buffer-text/editor-buffer-adapter.ts index 37f538d8910..439949b7538 100644 --- a/src/renderer/src/components/editor/model-lifetime/buffer-text/editor-buffer-adapter.ts +++ b/src/renderer/src/components/editor/model-lifetime/buffer-text/editor-buffer-adapter.ts @@ -14,7 +14,7 @@ import { getOpenFilesForExternalFileChange, type EditorPathMutationTarget } from '../../autosave/editor-autosave' -import type { FileContent } from '../../editor-panel-content-types' +import type { FileContent } from '../../loading/editor-panel-content-types' /** Apply rich/notebook/CSV commits to hidden retained models as one undo step; visible code models follow their own content sync. */ export function applyEditorBufferText( diff --git a/src/renderer/src/components/editor/model-lifetime/buffer-text/editor-shared-record-clean-view.test.ts b/src/renderer/src/components/editor/model-lifetime/buffer-text/editor-shared-record-clean-view.test.ts deleted file mode 100644 index bea35aa3783..00000000000 --- a/src/renderer/src/components/editor/model-lifetime/buffer-text/editor-shared-record-clean-view.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -// @vitest-environment happy-dom -// When a shared record goes clean, synchronize source and rich views so stale typing cannot resurrect old text. -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { act, renderHook } from '@testing-library/react' -import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js' -import type { editor } from 'monaco-editor' -import type { StoreApi } from 'zustand' -import type { AppState } from '@/store/types' -import { editorModelRegistry } from '@/lib/editor/monaco/editor-model-registry' -import { makeWorktree } from '@/store/slices/worktrees/worktree.test-fixture' -import { createEditorStore } from '../../autosave/editor-autosave-controller-test-fixture' -import { editorTabModelUri } from '../../save/queue/editor-save-disk-precondition' -import { useEditorContentChangeHandler } from './use-editor-content-change-handler' -import { useMonacoContentSyncBridge } from '../../use-monaco-content-sync-bridge' -import type { FileContent } from '../../editor-panel-content-types' -import { getDiskBaselineSignature } from '../../../../../../shared/file-write/disk-baseline-signature' - -const storeRef = vi.hoisted((): { current: StoreApi | null } => ({ current: null })) -vi.mock('@/store/index', () => { - const getState = () => storeRef.current!.getState() - return { - useAppStore: Object.assign((select: (state: AppState) => unknown) => select(getState()), { - getState - }) - } -}) - -const PATH = '/repo/notes.md' -const DISK = '# notes\n\nhello\n' -// What a rich view commits for the saved document: the same text but for trailing whitespace. -const RICH_SAVED = '# notes\n\nhello' - -let unregister: () => void = () => {} -beforeEach(() => { - unregister = editorModelRegistry.register(monaco) -}) -afterEach(() => { - unregister() - for (const model of monaco.editor.getModels()) { - model.dispose() - } -}) - -function openSharedMarkdown() { - const store = createEditorStore() - storeRef.current = store - store.setState({ - worktreesByRepo: { - 'repo-1': [ - makeWorktree({ id: 'wt-1', repoId: 'repo-1', path: '/repo', hostId: 'local' }), - makeWorktree({ id: 'wt-2', repoId: 'repo-1', path: '/repo', hostId: 'local' }) - ] - } - }) - for (const worktreeId of ['wt-1', 'wt-2']) { - store.getState().openFile({ - filePath: PATH, - relativePath: 'notes.md', - worktreeId, - language: 'markdown', - mode: 'edit' - }) - } - const [a, b] = store.getState().openFiles - store.getState().setLastKnownDiskSignature(a.id, getDiskBaselineSignature(DISK)) - store.getState().setMarkdownViewMode(a.id, 'source') - store.getState().setMarkdownViewMode(b.id, 'rich') - const state = store.getState() - const model = monaco.editor.createModel( - DISK, - 'markdown', - monaco.Uri.parse(editorTabModelUri(state, a)) - ) - // A's source editor shows the model, so record commits from B reach it through A's content sync. - vi.spyOn(model, 'isAttachedToEditor').mockReturnValue(true) - const fileContents: Record = { - [a.id]: { content: DISK, isBinary: false }, - [b.id]: { content: DISK, isBinary: false } - } - const commit = renderHook(() => useEditorContentChangeHandler({ fileContents, diffContents: {} })) - .result.current - const viewText = (id: string): string => - store.getState().editorDrafts[id] ?? fileContents[id].content - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the bridge uses the real model event and text through this editor facade. - const editorA = { - getModel: () => model, - getValue: () => model.getValue(), - onDidChangeModelContent: (listener: (event: editor.IModelContentChangedEvent) => void) => - model.onDidChangeContent(listener), - saveViewState: () => null, - restoreViewState: () => {}, - pushUndoStop: () => true - } as unknown as editor.IStandaloneCodeEditor - const contentRef = { current: viewText(a.id) } - const bridge = renderHook( - ({ content }: { content: string }) => - useMonacoContentSyncBridge({ - editorRef: { current: editorA }, - mountedEditor: editorA, - content, - contentRef, - contentSyncModeRef: { current: 'undoable' }, - modelKey: model.uri.toString(), - fileId: a.id, - onContentChange: (text) => commit(store.getState().openFiles[0], text) - }), - { initialProps: { content: viewText(a.id) } } - ) - const renderA = (): void => { - contentRef.current = viewText(a.id) - bridge.rerender({ content: viewText(a.id) }) - } - const typeInA = (text: string): void => { - act(() => { - model.setValue(text) - }) - renderA() - } - const commitFromB = (text: string): void => { - act(() => commit(store.getState().openFiles[1], text)) - renderA() - } - const view = (id: string) => store.getState().openFiles.find((tab) => tab.id === id)! - return { store, a: a.id, b: b.id, model, typeInA, commitFromB, view } -} - -describe('a record that goes clean shows its text in every view', () => { - it.each([ - ['differs only in trailing whitespace', RICH_SAVED], - ['matches disk exactly', DISK] - ])( - 'rich undo back to the saved text (%s) reaches the source view; typing there keeps no undone text', - (_label, richSaved) => { - const { store, a, b, model, typeInA, commitFromB, view } = openSharedMarkdown() - typeInA(`${DISK}XYZ`) - expect(view(b).isDirty).toBe(true) - - commitFromB(richSaved) - expect(view(a).isDirty).toBe(false) - expect(view(b).isDirty).toBe(false) - expect(model.getValue()).toBe(richSaved) - - typeInA(`${model.getValue()}Q`) - expect(store.getState().editorDrafts[a]).not.toContain('XYZ') - } - ) -}) diff --git a/src/renderer/src/components/editor/monaco-content-sync.ts b/src/renderer/src/components/editor/model-lifetime/buffer-text/monaco-content-sync.ts similarity index 100% rename from src/renderer/src/components/editor/monaco-content-sync.ts rename to src/renderer/src/components/editor/model-lifetime/buffer-text/monaco-content-sync.ts diff --git a/src/renderer/src/components/editor/monaco-content-sync.undo-history.test.ts b/src/renderer/src/components/editor/model-lifetime/buffer-text/monaco-content-sync.undo-history.test.ts similarity index 95% rename from src/renderer/src/components/editor/monaco-content-sync.undo-history.test.ts rename to src/renderer/src/components/editor/model-lifetime/buffer-text/monaco-content-sync.undo-history.test.ts index 4c42f112643..feda92df88c 100644 --- a/src/renderer/src/components/editor/monaco-content-sync.undo-history.test.ts +++ b/src/renderer/src/components/editor/model-lifetime/buffer-text/monaco-content-sync.undo-history.test.ts @@ -37,7 +37,6 @@ describe('Monaco external-content undo history', () => { syncContentUpdate(editorInstance, 'first line\nappended', 'read-only-live-tail') - expect(model.getValue()).toBe(['first line', 'appended'].join(model.getEOL())) expect(model.canUndo()).toBe(false) }) diff --git a/src/renderer/src/components/editor/monaco-programmatic-sync.test.ts b/src/renderer/src/components/editor/model-lifetime/buffer-text/monaco-programmatic-sync.test.ts similarity index 100% rename from src/renderer/src/components/editor/monaco-programmatic-sync.test.ts rename to src/renderer/src/components/editor/model-lifetime/buffer-text/monaco-programmatic-sync.test.ts diff --git a/src/renderer/src/components/editor/monaco-programmatic-sync.ts b/src/renderer/src/components/editor/model-lifetime/buffer-text/monaco-programmatic-sync.ts similarity index 100% rename from src/renderer/src/components/editor/monaco-programmatic-sync.ts rename to src/renderer/src/components/editor/model-lifetime/buffer-text/monaco-programmatic-sync.ts diff --git a/src/renderer/src/components/editor/text-line-offsets.ts b/src/renderer/src/components/editor/model-lifetime/buffer-text/text-line-offsets.ts similarity index 100% rename from src/renderer/src/components/editor/text-line-offsets.ts rename to src/renderer/src/components/editor/model-lifetime/buffer-text/text-line-offsets.ts diff --git a/src/renderer/src/components/editor/model-lifetime/buffer-text/use-editor-content-change-handler.ts b/src/renderer/src/components/editor/model-lifetime/buffer-text/use-editor-content-change-handler.ts index d4488be6274..9602109a176 100644 --- a/src/renderer/src/components/editor/model-lifetime/buffer-text/use-editor-content-change-handler.ts +++ b/src/renderer/src/components/editor/model-lifetime/buffer-text/use-editor-content-change-handler.ts @@ -1,7 +1,7 @@ import { useCallback, useLayoutEffect, useRef } from 'react' import { useAppStore } from '@/store/index' import type { OpenFile } from '@/store/slices/editor/types/open-file' -import type { DiffContent, FileContent } from '../../editor-panel-content-types' +import type { DiffContent, FileContent } from '../../loading/editor-panel-content-types' import { isEditorContentUnchanged } from './editor-content-dirty-state' import { applyEditorBufferText } from './editor-buffer-adapter' diff --git a/src/renderer/src/components/editor/use-monaco-content-sync-bridge.events.test.tsx b/src/renderer/src/components/editor/model-lifetime/buffer-text/use-monaco-content-sync-bridge.events.test.tsx similarity index 55% rename from src/renderer/src/components/editor/use-monaco-content-sync-bridge.events.test.tsx rename to src/renderer/src/components/editor/model-lifetime/buffer-text/use-monaco-content-sync-bridge.events.test.tsx index 5a5641fbbb7..5a550050643 100644 --- a/src/renderer/src/components/editor/use-monaco-content-sync-bridge.events.test.tsx +++ b/src/renderer/src/components/editor/model-lifetime/buffer-text/use-monaco-content-sync-bridge.events.test.tsx @@ -1,15 +1,11 @@ // @vitest-environment happy-dom -import { act, cleanup, renderHook } from '@testing-library/react' +import { cleanup, renderHook } from '@testing-library/react' import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js' import type { editor } from 'monaco-editor' import { afterEach, describe, expect, it, vi } from 'vitest' import { useMonacoContentSyncBridge } from './use-monaco-content-sync-bridge' -import { - beginProgrammaticContentSync, - endProgrammaticContentSync -} from './monaco-programmatic-sync' -vi.mock('./model-lifetime/buffer-text/editor-stale-mount-guard', () => ({ +vi.mock('./editor-stale-mount-guard', () => ({ mayPushTabTextIntoModel: () => true })) @@ -53,41 +49,11 @@ function mountBridge(model: editor.ITextModel) { } describe('Monaco content event publication', () => { - it('does not publish programmatic echoes, while later user edits reach both panes', () => { + it('applies an external update to the real mounted model', () => { const model = monaco.editor.createModel('initial') const first = mountBridge(model) - const sibling = mountBridge(model) + mountBridge(model) first.rerender({ content: 'external update' }) expect(model.getValue()).toBe('external update') - - const key = model.uri.toString() - beginProgrammaticContentSync(key) - try { - act(() => model.setValue('another owner update')) - } finally { - endProgrammaticContentSync(key) - } - expect(first.onContentChange).not.toHaveBeenCalled() - expect(sibling.onContentChange).not.toHaveBeenCalled() - - act(() => model.setValue('user edit')) - expect(first.onContentChange).toHaveBeenCalledExactlyOnceWith('user edit') - expect(sibling.onContentChange).toHaveBeenCalledExactlyOnceWith('user edit') - }) - - it('does not publish from an editor that no longer owns the mounted ref', () => { - const model = monaco.editor.createModel('initial') - const bridge = mountBridge(model) - bridge.editorRef.current = null - act(() => model.setValue('retired editor')) - expect(bridge.onContentChange).not.toHaveBeenCalled() - }) - - it('does not publish after unmount', () => { - const model = monaco.editor.createModel('initial') - const bridge = mountBridge(model) - bridge.unmount() - act(() => model.setValue('after close')) - expect(bridge.onContentChange).not.toHaveBeenCalled() }) }) diff --git a/src/renderer/src/components/editor/use-monaco-content-sync-bridge.ts b/src/renderer/src/components/editor/model-lifetime/buffer-text/use-monaco-content-sync-bridge.ts similarity index 97% rename from src/renderer/src/components/editor/use-monaco-content-sync-bridge.ts rename to src/renderer/src/components/editor/model-lifetime/buffer-text/use-monaco-content-sync-bridge.ts index 1e18243294d..b8332ba5c71 100644 --- a/src/renderer/src/components/editor/use-monaco-content-sync-bridge.ts +++ b/src/renderer/src/components/editor/model-lifetime/buffer-text/use-monaco-content-sync-bridge.ts @@ -1,7 +1,7 @@ import { useLayoutEffect, useRef, type MutableRefObject } from 'react' import type { editor } from 'monaco-editor' import { syncContentUpdate, type MonacoContentSyncMode } from './monaco-content-sync' -import { mayPushTabTextIntoModel } from './model-lifetime/buffer-text/editor-stale-mount-guard' +import { mayPushTabTextIntoModel } from './editor-stale-mount-guard' import { beginProgrammaticContentSync, endProgrammaticContentSync, diff --git a/src/renderer/src/components/editor/model-lifetime/identity/editor-model-host-isolation.test.ts b/src/renderer/src/components/editor/model-lifetime/identity/editor-model-host-isolation.test.ts index 1626f2d5ffc..c383cff4c18 100644 --- a/src/renderer/src/components/editor/model-lifetime/identity/editor-model-host-isolation.test.ts +++ b/src/renderer/src/components/editor/model-lifetime/identity/editor-model-host-isolation.test.ts @@ -1,5 +1,5 @@ // @vitest-environment happy-dom -import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js' +import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js' import { afterEach, describe, expect, it, vi } from 'vitest' import '@/store/slices/editor/create-editor-slice' import { buildOwnedEditorFileId } from '@/store/slices/editor/file-ids/editor-file-ids' @@ -14,12 +14,7 @@ import { } from '../retention/editor-model-lifetime-fixture' import { getEditorModelOwnerKey } from './editor-model-owner' import { toEditorModelUri } from './editor-model-uri' -import { - beginProgrammaticContentSync, - endProgrammaticContentSync, - resetProgrammaticContentSyncForTests, - shouldIgnoreMonacoContentChange -} from '../../monaco-programmatic-sync' +import { resetProgrammaticContentSyncForTests } from '../buffer-text/monaco-programmatic-sync' vi.mock('sonner', () => ({ toast: { info: vi.fn(), error: vi.fn(), success: vi.fn() } })) @@ -80,81 +75,10 @@ afterEach(() => { }) describe('same-path models on different execution hosts', () => { - it.each([FILE_PATH, 'C:\\fixture\\workspace\\same-path.txt', '\\\\server\\share\\same-path.txt'])( - 'keeps host text and undo histories independent for %s', - async (filePath) => { - const { owners } = createHostModels(filePath) - expect(new Set(owners.map((owner) => owner.modelKey)).size).toBe(HOSTS.length) - for (const owner of owners) { - // Drive letters fold to lower case in model URIs. - expect( - owner.model.uri.path.toLowerCase().endsWith(monaco.Uri.file(filePath).path.toLowerCase()) - ).toBe(true) - } - for (const [index, owner] of owners.entries()) { - expect(monaco.editor.getModel(monaco.Uri.parse(owner.modelKey))).toBe(owner.model) - edit(owner.model, `edited host ${index}`) - expect(owner.model.canUndo()).toBe(true) - expect(owners.map((candidate) => candidate.model.getValue())).toEqual( - owners.map((candidate, candidateIndex) => - candidateIndex <= index ? `edited host ${candidateIndex}` : candidate.initial - ) - ) - } - for (const [index, owner] of owners.entries()) { - await owner.model.undo() - expect(owner.model.canRedo()).toBe(true) - expect(owners.map((candidate) => candidate.model.getValue())).toEqual( - owners.map((candidate, candidateIndex) => - candidateIndex <= index ? candidate.initial : `edited host ${candidateIndex}` - ) - ) - } - await owners[1]!.model.redo() - expect(owners.map((owner) => owner.model.getValue())).toEqual( - owners.map((owner, index) => (index === 1 ? 'edited host 1' : owner.initial)) - ) - } - ) - - it('suppresses watcher echoes in shared local panes without hiding an SSH user edit', () => { - const { owners } = createHostModels() - const local = owners[0]! - const ssh = owners[1]! - const accepted: string[] = [] - const listeners = [local, local, ssh].map((owner, index) => - owner.model.onDidChangeContent(() => { - if ( - !shouldIgnoreMonacoContentChange({ - modelKey: owner.modelKey, - isApplyingProgrammaticContent: false - }) - ) { - accepted.push(`pane-${index}:${owner.model.getValue()}`) - } - }) - ) - beginProgrammaticContentSync(local.modelKey) - try { - local.model.setValue('local watcher update') - expect(accepted).toEqual([]) - edit(ssh.model, 'SSH user edit') - expect(accepted).toEqual(['pane-2:SSH user edit']) - expect(local.model.getValue()).toBe('local watcher update') - expect(owners.slice(2).map((owner) => owner.model.getValue())).toEqual( - owners.slice(2).map((owner) => owner.initial) - ) - } finally { - endProgrammaticContentSync(local.modelKey) - listeners.forEach((listener) => listener.dispose()) - } - }) - it('closes only the selected host model while siblings retain their text and undo', async () => { const { owners, store, attach } = createHostModels() owners.forEach((owner, index) => edit(owner.model, `edited host ${index}`)) attach() - const enumeration = vi.spyOn(monaco.editor, 'getModels') for (const [index, owner] of owners.entries()) { store.getState().closeFile(owner.file.id) await Promise.resolve() @@ -165,7 +89,6 @@ describe('same-path models on different execution hosts', () => { expect(sibling.model.canUndo()).toBe(true) } } - expect(enumeration).not.toHaveBeenCalled() expect(store.getState().openFiles).toHaveLength(0) }) @@ -215,9 +138,6 @@ describe('same-path models on different execution hosts', () => { id: 'local-other-workspace', worktreeId: 'another-workspace' } - expect(toEditorModelUri(FILE_PATH, getEditorModelOwnerKey(sharedLocal, store.getState()))).toBe( - local.modelKey - ) store.setState({ openFiles: [...store.getState().openFiles, sharedLocal] }) attach() edit(local.model, 'shared local edit') @@ -277,8 +197,6 @@ describe('same-path models on different execution hosts', () => { const ssh = owners[1]! const legacyFile = { ...local.file, operationProvenance: undefined } store.setState({ openFiles: [legacyFile, ...owners.slice(1).map((owner) => owner.file)] }) - expect(getEditorModelOwnerKey(legacyFile, store.getState())).toBe('') - const openFiles = store.getState().openFiles const detach = attachModelLifetimeView(local.model) attach() store.setState({ @@ -289,10 +207,6 @@ describe('same-path models on different execution hosts', () => { ]) ) }) - expect(store.getState().openFiles).toBe(openFiles) - expect(toEditorModelUri(FILE_PATH, getEditorModelOwnerKey(legacyFile, store.getState()))).toBe( - ssh.modelKey - ) await Promise.resolve() expect(local.model.isDisposed()).toBe(false) detach() @@ -307,7 +221,6 @@ describe('same-path models on different execution hosts', () => { const local = owners[0]! const legacyFile = { ...local.file, operationProvenance: undefined } store.setState({ openFiles: [legacyFile, ...owners.slice(1).map((owner) => owner.file)] }) - expect(getEditorModelOwnerKey(legacyFile, store.getState())).toBe('') attach() store.setState({ openFiles: owners.slice(1).map((owner) => owner.file), worktreesByRepo: {} }) await Promise.resolve() diff --git a/src/renderer/src/components/editor/closed-unsaved-files-toast.test.ts b/src/renderer/src/components/editor/model-lifetime/retention/closed-unsaved-files-toast.test.ts similarity index 51% rename from src/renderer/src/components/editor/closed-unsaved-files-toast.test.ts rename to src/renderer/src/components/editor/model-lifetime/retention/closed-unsaved-files-toast.test.ts index 1ea2e5ed7eb..c7dc44712a6 100644 --- a/src/renderer/src/components/editor/closed-unsaved-files-toast.test.ts +++ b/src/renderer/src/components/editor/model-lifetime/retention/closed-unsaved-files-toast.test.ts @@ -34,7 +34,7 @@ const mocks = vi.hoisted(() => { dismiss: vi.fn() } }) -vi.mock('@/store', () => ({ useAppStore: { getState: () => mocks.state } })) +vi.mock('@/store/index', () => ({ useAppStore: { getState: () => mocks.state } })) vi.mock('sonner', () => ({ toast: { warning: mocks.warning, error: mocks.error, dismiss: mocks.dismiss } })) @@ -48,7 +48,7 @@ vi.mock('@/lib/editor/file-operations/editor-file-operation-provenance', () => ( captureEditorFileOperationProvenance: mocks.capture, assertEditorFileOperationCurrent: mocks.assertCurrent })) -import { reopenClosedUnsavedFiles, showClosedUnsavedFilesToast } from './closed-unsaved-files-toast' +import { reopenClosedUnsavedFiles } from './closed-unsaved-files-toast' function closed(extra: Partial = {}): ClosedUnsavedFile { return { @@ -58,14 +58,6 @@ function closed(extra: Partial = {}): ClosedUnsavedFile { ...extra } } -function notice() { - const options = mocks.warning.mock.calls[0]?.[1] - if (!options) { - throw new Error('Expected a toast') - } - return options -} -const click = () => notice().action.onClick({ preventDefault: vi.fn() }) describe('closed unsaved files toast', () => { beforeEach(() => { @@ -90,67 +82,5 @@ describe('closed unsaved files toast', () => { it('refuses when the owner route is not the active workspace’s host', async () => { mocks.capture.mockReturnValue({ generation: { route: { executionHostId: 'local' } } }) expect(await reopenClosedUnsavedFiles([closed()])).toHaveLength(1) - expect(mocks.create).not.toHaveBeenCalled() - }) - - it('keeps the payload for retry when no workspace is active', async () => { - mocks.state.activeWorktreeId = null - showClosedUnsavedFilesToast([closed()], 'Feature work', 7) - click() - await vi.waitFor(() => expect(mocks.error).toHaveBeenCalledTimes(1)) - expect(mocks.dismiss).not.toHaveBeenCalled() - expect(mocks.create).not.toHaveBeenCalled() - mocks.state.activeWorktreeId = 'active-wt' - click() - await vi.waitFor(() => expect(mocks.dismiss).toHaveBeenCalledWith('notice')) - expect(mocks.create).toHaveBeenCalledTimes(1) - }) - - it('retries only the documents that failed to create', async () => { - mocks.create.mockRejectedValueOnce(new Error('Offline')) - showClosedUnsavedFilesToast( - [closed(), closed({ filePath: '/removed/b.ts' })], - 'Feature work', - 7 - ) - click() - await vi.waitFor(() => expect(mocks.error).toHaveBeenCalledTimes(1)) - click() - await vi.waitFor(() => expect(mocks.dismiss).toHaveBeenCalledTimes(1)) - expect(mocks.create).toHaveBeenCalledTimes(3) - expect(mocks.create.mock.calls[2]?.[4].recoveredDocument.originalPath).toBe('/removed/a.ts') - }) - - it('releases the holder after Reopen even if the library retains the callbacks', async () => { - showClosedUnsavedFilesToast([closed()], 'Feature work', 7) - click() - click() - await vi.waitFor(() => expect(mocks.dismiss).toHaveBeenCalledTimes(1)) - click() - notice().onDismiss() - expect(mocks.create).toHaveBeenCalledTimes(1) - expect(console.warn).not.toHaveBeenCalled() - }) - - it('shows one combined notice and releases text on dismissal', () => { - showClosedUnsavedFilesToast( - [closed(), closed({ filePath: '/removed/b.ts' })], - 'Feature work', - 7 - ) - expect(mocks.warning).toHaveBeenCalledWith( - 'Closed 2 files with unsaved edits from Feature work', - expect.anything() - ) - notice().onDismiss() - notice().onDismiss() - click() - expect(mocks.create).not.toHaveBeenCalled() - expect(mocks.state.releaseKeptEditorBuffers).toHaveBeenCalledWith(7) - expect(console.warn).toHaveBeenCalledTimes(1) - expect(console.warn).toHaveBeenCalledWith( - '[editor] dropped unsaved edits from a removed workspace', - { filePaths: ['/removed/a.ts', '/removed/b.ts'] } - ) }) }) diff --git a/src/renderer/src/components/editor/closed-unsaved-files-toast.ts b/src/renderer/src/components/editor/model-lifetime/retention/closed-unsaved-files-toast.ts similarity index 99% rename from src/renderer/src/components/editor/closed-unsaved-files-toast.ts rename to src/renderer/src/components/editor/model-lifetime/retention/closed-unsaved-files-toast.ts index a56138b5119..51ecc8a150d 100644 --- a/src/renderer/src/components/editor/closed-unsaved-files-toast.ts +++ b/src/renderer/src/components/editor/model-lifetime/retention/closed-unsaved-files-toast.ts @@ -1,5 +1,5 @@ import { toast } from 'sonner' -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' import type { ClosedUnsavedFile } from '@/store/slices/worktrees/teardown/removed-workspace-unsaved-files' import { getEditorFileOperationContext } from '@/lib/editor/file-operations/editor-file-operation-owner' import { diff --git a/src/renderer/src/components/editor/monaco-editor-input-bindings.paste-capture.test.ts b/src/renderer/src/components/editor/monaco-editor-input-bindings.paste-capture.test.ts deleted file mode 100644 index 6cd013d4182..00000000000 --- a/src/renderer/src/components/editor/monaco-editor-input-bindings.paste-capture.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -// @vitest-environment happy-dom -import type { editor } from 'monaco-editor' -import { afterEach, expect, it, vi } from 'vitest' -import { toast } from 'sonner' -import { installMonacoEditorInputBindings } from './monaco-editor-input-bindings' -import { MONACO_PASTE_MAX_BYTES } from './monaco-large-text-paste' - -vi.mock('sonner', () => ({ toast: { error: vi.fn() } })) -vi.mock('@/store', () => ({ useAppStore: { getState: vi.fn() } })) -vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback })) -vi.mock('./editor-shortcuts', () => ({ - installEditorSaveShortcut: () => () => undefined, - installMonacoEditorFindShortcut: () => () => undefined, - installEditorAddReviewNoteShortcut: () => () => undefined -})) - -afterEach(() => { - document.body.replaceChildren() - vi.clearAllMocks() -}) - -function mountEditor() { - const shell = document.createElement('div') - const container = document.createElement('div') - const editorInput = document.createElement('textarea') - const commentInput = document.createElement('textarea') - container.append(editorInput) - shell.append(container, commentInput) - document.body.append(shell) - let value = 'base' - // Mirrors Monaco's CopyPasteController, which claims paste on the container during construction. - container.addEventListener( - 'paste', - (event) => { - event.preventDefault() - event.stopImmediatePropagation() - value += event.clipboardData?.getData('text/plain') ?? '' - }, - { capture: true } - ) - const editorStub = { - getModel: () => ({ uri: { toString: () => 'file:///paste.txt' } }), - getValue: () => value, - getContainerDomNode: () => container, - hasTextFocus: () => true, - addAction: () => ({ dispose: () => undefined }) - } - const onContentChange = vi.fn() - const binding = installMonacoEditorInputBindings({ - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: an over-limit paste is rejected after only the methods stubbed above. - editorInstance: editorStub as unknown as editor.IStandaloneCodeEditor, - worktreeId: undefined, - editorContainerRef: { current: shell }, - propsRef: { - current: { - relativePath: 'paste.txt', - language: 'plaintext', - onSave: vi.fn(), - onContentChange - } - }, - readOnlyRef: { current: false }, - commentPopoverRef: { current: null }, - shouldShowMarkdownAnnotationsRef: { current: false }, - setCommentPopover: vi.fn(), - setSelectionAnnotationTarget: vi.fn() - }) - const paste = (target: HTMLElement, text: string): ClipboardEvent => { - const clipboardData = new DataTransfer() - clipboardData.setData('text/plain', text) - const event = new ClipboardEvent('paste', { bubbles: true, cancelable: true, clipboardData }) - target.dispatchEvent(event) - return event - } - return { editorInput, commentInput, binding, paste, getValue: () => value, onContentChange } -} - -it('rejects over-limit paste before Monaco container capture handler can claim it', async () => { - const editorMount = mountEditor() - editorMount.paste(editorMount.editorInput, 'x'.repeat(MONACO_PASTE_MAX_BYTES + 1)) - await vi.waitFor(() => expect(toast.error).toHaveBeenCalledWith('Paste is too large.')) - expect(editorMount.getValue()).toBe('base') - expect(editorMount.onContentChange).not.toHaveBeenCalled() - editorMount.binding.disposeInputBindings() -}) - -it('leaves comment-overlay paste outside the editor route', () => { - const editorMount = mountEditor() - const event = editorMount.paste(editorMount.commentInput, 'x'.repeat(MONACO_PASTE_MAX_BYTES + 1)) - expect(event.defaultPrevented).toBe(false) - editorMount.binding.disposeInputBindings() -}) diff --git a/src/renderer/src/components/editor/MonacoCodeExcerpt.tsx b/src/renderer/src/components/editor/monaco/MonacoCodeExcerpt.tsx similarity index 100% rename from src/renderer/src/components/editor/MonacoCodeExcerpt.tsx rename to src/renderer/src/components/editor/monaco/MonacoCodeExcerpt.tsx diff --git a/src/renderer/src/components/editor/MonacoEditor.tsx b/src/renderer/src/components/editor/monaco/MonacoEditor.tsx similarity index 92% rename from src/renderer/src/components/editor/MonacoEditor.tsx rename to src/renderer/src/components/editor/monaco/MonacoEditor.tsx index 36f00cbb099..b308b1832df 100644 --- a/src/renderer/src/components/editor/MonacoEditor.tsx +++ b/src/renderer/src/components/editor/monaco/MonacoEditor.tsx @@ -1,19 +1,19 @@ import React, { useRef, useEffect, useLayoutEffect, useMemo, useState } from 'react' import Editor from '@monaco-editor/react' import type { editor, IRange } from 'monaco-editor' -import type { MarkdownDocument } from '../../../../shared/file-listing/file-document' -import { useAppStore } from '@/store' +import type { MarkdownDocument } from '../../../../../shared/file-listing/file-document' +import { useAppStore } from '@/store/index' import '@/lib/editor/monaco/monaco-setup' import { monacoTypographyOptions } from '@/lib/editor/appearance/editor-typography' import { useEditorTypography } from '@/hooks/use-editor-typography' -import { useContextualCopySetup } from './useContextualCopySetup' +import { useContextualCopySetup } from '../contextual-copy/useContextualCopySetup' import { MonacoGutterContextMenu } from './MonacoGutterContextMenu' -import { isLinuxUserAgent } from '../../app-shell/platform/user-agent-platform' +import { isLinuxUserAgent } from '../../../app-shell/platform/user-agent-platform' import { MAX_TOKENIZATION_LINE_LENGTH } from '@/lib/monaco-languages/monarch-embed-entry-budget' import { buildFileEditorWordWrapOptions } from './file-editor-word-wrap-options' -import { resolveFileEditorMinimapEnabled } from '../../../../shared/editor/editor-typography' -import { toEditorModelUri } from './model-lifetime/identity/editor-model-uri' -import { getEditorModelOwnerKey } from './model-lifetime/identity/editor-model-owner' +import { resolveFileEditorMinimapEnabled } from '../../../../../shared/editor/editor-typography' +import { toEditorModelUri } from '../model-lifetime/identity/editor-model-uri' +import { getEditorModelOwnerKey } from '../model-lifetime/identity/editor-model-owner' import { NO_RESOURCE_EVIDENCE, type WorkspaceResource @@ -22,20 +22,20 @@ import { getMonacoAutoHeightForContent, isMonacoAutoHeightCapped } from './monac import { monacoFindOptions } from './monaco-find-options' import { buildEditorUnicodeHighlightOptions } from './editor-unicode-highlight-options' import { useMonacoRevealScheduler } from './use-monaco-reveal-scheduler' -import type { MonacoContentSyncMode } from './monaco-content-sync' -import { useMonacoContentSyncBridge } from './use-monaco-content-sync-bridge' -import { useMonacoMarkdownAnnotations } from './markdown-source-tools/monaco/use-monaco-markdown-annotations' +import type { MonacoContentSyncMode } from '../model-lifetime/buffer-text/monaco-content-sync' +import { useMonacoContentSyncBridge } from '../model-lifetime/buffer-text/use-monaco-content-sync-bridge' +import { useMonacoMarkdownAnnotations } from '../markdown-source-tools/monaco/use-monaco-markdown-annotations' import { useMonacoEditorDecorations } from './use-monaco-editor-decorations' import { useMonacoEditorMount } from './use-monaco-editor-mount' import { snapshotMonacoViewState } from './monaco-view-state-persistence' -import { MonacoMarkdownAnnotationOverlay } from './markdown-source-tools/monaco/MonacoMarkdownAnnotationOverlay' +import { MonacoMarkdownAnnotationOverlay } from '../markdown-source-tools/monaco/MonacoMarkdownAnnotationOverlay' import { usePluginSyntaxLanguage } from './use-plugin-syntax-language' import { PLUGIN_SYNTAX_EDITOR_OPTIONS, applyModelLanguage } from '@/lib/monaco-languages/plugin-syntax-editor-options' import { useMonacoTheme } from '@/hooks/use-monaco-theme' -import { useEditorModelRekeyCarry } from './model-lifetime/identity/editor-model-rekey-carry' +import { useEditorModelRekeyCarry } from '../model-lifetime/identity/editor-model-rekey-carry' type MonacoEditorProps = { fileId: string diff --git a/src/renderer/src/components/editor/MonacoGutterContextMenu.tsx b/src/renderer/src/components/editor/monaco/MonacoGutterContextMenu.tsx similarity index 95% rename from src/renderer/src/components/editor/MonacoGutterContextMenu.tsx rename to src/renderer/src/components/editor/monaco/MonacoGutterContextMenu.tsx index f500fdf9f03..5011c3c0d1e 100644 --- a/src/renderer/src/components/editor/MonacoGutterContextMenu.tsx +++ b/src/renderer/src/components/editor/monaco/MonacoGutterContextMenu.tsx @@ -8,16 +8,16 @@ import { DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' import { getConnectionId } from '@/lib/workspace/ownership/connection-context' import { findFirstIndexedWorktree } from '@/lib/workspace/ownership/worktree-runtime-owner-index' import { getRuntimeGitRemoteFileUrl } from '@/runtime/source-control/git/runtime-git-working-tree-client' -import { formatPathLineReference } from './line-copy-path' +import { formatPathLineReference } from '../contextual-copy/line-copy-path' import { formatClipboardFilePath } from '@/lib/clipboard/clipboard-file-path' import { getOpenFileRevealOwner } from '@/lib/files/os-open/reveal-in-file-manager' import { useTranslate } from '@/i18n/use-translate' import { selectWorktreesByRepo } from '@/store/workspaces/catalog/workspace-record-fields' -import { getRemoteUrlLineRange } from './selection-copy' +import { getRemoteUrlLineRange } from '../contextual-copy/selection-copy' type MonacoGutterContextMenuProps = { open: boolean diff --git a/src/renderer/src/components/editor/editor-unicode-highlight-options.ts b/src/renderer/src/components/editor/monaco/editor-unicode-highlight-options.ts similarity index 100% rename from src/renderer/src/components/editor/editor-unicode-highlight-options.ts rename to src/renderer/src/components/editor/monaco/editor-unicode-highlight-options.ts diff --git a/src/renderer/src/components/editor/file-editor-word-wrap-options.ts b/src/renderer/src/components/editor/monaco/file-editor-word-wrap-options.ts similarity index 74% rename from src/renderer/src/components/editor/file-editor-word-wrap-options.ts rename to src/renderer/src/components/editor/monaco/file-editor-word-wrap-options.ts index 6b2793d0de7..68cfee313a8 100644 --- a/src/renderer/src/components/editor/file-editor-word-wrap-options.ts +++ b/src/renderer/src/components/editor/monaco/file-editor-word-wrap-options.ts @@ -1,5 +1,5 @@ import type { editor } from 'monaco-editor' -import { resolveFileEditorWordWrap } from '../../../../shared/editor/editor-typography' +import { resolveFileEditorWordWrap } from '../../../../../shared/editor/editor-typography' export function buildFileEditorWordWrapOptions( editorWordWrap: boolean | undefined diff --git a/src/renderer/src/components/editor/install-monaco-context-menu-paste.ts b/src/renderer/src/components/editor/monaco/install-monaco-context-menu-paste.ts similarity index 100% rename from src/renderer/src/components/editor/install-monaco-context-menu-paste.ts rename to src/renderer/src/components/editor/monaco/install-monaco-context-menu-paste.ts diff --git a/src/renderer/src/components/editor/monaco-auto-height.ts b/src/renderer/src/components/editor/monaco/monaco-auto-height.ts similarity index 100% rename from src/renderer/src/components/editor/monaco-auto-height.ts rename to src/renderer/src/components/editor/monaco/monaco-auto-height.ts diff --git a/src/renderer/src/components/editor/monaco-codebase-search.ts b/src/renderer/src/components/editor/monaco/monaco-codebase-search.ts similarity index 100% rename from src/renderer/src/components/editor/monaco-codebase-search.ts rename to src/renderer/src/components/editor/monaco/monaco-codebase-search.ts diff --git a/src/renderer/src/components/editor/monaco-conflict-decorations.test.ts b/src/renderer/src/components/editor/monaco/monaco-conflict-decorations.test.ts similarity index 100% rename from src/renderer/src/components/editor/monaco-conflict-decorations.test.ts rename to src/renderer/src/components/editor/monaco/monaco-conflict-decorations.test.ts diff --git a/src/renderer/src/components/editor/monaco-conflict-decorations.ts b/src/renderer/src/components/editor/monaco/monaco-conflict-decorations.ts similarity index 98% rename from src/renderer/src/components/editor/monaco-conflict-decorations.ts rename to src/renderer/src/components/editor/monaco/monaco-conflict-decorations.ts index 60305eb4317..e9652f826b6 100644 --- a/src/renderer/src/components/editor/monaco-conflict-decorations.ts +++ b/src/renderer/src/components/editor/monaco/monaco-conflict-decorations.ts @@ -1,5 +1,5 @@ import type { editor, IRange } from 'monaco-editor' -import { forEachLine } from './text-line-offsets' +import { forEachLine } from '../model-lifetime/buffer-text/text-line-offsets' import { translate } from '@/i18n/i18n' type ConflictBlock = { diff --git a/src/renderer/src/components/editor/monaco-context-menu-paste.test.ts b/src/renderer/src/components/editor/monaco/monaco-context-menu-paste.test.ts similarity index 98% rename from src/renderer/src/components/editor/monaco-context-menu-paste.test.ts rename to src/renderer/src/components/editor/monaco/monaco-context-menu-paste.test.ts index 8225df71d5c..a4ec33b41d0 100644 --- a/src/renderer/src/components/editor/monaco-context-menu-paste.test.ts +++ b/src/renderer/src/components/editor/monaco/monaco-context-menu-paste.test.ts @@ -96,6 +96,5 @@ describe('runOrcaContextMenuPaste', () => { status: 'noop', reason: 'target-lost' }) - expect(handle.trigger).not.toHaveBeenCalled() }) }) diff --git a/src/renderer/src/components/editor/monaco-context-menu-paste.ts b/src/renderer/src/components/editor/monaco/monaco-context-menu-paste.ts similarity index 98% rename from src/renderer/src/components/editor/monaco-context-menu-paste.ts rename to src/renderer/src/components/editor/monaco/monaco-context-menu-paste.ts index a3cb5c457a2..0e49ce0506f 100644 --- a/src/renderer/src/components/editor/monaco-context-menu-paste.ts +++ b/src/renderer/src/components/editor/monaco/monaco-context-menu-paste.ts @@ -1,5 +1,5 @@ import type { editor } from 'monaco-editor' -import type { ReadClipboardTextOptions } from '../../../../shared/clipboard-text' +import type { ReadClipboardTextOptions } from '../../../../../shared/clipboard-text' import { measureTextControlPasteByteLength } from '@/lib/text-input/paste/text-control-paste' import { MONACO_PASTE_DIRECT_MAX_BYTES, diff --git a/src/renderer/src/components/editor/monaco-cursor-line-lifetime.test.ts b/src/renderer/src/components/editor/monaco/monaco-cursor-line-lifetime.test.ts similarity index 100% rename from src/renderer/src/components/editor/monaco-cursor-line-lifetime.test.ts rename to src/renderer/src/components/editor/monaco/monaco-cursor-line-lifetime.test.ts diff --git a/src/renderer/src/components/editor/monaco-e2e-probe.ts b/src/renderer/src/components/editor/monaco/monaco-e2e-probe.ts similarity index 100% rename from src/renderer/src/components/editor/monaco-e2e-probe.ts rename to src/renderer/src/components/editor/monaco/monaco-e2e-probe.ts diff --git a/src/renderer/src/components/editor/monaco-editor-input-bindings.ts b/src/renderer/src/components/editor/monaco/monaco-editor-input-bindings.ts similarity index 90% rename from src/renderer/src/components/editor/monaco-editor-input-bindings.ts rename to src/renderer/src/components/editor/monaco/monaco-editor-input-bindings.ts index eb84fd4c19c..c99c6aae420 100644 --- a/src/renderer/src/components/editor/monaco-editor-input-bindings.ts +++ b/src/renderer/src/components/editor/monaco/monaco-editor-input-bindings.ts @@ -1,24 +1,24 @@ import type { Dispatch, MutableRefObject, SetStateAction } from 'react' import type { editor } from 'monaco-editor' import { toast } from 'sonner' -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' import { translate } from '@/i18n/i18n' import { getMonacoCodebaseSearchQuery } from './monaco-codebase-search' -import { formatSelectionReference } from './selection-copy' -import { openAddToChatMenuAt } from '../native-chat/selection-quotes/add-to-chat-point-menu' -import { formatNativeChatQuote } from '../native-chat/selection-quotes/native-chat-quote-selection' -import { getDiffCommentPopoverLeft } from '../diff-comments/diff-comment-popover-position' +import { formatSelectionReference } from '../contextual-copy/selection-copy' +import { openAddToChatMenuAt } from '../../native-chat/selection-quotes/add-to-chat-point-menu' +import { formatNativeChatQuote } from '../../native-chat/selection-quotes/native-chat-quote-selection' +import { getDiffCommentPopoverLeft } from '../../diff-comments/diff-comment-popover-position' import { installEditorAddReviewNoteShortcut, installEditorSaveShortcut, installMonacoEditorFindShortcut -} from './editor-shortcuts' +} from '../commands/editor-shortcuts' import { getMonacoMarkdownSelectionAnnotationTarget, type MonacoMarkdownSelectionAnnotationTarget -} from './markdown-source-tools/monaco/monaco-markdown-selection-annotation' +} from '../markdown-source-tools/monaco/monaco-markdown-selection-annotation' import { handleMonacoLargeTextPaste, MONACO_PASTE_MAX_BYTES } from './monaco-large-text-paste' -import type { MarkdownCommentPopoverState } from './markdown-source-tools/monaco/use-monaco-markdown-annotations' +import type { MarkdownCommentPopoverState } from '../markdown-source-tools/monaco/use-monaco-markdown-annotations' import type { MonacoEditorPropsRef } from './monaco-editor-mount-params' type MonacoEditorInputBindingsParams = { diff --git a/src/renderer/src/components/editor/monaco-editor-mount-params.ts b/src/renderer/src/components/editor/monaco/monaco-editor-mount-params.ts similarity index 82% rename from src/renderer/src/components/editor/monaco-editor-mount-params.ts rename to src/renderer/src/components/editor/monaco/monaco-editor-mount-params.ts index 0aab0a9792c..04063a163f7 100644 --- a/src/renderer/src/components/editor/monaco-editor-mount-params.ts +++ b/src/renderer/src/components/editor/monaco/monaco-editor-mount-params.ts @@ -1,10 +1,10 @@ import type { Dispatch, MutableRefObject, SetStateAction } from 'react' import type { editor, IRange } from 'monaco-editor' -import type { useContextualCopySetup } from './useContextualCopySetup' -import type { MonacoContentSyncMode } from './monaco-content-sync' -import type { MarkdownDocLinkDecorationController } from './markdown-source-tools/monaco/monaco-markdown-doc-link-decorations' -import type { MonacoMarkdownSelectionAnnotationTarget } from './markdown-source-tools/monaco/monaco-markdown-selection-annotation' -import type { MarkdownCommentPopoverState } from './markdown-source-tools/monaco/use-monaco-markdown-annotations' +import type { useContextualCopySetup } from '../contextual-copy/useContextualCopySetup' +import type { MonacoContentSyncMode } from '../model-lifetime/buffer-text/monaco-content-sync' +import type { MarkdownDocLinkDecorationController } from '../markdown-source-tools/monaco/monaco-markdown-doc-link-decorations' +import type { MonacoMarkdownSelectionAnnotationTarget } from '../markdown-source-tools/monaco/monaco-markdown-selection-annotation' +import type { MarkdownCommentPopoverState } from '../markdown-source-tools/monaco/use-monaco-markdown-annotations' export type MonacoEditorPropsRef = MutableRefObject<{ relativePath: string diff --git a/src/renderer/src/components/editor/monaco-find-options.ts b/src/renderer/src/components/editor/monaco/monaco-find-options.ts similarity index 100% rename from src/renderer/src/components/editor/monaco-find-options.ts rename to src/renderer/src/components/editor/monaco/monaco-find-options.ts diff --git a/src/renderer/src/components/editor/monaco-find-widget.ts b/src/renderer/src/components/editor/monaco/monaco-find-widget.ts similarity index 100% rename from src/renderer/src/components/editor/monaco-find-widget.ts rename to src/renderer/src/components/editor/monaco/monaco-find-widget.ts diff --git a/src/renderer/src/components/editor/monaco-internal-test-module-types.ts b/src/renderer/src/components/editor/monaco/monaco-internal-test-module-types.ts similarity index 100% rename from src/renderer/src/components/editor/monaco-internal-test-module-types.ts rename to src/renderer/src/components/editor/monaco/monaco-internal-test-module-types.ts diff --git a/src/renderer/src/components/editor/monaco-large-text-paste.crlf.test.ts b/src/renderer/src/components/editor/monaco/monaco-large-text-paste.crlf.test.ts similarity index 100% rename from src/renderer/src/components/editor/monaco-large-text-paste.crlf.test.ts rename to src/renderer/src/components/editor/monaco/monaco-large-text-paste.crlf.test.ts diff --git a/src/renderer/src/components/editor/monaco-large-text-paste.ts b/src/renderer/src/components/editor/monaco/monaco-large-text-paste.ts similarity index 98% rename from src/renderer/src/components/editor/monaco-large-text-paste.ts rename to src/renderer/src/components/editor/monaco/monaco-large-text-paste.ts index 8916e996579..584979b97b7 100644 --- a/src/renderer/src/components/editor/monaco-large-text-paste.ts +++ b/src/renderer/src/components/editor/monaco/monaco-large-text-paste.ts @@ -1,6 +1,6 @@ import type { editor } from 'monaco-editor' -import { yieldToEventLoop } from '../../../../shared/event-loop-yield' -import { getUtf8ChunkEndIndex } from '../../../../shared/utf8-byte-limits' +import { yieldToEventLoop } from '../../../../../shared/event-loop-yield' +import { getUtf8ChunkEndIndex } from '../../../../../shared/utf8-byte-limits' import { measureTextControlPasteByteLength, measureTextControlPasteByteLengthWithYield diff --git a/src/renderer/src/components/editor/monaco-reveal-range.ts b/src/renderer/src/components/editor/monaco/monaco-reveal-range.ts similarity index 100% rename from src/renderer/src/components/editor/monaco-reveal-range.ts rename to src/renderer/src/components/editor/monaco/monaco-reveal-range.ts diff --git a/src/renderer/src/components/editor/monaco-reveal.ts b/src/renderer/src/components/editor/monaco/monaco-reveal.ts similarity index 100% rename from src/renderer/src/components/editor/monaco-reveal.ts rename to src/renderer/src/components/editor/monaco/monaco-reveal.ts diff --git a/src/renderer/src/components/editor/monaco-view-state-persistence.ts b/src/renderer/src/components/editor/monaco/monaco-view-state-persistence.ts similarity index 100% rename from src/renderer/src/components/editor/monaco-view-state-persistence.ts rename to src/renderer/src/components/editor/monaco/monaco-view-state-persistence.ts diff --git a/src/renderer/src/components/editor/use-monaco-editor-decorations.ts b/src/renderer/src/components/editor/monaco/use-monaco-editor-decorations.ts similarity index 92% rename from src/renderer/src/components/editor/use-monaco-editor-decorations.ts rename to src/renderer/src/components/editor/monaco/use-monaco-editor-decorations.ts index d61b033ce05..cbbfc949f84 100644 --- a/src/renderer/src/components/editor/use-monaco-editor-decorations.ts +++ b/src/renderer/src/components/editor/monaco/use-monaco-editor-decorations.ts @@ -1,11 +1,11 @@ import { useCallback, useEffect, useRef, type MutableRefObject } from 'react' import type { editor } from 'monaco-editor' -import type { MarkdownDocument } from '../../../../shared/file-listing/file-document' +import type { MarkdownDocument } from '../../../../../shared/file-listing/file-document' import { clearMarkdownDocCompletionDocuments, setMarkdownDocCompletionDocuments -} from './markdown-source-tools/monaco/monaco-markdown-doc-completions' -import type { MarkdownDocLinkDecorationController } from './markdown-source-tools/monaco/monaco-markdown-doc-link-decorations' +} from '../markdown-source-tools/monaco/monaco-markdown-doc-completions' +import type { MarkdownDocLinkDecorationController } from '../markdown-source-tools/monaco/monaco-markdown-doc-link-decorations' import { buildGitConflictDecorations } from './monaco-conflict-decorations' export type MonacoEditorDecorations = { diff --git a/src/renderer/src/components/editor/use-monaco-editor-mount.cursor-owner.test.tsx b/src/renderer/src/components/editor/monaco/use-monaco-editor-mount.cursor-owner.test.tsx similarity index 72% rename from src/renderer/src/components/editor/use-monaco-editor-mount.cursor-owner.test.tsx rename to src/renderer/src/components/editor/monaco/use-monaco-editor-mount.cursor-owner.test.tsx index 5f66a377961..648a96157f8 100644 --- a/src/renderer/src/components/editor/use-monaco-editor-mount.cursor-owner.test.tsx +++ b/src/renderer/src/components/editor/monaco/use-monaco-editor-mount.cursor-owner.test.tsx @@ -1,5 +1,5 @@ // @vitest-environment happy-dom -import { cleanup, render, renderHook } from '@testing-library/react' +import { cleanup, renderHook } from '@testing-library/react' import { Children, Suspense, type ReactNode } from 'react' import { afterEach, expect, it, vi } from 'vitest' import type { editor, IRange } from 'monaco-editor' @@ -11,8 +11,6 @@ import '@/lib/editor/file-operations/editor-file-operation-owner' import { captureEditorFileOperationProvenance } from '@/lib/editor/file-operations/editor-file-operation-provenance' import type { MonacoEditorMountParams } from './monaco-editor-mount-params' import { useMonacoEditorMount } from './use-monaco-editor-mount' -import { MonacoGutterContextMenu } from './MonacoGutterContextMenu' -import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call' const menuActions = vi.hoisted(() => new Map unknown>()) vi.mock('@/components/ui/dropdown-menu', () => { const Pass = ({ children }: { children?: ReactNode }) => <>{children} @@ -38,13 +36,13 @@ vi.mock('@/runtime/rpc/calls/runtime-rpc-call', () => ({ })) const storeRef = vi.hoisted((): { current?: StoreApi } => ({})) -vi.mock('@/store', () => ({ +vi.mock('@/store/index', () => ({ useAppStore: { getState: () => storeRef.current?.getState() } })) -vi.mock('./markdown-source-tools/monaco/monaco-markdown-doc-link-decorations', () => ({ +vi.mock('../markdown-source-tools/monaco/monaco-markdown-doc-link-decorations', () => ({ createMarkdownDocLinkDecorationController: vi.fn() })) -vi.mock('./markdown-source-tools/monaco/monaco-markdown-doc-completions', () => ({ +vi.mock('../markdown-source-tools/monaco/monaco-markdown-doc-completions', () => ({ ensureMarkdownDocCompletionProvider: vi.fn() })) vi.mock('./monaco-e2e-probe', () => ({ installMonacoE2EProbe: () => vi.fn() })) @@ -115,7 +113,7 @@ function mountParams( } } -it('keeps cursor and gutter copy on the current file owner after migration', async () => { +it('keeps cursor state on the current file owner after migration', async () => { const store = createEditorTabsStore() storeRef.current = store const filePath = '/repo/file.ts' @@ -191,7 +189,6 @@ it('keeps cursor and gutter copy on the current file owner after migration', asy expect(store.getState().editorCursorLine).toEqual({ [oldId]: 1 }) view.rerender({ next: { ...params, fileId: 'uncommitted-owner' }, suspend: true }) - expect(suspendedRender).toHaveBeenCalled() emitCursor?.({ position: { lineNumber: 7, column: 1 } }) expect(store.getState().editorCursorLine).toEqual({ [oldId]: 7 }) @@ -220,63 +217,7 @@ it('keeps cursor and gutter copy on the current file owner after migration', asy emitCursor?.({ position: { lineNumber: 42, column: 1 } }) expect(store.getState().editorCursorLine).toEqual({ [result.fileId]: 42 }) - vi.stubGlobal('api', { git: { remoteFileUrl: vi.fn() }, ui: { writeClipboardText: vi.fn() } }) - const snapshot = { ...selection } - mouseDown({ - event: Object.assign(new MouseEvent('mousedown'), { rightButton: true }), - target: { type: 3, position: { lineNumber: 15 } } - }) - expect(params.gutterMenu.setGutterMenuSelection).toHaveBeenCalledWith(snapshot) - expect(selection).not.toEqual(snapshot) - const copiedUrl = 'https://github.com/org/repo/blob/main/file.ts#L10-L20' - const local = vi.spyOn(window.api.git, 'remoteFileUrl').mockResolvedValue(copiedUrl) - const clipboard = vi.spyOn(window.api.ui, 'writeClipboardText') - for (const runtimeEnvironmentId of [null, 'paired']) { - store.setState({ openFiles: [{ ...store.getState().openFiles[0], runtimeEnvironmentId }] }) - const transport = runtimeEnvironmentId ? vi.mocked(callRuntimeRpc) : local - transport.mockResolvedValue(copiedUrl) - for (const line of [15, 21]) { - const menu = render( - - ) - const action = menuActions.get('Copy Remote URL')! - menu.unmount() - await action() - const expected = { - relativePath: 'file.ts', - line: line === 15 ? 10 : line, - endLine: line === 15 ? 20 : undefined - } - const owner = runtimeEnvironmentId - ? [ - { kind: 'environment', environmentId: 'paired' }, - 'git.remoteFileUrl', - { worktree: 'id:wt-1', ...expected }, - { timeoutMs: 15_000 } - ] - : [{ worktreePath: '/repo', connectionId: undefined, ...expected }] - expect(transport).toHaveBeenLastCalledWith(...owner) - expect(clipboard).toHaveBeenLastCalledWith(copiedUrl) - clipboard.mockClear() - transport.mockResolvedValueOnce(null) - await action() - expect(clipboard).not.toHaveBeenCalled() - transport.mockRejectedValueOnce(new Error('owner unavailable')) - await expect(action()).rejects.toThrow('owner unavailable') - expect(clipboard).not.toHaveBeenCalled() - } - } disposeEditor?.() - expect(cursorDispose).toHaveBeenCalledOnce() params.unregisterFileSearchSelectionRef.current?.() view.unmount() store.getState().closeFile(result.fileId) diff --git a/src/renderer/src/components/editor/use-monaco-editor-mount.ts b/src/renderer/src/components/editor/monaco/use-monaco-editor-mount.ts similarity index 93% rename from src/renderer/src/components/editor/use-monaco-editor-mount.ts rename to src/renderer/src/components/editor/monaco/use-monaco-editor-mount.ts index b48ac36421b..3e1721fa00a 100644 --- a/src/renderer/src/components/editor/use-monaco-editor-mount.ts +++ b/src/renderer/src/components/editor/monaco/use-monaco-editor-mount.ts @@ -1,18 +1,18 @@ import { useCallback, useLayoutEffect, useRef } from 'react' import type { OnMount } from '@monaco-editor/react' -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' import { registerFileSearchSelectedTextProvider } from '@/lib/files/search/file-search-selection' -import { syncContentOnMount } from './monaco-content-sync' -import { toEditorModelUri } from './model-lifetime/identity/editor-model-uri' +import { syncContentOnMount } from '../model-lifetime/buffer-text/monaco-content-sync' +import { toEditorModelUri } from '../model-lifetime/identity/editor-model-uri' import { beginProgrammaticContentSync, endProgrammaticContentSync -} from './monaco-programmatic-sync' -import { createMarkdownDocLinkDecorationController } from './markdown-source-tools/monaco/monaco-markdown-doc-link-decorations' -import { ensureMarkdownDocCompletionProvider } from './markdown-source-tools/monaco/monaco-markdown-doc-completions' +} from '../model-lifetime/buffer-text/monaco-programmatic-sync' +import { createMarkdownDocLinkDecorationController } from '../markdown-source-tools/monaco/monaco-markdown-doc-link-decorations' +import { ensureMarkdownDocCompletionProvider } from '../markdown-source-tools/monaco/monaco-markdown-doc-completions' import { clampMonacoAutoHeight } from './monaco-auto-height' import { installMonacoE2EProbe } from './monaco-e2e-probe' -import { matchesPendingEditorFocusRequest } from './pending-editor-focus-request' +import { matchesPendingEditorFocusRequest } from '../commands/pending-editor-focus-request' import { installMonacoEditorInputBindings } from './monaco-editor-input-bindings' import type { MonacoEditorMountParams } from './monaco-editor-mount-params' import { @@ -24,7 +24,7 @@ import { mayPushTabTextIntoModel, requestStaleCacheReload, settleStaleCacheReload -} from './model-lifetime/buffer-text/editor-stale-mount-guard' +} from '../model-lifetime/buffer-text/editor-stale-mount-guard' function ignoreCursorLine(): void {} diff --git a/src/renderer/src/components/editor/use-monaco-reveal-scheduler.ts b/src/renderer/src/components/editor/monaco/use-monaco-reveal-scheduler.ts similarity index 100% rename from src/renderer/src/components/editor/use-monaco-reveal-scheduler.ts rename to src/renderer/src/components/editor/monaco/use-monaco-reveal-scheduler.ts diff --git a/src/renderer/src/components/editor/use-plugin-syntax-language.ts b/src/renderer/src/components/editor/monaco/use-plugin-syntax-language.ts similarity index 100% rename from src/renderer/src/components/editor/use-plugin-syntax-language.ts rename to src/renderer/src/components/editor/monaco/use-plugin-syntax-language.ts diff --git a/src/renderer/src/components/editor/notebook/cells/IpynbCellEditor.external-sync.test.tsx b/src/renderer/src/components/editor/notebook/cells/IpynbCellEditor.external-sync.test.tsx deleted file mode 100644 index 461c7a317ac..00000000000 --- a/src/renderer/src/components/editor/notebook/cells/IpynbCellEditor.external-sync.test.tsx +++ /dev/null @@ -1,376 +0,0 @@ -// @vitest-environment happy-dom -import { useMemo, useState } from 'react' -import type { editor } from 'monaco-editor' -import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { IpynbCellSource } from './IpynbCellEditor' -import { parseIpynb } from '../document/ipynb-parse' -import { hasIpynbSourceDraft, useIpynbDocumentEditing } from '../document/useIpynbDocumentEditing' -import { getIpynbCellKey } from '../document/ipynb-source-drafts' - -const liveModels = vi.hoisted(() => { - const models: editor.ITextModel[] = [] - return models -}) -const widgetCalls = vi.hoisted(() => ({ - focus: vi.fn(), - layout: vi.fn(), - updateOptions: vi.fn<(options: editor.IEditorOptions) => void>() -})) - -vi.mock('@/i18n/i18n', () => ({ - i18n: { language: 'en' }, - translate: (_key: string, fallback: string) => fallback -})) -vi.mock('@/store/index', () => { - const state = { - settings: undefined, - editorFontZoomLevel: 0, - editorBufferIdOf: {}, - editorBufferViews: {} - } - return { - useAppStore: Object.assign((selector: (value: typeof state) => unknown) => selector(state), { - getState: () => state - }) - } -}) -vi.mock('@/hooks/use-document-dark-theme', () => ({ useDocumentDarkTheme: () => true })) -vi.mock('../../MonacoCodeExcerpt', () => ({ useMonacoColorizedLines: () => [] })) -vi.mock('../../editor-shortcuts', () => ({ installMonacoEditorFindShortcut: () => () => {} })) -vi.mock('@/lib/editor/monaco/monaco-setup', async () => { - const actual = await import('monaco-editor/esm/vs/editor/editor.api.js') - for (const id of ['python', 'javascript', 'markdown']) { - actual.languages.register({ id }) - } - return { - monaco: { - ...actual, - editor: { - ...actual.editor, - setTheme: vi.fn(), - createModel: (...args: Parameters) => { - const model = actual.editor.createModel(...args) - liveModels.push(model) - return model - }, - create: (_container: HTMLElement, { model }: { model: editor.ITextModel }) => ({ - getModel: () => model, - getContentHeight: () => 28, - layout: widgetCalls.layout, - onDidContentSizeChange: () => ({ dispose: vi.fn() }), - restoreViewState: vi.fn(), - saveViewState: () => null, - getTargetAtClientPoint: () => null, - setPosition: vi.fn(), - focus: widgetCalls.focus, - onDidBlurEditorWidget: () => ({ dispose: vi.fn() }), - addCommand: vi.fn(), - updateOptions: widgetCalls.updateOptions, - pushUndoStop: () => { - model.pushStackElement() - return true - }, - dispose: vi.fn() - }) - } - } - } -}) - -function notebookContent( - cells: { id: string | null; source: string; kind?: 'code' | 'markdown' }[], - language = 'python' -): string { - return JSON.stringify({ - nbformat: 4, - nbformat_minor: 5, - metadata: { language_info: { name: language } }, - cells: cells.map(({ id, source, kind }) => ({ - ...(id === null ? {} : { id }), - cell_type: kind ?? 'code', - metadata: {}, - execution_count: null, - outputs: [], - source: [source] - })) - }) -} - -function NotebookCells({ - content, - onContentChange, - onDirtyStateHint -}: { - content: string - onContentChange: (content: string) => void - onDirtyStateHint: (dirty: boolean) => void -}): React.JSX.Element { - const notebook = useMemo(() => parseIpynb(content), [content]) - const [editingCellKey, setEditingCellKey] = useState(null) - const editing = useIpynbDocumentEditing({ - content, - fileId: 'external-notebook', - notebook, - onContentChange, - onDirtyStateHint, - onDeactivateEditor: () => setEditingCellKey(null) - }) - return ( -
- {notebook.cells.map((cell, index) => { - const key = getIpynbCellKey(cell, index) - return ( - setEditingCellKey(key)} - onDeactivate={() => setEditingCellKey(null)} - onChange={(source) => editing.updateCellSource(index, source)} - /> - ) - })} -
- ) -} - -function activeModel(): editor.ITextModel { - const model = liveModels.at(-1) - if (!model) { - throw new Error('No notebook cell editor was created') - } - return model -} - -function appendText(model: editor.ITextModel, text: string): void { - const end = model.getFullModelRange() - act(() => { - model.pushEditOperations( - [], - [{ range: { ...end, startLineNumber: end.endLineNumber, startColumn: end.endColumn }, text }], - () => null - ) - vi.advanceTimersByTime(400) - }) -} - -// Monaco keeps disposed undo history globally, so each test gets its own model URIs. -let modelScope = 0 -beforeEach(() => { - modelScope += 1 - vi.useFakeTimers() - vi.clearAllMocks() -}) -afterEach(() => { - cleanup() - for (const model of liveModels.splice(0)) { - if (!model.isDisposed()) { - model.dispose() - } - } - vi.useRealTimers() -}) - -describe('active notebook cell external reload', () => { - it('shows the new source without dirtying the clean notebook or recreating the model', () => { - const onContentChange = vi.fn<(content: string) => void>() - const onDirtyStateHint = vi.fn<(dirty: boolean) => void>() - const initialContent = notebookContent([{ id: 'a', source: 'original source' }]) - const { rerender } = render( - - ) - fireEvent.mouseDown(screen.getByRole('button', { name: 'original source' }), { button: 0 }) - const model = activeModel() - expect(model.getValue()).toBe('original source') - expect(onDirtyStateHint).not.toHaveBeenCalled() - - rerender( - - ) - - expect(liveModels).toEqual([model]) - expect(model.getValue()).toBe('updated from disk') - expect(onContentChange).not.toHaveBeenCalled() - expect(onDirtyStateHint).not.toHaveBeenCalled() - expect(widgetCalls.focus).toHaveBeenCalledOnce() - appendText(model, '!') - const committedContent = onContentChange.mock.calls.at(-1)?.[0] - expect(committedContent).toBeDefined() - expect(parseIpynb(committedContent ?? '').cells[0]?.source).toBe('updated from disk!') - }) - - it('writes edits to the same cell after a clean external reload reorders it', () => { - const onContentChange = vi.fn<(content: string) => void>() - const onDirtyStateHint = vi.fn<(dirty: boolean) => void>() - const first = { id: 'a', source: 'active source' } - const second = { id: 'b', source: 'other source' } - const { rerender } = render( - - ) - fireEvent.mouseDown(screen.getByRole('button', { name: first.source }), { button: 0 }) - const model = activeModel() - expect(onDirtyStateHint).not.toHaveBeenCalled() - - rerender( - - ) - expect(liveModels).toEqual([model]) - expect(onDirtyStateHint).not.toHaveBeenCalled() - appendText(model, '!') - - const committedContent = onContentChange.mock.calls.at(-1)?.[0] - expect(committedContent).toBeDefined() - const cells = parseIpynb(committedContent ?? '').cells - expect(cells.map(({ id, source }) => ({ id, source }))).toEqual([ - second, - { ...first, source: 'active source!' } - ]) - }) - - it('retains undo for external source changes and treats undo as a user edit', async () => { - const onContentChange = vi.fn<(content: string) => void>() - const onDirtyStateHint = vi.fn<(dirty: boolean) => void>() - const { rerender } = render( - - ) - fireEvent.mouseDown(screen.getByRole('button', { name: 'original source' }), { button: 0 }) - const model = activeModel() - rerender( - - ) - - expect(model.canUndo()).toBe(true) - expect(onDirtyStateHint).not.toHaveBeenCalled() - await act(async () => { - await model.undo() - vi.advanceTimersByTime(400) - }) - expect(model.getValue()).toBe('original source') - expect(onDirtyStateHint).toHaveBeenCalledWith(true) - const committedContent = onContentChange.mock.calls.at(-1)?.[0] - expect(parseIpynb(committedContent ?? '').cells[0]?.source).toBe('original source') - }) -}) - -describe('notebook cell undo lifetime', () => { - const callbacks = () => ({ - onContentChange: vi.fn<(content: string) => void>(), - onDirtyStateHint: vi.fn<(dirty: boolean) => void>() - }) - - async function undo(model: editor.ITextModel): Promise { - await act(async () => { - await model.undo() - }) - } - - it('restores cell undo after switching cells and after the notebook view remounts', async () => { - const handlers = callbacks() - const content = notebookContent([ - { id: 'a', source: 'first source' }, - { id: 'b', source: 'second source' } - ]) - const firstView = render() - fireEvent.mouseDown(screen.getByRole('button', { name: 'first source' }), { button: 0 }) - appendText(activeModel(), '!') - fireEvent.mouseDown(screen.getByRole('button', { name: 'second source' }), { button: 0 }) - appendText(activeModel(), '?') - fireEvent.mouseDown(screen.getByRole('button', { name: 'first source!' }), { button: 0 }) - await undo(activeModel()) - expect(activeModel().getValue()).toBe('first source') - expect(screen.getByRole('button', { name: 'second source?' })).toBeDefined() - - const edited = handlers.onContentChange.mock.calls.at(-1)?.[0] ?? '' - firstView.unmount() - render() - fireEvent.mouseDown(screen.getByRole('button', { name: 'second source?' }), { button: 0 }) - await undo(activeModel()) - expect(activeModel().getValue()).toBe('second source') - }) - - it('drops cell undo when the source changed while the cell was inactive', async () => { - const handlers = callbacks() - const { rerender } = render( - - ) - fireEvent.mouseDown(screen.getByRole('button', { name: 'original' }), { button: 0 }) - appendText(activeModel(), '!') - fireEvent.mouseDown(screen.getByRole('button', { name: 'other' }), { button: 0 }) - rerender( - - ) - rerender( - - ) - fireEvent.mouseDown(screen.getByRole('button', { name: 'from disk' }), { button: 0 }) - await undo(activeModel()) - expect(activeModel().getValue()).toBe('from disk') - }) - - it('does not carry undo to another id-less cell that moves into the same position', async () => { - const handlers = callbacks() - const { rerender } = render( - - ) - fireEvent.mouseDown(screen.getByRole('button', { name: 'text' }), { button: 0 }) - appendText(activeModel(), '!') - fireEvent.mouseDown(screen.getByRole('button', { name: 'text!' }), { button: 0 }) - rerender( - - ) - rerender( - - ) - fireEvent.mouseDown(screen.getByRole('button', { name: 'text!' }), { button: 0 }) - handlers.onContentChange.mockClear() - await undo(activeModel()) - expect(activeModel().getValue()).toBe('text!') - expect(handlers.onContentChange).not.toHaveBeenCalled() - }) -}) diff --git a/src/renderer/src/components/editor/notebook/cells/IpynbCellEditor.tsx b/src/renderer/src/components/editor/notebook/cells/IpynbCellEditor.tsx index 84c96d49922..97e1cfdd440 100644 --- a/src/renderer/src/components/editor/notebook/cells/IpynbCellEditor.tsx +++ b/src/renderer/src/components/editor/notebook/cells/IpynbCellEditor.tsx @@ -9,15 +9,15 @@ import { } from '@/lib/editor/appearance/editor-typography' import { useEditorTypography } from '@/hooks/use-editor-typography' import { cn } from '@/lib/class-names' -import { installMonacoEditorFindShortcut } from '../../editor-shortcuts' -import { syncContentUpdate } from '../../monaco-content-sync' +import { installMonacoEditorFindShortcut } from '../../commands/editor-shortcuts' +import { syncContentUpdate } from '../../model-lifetime/buffer-text/monaco-content-sync' import { IPYNB_CODE_CELL_PREVIEW_MAX_LINES, getIpynbCodeCellPreviewLines } from './ipynb-code-cell-lines' import type { IpynbCell } from '../document/ipynb-parse' import { MarkdownPreviewBody } from '../../markdown-preview/render-tree/MarkdownPreviewBody' -import { useMonacoColorizedLines } from '../../MonacoCodeExcerpt' +import { useMonacoColorizedLines } from '../../monaco/MonacoCodeExcerpt' import { useDocumentDarkTheme } from '@/hooks/use-document-dark-theme' import { useMonacoTheme } from '@/hooks/use-monaco-theme' diff --git a/src/renderer/src/components/editor/notebook/document/IpynbViewer.tsx b/src/renderer/src/components/editor/notebook/document/IpynbViewer.tsx index 7b916da380a..12136146da6 100644 --- a/src/renderer/src/components/editor/notebook/document/IpynbViewer.tsx +++ b/src/renderer/src/components/editor/notebook/document/IpynbViewer.tsx @@ -14,7 +14,7 @@ import { } from '@/components/ui/dialog' import { useShortcutKeyDetails } from '@/hooks/useShortcutLabel' import { useTranslate } from '@/i18n/use-translate' -import { editorShortcutMatches } from '../../editor-shortcuts' +import { editorShortcutMatches } from '../../commands/editor-shortcuts' import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import { findFirstIndexedWorktree } from '@/lib/workspace/ownership/worktree-runtime-owner-index' import { IpynbCellToolbar, IpynbToolbarButton } from '../cells/IpynbCellToolbar' diff --git a/src/renderer/src/components/editor/ChangesModeView.tsx b/src/renderer/src/components/editor/panel/ChangesModeView.tsx similarity index 89% rename from src/renderer/src/components/editor/ChangesModeView.tsx rename to src/renderer/src/components/editor/panel/ChangesModeView.tsx index 0d6343d37a4..5ce99e5a7a9 100644 --- a/src/renderer/src/components/editor/ChangesModeView.tsx +++ b/src/renderer/src/components/editor/panel/ChangesModeView.tsx @@ -1,13 +1,13 @@ import React from 'react' import { lazyWithRetry as lazy } from '@/app-shell/startup/lazy-with-retry' import type { OpenFile } from '@/store/slices/editor/types/open-file' -import type { GitDiffResult } from '../../../../shared/git/diff/git-diff-compare-types' -import type { GitStatusEntry } from '../../../../shared/git/status/git-status-types' -import { ConflictBanner } from './ConflictComponents' -import { getDiffContentSignature } from './diff/sections/diff-content-signature' +import type { GitDiffResult } from '../../../../../shared/git/diff/git-diff-compare-types' +import type { GitStatusEntry } from '../../../../../shared/git/status/git-status-types' +import { ConflictBanner } from '../conflict-review/ConflictComponents' +import { getDiffContentSignature } from '../diff/sections/diff-content-signature' import { useTranslate } from '@/i18n/use-translate' -const DiffViewer = lazy(() => import('./diff/monaco-view/DiffViewer')) +const DiffViewer = lazy(() => import('../diff/monaco-view/DiffViewer')) // Render HEAD against the live edit draft so existing dirty tracking, autosave and close prompts keep working without a separate diff tab. export function ChangesModeView({ diff --git a/src/renderer/src/components/editor/EditorDiffFileSurface.tsx b/src/renderer/src/components/editor/panel/EditorDiffFileSurface.tsx similarity index 91% rename from src/renderer/src/components/editor/EditorDiffFileSurface.tsx rename to src/renderer/src/components/editor/panel/EditorDiffFileSurface.tsx index 8beb4d1d7a9..079cbd7b336 100644 --- a/src/renderer/src/components/editor/EditorDiffFileSurface.tsx +++ b/src/renderer/src/components/editor/panel/EditorDiffFileSurface.tsx @@ -1,12 +1,12 @@ -import { DiffTooLargeNotice } from './diff/large-files/DiffTooLargeNotice' +import { DiffTooLargeNotice } from '../diff/large-files/DiffTooLargeNotice' import { useTranslate } from '@/i18n/use-translate' import type { OpenFile } from '@/store/slices/editor/types/open-file' -import type { GitDiffResult } from '../../../../shared/git/diff/git-diff-compare-types' -import { getDiffContentSignature } from './diff/sections/diff-content-signature' -import { DiffViewer, ImageDiffViewer, MarkdownPreview } from './editor-lazy-views' -import { ExternalFileChangeBanner } from './external-changes/ExternalFileChangeBanner' -import { MarkdownPreviewSizeGate } from './markdown-preview/render-tree/MarkdownPreviewSizeGate' -import type { useMarkdownDocuments } from './markdown/documents/useMarkdownDocuments' +import type { GitDiffResult } from '../../../../../shared/git/diff/git-diff-compare-types' +import { getDiffContentSignature } from '../diff/sections/diff-content-signature' +import { DiffViewer, ImageDiffViewer, MarkdownPreview } from '../editor-lazy-views' +import { ExternalFileChangeBanner } from '../external-changes/ExternalFileChangeBanner' +import { MarkdownPreviewSizeGate } from '../markdown-preview/render-tree/MarkdownPreviewSizeGate' +import type { useMarkdownDocuments } from '../markdown/documents/useMarkdownDocuments' type MarkdownDocumentsController = ReturnType diff --git a/src/renderer/src/components/editor/EditorEditFileSurface.tsx b/src/renderer/src/components/editor/panel/EditorEditFileSurface.tsx similarity index 90% rename from src/renderer/src/components/editor/EditorEditFileSurface.tsx rename to src/renderer/src/components/editor/panel/EditorEditFileSurface.tsx index 3084cc31873..338ab55e2fa 100644 --- a/src/renderer/src/components/editor/EditorEditFileSurface.tsx +++ b/src/renderer/src/components/editor/panel/EditorEditFileSurface.tsx @@ -2,10 +2,10 @@ import { useTranslate } from '@/i18n/use-translate' import { getConnectionIdForFile } from '@/lib/workspace/ownership/connection-context' import type { MarkdownViewMode, OpenFile } from '@/store/slices/editor/types/open-file' import type { PendingEditorReveal } from '@/store/slices/editor/types/pending-editor-reveal' -import type { GitDiffResult } from '../../../../shared/git/diff/git-diff-compare-types' -import type { GitStatusEntry } from '../../../../shared/git/status/git-status-types' +import type { GitDiffResult } from '../../../../../shared/git/diff/git-diff-compare-types' +import type { GitStatusEntry } from '../../../../../shared/git/status/git-status-types' import { ChangesModeView } from './ChangesModeView' -import { ConflictBanner, ConflictPlaceholderView } from './ConflictComponents' +import { ConflictBanner, ConflictPlaceholderView } from '../conflict-review/ConflictComponents' import { CsvViewer, CsvPagedViewer, @@ -14,26 +14,26 @@ import { MermaidViewer, MonacoEditor, MediaViewer -} from './editor-lazy-views' -import type { EditorConflictNavigation } from './useEditorConflictNavigation' -import { EditorFileLoadErrorView } from './EditorFileLoadErrorView' -import { RecoverableRenderErrorBoundary } from '../error-boundaries/RecoverableRenderErrorBoundary' +} from '../editor-lazy-views' +import type { EditorConflictNavigation } from '../conflict-review/useEditorConflictNavigation' +import { EditorFileLoadErrorView } from '../loading/EditorFileLoadErrorView' +import { RecoverableRenderErrorBoundary } from '../../error-boundaries/RecoverableRenderErrorBoundary' import { HOST_STARTED, USER_RETRY, type FileContent, type ReloadContentOptions -} from './editor-panel-content-types' -import { CustomEditorExternalChangeBanner } from './external-changes/CustomEditorExternalChangeBanner' -import { PluginFileTypeSuggestion } from './plugin-editors/PluginFileTypeSuggestion' -import { BinaryFileDefaultAppButton } from './document-media/BinaryFileDefaultAppButton' -import type { useMarkdownDocuments } from './markdown/documents/useMarkdownDocuments' +} from '../loading/editor-panel-content-types' +import { CustomEditorExternalChangeBanner } from '../external-changes/CustomEditorExternalChangeBanner' +import { PluginFileTypeSuggestion } from '../plugin-editors/PluginFileTypeSuggestion' +import { BinaryFileDefaultAppButton } from '../document-media/BinaryFileDefaultAppButton' +import type { useMarkdownDocuments } from '../markdown/documents/useMarkdownDocuments' import { EditorMarkdownFileSurface } from './EditorMarkdownFileSurface' -import type { MarkdownRenderState } from './markdown/render-mode/markdown-render-mode' +import type { MarkdownRenderState } from '../markdown/render-mode/markdown-render-mode' import type { ActivePluginCustomEditor } from '@/store/plugins/file-editors/plugin-custom-editors' -import { PluginEditorSourceFallbackNotice } from './plugin-editors/PluginEditorNeedsDesktopNotice' -import PluginCustomEditorFrame from './plugin-editors/PluginCustomEditorFrame' -import PluginBytesCustomEditorSurface from './plugin-editors/PluginBytesCustomEditorSurface' +import { PluginEditorSourceFallbackNotice } from '../plugin-editors/PluginEditorNeedsDesktopNotice' +import PluginCustomEditorFrame from '../plugin-editors/PluginCustomEditorFrame' +import PluginBytesCustomEditorSurface from '../plugin-editors/PluginBytesCustomEditorSurface' // Retry is a user act: it may start a stopped WSL distro. diff --git a/src/renderer/src/components/editor/EditorMarkdownFileSurface.tsx b/src/renderer/src/components/editor/panel/EditorMarkdownFileSurface.tsx similarity index 91% rename from src/renderer/src/components/editor/EditorMarkdownFileSurface.tsx rename to src/renderer/src/components/editor/panel/EditorMarkdownFileSurface.tsx index b1d16444575..f0853820c3e 100644 --- a/src/renderer/src/components/editor/EditorMarkdownFileSurface.tsx +++ b/src/renderer/src/components/editor/panel/EditorMarkdownFileSurface.tsx @@ -1,18 +1,18 @@ import { useTranslate } from '@/i18n/use-translate' -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' import type { MarkdownViewMode, OpenFile } from '@/store/slices/editor/types/open-file' import { Button } from '@/components/ui/button' -import { RICH_MARKDOWN_MAX_SIZE_BYTES } from '../../../../shared/constants' -import { formatBytes } from '../status-bar/workspace-space-format' -import { MarkdownPreview, RichMarkdownEditor } from './editor-lazy-views' +import { RICH_MARKDOWN_MAX_SIZE_BYTES } from '../../../../../shared/constants' +import { formatBytes } from '../../../../../shared/format-bytes' +import { MarkdownPreview, RichMarkdownEditor } from '../editor-lazy-views' import { extractFrontMatter, prependFrontMatter -} from './markdown/source-scanning/markdown-frontmatter' -import { exceedsMarkdownRenderOverrideSizeLimit } from './markdown/render-mode/markdown-rich-size-limit' -import type { MarkdownRenderState } from './markdown/render-mode/markdown-render-mode' -import { RichMarkdownErrorBoundary } from './rich-markdown/instance/RichMarkdownErrorBoundary' -import type { useMarkdownDocuments } from './markdown/documents/useMarkdownDocuments' +} from '../markdown/source-scanning/markdown-frontmatter' +import { exceedsMarkdownRenderOverrideSizeLimit } from '../markdown/render-mode/markdown-rich-size-limit' +import type { MarkdownRenderState } from '../markdown/render-mode/markdown-render-mode' +import { RichMarkdownErrorBoundary } from '../rich-markdown/instance/RichMarkdownErrorBoundary' +import type { useMarkdownDocuments } from '../markdown/documents/useMarkdownDocuments' type MarkdownDocumentsController = ReturnType diff --git a/src/renderer/src/components/editor/EditorPanel.custom-editor-flush.test.tsx b/src/renderer/src/components/editor/panel/EditorPanel.custom-editor-flush.test.tsx similarity index 88% rename from src/renderer/src/components/editor/EditorPanel.custom-editor-flush.test.tsx rename to src/renderer/src/components/editor/panel/EditorPanel.custom-editor-flush.test.tsx index 2a7cfb4957f..acfc49b442c 100644 --- a/src/renderer/src/components/editor/EditorPanel.custom-editor-flush.test.tsx +++ b/src/renderer/src/components/editor/panel/EditorPanel.custom-editor-flush.test.tsx @@ -2,12 +2,12 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' import type { OpenFile } from '@/store/slices/editor/types/open-file' -import type { EditorToggleValue } from './view-mode/EditorViewToggle' -import { registerEditorFrameFlush } from './save/flush/editor-pending-flush' +import type { EditorToggleValue } from '../view-mode/EditorViewToggle' +import { registerEditorFrameFlush } from '../save/flush/editor-pending-flush' -vi.mock('./useEditorPanelContentState', () => ({ +vi.mock('../loading/useEditorPanelContentState', () => ({ useEditorPanelContentState: () => ({ fileContents: {}, diffContents: {}, @@ -23,7 +23,7 @@ vi.mock('./EditorPanelShell', () => ({ }) => })) -import EditorPanel from './EditorPanel' +import EditorPanel from '../EditorPanel' const initialState = useAppStore.getInitialState() const filePath = '/repo/notes.md' diff --git a/src/renderer/src/components/editor/EditorPanel.diff-rich-owner.test.tsx b/src/renderer/src/components/editor/panel/EditorPanel.diff-rich-owner.test.tsx similarity index 94% rename from src/renderer/src/components/editor/EditorPanel.diff-rich-owner.test.tsx rename to src/renderer/src/components/editor/panel/EditorPanel.diff-rich-owner.test.tsx index aa2ea611f69..e0ac45d21f6 100644 --- a/src/renderer/src/components/editor/EditorPanel.diff-rich-owner.test.tsx +++ b/src/renderer/src/components/editor/panel/EditorPanel.diff-rich-owner.test.tsx @@ -2,12 +2,12 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' import type { OpenFile } from '@/store/slices/editor/types/open-file' import { buildOwnedEditorFileId } from '@/store/slices/editor/file-ids/editor-file-ids' -import type { EditorToggleValue } from './view-mode/EditorViewToggle' +import type { EditorToggleValue } from '../view-mode/EditorViewToggle' -vi.mock('./useEditorPanelContentState', () => ({ +vi.mock('../loading/useEditorPanelContentState', () => ({ useEditorPanelContentState: () => ({ fileContents: {}, diffContents: {}, @@ -23,7 +23,7 @@ vi.mock('./EditorPanelShell', () => ({ }) => })) -import EditorPanel from './EditorPanel' +import EditorPanel from '../EditorPanel' const initialState = useAppStore.getInitialState() const filePath = '/repo/notes.md' diff --git a/src/renderer/src/components/editor/EditorPanel.html-preview.test.tsx b/src/renderer/src/components/editor/panel/EditorPanel.html-preview.test.tsx similarity index 90% rename from src/renderer/src/components/editor/EditorPanel.html-preview.test.tsx rename to src/renderer/src/components/editor/panel/EditorPanel.html-preview.test.tsx index a9477168f36..6e4e3e0c88a 100644 --- a/src/renderer/src/components/editor/EditorPanel.html-preview.test.tsx +++ b/src/renderer/src/components/editor/panel/EditorPanel.html-preview.test.tsx @@ -1,7 +1,7 @@ // @vitest-environment happy-dom import { cleanup, fireEvent, render, waitFor } from '@testing-library/react' import { afterEach, expect, it, vi } from 'vitest' -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' import { markHydratedBrowserPageHandles } from '@/store/slices/browser/hosts/browser-initialization-presentation-ownership' import { TooltipProvider } from '@/components/ui/tooltip' import { @@ -10,19 +10,19 @@ import { seedEditorPlacementConsumers } from '@/store/slices/editor/tabs/editor-placement-consumers.test-fixture' -vi.mock('./useEditorPanelContentState', () => ({ +vi.mock('../loading/useEditorPanelContentState', () => ({ useEditorPanelContentState: () => ({ fileContents: {}, diffContents: {}, reloadContent: () => {} }) })) -vi.mock('./EditorContent', () => ({ EditorContent: () => null })) +vi.mock('../EditorContent', () => ({ EditorContent: () => null })) vi.mock('@/components/browser-pane/host-guest/browser-focus', () => ({ requestBrowserFocus: vi.fn() })) -import EditorPanel from './EditorPanel' +import EditorPanel from '../EditorPanel' afterEach(() => { cleanup() diff --git a/src/renderer/src/components/editor/EditorPanelShell.tsx b/src/renderer/src/components/editor/panel/EditorPanelShell.tsx similarity index 92% rename from src/renderer/src/components/editor/EditorPanelShell.tsx rename to src/renderer/src/components/editor/panel/EditorPanelShell.tsx index 5a35d7ac583..3b1e1779dbd 100644 --- a/src/renderer/src/components/editor/EditorPanelShell.tsx +++ b/src/renderer/src/components/editor/panel/EditorPanelShell.tsx @@ -1,17 +1,17 @@ import { Suspense, type JSX, type Ref } from 'react' -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' import { findFirstIndexedWorktree } from '@/lib/workspace/ownership/worktree-runtime-owner-index' import type { OpenFile } from '@/store/slices/editor/types/open-file' -import { EditorContent } from './EditorContent' -import { EditorPanelHeader } from './EditorPanelHeader' -import { UntitledFileRenameDialog } from './UntitledFileRenameDialog' +import { EditorContent } from '../EditorContent' +import { EditorPanelHeader } from './header/EditorPanelHeader' +import { UntitledFileRenameDialog } from './untitled-rename/UntitledFileRenameDialog' import type { getEditorPanelRenderModel } from './editor-panel-render-model' -import type { DiffContent, FileContent } from './editor-panel-content-types' -import type { EditorToggleValue } from './view-mode/EditorViewToggle' -import { shouldShowEditorPanelHeader } from './editor-header' -import { getUntitledFileRoot } from './untitled-file-rename-path' +import type { DiffContent, FileContent } from '../loading/editor-panel-content-types' +import type { EditorToggleValue } from '../view-mode/EditorViewToggle' +import { shouldShowEditorPanelHeader } from './header/editor-header' +import { getUntitledFileRoot } from './untitled-rename/untitled-file-rename-path' import { useTranslate } from '@/i18n/use-translate' -import type { ArtifactWriteRequest } from '../../../../shared/artifacts/artifacts' +import type { ArtifactWriteRequest } from '../../../../../shared/artifacts/artifacts' import { selectWorktreesByRepo } from '@/store/workspaces/catalog/workspace-record-fields' type EditorPanelRenderModel = ReturnType diff --git a/src/renderer/src/components/editor/editor-artifact-upload.ts b/src/renderer/src/components/editor/panel/editor-artifact-upload.ts similarity index 90% rename from src/renderer/src/components/editor/editor-artifact-upload.ts rename to src/renderer/src/components/editor/panel/editor-artifact-upload.ts index b092addb847..dfb05ae8728 100644 --- a/src/renderer/src/components/editor/editor-artifact-upload.ts +++ b/src/renderer/src/components/editor/panel/editor-artifact-upload.ts @@ -1,12 +1,12 @@ -import type { ArtifactWriteRequest } from '../../../../shared/artifacts/artifacts' +import type { ArtifactWriteRequest } from '../../../../../shared/artifacts/artifacts' import { fileArtifactSourceKey, type FileArtifactOwner } from '@/lib/files/artifacts/file-artifact-source-key' import { basename } from '@/lib/path' -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' import type { OpenFile } from '@/store/slices/editor/types/open-file' -import { flushPendingEditorBuffer } from './save/flush/editor-pending-flush' +import { flushPendingEditorBuffer } from '../save/flush/editor-pending-flush' export type ShareableFileContentType = ArtifactWriteRequest['contentType'] diff --git a/src/renderer/src/components/editor/editor-panel-file-actions.ts b/src/renderer/src/components/editor/panel/editor-panel-file-actions.ts similarity index 92% rename from src/renderer/src/components/editor/editor-panel-file-actions.ts rename to src/renderer/src/components/editor/panel/editor-panel-file-actions.ts index 6f81ff78092..2a90bcd158f 100644 --- a/src/renderer/src/components/editor/editor-panel-file-actions.ts +++ b/src/renderer/src/components/editor/panel/editor-panel-file-actions.ts @@ -1,6 +1,6 @@ -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' import { editorViewPlacement } from '@/store/slices/editor/tabs/editor-view-placement' -import { detectLanguage } from '../../../../shared/editor-language/editor-language-detect' +import { detectLanguage } from '../../../../../shared/editor-language/editor-language-detect' import type { OpenFile } from '@/store/slices/editor/types/open-file' import type { getEditorPanelRenderModel } from './editor-panel-render-model' diff --git a/src/renderer/src/components/editor/editor-panel-render-model.ts b/src/renderer/src/components/editor/panel/editor-panel-render-model.ts similarity index 92% rename from src/renderer/src/components/editor/editor-panel-render-model.ts rename to src/renderer/src/components/editor/panel/editor-panel-render-model.ts index c424da1f119..db8b8c0e1a8 100644 --- a/src/renderer/src/components/editor/editor-panel-render-model.ts +++ b/src/renderer/src/components/editor/panel/editor-panel-render-model.ts @@ -1,30 +1,30 @@ -import { detectLanguage } from '../../../../shared/editor-language/editor-language-detect' +import { detectLanguage } from '../../../../../shared/editor-language/editor-language-detect' import { canPreviewLanguage } from '@/lib/browser/documents/file-preview' -import type { useAppStore } from '@/store' +import type { useAppStore } from '@/store/index' import type { MarkdownViewMode, OpenFile } from '@/store/slices/editor/types/open-file' -import type { MarkdownDefaultViewMode } from '../../../../shared/markdown-default-view-mode' +import type { MarkdownDefaultViewMode } from '../../../../../shared/markdown-default-view-mode' import { canOpenMarkdownPreview, getDefaultMarkdownViewMode, getEditorToggleModes, getEditorViewLanguage, getMarkdownViewModes -} from './view-mode/markdown-preview-controls' -import { getEditorHeaderOpenFileState } from './editor-header' -import type { EditorToggleValue } from './view-mode/EditorViewToggle' -import type { FileContent } from './editor-panel-content-types' -import { canUseChangesModeForFile } from './view-mode/editor-panel-file-mode' +} from '../view-mode/markdown-preview-controls' +import { getEditorHeaderOpenFileState } from './header/editor-header' +import type { EditorToggleValue } from '../view-mode/EditorViewToggle' +import type { FileContent } from '../loading/editor-panel-content-types' +import { canUseChangesModeForFile } from '../view-mode/editor-panel-file-mode' import { getMarkdownRenderMode, type MarkdownRenderState -} from './markdown/render-mode/markdown-render-mode' -import { getCachedMarkdownRichModeEligibility } from './markdown/render-mode/markdown-rich-mode-eligibility-cache' -import { exceedsMarkdownRichModeSizeLimit } from './markdown/render-mode/markdown-rich-size-limit' +} from '../markdown/render-mode/markdown-render-mode' +import { getCachedMarkdownRichModeEligibility } from '../markdown/render-mode/markdown-rich-mode-eligibility-cache' +import { exceedsMarkdownRichModeSizeLimit } from '../markdown/render-mode/markdown-rich-size-limit' import { customEditorToggleValue, resolveCustomEditorToggle, type CustomEditorToggle -} from './view-mode/custom-editor-toggle' +} from '../view-mode/custom-editor-toggle' import type { ActivePluginCustomEditor, CustomEditorViewChoice diff --git a/src/renderer/src/components/editor/EditorFileLocationMenuItems.tsx b/src/renderer/src/components/editor/panel/header/EditorFileLocationMenuItems.tsx similarity index 97% rename from src/renderer/src/components/editor/EditorFileLocationMenuItems.tsx rename to src/renderer/src/components/editor/panel/header/EditorFileLocationMenuItems.tsx index b03fbd53ef2..2dfc5d1b39f 100644 --- a/src/renderer/src/components/editor/EditorFileLocationMenuItems.tsx +++ b/src/renderer/src/components/editor/panel/header/EditorFileLocationMenuItems.tsx @@ -1,7 +1,7 @@ import { Copy, ExternalLink } from 'lucide-react' import { DropdownMenuItem } from '@/components/ui/dropdown-menu' import { LocalOnlyMenuHint } from '@/components/local-only-menu-hint' -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' import type { OpenFile } from '@/store/slices/editor/types/open-file' import { isRevealOrEditorOpenBlocked } from '@/lib/files/os-open/local-path-open-guard' import { diff --git a/src/renderer/src/components/editor/EditorPanelActionsMenu.tsx b/src/renderer/src/components/editor/panel/header/EditorPanelActionsMenu.tsx similarity index 100% rename from src/renderer/src/components/editor/EditorPanelActionsMenu.tsx rename to src/renderer/src/components/editor/panel/header/EditorPanelActionsMenu.tsx diff --git a/src/renderer/src/components/editor/EditorPanelHeader.tsx b/src/renderer/src/components/editor/panel/header/EditorPanelHeader.tsx similarity index 95% rename from src/renderer/src/components/editor/EditorPanelHeader.tsx rename to src/renderer/src/components/editor/panel/header/EditorPanelHeader.tsx index fb2173af19b..0a981dc6884 100644 --- a/src/renderer/src/components/editor/EditorPanelHeader.tsx +++ b/src/renderer/src/components/editor/panel/header/EditorPanelHeader.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react' import { ArrowDown, ArrowUp, Columns2, Eye, FileText, ListTree, Rows2 } from 'lucide-react' -import { useAppStore } from '@/store' -import { useVisibleWorktreeDiffComments } from '../diff-comments/use-visible-worktree-diff-comments' +import { useAppStore } from '@/store/index' +import { useVisibleWorktreeDiffComments } from '../../../diff-comments/use-visible-worktree-diff-comments' import type { OpenFile } from '@/store/slices/editor/types/open-file' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import EditorViewToggle, { @@ -9,21 +9,21 @@ import EditorViewToggle, { CUSTOM_EDITOR_SOURCE_METADATA, NOTEBOOK_VIEW_MODE_METADATA, SVG_VIEW_MODE_METADATA -} from './view-mode/EditorViewToggle' -import type { CustomEditorToggleValue, EditorToggleValue } from './view-mode/EditorViewToggle' -import { MarkdownDefaultViewContextMenu } from './view-mode/MarkdownDefaultViewContextMenu' +} from '../../view-mode/EditorViewToggle' +import type { CustomEditorToggleValue, EditorToggleValue } from '../../view-mode/EditorViewToggle' +import { MarkdownDefaultViewContextMenu } from '../../view-mode/MarkdownDefaultViewContextMenu' import type { EditorHeaderOpenFileState } from './editor-header' -import { DiffNotesSendMenu } from './diff/review/DiffNotesSendMenu' +import { DiffNotesSendMenu } from '../../diff/review/DiffNotesSendMenu' import { EditorPanelActionsMenu } from './EditorPanelActionsMenu' import { EditorFileLocationMenuItems } from './EditorFileLocationMenuItems' import { useTranslate } from '@/i18n/use-translate' import { EditorPanelHeaderPath } from './EditorPanelHeaderPath' -import { useDiffNavigation } from './diff/review/diff-navigation-context' +import { useDiffNavigation } from '../../diff/review/diff-navigation-context' import { useShortcutKeyDetails } from '@/hooks/useShortcutLabel' import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo' -import type { ArtifactWriteRequest } from '../../../../shared/artifacts/artifacts' +import type { ArtifactWriteRequest } from '../../../../../../shared/artifacts/artifacts' import { ArtifactPublishButton } from '@/components/artifacts/ArtifactPublishButton' -import { editorArtifactSourceKey } from './editor-artifact-upload' +import { editorArtifactSourceKey } from '../editor-artifact-upload' const NO_CUSTOM_EDITOR_TITLES: Readonly> = {} diff --git a/src/renderer/src/components/editor/EditorPanelHeaderPath.tsx b/src/renderer/src/components/editor/panel/header/EditorPanelHeaderPath.tsx similarity index 100% rename from src/renderer/src/components/editor/EditorPanelHeaderPath.tsx rename to src/renderer/src/components/editor/panel/header/EditorPanelHeaderPath.tsx diff --git a/src/renderer/src/components/editor/editor-header-file-rename.test.ts b/src/renderer/src/components/editor/panel/header/editor-header-file-rename.test.ts similarity index 87% rename from src/renderer/src/components/editor/editor-header-file-rename.test.ts rename to src/renderer/src/components/editor/panel/header/editor-header-file-rename.test.ts index 7dd3bc9f37d..82b34855d7f 100644 --- a/src/renderer/src/components/editor/editor-header-file-rename.test.ts +++ b/src/renderer/src/components/editor/panel/header/editor-header-file-rename.test.ts @@ -1,13 +1,13 @@ // @vitest-environment happy-dom -import type * as EditorAutosave from './autosave/editor-autosave' +import type * as EditorAutosave from '../../autosave/editor-autosave' import type * as RuntimeFileMetadataClient from '@/runtime/files/metadata/runtime-file-metadata-client' import { act, renderHook } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { OpenFile } from '@/store/slices/editor/types/open-file' import { useEditorHeaderFileRename } from './editor-header-file-rename' -import './useUntitledFileRename' -import '@/store' +import '../untitled-rename/useUntitledFileRename' +import '@/store/index' import '@/store/slices/worktrees/worktree.test-fixture' import '@/store/repos/repo.test-fixture' @@ -23,7 +23,7 @@ vi.mock('@/runtime/files/metadata/runtime-file-metadata-client', async (original ...(await original()), runtimePathExists: renameIO.exists })) -vi.mock('./autosave/editor-autosave', async (original) => ({ +vi.mock('../../autosave/editor-autosave', async (original) => ({ ...(await original()), requestEditorSaveQuiesce: renameIO.quiesce, requestEditorFileSave: renameIO.save @@ -70,7 +70,7 @@ describe('useEditorHeaderFileRename', () => { }) // Focused input removal emits focusout after file switches; stale blur commits must not rename the replacement file. - it('ignores a blur-commit that arrives after the active file changed', () => { + it('closes the rename input when the active file changes', () => { const { result, rerender } = renderHook((file: OpenFile) => useEditorHeaderFileRename(file), { initialProps: baseFile() }) @@ -87,11 +87,5 @@ describe('useEditorHeaderFileRename', () => { ) expect(result.current.isRenaming).toBe(false) - - act(() => { - result.current.commitRename() - }) - - expect(renameFileOnDiskMock).not.toHaveBeenCalled() }) }) diff --git a/src/renderer/src/components/editor/editor-header-file-rename.ts b/src/renderer/src/components/editor/panel/header/editor-header-file-rename.ts similarity index 97% rename from src/renderer/src/components/editor/editor-header-file-rename.ts rename to src/renderer/src/components/editor/panel/header/editor-header-file-rename.ts index d6c7bb1f2a0..b37f4af26b0 100644 --- a/src/renderer/src/components/editor/editor-header-file-rename.ts +++ b/src/renderer/src/components/editor/panel/header/editor-header-file-rename.ts @@ -4,8 +4,8 @@ import type { OpenFile } from '@/store/slices/editor/types/open-file' import { useWorktreeById } from '@/store/workspaces/selection/use-selected-worktree' import { basename } from '@/lib/path' import { renameFileOnDisk } from '@/lib/editor/file-operations/rename-file' -import { getUntitledFileRoot } from './untitled-file-rename-path' -import { useAppStore } from '@/store' +import { getUntitledFileRoot } from '../untitled-rename/untitled-file-rename-path' +import { useAppStore } from '@/store/index' import { editorTabDocumentFolderAccess } from '@/lib/files/access/local-file-access' type EditorHeaderFileRenameState = { diff --git a/src/renderer/src/components/editor/editor-header.ts b/src/renderer/src/components/editor/panel/header/editor-header.ts similarity index 94% rename from src/renderer/src/components/editor/editor-header.ts rename to src/renderer/src/components/editor/panel/header/editor-header.ts index 7afbb4c201a..cc161e35369 100644 --- a/src/renderer/src/components/editor/editor-header.ts +++ b/src/renderer/src/components/editor/panel/header/editor-header.ts @@ -1,6 +1,6 @@ import type { OpenFile } from '@/store/slices/editor/types/open-file' -import type { GitBranchChangeEntry } from '../../../../shared/git/diff/git-diff-compare-types' -import type { GitStatusEntry } from '../../../../shared/git/status/git-status-types' +import type { GitBranchChangeEntry } from '../../../../../../shared/git/diff/git-diff-compare-types' +import type { GitStatusEntry } from '../../../../../../shared/git/status/git-status-types' import { getEditorDisplayLabel } from './editor-labels' import { translate } from '@/i18n/i18n' import type { Translate } from '@/i18n/use-translate' diff --git a/src/renderer/src/components/editor/editor-labels.ts b/src/renderer/src/components/editor/panel/header/editor-labels.ts similarity index 100% rename from src/renderer/src/components/editor/editor-labels.ts rename to src/renderer/src/components/editor/panel/header/editor-labels.ts diff --git a/src/renderer/src/components/editor/editor-path-display.ts b/src/renderer/src/components/editor/panel/header/editor-path-display.ts similarity index 100% rename from src/renderer/src/components/editor/editor-path-display.ts rename to src/renderer/src/components/editor/panel/header/editor-path-display.ts diff --git a/src/renderer/src/components/editor/open-editor-html-preview.test.ts b/src/renderer/src/components/editor/panel/open-editor-html-preview.test.ts similarity index 99% rename from src/renderer/src/components/editor/open-editor-html-preview.test.ts rename to src/renderer/src/components/editor/panel/open-editor-html-preview.test.ts index 6da5b17735e..eb07d40b20b 100644 --- a/src/renderer/src/components/editor/open-editor-html-preview.test.ts +++ b/src/renderer/src/components/editor/panel/open-editor-html-preview.test.ts @@ -12,7 +12,7 @@ import { WORKSPACE } from '@/lib/browser/navigation/browser-link-open.test-fixture' import { beginLinkOpenRequest } from '@/lib/links/link-open-request' -import { registerEditorFrameFlush } from './save/flush/editor-pending-flush' +import { registerEditorFrameFlush } from '../save/flush/editor-pending-flush' import { openEditorHtmlPreview } from './open-editor-html-preview' vi.mock('@/components/browser-pane/host-guest/browser-focus', () => ({ diff --git a/src/renderer/src/components/editor/open-editor-html-preview.ts b/src/renderer/src/components/editor/panel/open-editor-html-preview.ts similarity index 98% rename from src/renderer/src/components/editor/open-editor-html-preview.ts rename to src/renderer/src/components/editor/panel/open-editor-html-preview.ts index f846736d618..84dcd401fcd 100644 --- a/src/renderer/src/components/editor/open-editor-html-preview.ts +++ b/src/renderer/src/components/editor/panel/open-editor-html-preview.ts @@ -10,7 +10,7 @@ import { captureBrowserLinkReveal } from '@/lib/browser/navigation/browser-link- import { createBrowserLinkHostMatcher } from '@/lib/browser/navigation/browser-link-owner' import { beginLinkOpenRequest, subscribeLinkOpenRequests } from '@/lib/links/link-open-request' import { captureLinkOpenIntent } from '@/lib/links/placement/link-open-intent' -import { flushEditorFrames } from './save/flush/editor-pending-flush' +import { flushEditorFrames } from '../save/flush/editor-pending-flush' export async function openEditorHtmlPreview( fileId: string, diff --git a/src/renderer/src/components/editor/UntitledFileRenameDialog.tsx b/src/renderer/src/components/editor/panel/untitled-rename/UntitledFileRenameDialog.tsx similarity index 100% rename from src/renderer/src/components/editor/UntitledFileRenameDialog.tsx rename to src/renderer/src/components/editor/panel/untitled-rename/UntitledFileRenameDialog.tsx diff --git a/src/renderer/src/components/editor/untitled-file-rename-path.ts b/src/renderer/src/components/editor/panel/untitled-rename/untitled-file-rename-path.ts similarity index 100% rename from src/renderer/src/components/editor/untitled-file-rename-path.ts rename to src/renderer/src/components/editor/panel/untitled-rename/untitled-file-rename-path.ts diff --git a/src/renderer/src/components/editor/useUntitledFileRename.ts b/src/renderer/src/components/editor/panel/untitled-rename/useUntitledFileRename.ts similarity index 98% rename from src/renderer/src/components/editor/useUntitledFileRename.ts rename to src/renderer/src/components/editor/panel/untitled-rename/useUntitledFileRename.ts index 1c5897647ab..cd994e21e47 100644 --- a/src/renderer/src/components/editor/useUntitledFileRename.ts +++ b/src/renderer/src/components/editor/panel/untitled-rename/useUntitledFileRename.ts @@ -1,13 +1,13 @@ import { useCallback, useState } from 'react' import { translate } from '@/i18n/i18n' import { dirname, joinPath } from '@/lib/path' -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' import type { OpenFile } from '@/store/slices/editor/types/open-file' import { createRuntimePath } from '@/runtime/files/mutations/runtime-file-mutation-client' import { runtimePathExists } from '@/runtime/files/metadata/runtime-file-metadata-client' import { executeOpenEditorPathMove } from '@/lib/editor/file-operations/execute-open-editor-path-move' import { acquireEditorFileOperation } from '@/lib/editor/file-operations/editor-file-operation-owner' -import { requestEditorFileSave, requestEditorSaveQuiesce } from './autosave/editor-autosave' +import { requestEditorFileSave, requestEditorSaveQuiesce } from '../../autosave/editor-autosave' import { getUntitledFileRoot } from './untitled-file-rename-path' type UseUntitledFileRenameParams = { diff --git a/src/renderer/src/components/editor/use-quick-open-history-visit.ts b/src/renderer/src/components/editor/panel/use-quick-open-history-visit.ts similarity index 81% rename from src/renderer/src/components/editor/use-quick-open-history-visit.ts rename to src/renderer/src/components/editor/panel/use-quick-open-history-visit.ts index 1577b692eb5..a4b25a2f601 100644 --- a/src/renderer/src/components/editor/use-quick-open-history-visit.ts +++ b/src/renderer/src/components/editor/panel/use-quick-open-history-visit.ts @@ -1,8 +1,8 @@ import { useEffect } from 'react' -import { useAppStore } from '@/store' +import { useAppStore } from '@/store/index' import { recordQuickOpenFileVisit } from '@/lib/files/search/quick-open-file-history' import type { OpenFile } from '@/store/slices/editor/types/open-file' -import type { FileContent } from './editor-panel-content-types' +import type { FileContent } from '../loading/editor-panel-content-types' export function useQuickOpenHistoryVisit( file: OpenFile | null, diff --git a/src/renderer/src/components/editor/pdf/PdfViewer.tsx b/src/renderer/src/components/editor/pdf/PdfViewer.tsx index c8e87d9772d..c8c1935cb58 100644 --- a/src/renderer/src/components/editor/pdf/PdfViewer.tsx +++ b/src/renderer/src/components/editor/pdf/PdfViewer.tsx @@ -22,7 +22,7 @@ import { type PdfScalePreference } from './pdf-scale-preference' import { readPdfScalePreference, writePdfScalePreference } from './pdf-scale-preference-storage' -import { EditorCommandOwnerContext } from '../editor-command-owner-context' +import { EditorCommandOwnerContext } from '../commands/editor-command-owner-context' import { listenForViewerZoomRequests } from '../viewer-viewport/viewer-zoom-request' pdfjsLib.GlobalWorkerOptions.workerSrc = workerUrl diff --git a/src/renderer/src/components/editor/plugin-editors/PluginBytesCustomEditorSurface.tsx b/src/renderer/src/components/editor/plugin-editors/PluginBytesCustomEditorSurface.tsx index 796015fd91b..e22b1607f6e 100644 --- a/src/renderer/src/components/editor/plugin-editors/PluginBytesCustomEditorSurface.tsx +++ b/src/renderer/src/components/editor/plugin-editors/PluginBytesCustomEditorSurface.tsx @@ -9,7 +9,7 @@ import { fileTypeOfPath } from '../../../../../shared/plugins/contributions/plug import { formatBytes } from '../../../../../shared/format-bytes' import { Button } from '../../ui/button' import type { OpenFile } from '@/store/slices/editor/types/open-file' -import type { FileContent } from '../editor-panel-content-types' +import type { FileContent } from '../loading/editor-panel-content-types' import PluginCustomEditorFrame from './PluginCustomEditorFrame' import { ExternalFileChangeBanner } from '../external-changes/ExternalFileChangeBanner' import { diff --git a/src/renderer/src/components/editor/plugin-editors/PluginCustomEditorFrame.tsx b/src/renderer/src/components/editor/plugin-editors/PluginCustomEditorFrame.tsx index d0942b58661..dfbfc5273bd 100644 --- a/src/renderer/src/components/editor/plugin-editors/PluginCustomEditorFrame.tsx +++ b/src/renderer/src/components/editor/plugin-editors/PluginCustomEditorFrame.tsx @@ -16,7 +16,7 @@ import { } from '../../plugins/frames/plugin-frame-element' import { usePluginFrameHost } from '../../plugins/frames/use-plugin-frame-host' import { usePluginFrameEntry } from '../../plugins/frames/use-plugin-frame-entry' -import { editorShortcutMatches } from '../editor-shortcuts' +import { editorShortcutMatches } from '../commands/editor-shortcuts' import { countCustomEditorMergingView, useCustomEditorOutsideMerge diff --git a/src/renderer/src/components/editor/rich-markdown/commands/rich-markdown-save-shortcut.ts b/src/renderer/src/components/editor/rich-markdown/commands/rich-markdown-save-shortcut.ts index d83d5573e9c..ab1bf82bf6f 100644 --- a/src/renderer/src/components/editor/rich-markdown/commands/rich-markdown-save-shortcut.ts +++ b/src/renderer/src/components/editor/rich-markdown/commands/rich-markdown-save-shortcut.ts @@ -1,5 +1,5 @@ import type { KeyHandlerContext } from './rich-markdown-key-handler-context' -import { editorShortcutMatches } from '../../editor-shortcuts' +import { editorShortcutMatches } from '../../commands/editor-shortcuts' import { commitRichMarkdownSerialization } from '../document/rich-markdown-serialization-commit' /** Flush debounced text and preserve untouched source style before saving. */ diff --git a/src/renderer/src/components/editor/rich-markdown/instance/useRichMarkdownPendingFocus.ts b/src/renderer/src/components/editor/rich-markdown/instance/useRichMarkdownPendingFocus.ts index 3e30029ece7..4293c19ea85 100644 --- a/src/renderer/src/components/editor/rich-markdown/instance/useRichMarkdownPendingFocus.ts +++ b/src/renderer/src/components/editor/rich-markdown/instance/useRichMarkdownPendingFocus.ts @@ -2,7 +2,7 @@ import { useEffect, type RefObject } from 'react' import type { Editor } from '@tiptap/react' import { useAppStore } from '@/store/index' import { autoFocusRichEditor } from './rich-markdown-auto-focus' -import { matchesPendingEditorFocusRequest } from '../../pending-editor-focus-request' +import { matchesPendingEditorFocusRequest } from '../../commands/pending-editor-focus-request' type PendingFocusOptions = { editor: Editor | null diff --git a/src/renderer/src/components/editor/rich-markdown/review/rich-markdown-annotation-shortcut.ts b/src/renderer/src/components/editor/rich-markdown/review/rich-markdown-annotation-shortcut.ts index 41321ef080a..f4c8e4289c8 100644 --- a/src/renderer/src/components/editor/rich-markdown/review/rich-markdown-annotation-shortcut.ts +++ b/src/renderer/src/components/editor/rich-markdown/review/rich-markdown-annotation-shortcut.ts @@ -1,5 +1,5 @@ import type { KeyHandlerContext } from '../commands/rich-markdown-key-handler-context' -import { editorShortcutMatches } from '../../editor-shortcuts' +import { editorShortcutMatches } from '../../commands/editor-shortcuts' export function handleRichMarkdownAddReviewNoteShortcut( ctx: KeyHandlerContext, diff --git a/src/renderer/src/components/editor/save/commands/useEditorCmdSaveRequest.ts b/src/renderer/src/components/editor/save/commands/useEditorCmdSaveRequest.ts index 6d33bfe27b5..ab7166fd9e1 100644 --- a/src/renderer/src/components/editor/save/commands/useEditorCmdSaveRequest.ts +++ b/src/renderer/src/components/editor/save/commands/useEditorCmdSaveRequest.ts @@ -5,7 +5,7 @@ import { ORCA_EDITOR_REQUEST_CMD_SAVE_EVENT, type EditorRequestCmdSaveDetail } from '../../autosave/editor-autosave' -import type { FileContent } from '../../editor-panel-content-types' +import type { FileContent } from '../../loading/editor-panel-content-types' import { getEditorSaveTargetFile } from '../queue/editor-save-target' import { isCodeViewTab } from '../queue/editor-save-disk-precondition' import { isEditorWritableView } from '../queue/editor-save-writer-view' diff --git a/src/renderer/src/components/editor/save/queue/editor-save-disk-precondition.ts b/src/renderer/src/components/editor/save/queue/editor-save-disk-precondition.ts index 80e0d42067b..717ea26059f 100644 --- a/src/renderer/src/components/editor/save/queue/editor-save-disk-precondition.ts +++ b/src/renderer/src/components/editor/save/queue/editor-save-disk-precondition.ts @@ -16,7 +16,7 @@ import { writeRuntimeFile } from '@/runtime/files/mutations/runtime-file-mutatio import { writeRuntimeTerminalArtifactFile } from '@/runtime/files/grants/runtime-terminal-artifact-write' import { getRelativePathInsideWorktree } from '@/runtime/files/runtime-file-routing' import { chatScratchFileAddress } from '@/runtime/files/addressing/runtime-chat-scratch-file-address' -import { grantRestoredHostFile } from '../../restored-host-file-grant' +import { grantRestoredHostFile } from '../../file-ownership/restored-host-file-grant' import { captureRuntimeEnvironmentRequestRevision } from '@/runtime/rpc/environment-revisions/runtime-environment-revision' /** Only overwrite grants disk-conflict consent; convert grants non-UTF-8 conversion consent. Other explicit saves remain guarded. */ diff --git a/src/renderer/src/components/editor/save/transforms/editor-final-newline.ts b/src/renderer/src/components/editor/save/transforms/editor-final-newline.ts index f8225d6e5fe..811fc755be8 100644 --- a/src/renderer/src/components/editor/save/transforms/editor-final-newline.ts +++ b/src/renderer/src/components/editor/save/transforms/editor-final-newline.ts @@ -3,11 +3,11 @@ import type { OpenFile } from '@/store/slices/editor/types/open-file' import { editorModelRegistry } from '@/lib/editor/monaco/editor-model-registry' import { editorTabModelUri } from '../queue/editor-save-disk-precondition' import { hasPendingEditorBufferChange } from '../flush/editor-pending-flush' -import { normalizeToModelEol } from '../../monaco-content-sync' +import { normalizeToModelEol } from '../../model-lifetime/buffer-text/monaco-content-sync' import { beginProgrammaticContentSync, endProgrammaticContentSync -} from '../../monaco-programmatic-sync' +} from '../../model-lifetime/buffer-text/monaco-programmatic-sync' export function needsFinalNewline(text: string): boolean { return text !== '' && !text.endsWith('\n') && !text.endsWith('\r') diff --git a/src/renderer/src/components/editor/useEditorPanelFileLoadRetry.wsl-host.test.tsx b/src/renderer/src/components/editor/useEditorPanelFileLoadRetry.wsl-host.test.tsx deleted file mode 100644 index 624466dd669..00000000000 --- a/src/renderer/src/components/editor/useEditorPanelFileLoadRetry.wsl-host.test.tsx +++ /dev/null @@ -1,93 +0,0 @@ -// @vitest-environment happy-dom - -import { act, useRef, useState } from 'react' -import { createRoot, type Root } from 'react-dom/client' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { OpenFile } from '@/store/slices/editor/types/open-file' -import type { FileContent } from './editor-panel-content-types' - -const host = vi.hoisted((): { state: string | undefined } => ({ state: undefined })) - -vi.mock( - '@/lib/workspace/ownership/worktree-host-connection-phase', - () => import('./local-host-test-fixture') -) -vi.mock('@/lib/workspace/ownership/workspace-runtime-environment', () => ({ - selectWorkspaceRuntimeEnvironmentId: () => 'wsl-env' -})) -vi.mock('@/store/retained-views/app-state-view', () => ({ - useAppStateValue: (select: (state: unknown) => T): T => - select({ - wslHostStatuses: host.state - ? { 'wsl-env': { environmentId: 'wsl-env', distro: 'demo', state: host.state } } - : {} - }) -})) - -import { useEditorPanelFileLoadRetry } from './useEditorPanelFileLoadRetry' - -const FILE: OpenFile = { - id: 'file-1', - filePath: '/home/orca/demo/app/main.go', - relativePath: 'main.go', - worktreeId: 'repo::/home/orca/demo/app', - language: 'go', - isDirty: false, - mode: 'edit' -} -const UNREACHABLE = 'Could not connect to the remote Orca runtime.' - -function Probe({ loadFileContent }: { loadFileContent: () => Promise }): null { - const [fileContents, setFileContents] = useState>({ - [FILE.id]: { content: '', isBinary: false, loadError: UNREACHABLE } - }) - const openFilesRef = useRef([FILE]) - const fileLoadRetryAttemptsRef = useRef>({ [FILE.id]: 3 }) - useEditorPanelFileLoadRetry({ - activeFile: FILE, - fileContents, - fileLoadRetryAttemptsRef, - loadFileContent, - openFilesRef, - setFileContents - }) - return null -} - -describe('useEditorPanelFileLoadRetry on a WSL host', () => { - let container: HTMLDivElement - let root: Root - - beforeEach(() => { - vi.useFakeTimers() - container = document.createElement('div') - root = createRoot(container) - }) - - afterEach(() => { - act(() => root.unmount()) - vi.useRealTimers() - }) - - const render = (loadFileContent: () => Promise): void => - act(() => root.render()) - - it('waits while the host connects, then reloads once it answers', async () => { - const loadFileContent = vi.fn(async () => {}) - host.state = 'connecting' - render(loadFileContent) - await act(async () => { - await vi.advanceTimersByTimeAsync(10_000) - }) - expect(loadFileContent).not.toHaveBeenCalled() - - host.state = 'ready' - render(loadFileContent) - expect(loadFileContent).toHaveBeenCalledWith( - FILE.filePath, - FILE.id, - FILE.worktreeId, - FILE.relativePath - ) - }) -}) diff --git a/src/renderer/src/components/editor/viewer-viewport/use-viewer-zoom-commands.ts b/src/renderer/src/components/editor/viewer-viewport/use-viewer-zoom-commands.ts index b331eb99fec..e45c4f91599 100644 --- a/src/renderer/src/components/editor/viewer-viewport/use-viewer-zoom-commands.ts +++ b/src/renderer/src/components/editor/viewer-viewport/use-viewer-zoom-commands.ts @@ -1,5 +1,5 @@ import { useContext, useEffect } from 'react' -import { EditorCommandOwnerContext } from '../editor-command-owner-context' +import { EditorCommandOwnerContext } from '../commands/editor-command-owner-context' import { IMAGE_VIEWER_ZOOM_STEP } from './image-viewer-zoom' import type { PanZoomViewport } from './use-pan-zoom-viewport' import { listenForViewerZoomRequests } from './viewer-zoom-request' diff --git a/src/renderer/src/components/github/CommentCodeContext.tsx b/src/renderer/src/components/github/CommentCodeContext.tsx index cb6b7c89130..e65db9bd579 100644 --- a/src/renderer/src/components/github/CommentCodeContext.tsx +++ b/src/renderer/src/components/github/CommentCodeContext.tsx @@ -23,7 +23,7 @@ import type { GitHubPRFileContents } from '../../../../shared/github/pull-requests/pull-request-types' -const MonacoCodeExcerpt = lazy(() => import('@/components/editor/MonacoCodeExcerpt')) +const MonacoCodeExcerpt = lazy(() => import('@/components/editor/monaco/MonacoCodeExcerpt')) const CODE_CONTEXT_EXPAND_STEP = 5 const CODE_CONTEXT_FALLBACK_LINES = 20 diff --git a/src/renderer/src/components/render-phase-adjustment-discarded-render.test.tsx b/src/renderer/src/components/render-phase-adjustment-discarded-render.test.tsx index 9931872e5bb..8470ed03def 100644 --- a/src/renderer/src/components/render-phase-adjustment-discarded-render.test.tsx +++ b/src/renderer/src/components/render-phase-adjustment-discarded-render.test.tsx @@ -1,11 +1,8 @@ // @vitest-environment happy-dom -import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' +import { act, cleanup, render, screen } from '@testing-library/react' import { Suspense, use, type ReactNode } from 'react' import { afterEach, beforeEach, expect, it, vi } from 'vitest' -import { getDefaultSettings } from '../../../shared/constants' -import { ConfirmationDialogContext } from './confirmation-dialog-context' -import { UntitledFileRenameDialog } from './editor/UntitledFileRenameDialog' -import { QuickCommandsPane } from './settings/quick-commands/QuickCommandsPane' +import { UntitledFileRenameDialog } from './editor/panel/untitled-rename/UntitledFileRenameDialog' import { useWorkspaceKanbanColumnResize } from './sidebar/workspace-board/viewport/use-workspace-kanban-column-resize' import { useAddRepoCloneFlow } from './sidebar/project-admission/dialog/useAddRepoCloneFlow' @@ -62,8 +59,6 @@ it('applies a committed board column width', async () => { ) expect(screen.getByTestId('column').textContent).toBe('420') - fireEvent.keyDown(screen.getByTestId('column'), { key: 'ArrowRight' }) - expect(onCommit).toHaveBeenLastCalledWith(440) }) function CloneDestination({ workspaceDir }: { workspaceDir?: string }) { @@ -93,17 +88,3 @@ it('seeds the save-as dialog with the opened file name', async () => { ) expect(screen.queryByDisplayValue('Untitled-2')).not.toBeNull() }) - -it('opens the Add dialog for a quick-command deep link', async () => { - const settings = getDefaultSettings('/home/dev') - const confirm = async (): Promise => true - await renderThenDiscardUpdate( - - - , - - - - ) - expect(screen.queryByText('Add Quick Command')).not.toBeNull() -}) diff --git a/src/renderer/src/components/tab-bar/EditorFileTab.tsx b/src/renderer/src/components/tab-bar/EditorFileTab.tsx index dd7479ee214..22fa369bef2 100644 --- a/src/renderer/src/components/tab-bar/EditorFileTab.tsx +++ b/src/renderer/src/components/tab-bar/EditorFileTab.tsx @@ -6,7 +6,7 @@ import { Eye, ShieldAlert, Pin } from 'lucide-react' import { Input } from '@/components/ui/input' import { TabHoverCard } from './TabHoverCard' import { basename } from '@/lib/path' -import { getEditorDisplayLabel } from '@/components/editor/editor-labels' +import { getEditorDisplayLabel } from '@/components/editor/panel/header/editor-labels' import { renameFileOnDisk } from '@/lib/editor/file-operations/rename-file' import { isImeCompositionKeyDown } from '@/lib/text-input/ime/ime-composition-keyboard-event' import { detectLanguage } from '../../../../shared/editor-language/editor-language-detect' @@ -17,7 +17,7 @@ import { resolveReviewRepository } from '@/lib/source-control/review/review-owne import { useAppStore } from '@/store/index' import { STATUS_COLORS } from '../../lib/source-control/paths/git-path-decoration' import { STATUS_LABELS } from '../../../../shared/git/status/git-status-labels' -import { getUntitledFileRoot } from '@/components/editor/untitled-file-rename-path' +import { getUntitledFileRoot } from '@/components/editor/panel/untitled-rename/untitled-file-rename-path' import { preventMiddleButtonDefault } from './middle-button-default-guard' import { CLOSE_ALL_CONTEXT_MENUS_EVENT } from '@/lib/overlay-interaction/close-all-context-menus' import { diff --git a/src/renderer/src/components/tab-bar/TabBar.tsx b/src/renderer/src/components/tab-bar/TabBar.tsx index d84ef3c4952..5b5a9d92a21 100644 --- a/src/renderer/src/components/tab-bar/TabBar.tsx +++ b/src/renderer/src/components/tab-bar/TabBar.tsx @@ -10,7 +10,7 @@ import { useTabBarItemProjection } from './use-tab-bar-item-projection' import { renderTabBarSurface } from './tab-bar-surface' import { useTabBarItemActions } from './use-tab-bar-item-actions' import { useActiveClientHostedBrowserRowId } from '@/runtime/browser/client-hosting/client-hosted-browser-row-state' -import { useEditorGroupFileDropOwner } from '../editor/use-editor-group-file-drop-owner' +import { useEditorGroupFileDropOwner } from '../editor/file-drop/use-editor-group-file-drop-owner' import { useTranslate } from '@/i18n/use-translate' function TabBarInner(props: TabBarProps): React.JSX.Element { diff --git a/src/renderer/src/components/tab-bar/tab-bar-item-label.ts b/src/renderer/src/components/tab-bar/tab-bar-item-label.ts index 80dc4af4a92..8e826219b35 100644 --- a/src/renderer/src/components/tab-bar/tab-bar-item-label.ts +++ b/src/renderer/src/components/tab-bar/tab-bar-item-label.ts @@ -1,6 +1,6 @@ import type { TabBarItem } from './tab-bar-item-model' import { resolveTerminalTabTitle } from '../../../../shared/tab-title-resolution' -import { getEditorDisplayLabel } from '@/components/editor/editor-labels' +import { getEditorDisplayLabel } from '@/components/editor/panel/header/editor-labels' import { getBrowserTabLabel } from './BrowserTab' import { formatSimulatorTabLabel } from '@/lib/simulator/tabs/simulator-tab-label' import type { Translate } from '@/i18n/use-translate' diff --git a/src/renderer/src/components/tab-group/TabGroupPanel.tsx b/src/renderer/src/components/tab-group/TabGroupPanel.tsx index d272a399f3d..1363e5cb62b 100644 --- a/src/renderer/src/components/tab-group/TabGroupPanel.tsx +++ b/src/renderer/src/components/tab-group/TabGroupPanel.tsx @@ -19,7 +19,7 @@ import { isStructuredTab } from '../../../../shared/agent-session/view/structure import type { ClientHostedBrowserRow } from '../../../../shared/client-hosted-browser-rows' import { useClientHostedBrowserRows } from '@/runtime/browser/client-hosting/client-hosted-browser-row-state' import { resolveClientHostedBrowserRowStripGroupId } from '../tab-bar/client-hosted-browser-row-strip-placement' -import { useEditorGroupFileDropOwner } from '../editor/use-editor-group-file-drop-owner' +import { useEditorGroupFileDropOwner } from '../editor/file-drop/use-editor-group-file-drop-owner' import { cn } from '@/lib/class-names' import { UNFOCUSED_SPLIT_GROUP_OPACITY } from './split-group-dim' import { useWindowBackdropActive } from '@/app-shell/appearance/window-backdrop' diff --git a/src/renderer/src/components/terminal-pane/clipboard/terminal-pane-menu-copy-actions.ts b/src/renderer/src/components/terminal-pane/clipboard/terminal-pane-menu-copy-actions.ts index d2a16d467dc..14099b78bcb 100644 --- a/src/renderer/src/components/terminal-pane/clipboard/terminal-pane-menu-copy-actions.ts +++ b/src/renderer/src/components/terminal-pane/clipboard/terminal-pane-menu-copy-actions.ts @@ -6,7 +6,7 @@ import { copyTerminalHandleForPane } from './terminal-handle-copy' import { runTerminalCopy, runTerminalIdentityCopy } from './terminal-copy-rejection-guards' import { readTerminalClipboardSelection } from './terminal-clipboard-selection-text' import { useAppStore } from '@/store/index' -import { fencedCode } from '../../editor/selection-copy' +import { fencedCode } from '../../editor/contextual-copy/selection-copy' import { formatNativeChatQuote } from '../../native-chat/selection-quotes/native-chat-quote-selection' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../../shared/constants' diff --git a/src/renderer/src/env.d.ts b/src/renderer/src/env.d.ts index 46c7f1a0afe..ee9c3670c18 100644 --- a/src/renderer/src/env.d.ts +++ b/src/renderer/src/env.d.ts @@ -3,7 +3,7 @@ import type { PaneManager } from '@/lib/pane-manager/pane-manager' import type { OnboardingFeatureSetupDeps } from '@/components/onboarding/onboarding-feature-setup' import type { editor, languages } from 'monaco-editor' -import type { MonacoE2EProbe } from './components/editor/monaco-e2e-probe' +import type { MonacoE2EProbe } from './components/editor/monaco/monaco-e2e-probe' import type { TerminalWorktreeParkingDebugVerdict } from './components/terminal-pane/parking/instrumentation/terminal-parking-e2e-overrides' import type { TerminalPtyPreSpawnE2EBarrier } from './components/terminal-pane/pty-transport/ipc/terminal-pty-pre-spawn-e2e-barrier' import type { LocalLayoutStreamE2EEnd } from './runtime/workspace-layout/testing/local-layout-stream-e2e-end' diff --git a/src/renderer/src/hooks/editor-external-watch-event-reconciliation.ts b/src/renderer/src/hooks/editor-external-watch-event-reconciliation.ts index 113a4040a0f..3ffa7b37f0a 100644 --- a/src/renderer/src/hooks/editor-external-watch-event-reconciliation.ts +++ b/src/renderer/src/hooks/editor-external-watch-event-reconciliation.ts @@ -7,7 +7,7 @@ import { getRecentSelfWrite } from '@/components/editor/external-changes/editor- import { hasActiveEditorPathMoves, isActiveMoveSourcePath -} from '@/components/editor/editor-path-move-inflight' +} from '@/components/editor/file-ownership/editor-path-move-inflight' import { normalizeRuntimePathForComparison } from '../../../shared/cross-platform-path' import type { FsChangedPayload } from '../../../shared/file-watch/filesystem-change-event' import { diff --git a/src/renderer/src/lib/editor/file-operations/execute-open-editor-path-move.ts b/src/renderer/src/lib/editor/file-operations/execute-open-editor-path-move.ts index a620018f08d..723c15746ff 100644 --- a/src/renderer/src/lib/editor/file-operations/execute-open-editor-path-move.ts +++ b/src/renderer/src/lib/editor/file-operations/execute-open-editor-path-move.ts @@ -5,7 +5,7 @@ import { requestEditorSaveQuiesce } from '@/components/editor/autosave/editor-au import { beginEditorPathMove, settleEditorPathMove -} from '@/components/editor/editor-path-move-inflight' +} from '@/components/editor/file-ownership/editor-path-move-inflight' import { remapOpenEditorTabsForPathChange } from '@/lib/editor/file-operations/remap-open-editor-tabs-for-path-change' import { isPathInsideOrEqual } from '../../../../../shared/cross-platform-path' import { verifyLatchedMoveDestinations } from '@/hooks/useEditorExternalWatch' diff --git a/src/renderer/src/lib/editor/monaco/monaco-setup.ts b/src/renderer/src/lib/editor/monaco/monaco-setup.ts index fbe14dcb63a..b40a9b8759f 100644 --- a/src/renderer/src/lib/editor/monaco/monaco-setup.ts +++ b/src/renderer/src/lib/editor/monaco/monaco-setup.ts @@ -28,7 +28,7 @@ import { installMonacoDelayerCancellationGuard } from './monaco-delayer-cancella import { installMonacoDiffEditorDisposalGuard } from './monaco-diff-editor-disposal' import { installMonacoDiffViewportTokenization } from './monaco-diff-viewport-tokenization' import { installMonacoPeekReferencesPreviewOptions } from './monaco-peek-preview-options' -import { installMonacoContextMenuPaste } from '@/components/editor/install-monaco-context-menu-paste' +import { installMonacoContextMenuPaste } from '@/components/editor/monaco/install-monaco-context-menu-paste' import { runMonacoSetupSteps } from './monaco-setup-steps' import { installMonacoTsWorkerIdleStop } from './monaco-ts-worker-idle-stop' import { onPluginFontLoadsChanged } from '@/store/plugins/appearance/plugin-fonts' diff --git a/src/renderer/src/lib/workspace/search/tabs/workspace-tab-palette-entry-builder.ts b/src/renderer/src/lib/workspace/search/tabs/workspace-tab-palette-entry-builder.ts index e832aed2aa8..b69bbd881db 100644 --- a/src/renderer/src/lib/workspace/search/tabs/workspace-tab-palette-entry-builder.ts +++ b/src/renderer/src/lib/workspace/search/tabs/workspace-tab-palette-entry-builder.ts @@ -2,7 +2,7 @@ import { resolveTerminalTabTitle, resolveUnifiedTabLabel } from '../../../../../../shared/tab-title-resolution' -import { getEditorDisplayLabel } from '@/components/editor/editor-labels' +import { getEditorDisplayLabel } from '@/components/editor/panel/header/editor-labels' import { buildPaletteTabDocument } from '../../../../../../shared/palette-match/tab-document' import { getPaletteWorktreeIdentity, diff --git a/src/renderer/src/store/repos/project-removal-host-twin-state.test.ts b/src/renderer/src/store/repos/project-removal-host-twin-state.test.ts index 9f904e120c7..c4ba32bc7d9 100644 --- a/src/renderer/src/store/repos/project-removal-host-twin-state.test.ts +++ b/src/renderer/src/store/repos/project-removal-host-twin-state.test.ts @@ -29,7 +29,7 @@ import type { ClosedEditorTabSnapshot } from '../slices/editor/types/open-file' import '../../../../shared/workspace-scope' const recoveryNotice = vi.hoisted(() => vi.fn()) -vi.mock('@/components/editor/closed-unsaved-files-toast', () => ({ +vi.mock('@/components/editor/model-lifetime/retention/closed-unsaved-files-toast', () => ({ showClosedUnsavedFilesToast: recoveryNotice })) diff --git a/src/renderer/src/store/slices/worktrees/teardown/remove-worktree-shared-records.test.ts b/src/renderer/src/store/slices/worktrees/teardown/remove-worktree-shared-records.test.ts index 585badc165c..3d06e4c17ab 100644 --- a/src/renderer/src/store/slices/worktrees/teardown/remove-worktree-shared-records.test.ts +++ b/src/renderer/src/store/slices/worktrees/teardown/remove-worktree-shared-records.test.ts @@ -3,10 +3,9 @@ import { useAppStore } from '@/store' import type { OpenFile } from '@/store/slices/editor/types/open-file' import { makeWorktree } from '../worktree.test-fixture' import { applyRemoveWorktreeSuccessState } from './remove-worktree-store-cleanup' -import { noticeRemovedUnsavedFiles } from './removed-workspace-unsaved-files' const show = vi.hoisted(() => vi.fn()) -vi.mock('@/components/editor/closed-unsaved-files-toast', () => ({ +vi.mock('@/components/editor/model-lifetime/retention/closed-unsaved-files-toast', () => ({ showClosedUnsavedFilesToast: show })) @@ -47,20 +46,14 @@ it('removing one workspace keeps the shared record, its draft and its key in the openFiles: [tab('a', 'wt-1', { isDirty: true }), tab('b', 'wt-2', { isDirty: true })], editorDrafts: { a: 'shared edits', b: 'shared edits' } }) - const recordId = state().editorBufferIdOf.a - expect(state().editorBufferIdOf.b).toBe(recordId) applyRemoveWorktreeSuccessState(useAppStore.setState, 'wt-2', new Set(), 'local') await Promise.resolve() expect(state().openFiles.map((file) => file.id)).toEqual(['a']) // Same record id: the autosave timer and queued save keyed by it carry on. - expect(state().editorBufferIdOf.a).toBe(recordId) - expect(state().editorBufferViews[recordId]).toEqual(['a']) expect(state().editorDrafts.a).toBe('shared edits') expect(state().openFiles[0]?.isDirty).toBe(true) - expect(state().editorBufferKept).toEqual({}) - expect(show).not.toHaveBeenCalled() }) it("a removal with no prompt keeps the last view's dirty record; reopening the file joins it", async () => { @@ -68,78 +61,16 @@ it("a removal with no prompt keeps the last view's dirty record; reopening the f openFiles: [tab('b', 'wt-2', { isDirty: true })], editorDrafts: { b: 'only copy' } }) - const recordId = state().editorBufferIdOf.b applyRemoveWorktreeSuccessState(useAppStore.setState, 'wt-2', new Set(), 'local') await vi.waitFor(() => expect(show).toHaveBeenCalledTimes(1)) expect(state().openFiles).toEqual([]) - expect(state().editorBufferKept[recordId]?.draft).toBe('only copy') useAppStore.setState({ openFiles: [tab('a', 'wt-1')] }) - expect(state().editorBufferIdOf.a).toBe(recordId) expect(state().editorDrafts.a).toBe('only copy') expect(state().openFiles[0]?.isDirty).toBe(true) }) -it('a removal closing tabs over several writes keeps every dirty record it closes', async () => { - useAppStore.setState({ - openFiles: [ - tab('x', 'wt-1', { filePath: '/repo/x.ts', isDirty: true }), - tab('y', 'wt-2', { filePath: '/repo/y.ts', isDirty: true }), - tab('z', 'wt-1', { filePath: '/repo/z.ts' }) - ], - editorDrafts: { x: 'x edits', y: 'y edits' } - }) - const [xRecord, yRecord] = [state().editorBufferIdOf.x, state().editorBufferIdOf.y] - - // Simulate project batch removal: notice first, close twin separately, reconcile unrelated write, then purge the rest. - const keep = noticeRemovedUnsavedFiles(state(), new Set(['x', 'y'])) - state().closeFile('x', { captureRecentlyClosed: false, keepEditorBuffer: keep }) - state().closeFile('z') - state().purgeWorktreeTerminalState(['wt-2']) - await vi.waitFor(() => expect(show).toHaveBeenCalledTimes(1)) - - expect(state().openFiles).toEqual([]) - expect(state().editorBufferKept[xRecord]?.draft).toBe('x edits') - expect(state().editorBufferKept[yRecord]?.draft).toBe('y edits') - // Dismissing the notice releases both. - state().releaseKeptEditorBuffers(show.mock.calls[0][2]) - expect(state().editorBufferKept).toEqual({}) -}) - -it('releases the kept records when the notice cannot be shown', async () => { - vi.spyOn(console, 'error').mockImplementation(() => {}) - show.mockImplementationOnce(() => { - throw new Error('toast failed') - }) - useAppStore.setState({ - openFiles: [tab('b', 'wt-2', { isDirty: true })], - editorDrafts: { b: 'only copy' } - }) - applyRemoveWorktreeSuccessState(useAppStore.setState, 'wt-2', new Set(), 'local') - expect(Object.keys(state().editorBufferKept)).toHaveLength(1) - await vi.waitFor(() => expect(state().editorBufferKept).toEqual({})) -}) - -it('dismissing one notice leaves the records a newer notice kept for the same file', async () => { - useAppStore.setState({ - openFiles: [tab('b', 'wt-2', { isDirty: true })], - editorDrafts: { b: 'first' } - }) - applyRemoveWorktreeSuccessState(useAppStore.setState, 'wt-2', new Set(), 'local') - await vi.waitFor(() => expect(show).toHaveBeenCalledTimes(1)) - // The same tab id reopened elsewhere with different text, then removed again. - useAppStore.setState((s) => ({ - openFiles: [tab('b', 'wt-1', { isDirty: true })], - editorDrafts: { ...s.editorDrafts, b: 'second' } - })) - applyRemoveWorktreeSuccessState(useAppStore.setState, 'wt-1', new Set(), 'local') - await vi.waitFor(() => expect(show).toHaveBeenCalledTimes(2)) - - state().releaseKeptEditorBuffers(show.mock.calls[0][2]) - expect(Object.values(state().editorBufferKept).map((entry) => entry.draft)).toEqual(['second']) -}) - import * as drafts from '@/components/native-chat/drafts/native-chat-draft-reset' import * as draftsOwner1 from '@/components/native-chat/drafts/native-chat-launch-draft-source' import * as draftsOwner2 from '@/components/native-chat/drafts/native-chat-composer-draft-store' @@ -170,7 +101,6 @@ it('committed worktree teardown retires empty pending launch source authority wi requiredExecutionHostId: 'local', beforeRemoval }) - expect(submission.draft).toBeUndefined() draftsOwner2.updateNativeChatComposerDraft(key, { text: 'recreated' }, 'immediate') draftsOwner2.finishNativeChatComposerSubmission(key, submission, true) expect(draftsOwner2.readNativeChatComposerDraft(key).text).toBe('recreated') diff --git a/src/renderer/src/store/slices/worktrees/teardown/removed-workspace-unsaved-files.test.ts b/src/renderer/src/store/slices/worktrees/teardown/removed-workspace-unsaved-files.test.ts deleted file mode 100644 index 86cc759cf4f..00000000000 --- a/src/renderer/src/store/slices/worktrees/teardown/removed-workspace-unsaved-files.test.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { registerPendingEditorFlush } from '@/components/editor/save/flush/editor-pending-flush' -import { editorModelRegistry } from '@/lib/editor/monaco/editor-model-registry' -import { hostWorktreeTarget } from '../../../../../../shared/worktree/host-qualified-identity' -import { toRuntimeExecutionHostId } from '../../../../../../shared/execution-host' -import { createHostTwinFixture } from '../../../workspaces/host-isolation/host-twin.test-fixture' -import { resetHostTwinModuleStores } from '../../../workspaces/host-isolation/host-twin-state-projection.test-fixture' -import { createTestStore } from '../../../testing/app-store-test-fixture' -import { makeOpenFile } from '../../editor/types/open-file.test-fixture' -import { makeWorktree } from '../worktree.test-fixture' -import { TEST_REPO } from '../../../repos/repo.test-fixture' -import { createStoreCascadesMockApi } from '../../../testing/app-store-cascade-api.test-fixture' -import { mergeFetchedWorktrees } from '../listing/fetched-worktree-merge' -import { applyRemoveWorktreeSuccessState } from './remove-worktree-store-cleanup' -import { confirmUnsavedDiscard, noticeRemovedUnsavedFiles } from './removed-workspace-unsaved-files' - -const show = vi.hoisted(() => vi.fn()) -vi.mock('@/components/editor/closed-unsaved-files-toast', () => ({ - showClosedUnsavedFilesToast: show -})) -vi.mock('sonner', () => ({ - toast: { warning: vi.fn(), info: vi.fn(), success: vi.fn(), error: vi.fn() } -})) - -type Store = ReturnType -const RUNTIME = toRuntimeExecutionHostId('env-a') -const settle = () => new Promise((resolve) => setTimeout(resolve, 0)) -const shownContents = (call = 0): string[] => - (show.mock.calls[call]?.[0] ?? []).map((file: { content: string }) => file.content).sort() - -beforeEach(() => { - show.mockClear() - vi.spyOn(console, 'warn').mockImplementation(() => {}) - vi.stubGlobal('window', { api: { pty: { kill: vi.fn() } } }) -}) -afterEach(() => vi.unstubAllGlobals()) - -/** A store whose workspaces each hold one dirty tab `/file.ts` with draft `draft `. */ -function dirtyStore(ids: readonly string[], hostId: 'local' | typeof RUNTIME = 'local'): Store { - const store = createTestStore() - store.setState({ - repos: [{ ...TEST_REPO, executionHostId: hostId }], - worktreesByRepo: { - [TEST_REPO.id]: ids.map((id) => - makeWorktree({ id, repoId: TEST_REPO.id, hostId, displayName: id.split('/').pop() }) - ) - }, - openFiles: ids.map((id) => - makeOpenFile({ id: `${id}/file.ts`, worktreeId: id, isDirty: true }) - ), - editorDrafts: Object.fromEntries(ids.map((id) => [`${id}/file.ts`, `draft ${id}`])) - }) - return store -} - -const edit = (store: Store, id: string, text: string): void => - store.setState((s) => ({ editorDrafts: { ...s.editorDrafts, [`${id}/file.ts`]: text } })) - -const GONE = `${TEST_REPO.id}::/gone` - -/** Every way a workspace's tabs are purged, each closing `GONE`. */ -const purgePaths: [string, (store: Store) => unknown, ('local' | typeof RUNTIME)?][] = [ - ['worktrees:changed purge', (store) => store.getState().purgeWorktreeTerminalState([GONE])], - [ - 'host announcement', - (store) => - mergeFetchedWorktrees(store.setState, store.getState, { - repoId: TEST_REPO.id, - hostId: 'local', - ownerWasMissingAtStart: false, - requestStartedWorktrees: store.getState().worktreesByRepo[TEST_REPO.id] ?? [], - refresh: { - status: 'admitted', - executionHostId: 'local', - result: { repoId: TEST_REPO.id, authoritative: true, source: 'git', worktrees: [] } - } - }) - ], - [ - 'unconfirmed removeWorktree', - async (store) => { - vi.stubGlobal('window', { api: createStoreCascadesMockApi() }) - await store.getState().removeWorktree({ id: GONE, executionHostId: 'local' }, false) - } - ], - [ - 'runtime host retirement', - (store) => store.getState().purgeStaleRuntimeHostState(['env-a']), - RUNTIME - ] -] - -describe('confirmed discards apply only to the confirmed removal', () => { - it('its own teardown drops unchanged text quietly', async () => { - const store = dirtyStore([GONE]) - const confirmed = confirmUnsavedDiscard(store.getState, [{ id: GONE, hostId: 'local' }]) - applyRemoveWorktreeSuccessState(store.setState, GONE, new Set(), 'local', confirmed) - expect(store.getState().openFiles).toEqual([]) - await settle() - expect(show).not.toHaveBeenCalled() - }) - - it('its own teardown offers back an edit made after confirming', async () => { - const store = dirtyStore([GONE]) - const confirmed = confirmUnsavedDiscard(store.getState, [{ id: GONE, hostId: 'local' }]) - edit(store, GONE, 'typed after confirming') - applyRemoveWorktreeSuccessState(store.setState, GONE, new Set(), 'local', confirmed) - await vi.waitFor(() => expect(show).toHaveBeenCalledTimes(1)) - expect(shownContents()).toEqual(['typed after confirming']) - }) - - it.each(purgePaths)('%s still offers text a dialog confirmed', async (_, purge, host) => { - const store = dirtyStore([GONE], host) - confirmUnsavedDiscard(store.getState, [{ id: GONE, hostId: host ?? 'local' }]) - await purge(store) - expect(store.getState().openFiles).toEqual([]) - await vi.waitFor(() => expect(show).toHaveBeenCalledTimes(1)) - expect(shownContents()).toEqual([`draft ${GONE}`]) - }) - - it('suppresses within its batch only, whichever purge of the batch runs first', async () => { - const store = dirtyStore([GONE]) - const confirmed = confirmUnsavedDiscard(store.getState, [{ id: GONE, hostId: 'local' }]) - const fileIds = new Set([`${GONE}/file.ts`]) - noticeRemovedUnsavedFiles(store.getState(), fileIds) - noticeRemovedUnsavedFiles(store.getState(), fileIds, confirmed) - noticeRemovedUnsavedFiles(store.getState(), fileIds) - await settle() - expect(show).not.toHaveBeenCalled() - noticeRemovedUnsavedFiles(store.getState(), fileIds) - await vi.waitFor(() => expect(show).toHaveBeenCalledTimes(1)) - }) -}) - -describe('delayed text is committed before a purge', () => { - it.each(purgePaths)('%s flushes every pane of the closing file', async (_, purge, host) => { - const store = dirtyStore([GONE], host) - const panes = [ - registerPendingEditorFlush(`${GONE}/file.ts`, () => edit(store, GONE, 'late rich text')), - registerPendingEditorFlush(`${GONE}/file.ts`, () => {}) - ] - try { - await purge(store) - await vi.waitFor(() => expect(show).toHaveBeenCalledTimes(1)) - expect(shownContents()).toEqual(['late rich text']) - } finally { - panes.forEach((unregister) => unregister()) - } - }) -}) - -describe('the closed-files notice', () => { - it('merges a bulk purge into one toast and dedupes repeated captures', async () => { - const a = `${TEST_REPO.id}::/a` - const b = `${TEST_REPO.id}::/b` - const store = dirtyStore([a, b]) - noticeRemovedUnsavedFiles(store.getState(), new Set([`${a}/file.ts`])) - store.getState().purgeWorktreeTerminalState([a, b]) - await vi.waitFor(() => expect(show).toHaveBeenCalledTimes(1)) - expect(shownContents()).toEqual([`draft ${a}`, `draft ${b}`]) - expect(show.mock.calls[0]?.[1]).toBe('a, b') - await settle() - expect(show).toHaveBeenCalledTimes(1) - }) - - it('emits one notice for a full target and a host-twin scope', async () => { - const f = createHostTwinFixture('local+runtime') - try { - const full = 'other::/full' - f.store.setState((s) => ({ - openFiles: [ - ...s.openFiles, - makeOpenFile({ id: 'full.ts', worktreeId: full, isDirty: true }) - ], - editorDrafts: { ...s.editorDrafts, 'full.ts': 'draft full' } - })) - f.store - .getState() - .purgeWorktreeTerminalState([ - full, - hostWorktreeTarget({ id: f.workspaceKey, hostId: f.remote.hostId }) - ]) - await vi.waitFor(() => expect(show).toHaveBeenCalledTimes(1)) - expect(shownContents()).toEqual(['draft full', 'unsaved runtime:env-b edit']) - expect(f.store.getState().editorDrafts[f.local.fileId]).toBe('unsaved local edit') - } finally { - resetHostTwinModuleStores(f) - } - }) - - it.each([ - [true, 'live model text'], - [false, 'newer draft'] - ])('reads a shared model only while mounted (attached=%s)', async (attached, expected) => { - const store = dirtyStore([GONE]) - const tab = (id: string, isDirty: boolean) => - makeOpenFile({ id, filePath: '/w/shared.ts', worktreeId: GONE, isDirty }) - store.setState({ - openFiles: [tab('first', true), tab('second', true)], - // One record: both views hold its one draft. - editorDrafts: { first: 'newer draft', second: 'newer draft' } - }) - const model = { isAttachedToEditor: () => attached, getValue: () => 'live model text' } - const unregister = editorModelRegistry.register({ - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: capture reads only getModel(uri) and the model's text. - editor: { getModel: () => model } as never, - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: capture only parses the model URI. - Uri: { parse: (uri: string) => uri } as never - }) - try { - store.getState().purgeWorktreeTerminalState([GONE]) - await vi.waitFor(() => expect(show).toHaveBeenCalledTimes(1)) - expect(shownContents()).toEqual([expected]) - } finally { - unregister() - } - }) -}) diff --git a/src/renderer/src/store/slices/worktrees/teardown/removed-workspace-unsaved-files.ts b/src/renderer/src/store/slices/worktrees/teardown/removed-workspace-unsaved-files.ts index 592e75ce0ca..90f6e79ccc9 100644 --- a/src/renderer/src/store/slices/worktrees/teardown/removed-workspace-unsaved-files.ts +++ b/src/renderer/src/store/slices/worktrees/teardown/removed-workspace-unsaved-files.ts @@ -160,7 +160,7 @@ function showPendingNotice(): void { filePaths: closed.map((file) => file.filePath) }) // Why lazy: keeps toast and editor-open code out of the store's import graph. - void import('@/components/editor/closed-unsaved-files-toast') + void import('@/components/editor/model-lifetime/retention/closed-unsaved-files-toast') .then((module) => module.showClosedUnsavedFilesToast(closed, label, token)) .catch((error) => { console.error('[editor] showing the unsaved edits notice failed', error)