| 1 | import { lazy, Suspense, useState, type ComponentProps, type KeyboardEvent, type PointerEvent, type ReactNode } from "react"; |
| 2 | import type { DockNavigation } from "./dockNavigation"; |
| 3 | import type { FileNavigationOwner } from "../lib/fileNavigationOwner"; |
| 4 | import { fileNavigationOwner } from "../lib/fileNavigationCommands"; |
| 5 | import type { Translator } from "../lib/i18n"; |
| 6 | import type { RightDockMode } from "../store/layout"; |
| 7 | import type { TabItem } from "../store/activityBar"; |
| 8 | import { useActivityBarStore } from "../store/activityBar"; |
| 9 | import { readWorkspaceTreeMemory, workspaceViewMemoryKey } from "../lib/workspaceViewMemory"; |
| 10 | import { useDockViewRequests } from "./useDockViewRequests"; |
| 11 | |
| 12 | // The tab strip, its drag state machine and the add menu are a deferred |
| 13 | // surface: the dock is closed on most launches, so keep them out of the |
| 14 | // initial bundle. |
| 15 | const TabContainer = lazy(() => import("../components/TabContainer/TabContainer").then((module) => ({ default: module.TabContainer }))); |
| 16 | |
| 17 | const ContextPanel = lazy(() => import("../components/ContextPanel").then((module) => ({ default: module.ContextPanel }))); |
| 18 | const RemotePanel = lazy(() => import("../components/RemotePanel").then((module) => ({ default: module.RemotePanel }))); |
| 19 | const BrowserSurface = lazy(() => import("../components/BrowserPanelEntry")); |
| 20 | const WorkspacePanel = lazy(async () => { |
| 21 | const [module] = await Promise.all([ |
| 22 | import("../components/WorkspacePanel"), |
| 23 | import("../components/WorkspacePanelStability.css"), |
| 24 | ]); |
| 25 | return { default: module.WorkspacePanel }; |
| 26 | }); |
| 27 | |
| 28 | export type WorkspaceDockRegionProps = { |
| 29 | navigation?: DockNavigation; |
| 30 | /** Navigation state each dock instance reads; the runtime passes the one it holds. */ |
| 31 | fileNavigation?: FileNavigationOwner; |
| 32 | visible: boolean; |
| 33 | overlay: boolean; |
| 34 | mode: RightDockMode; |
| 35 | showContext: boolean; |
| 36 | t: Translator; |
| 37 | /** Opens (or activates) the dock view a tab-picker entry stands for. */ |
| 38 | onPickEntry: (entryId: string) => void; |
| 39 | remote: ComponentProps<typeof RemotePanel>; |
| 40 | context: ComponentProps<typeof ContextPanel>; |
| 41 | workspace: ComponentProps<typeof WorkspacePanel>; |
| 42 | workspaceKey: string; |
| 43 | workspaceRoot?: string; |
| 44 | resizer?: { |
| 45 | min: number; |
| 46 | max: number; |
| 47 | value: number; |
| 48 | onPointerDown: (event: PointerEvent<HTMLButtonElement>) => void; |
| 49 | onKeyDown: (event: KeyboardEvent<HTMLButtonElement>) => void; |
| 50 | onReset: () => void; |
| 51 | }; |
| 52 | }; |
| 53 | |
| 54 | /** The right-hand dock shared by every workspace tab. */ |
| 55 | export function WorkspaceDockRegion(props: WorkspaceDockRegionProps) { |
| 56 | const { visible, overlay, mode, showContext, t } = props; |
| 57 | const firstFileTabId = useActivityBarStore(state => state.tabs.find(tab => tab.type === "file")?.id); |
| 58 | const loadedRoot = useActivityBarStore(state => state.workspaceRoot); |
| 59 | const activeTabId = useActivityBarStore(state => state.activeTabId); |
| 60 | const tabs = useActivityBarStore(state => state.tabs); |
| 61 | const { fileNavigation: fileNavigationProp } = props; |
| 62 | // A standalone region keeps one instance for its whole life; the app shell |
| 63 | // always passes the instance the runtime holds. |
| 64 | const [fallbackFileNavigation] = useState(fileNavigationOwner); |
| 65 | const fileNavigation = fileNavigationProp ?? fallbackFileNavigation; |
| 66 | const projectReady = loadedRoot === (props.workspaceRoot ?? props.workspace.cwd ?? ""); |
| 67 | const requests = useDockViewRequests(`${props.workspaceKey}::${props.workspace.tabId ?? ""}`, visible && projectReady ? activeTabId : null, props.workspace, props.navigation, tabs.map(tab => tab.id)); |
| 68 | |
| 69 | const renderTab = (tab: TabItem): ReactNode => { |
| 70 | if (!projectReady) return null; |
| 71 | if (firstFileTabId) readWorkspaceTreeMemory(workspaceViewMemoryKey(props.workspaceKey, firstFileTabId, true)); |
| 72 | switch (tab.type) { |
| 73 | case "context": |
| 74 | if (showContext) return <ContextPanel {...props.context} />; |
| 75 | return <WorkspacePanel key={`${props.workspaceKey}::${tab.id}`} {...props.workspace} {...requests} |
| 76 | dockTabId={tab.id} fileNavigation={fileNavigation} |
| 77 | workspaceMemoryKey={workspaceViewMemoryKey(props.workspaceKey, tab.id)} workspaceMemoryVisitId={0} />; |
| 78 | case "remote": |
| 79 | return <RemotePanel key={`${props.workspaceKey}::${props.workspace.tabId}::${tab.id}`} {...props.remote} tabId={props.workspace.tabId} dockTabId={tab.id} fileNavigation={fileNavigation} navigationSignal={requests.navigationSignal} />; |
| 80 | case "browser": |
| 81 | return <BrowserSurface surface="panel" taskId={props.workspace.tabId} />; |
| 82 | default: |
| 83 | return ( |
| 84 | <WorkspacePanel |
| 85 | key={`${props.workspaceKey}::${tab.id}`} |
| 86 | {...props.workspace} |
| 87 | {...requests} |
| 88 | dockTabId={tab.id} |
| 89 | fileNavigation={fileNavigation} |
| 90 | workspaceMemoryKey={workspaceViewMemoryKey(props.workspaceKey, tab.id, tab.id === firstFileTabId)} |
| 91 | workspaceMemoryVisitId={0} |
| 92 | initialViewMode={tab.type === "changed" ? "changed" : "files"} |
| 93 | /> |
| 94 | ); |
| 95 | } |
| 96 | }; |
| 97 | |
| 98 | return ( |
| 99 | <> |
| 100 | {props.resizer && ( |
| 101 | <button |
| 102 | className="workspace-panel-resizer" type="button" role="separator" aria-orientation="vertical" |
| 103 | aria-label={t("rightDock.resize")} aria-valuemin={props.resizer.min} |
| 104 | aria-valuemax={props.resizer.max} aria-valuenow={props.resizer.value} |
| 105 | onPointerDown={props.resizer.onPointerDown} onKeyDown={props.resizer.onKeyDown} |
| 106 | onDoubleClick={props.resizer.onReset} |
| 107 | /> |
| 108 | )} |
| 109 | {visible && ( |
| 110 | <aside className={["workbench-dock", `workbench-dock--${mode}`, overlay ? "workbench-dock--overlay" : ""].join(" ")} aria-label={t("rightDock.workbench")}> |
| 111 | <div className="workbench-dock__panel"> |
| 112 | <Suspense fallback={null}> |
| 113 | <TabContainer key={loadedRoot} renderTab={renderTab} onPickEntry={props.onPickEntry} /> |
| 114 | </Suspense> |
| 115 | </div> |
| 116 | </aside> |
| 117 | )} |
| 118 | </> |
| 119 | ); |
| 120 | } |
| 121 |