Files
paseo/packages/app/src/panels/panel-instance-attributes.ts
T
Mohamed BoudraandGitHub 4bda2dfea9 Edit workspace files directly on web (#2270)
* feat(files): edit workspace files on web

Keep source buffers synchronized with host file changes and require an explicit overwrite or reload when revisions diverge.

* feat(panels): surface and protect modified tabs

Expose tooltip and modification state through the generic panel boundary so tabs can show stable metadata and guard every close route consistently.

* fix(tests): use portable fake timeout handle

* fix(files): harden editor conflict handling

Preserve modified panel state across tab eviction, use precise revisions for optimistic writes, coalesce concurrent file watchers, and localize the editor interface.

* fix(files): close editor concurrency gaps

Coalesce clean reloads, preserve subscriber identities and file permissions, suspend pending saves during close confirmation, and carry precise revisions through file reads.

* test(files): expect read revision metadata
2026-07-21 04:41:19 +08:00

119 lines
3.8 KiB
TypeScript

import { useCallback, useEffect, useMemo, useSyncExternalStore } from "react";
import { usePaneContext } from "@/panels/pane-context";
export interface PanelInstanceIdentity {
serverId: string;
workspaceId: string;
tabId: string;
}
export interface PanelInstanceAttributes {
modified: boolean;
suspendPendingSave?: () => () => void;
}
const DEFAULT_ATTRIBUTES: PanelInstanceAttributes = { modified: false };
const attributesByPanel = new Map<string, PanelInstanceAttributes>();
const listenersByPanel = new Map<string, Set<() => void>>();
const allListeners = new Set<() => void>();
let attributesRevision = 0;
export function buildPanelInstanceKey(identity: PanelInstanceIdentity): string {
return `${identity.serverId}:${identity.workspaceId}:${identity.tabId}`;
}
export function getPanelInstanceAttributes(
identity: PanelInstanceIdentity,
): PanelInstanceAttributes {
return attributesByPanel.get(buildPanelInstanceKey(identity)) ?? DEFAULT_ATTRIBUTES;
}
export function setPanelInstanceAttributes(
identity: PanelInstanceIdentity,
attributes: PanelInstanceAttributes,
): void {
const key = buildPanelInstanceKey(identity);
const previous = attributesByPanel.get(key) ?? DEFAULT_ATTRIBUTES;
if (
previous.modified === attributes.modified &&
previous.suspendPendingSave === attributes.suspendPendingSave
) {
return;
}
if (attributes.modified) attributesByPanel.set(key, attributes);
else attributesByPanel.delete(key);
attributesRevision += 1;
for (const listener of listenersByPanel.get(key) ?? []) listener();
for (const listener of allListeners) listener();
}
export function useModifiedPanelTabIds(input: {
serverId: string;
workspaceId: string;
tabIds: string[];
}): Set<string> {
const revision = useSyncExternalStore(
useCallback((listener: () => void) => {
allListeners.add(listener);
return () => allListeners.delete(listener);
}, []),
() => attributesRevision,
() => attributesRevision,
);
return useMemo(() => {
void revision;
return new Set(
input.tabIds.filter(
(tabId) =>
getPanelInstanceAttributes({
serverId: input.serverId,
workspaceId: input.workspaceId,
tabId,
}).modified,
),
);
}, [input.serverId, input.tabIds, input.workspaceId, revision]);
}
export function subscribePanelInstanceAttributes(
identity: PanelInstanceIdentity,
listener: () => void,
): () => void {
const key = buildPanelInstanceKey(identity);
const listeners = listenersByPanel.get(key) ?? new Set<() => void>();
listeners.add(listener);
listenersByPanel.set(key, listeners);
return () => {
listeners.delete(listener);
if (listeners.size === 0) listenersByPanel.delete(key);
};
}
export function usePanelInstanceAttributes({
serverId,
workspaceId,
tabId,
}: PanelInstanceIdentity): PanelInstanceAttributes {
const subscribe = useCallback(
(listener: () => void) =>
subscribePanelInstanceAttributes({ serverId, workspaceId, tabId }, listener),
[serverId, tabId, workspaceId],
);
const getSnapshot = useCallback(
() => getPanelInstanceAttributes({ serverId, workspaceId, tabId }),
[serverId, tabId, workspaceId],
);
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}
export function usePublishPanelInstanceAttributes(attributes: PanelInstanceAttributes): void {
const { serverId, workspaceId, tabId } = usePaneContext();
const modified = attributes.modified;
const suspendPendingSave = attributes.suspendPendingSave;
useEffect(() => {
const identity = { serverId, workspaceId, tabId };
setPanelInstanceAttributes(identity, { modified, suspendPendingSave });
return () => setPanelInstanceAttributes(identity, DEFAULT_ATTRIBUTES);
}, [modified, serverId, suspendPendingSave, tabId, workspaceId]);
}