134 lines · 3.5 KB · TypeScript
RawDownload
/** * Pure utility functions for immutable pane manipulation. * All functions return new objects (no mutation). */ import type { Pane, PaneLayout } from '@renderer/types/panes';import type { Tab } from '@renderer/types/tabs'; /** * Find a pane by its ID. */export function findPane(layout: PaneLayout, paneId: string): Pane | undefined {  return layout.panes.find((p) => p.id === paneId);} /** * Find which pane contains a given tab. */export function findPaneByTabId(layout: PaneLayout, tabId: string): Pane | undefined {  return layout.panes.find((p) => p.tabs.some((t) => t.id === tabId));} /** * Replace a pane immutably in the layout. */export function updatePane(layout: PaneLayout, updatedPane: Pane): PaneLayout {  return {    ...layout,    panes: layout.panes.map((p) => (p.id === updatedPane.id ? updatedPane : p)),  };} /** * Remove a pane and redistribute its width to a neighbor. * If removing the focused pane, focus shifts to the nearest neighbor. */export function removePane(layout: PaneLayout, paneId: string): PaneLayout {  const index = layout.panes.findIndex((p) => p.id === paneId);  if (index === -1 || layout.panes.length <= 1) return layout;   const removedPane = layout.panes[index];  const newPanes = layout.panes.filter((p) => p.id !== paneId);   // Redistribute width to the nearest neighbor  const neighborIndex = index > 0 ? index - 1 : 0;  const redistributed = newPanes.map((p, i) =>    i === neighborIndex ? { ...p, widthFraction: p.widthFraction + removedPane.widthFraction } : p  );   // Equalize to avoid floating point drift  const equalized = redistributeWidths(redistributed);   // Update focus if the removed pane was focused  let newFocusedId = layout.focusedPaneId;  if (layout.focusedPaneId === paneId) {    const focusTarget = equalized[Math.min(index, equalized.length - 1)];    newFocusedId = focusTarget.id;  }   return {    panes: equalized,    focusedPaneId: newFocusedId,  };} /** * Insert a new pane adjacent to an existing pane. */export function insertPane(  layout: PaneLayout,  adjacentPaneId: string,  newPane: Pane,  direction: 'left' | 'right'): PaneLayout {  const index = layout.panes.findIndex((p) => p.id === adjacentPaneId);  if (index === -1) return layout;   const insertAt = direction === 'right' ? index + 1 : index;  const newPanes = [...layout.panes];  newPanes.splice(insertAt, 0, newPane);   return {    ...layout,    panes: redistributeWidths(newPanes),  };} /** * Equalize widths across all panes so they sum to 1. */function redistributeWidths(panes: Pane[]): Pane[] {  if (panes.length === 0) return panes;  const fraction = 1 / panes.length;  return panes.map((p) => ({ ...p, widthFraction: fraction }));} /** * Extract the focused pane's tab state for root-level sync. */export function syncFocusedPaneState(layout: PaneLayout): {  openTabs: Tab[];  activeTabId: string | null;  selectedTabIds: string[];} {  const focused = findPane(layout, layout.focusedPaneId);  if (!focused) {    return { openTabs: [], activeTabId: null, selectedTabIds: [] };  }  return {    openTabs: focused.tabs,    activeTabId: focused.activeTabId,    selectedTabIds: focused.selectedTabIds,  };} /** * Get all tabs across all panes (flat list). */export function getAllTabs(layout: PaneLayout): Tab[] {  return layout.panes.flatMap((p) => p.tabs);} /** * Create a new empty pane with a unique ID. */export function createEmptyPane(id: string): Pane {  return {    id,    tabs: [],    activeTabId: null,    selectedTabIds: [],    widthFraction: 0,  };}