@pierre/diffs
Version:
388 lines (387 loc) • 16.8 kB
JavaScript
"use client";
import { areManagedSnapshotsEqual } from "../utils/areManagedSnapshotsEqual.js";
import { areOptionsEqual } from "../utils/areOptionsEqual.js";
import { CodeView as CodeView$1 } from "../components/CodeView.js";
import { useStableCallback } from "./utils/useStableCallback.js";
import { useCreateEditor } from "./EditContext.js";
import { renderDiffChildren } from "./utils/renderDiffChildren.js";
import { renderFileChildren } from "./utils/renderFileChildren.js";
import { WorkerPoolContext } from "./WorkerPoolContext.js";
import { forwardRef, memo, useContext, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
import { createPortal, flushSync } from "react-dom";
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
//#region src/react/CodeView.tsx
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
function createDefaultCache(controlled) {
return {
instance: void 0,
items: void 0,
controlled,
managedOptions: void 0,
disableFlushSync: false,
slotCoordinator: void 0
};
}
function CodeViewInner(props, ref) {
const { className, containerRef, disableWorkerPool = false, editorOptions, getEditStateKey, initialItems, items: controlledItems, onItemEditChange, onItemEditComplete, onScroll, onSelectedLinesChange, options, renderAnnotation, renderCodeViewFooter, renderCodeViewHeader, renderCustomHeader, renderGutterUtility, renderHeaderFilenameSuffix, renderHeaderMetadata, renderHeaderPrefix, selectedLines, style } = props;
const controlled = controlledItems !== void 0;
const contextCreateEditor = useCreateEditor();
const poolManager = useContext(WorkerPoolContext);
const cachedDataRef = useRef(createDefaultCache(controlled));
const hasCustomHeader = renderCustomHeader != null;
const hasAnnotationRenderer = renderAnnotation != null;
const hasGutterRenderer = renderGutterUtility != null;
const hasHeaderRenderers = hasCustomHeader || renderHeaderPrefix != null || renderHeaderFilenameSuffix != null || renderHeaderMetadata != null;
const hasRenderers = hasHeaderRenderers || hasAnnotationRenderer || hasGutterRenderer;
const hasCodeViewHeader = renderCodeViewHeader != null;
const hasCodeViewFooter = renderCodeViewFooter != null;
const emitSelectedLinesChange = useStableCallback((selection) => {
onSelectedLinesChange?.(selection);
});
const controlledSelection = selectedLines !== void 0;
const createEditor = useStableCallback((editorType, options, editStateKey) => {
if (contextCreateEditor == null) throw new Error("CodeView: EditContext is not attached");
return contextCreateEditor(editorType, {
...editorOptions,
...options
}, editStateKey);
});
const emitItemEditChange = useStableCallback((event, item) => {
onItemEditChange?.(event, item);
});
const emitItemEditComplete = useStableCallback((event, item, nextItem) => onItemEditComplete?.(event, item, nextItem) ?? "reject");
const managedOptions = useMemo(() => createManagedCodeViewOptions({
options,
hasCustomHeader,
hasGutterRenderer,
hasCodeViewHeader,
hasCodeViewFooter,
onSelectedLinesChange: onSelectedLinesChange != null ? emitSelectedLinesChange : void 0,
controlledSelection,
getEditStateKey,
createEditor: contextCreateEditor != null ? createEditor : void 0,
onItemEditChange: onItemEditChange != null ? emitItemEditChange : void 0,
onItemEditComplete: onItemEditComplete != null ? emitItemEditComplete : void 0
}), [
contextCreateEditor,
controlledSelection,
createEditor,
emitItemEditChange,
emitItemEditComplete,
emitSelectedLinesChange,
hasCodeViewFooter,
hasCodeViewHeader,
hasCustomHeader,
hasGutterRenderer,
getEditStateKey,
onItemEditChange,
onItemEditComplete,
onSelectedLinesChange,
options
]);
const [slotContentStore] = useState(() => createSlotContentStore());
const [, forceUpdate] = useState({});
const nodeRef = useStableCallback((node) => {
if (cachedDataRef.current.instance != null && (node == null || node !== cachedDataRef.current.instance.getContainerElement())) {
cachedDataRef.current.instance.cleanUp();
slotContentStore.publish(void 0);
cachedDataRef.current = createDefaultCache(controlled);
}
if (node != null && node !== cachedDataRef.current.instance?.getContainerElement()) {
cachedDataRef.current.instance = new CodeView$1(managedOptions, !disableWorkerPool ? poolManager : void 0, true);
cachedDataRef.current.instance.setup(node);
}
if (typeof containerRef === "function") containerRef(node);
else if (containerRef != null) containerRef.current = node;
});
const onSnapshotChange = useStableCallback((snapshot) => {
if (cachedDataRef.current.disableFlushSync) slotContentStore.publish(snapshot);
else flushSync(() => {
slotContentStore.publish(snapshot);
});
});
const slotCoordinator = useMemo(() => {
if (!hasHeaderRenderers && !hasAnnotationRenderer && !hasGutterRenderer && !hasCodeViewHeader && !hasCodeViewFooter) return;
else return {
hasHeaderRenderers,
hasAnnotationRenderer,
hasGutterRenderer,
onSnapshotChange
};
}, [
onSnapshotChange,
hasAnnotationRenderer,
hasGutterRenderer,
hasHeaderRenderers,
hasCodeViewHeader,
hasCodeViewFooter
]);
useIsomorphicLayoutEffect(() => {
return onScroll != null ? cachedDataRef.current.instance?.subscribeToScroll(onScroll) : void 0;
});
useIsomorphicLayoutEffect(() => {
const { instance, controlled: prevControlled, items: prevItems, managedOptions: prevManagedOptions, slotCoordinator: prevSlotCoordinator } = cachedDataRef.current;
if (instance == null) return;
try {
cachedDataRef.current.disableFlushSync = true;
let shouldRender = false;
if (!areOptionsEqual(managedOptions, prevManagedOptions)) {
cachedDataRef.current.managedOptions = managedOptions;
instance.setOptions(managedOptions);
shouldRender = true;
}
if (prevControlled !== controlled) {
console.error("CodeView: cannot switch between controlled and uncontrolled modes. Remount with a new key instead.");
return;
}
if (controlled) {
if (controlledItems !== prevItems) if (areItemListsEqual(prevItems, controlledItems)) cachedDataRef.current.items = controlledItems;
else if (isAppendOnlyItemUpdate(prevItems, controlledItems)) {
cachedDataRef.current.items = controlledItems;
instance.addItems(controlledItems.slice(prevItems.length));
} else {
cachedDataRef.current.items = controlledItems;
instance.setItems(controlledItems);
shouldRender = true;
}
} else if (prevItems == null) {
const seedItems = initialItems ?? [];
cachedDataRef.current.items = seedItems;
if (seedItems.length > 0) {
instance.setItems(seedItems);
shouldRender = true;
}
}
if (selectedLines !== void 0) instance.setSelectedLines(selectedLines, { notify: false });
const slotPublish = instance.setSlotCoordinator(slotCoordinator);
let forceInlinePublish = false;
if (slotCoordinator !== prevSlotCoordinator) {
if (slotCoordinator == null || prevSlotCoordinator == null) forceInlinePublish = true;
cachedDataRef.current.slotCoordinator = slotCoordinator;
}
if (shouldRender || slotPublish) instance.render(true);
if (slotPublish && slotCoordinator == null) slotContentStore.publish(void 0);
if (forceInlinePublish) forceUpdate({});
} finally {
cachedDataRef.current.disableFlushSync = false;
}
});
useImperativeHandle(ref, () => ({
addItems(items) {
const { controlled, instance } = cachedDataRef.current;
assertUncontrolledCodeViewAction(controlled, "addItems");
if (instance == null) console.error("CodeView.addItems: no valid instance to append items with", items);
else instance.addItems(items);
},
getItem(id) {
const { instance } = cachedDataRef.current;
if (instance == null) {
console.error("CodeView.getItem: no valid instance exists", id);
return;
} else return instance.getItem(id);
},
removeItem(id) {
const { controlled, instance } = cachedDataRef.current;
assertUncontrolledCodeViewAction(controlled, "removeItem");
if (instance == null) {
console.error("CodeView.removeItem: no valid instance to remove item from", id);
return false;
}
return instance.removeItem(id);
},
updateItem(item) {
const { controlled, instance } = cachedDataRef.current;
assertUncontrolledCodeViewAction(controlled, "updateItem");
if (instance == null) {
console.error("CodeView.updateItem: no valid instance to update item with", item);
return false;
}
return instance.updateItem(item);
},
updateItemId(oldId, newId) {
const { controlled, instance } = cachedDataRef.current;
assertUncontrolledCodeViewAction(controlled, "updateItemId");
if (instance == null) {
console.error("CodeView.updateItemId: no valid instance to update item id with", oldId, newId);
return false;
}
return instance.updateItemId(oldId, newId);
},
scrollTo(target) {
const { instance } = cachedDataRef.current;
if (instance == null) console.error("CodeView.scrollTo: no valid instance to scroll with", target);
else instance.scrollTo(target);
},
setSelectedLines(selection) {
const { instance } = cachedDataRef.current;
if (instance == null) console.error("CodeView.setSelectedLines: no valid instance to update selection with", selection);
else {
instance.setSelectedLines(selection, { notify: false });
emitSelectedLinesChange(selection);
}
},
getSelectedLines() {
const { instance } = cachedDataRef.current;
if (instance == null) {
console.error("CodeView.getSelectedLines: no valid instance exists");
return null;
} else return instance.getSelectedLines();
},
clearSelectedLines() {
const { instance } = cachedDataRef.current;
if (instance == null) console.error("CodeView.clearSelectedLines: no valid instance to update selection with");
else {
instance.clearSelectedLines({ notify: false });
emitSelectedLinesChange(null);
}
},
getEditor(id) {
const { instance } = cachedDataRef.current;
if (instance == null) {
console.error("CodeView.getEditor: no valid instance exists", id);
return;
}
return instance.getEditor(id);
},
getInstance() {
return cachedDataRef.current.instance;
}
}), [emitSelectedLinesChange]);
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
ref: nodeRef,
className,
style
}), (hasRenderers || hasCodeViewHeader || hasCodeViewFooter) && /* @__PURE__ */ jsx(SlotPortals, {
managedContentStore: slotContentStore,
renderCustomHeader,
renderHeaderPrefix,
renderHeaderFilenameSuffix,
renderHeaderMetadata,
renderAnnotation,
renderGutterUtility,
renderCodeViewHeader,
renderCodeViewFooter
})] });
}
const CodeView = forwardRef(CodeViewInner);
function isAppendOnlyItemUpdate(previousItems, nextItems) {
if (previousItems == null || nextItems.length <= previousItems.length) return false;
if (previousItems.length === 0) return true;
for (let index = 0; index < previousItems.length; index++) if (nextItems[index] !== previousItems[index]) return false;
return true;
}
function areItemListsEqual(previousItems, nextItems) {
if (previousItems == null || previousItems.length !== nextItems.length) return false;
for (let index = 0; index < previousItems.length; index++) if (previousItems[index] !== nextItems[index]) return false;
return true;
}
function assertUncontrolledCodeViewAction(controlled, action) {
if (!controlled) return;
throw new Error(`CodeView.${action} cannot be used when CodeView is controlled. Use initialItems for imperative item updates.`);
}
function createSlotContentStore() {
let snapshot;
const listeners = /* @__PURE__ */ new Set();
return {
getSnapshot() {
return snapshot;
},
publish(nextSnapshot) {
if (areManagedSnapshotsEqual(snapshot, nextSnapshot)) return;
snapshot = nextSnapshot;
for (const listener of listeners) listener();
},
subscribe(listener) {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}
};
}
function createManagedCodeViewOptions({ options, hasCustomHeader, hasGutterRenderer, hasCodeViewHeader, hasCodeViewFooter, onSelectedLinesChange, controlledSelection, getEditStateKey, createEditor, onItemEditChange, onItemEditComplete }) {
const managedOptions = {
...options,
controlledSelection,
onSelectedLinesChange,
createEditor
};
if (getEditStateKey != null) managedOptions.getEditStateKey = getEditStateKey;
if (onItemEditChange != null) managedOptions.onItemEditChange = onItemEditChange;
if (onItemEditComplete != null) managedOptions.onItemEditComplete = onItemEditComplete;
if (hasCustomHeader) managedOptions.renderCustomHeader = noopRender;
if (hasGutterRenderer) managedOptions.renderGutterUtility = noopRender;
if (hasCodeViewHeader) managedOptions.renderCodeViewHeader = noopRender;
if (hasCodeViewFooter) managedOptions.renderCodeViewFooter = noopRender;
return managedOptions;
}
const SlotPortals = memo(function SlotPortals({ managedContentStore, renderCustomHeader, renderHeaderPrefix, renderHeaderFilenameSuffix, renderHeaderMetadata, renderAnnotation, renderGutterUtility, renderCodeViewHeader, renderCodeViewFooter }) {
"use no memo";
const subscribe = useStableCallback((listener) => managedContentStore.subscribe(listener));
const getSnapshot = useStableCallback(() => managedContentStore.getSnapshot());
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
let itemKeys = "";
for (const item of snapshot?.items ?? []) itemKeys += `${item.id}:${item.version}:${item.type}`;
return /* @__PURE__ */ jsxs(Fragment, { children: [
useMemo(() => {
return snapshot?.items?.map((renderedItem) => createPortal(renderCodeViewItemChildren({
renderedItem,
renderCustomHeader,
renderHeaderPrefix,
renderHeaderFilenameSuffix,
renderHeaderMetadata,
renderAnnotation,
renderGutterUtility
}), renderedItem.element, renderedItem.id));
}, [
renderCustomHeader,
renderHeaderPrefix,
renderHeaderFilenameSuffix,
renderHeaderMetadata,
renderAnnotation,
renderGutterUtility,
itemKeys
]),
useMemo(() => {
return renderCodeViewHeader != null && snapshot?.header != null ? createPortal(renderCodeViewHeader(), snapshot.header) : null;
}, [renderCodeViewHeader, snapshot?.header]),
useMemo(() => {
return renderCodeViewFooter != null && snapshot?.footer != null ? createPortal(renderCodeViewFooter(), snapshot.footer) : null;
}, [renderCodeViewFooter, snapshot?.footer])
] });
});
function renderCodeViewItemChildren({ renderedItem, renderCustomHeader, renderHeaderPrefix, renderHeaderFilenameSuffix, renderHeaderMetadata, renderAnnotation, renderGutterUtility }) {
if (renderedItem.type === "diff") {
const { item, instance } = renderedItem;
return renderDiffChildren({
fileDiff: item.fileDiff,
renderCustomHeader: renderCustomHeader != null ? () => renderCustomHeader(item) : void 0,
renderHeaderPrefix: renderHeaderPrefix != null ? () => renderHeaderPrefix(item) : void 0,
renderHeaderFilenameSuffix: renderHeaderFilenameSuffix != null ? () => renderHeaderFilenameSuffix(item) : void 0,
renderHeaderMetadata: renderHeaderMetadata != null ? () => renderHeaderMetadata(item) : void 0,
renderAnnotation: renderAnnotation != null ? (annotation) => renderAnnotation(annotation, item) : void 0,
lineAnnotations: item.annotations,
renderGutterUtility: renderGutterUtility != null ? (getHoveredLine) => renderGutterUtility(getHoveredLine, item) : void 0,
getHoveredLine: instance.getHoveredLine,
getAnnotationSlotName: instance.getAnnotationSlotName
});
} else {
const { item, instance } = renderedItem;
return renderFileChildren({
file: item.file,
renderCustomHeader: renderCustomHeader != null ? () => renderCustomHeader(item) : void 0,
renderHeaderPrefix: renderHeaderPrefix != null ? () => renderHeaderPrefix(item) : void 0,
renderHeaderFilenameSuffix: renderHeaderFilenameSuffix != null ? () => renderHeaderFilenameSuffix(item) : void 0,
renderHeaderMetadata: renderHeaderMetadata != null ? () => renderHeaderMetadata(item) : void 0,
renderAnnotation: renderAnnotation != null ? (annotation) => renderAnnotation(annotation, item) : void 0,
lineAnnotations: item.annotations,
renderGutterUtility: renderGutterUtility != null ? (getHoveredLine) => renderGutterUtility(getHoveredLine, item) : void 0,
getHoveredLine: instance.getHoveredLine,
getAnnotationSlotName: instance.getAnnotationSlotName
});
}
}
function noopRender() {}
//#endregion
export { CodeView };
//# sourceMappingURL=CodeView.js.map