// src/layout/hooks/useWorkspaceViews.ts import { useState, useEffect, useRef } from 'react' import type { WorkspaceLeaf } from '../../plugin/workspace' import type { View } from '../../plugin/views/view' import type { App } from '../../plugin/app' interface WorkspaceViewsState { leftLeaves: readonly WorkspaceLeaf[] rightLeaves: readonly WorkspaceLeaf[] registeredTypes: string[] } /** * Internal snapshot that captures both leaf identity AND view identity * at the time of setState. The leaf objects are mutable (their .view * property changes when a plugin calls openView/setViewState), so we * must snapshot the view references separately to detect changes. */ interface ViewsSnapshot { state: WorkspaceViewsState leftViews: (View | null)[] rightViews: (View | null)[] } function snapshotViews(leaves: readonly WorkspaceLeaf[]): (View | null)[] { return leaves.map(l => l.view) } /** * Compare current workspace leaves against a snapshot. * Checks both leaf object identity and view identity (from the snapshot). */ function leafArrayChanged( snapshotLeaves: readonly WorkspaceLeaf[], snapshotViews: (View | null)[], currentLeaves: readonly WorkspaceLeaf[], ): boolean { if (snapshotLeaves.length !== currentLeaves.length) return true for (let i = 0; i < snapshotLeaves.length; i++) { if (snapshotLeaves[i] !== currentLeaves[i]) return true // Compare current view against the SNAPSHOTTED view reference, // not the leaf's current .view (which would be the same since // snapshotLeaves[i] IS the same mutable object as currentLeaves[i]) if (snapshotViews[i] !== currentLeaves[i].view) return true } return false } function arraysEqual(a: string[], b: string[]): boolean { if (a.length !== b.length) return false for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return false } return true } /** * React hook that subscribes to Workspace view registry and panel leaf changes. * Re-renders when views are registered/unregistered or layout changes. * * Uses a snapshot-based comparison to detect changes: leaf identity is compared * by object reference, and view identity is compared against a captured snapshot * (not the live mutable .view property on the leaf object). This correctly * detects when a view is swapped on an existing leaf via openView/setViewState. */ export function useWorkspaceViews(app: App): WorkspaceViewsState { const workspace = app.workspace const [state, setState] = useState(() => ({ leftLeaves: workspace.getLeftLeaves(), rightLeaves: workspace.getRightLeaves(), registeredTypes: workspace.getRegisteredViewTypes(), })) // Keep a snapshot ref that includes view references at the time of last setState const snapshotRef = useRef({ state, leftViews: snapshotViews(workspace.getLeftLeaves()), rightViews: snapshotViews(workspace.getRightLeaves()), }) snapshotRef.current.state = state useEffect(() => { const refresh = () => { const currentLeft = workspace.getLeftLeaves() const currentRight = workspace.getRightLeaves() const currentTypes = workspace.getRegisteredViewTypes() const snap = snapshotRef.current // Skip setState if nothing actually changed const leftChanged = leafArrayChanged(snap.state.leftLeaves, snap.leftViews, currentLeft) const rightChanged = leafArrayChanged(snap.state.rightLeaves, snap.rightViews, currentRight) const typesChanged = !arraysEqual(snap.state.registeredTypes, currentTypes) if (!leftChanged && !rightChanged && !typesChanged) { return } const newState: WorkspaceViewsState = { leftLeaves: currentLeft, rightLeaves: currentRight, registeredTypes: currentTypes, } // Update snapshot with current view references snapshotRef.current = { state: newState, leftViews: snapshotViews(currentLeft), rightViews: snapshotViews(currentRight), } setState(newState) } // view-registered and view-unregistered are Reams-novel events (on reamsEvents) // layout-change is Obsidian-compatible (on workspace) const reamsRefs = [ app.reamsEvents.on('view-registered', refresh), app.reamsEvents.on('view-unregistered', refresh), ] const workspaceRef = workspace.on('layout-change', refresh) // Sync on mount in case state changed between initial render and effect refresh() return () => { for (const ref of reamsRefs) { app.reamsEvents.offref(ref) } workspace.offref(workspaceRef) } }, [app, workspace]) return state }