Files
kandev/apps/web/lib/state/layout-manager/comparator.ts
T

105 lines
3.6 KiB
TypeScript

import type { SerializedDockview } from "dockview-react";
import type { LayoutState, LayoutGroup } from "./types";
import { filterEphemeral } from "./serializer";
import { STRUCTURAL_COMPONENTS } from "./constants";
/**
* Extract a structural fingerprint from a group: sorted component names.
* Ignores panel IDs, params, sizes — only the *kind* of panels matters.
*/
function groupFingerprint(group: LayoutGroup): string {
return group.panels
.map((p) => p.component)
.sort()
.join(",");
}
/**
* Check whether two LayoutStates have the same structural skeleton.
*
* Both layouts are filtered through `filterEphemeral` first so that
* transient panels (file-editors, diffs, commit-details) don't cause
* false mismatches.
*
* Two layouts match when:
* - Same number of columns, same column IDs in the same order
* - Each column has the same number of groups
* - Each group has the same set of panel component types
*
* Sizes, proportions, active panels, and panel params are ignored.
*/
export function layoutStructuresMatch(a: LayoutState, b: LayoutState): boolean {
const fa = filterEphemeral(a);
const fb = filterEphemeral(b);
if (fa.columns.length !== fb.columns.length) return false;
for (let i = 0; i < fa.columns.length; i++) {
const colA = fa.columns[i];
const colB = fb.columns[i];
if (colA.id !== colB.id) return false;
if (colA.groups.length !== colB.groups.length) return false;
for (let j = 0; j < colA.groups.length; j++) {
if (groupFingerprint(colA.groups[j]) !== groupFingerprint(colB.groups[j])) {
return false;
}
}
}
return true;
}
/**
* Build a component-count fingerprint from a SerializedDockview.
* Returns a sorted "component:count" string for structural comparison.
* Counts preserve multiplicity (e.g. two terminals ≠ one terminal).
*/
function componentCountsFromSerialized(serialized: SerializedDockview): string {
const counts = new Map<string, number>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const panels = (serialized as any).panels as
| Record<string, { contentComponent?: string }>
| undefined;
if (!panels) return "";
for (const p of Object.values(panels)) {
if (p.contentComponent && STRUCTURAL_COMPONENTS.has(p.contentComponent)) {
counts.set(p.contentComponent, (counts.get(p.contentComponent) ?? 0) + 1);
}
}
return [...counts.entries()]
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}:${v}`)
.join(",");
}
/** Build a component-count fingerprint from a LayoutState (filtered). */
function componentCountsFromLayout(state: LayoutState): string {
const counts = new Map<string, number>();
const filtered = filterEphemeral(state);
for (const col of filtered.columns) {
for (const group of col.groups) {
for (const panel of group.panels) {
if (STRUCTURAL_COMPONENTS.has(panel.component)) {
counts.set(panel.component, (counts.get(panel.component) ?? 0) + 1);
}
}
}
}
return [...counts.entries()]
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}:${v}`)
.join(",");
}
/**
* Quick check: do a live LayoutState and a saved SerializedDockview have
* the same structural panels (including multiplicity)?
*
* This is a weaker check than `layoutStructuresMatch` (doesn't verify
* column arrangement) but works without deserializing the grid tree.
* Counts are compared so two terminals ≠ one terminal.
*/
export function savedLayoutMatchesLive(live: LayoutState, saved: SerializedDockview): boolean {
return componentCountsFromLayout(live) === componentCountsFromSerialized(saved);
}