Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177<script lang="ts"> import type { FileDiffMetadata } from "@pierre/diffs"; import { SvelteMap } from "svelte/reactivity"; import DiffCodeView, { type CodeViewApi, type CodeViewFile } from "$lib/components/repo/DiffCodeView.svelte"; import { jumpToAnchoredFile } from "$lib/components/repo/fileAnchor.svelte"; import type { PatchStream } from "$lib/api/patchStream"; import type { RepoDiff, RepoDiffDeps } from "$lib/api/repoDiff"; import type { DiffStyle } from "$lib/components/repo/pierre"; import type { DiffEntry } from "$lib/components/repo/fileDiff"; import { createDiffQueue } from "$lib/components/repo/diffQueue";
interface Props { deps: RepoDiffDeps; diff: RepoDiff; blobUrl?: (file: CodeViewFile) => string | undefined; diffStyle?: DiffStyle; wrap?: boolean; isOpen?: (key: string) => boolean; onSetOpen?: (key: string, open: boolean) => void; /** the whole diff in one request, when the route knows where to get it */ patchUrl?: string; /** a patch already streaming in parallel with this diff's file list */ patch?: PatchStream; dockTop?: () => number; onScrollExtent?: (max: number) => void; }
let { deps, diff, blobUrl, diffStyle = "unified", wrap = false, isOpen = () => true, onSetOpen, patchUrl, patch, dockTop, onScrollExtent }: Props = $props();
// parsed metadata only: raw text is dropped after parsing. a map, because // rebuilding a record per filled file is quadratic over a diff with thousands // of them. values stay unproxied, because pierre posts them to its highlight // worker and structured-cloning a reactive proxy throws. // nothing is evicted: the view measures every item from its metadata, so // dropping hunks would shrink the scrollbar instead of freeing anything that // matters - it virtualizes the rendering, not the data const entries = new SvelteMap<string, DiffEntry>();
// while a patch is on its way, the queue leaves the files to it: one request // for the whole diff beats two blob fetches per file, and racing them fills // the page one card at a time. flipped the moment the patch settles - what it // did not carry (an interdiff's rebased side, a root commit, a mirror that // refused) is fetched per file, which is what this queue is for let patchExpected = $state(Boolean(patch ?? patchUrl));
const queue = createDiffQueue({ deps: () => deps, diff: () => diff, entry: (key) => entries.get(key), put: (parsed) => { for (const [key, entry] of Object.entries(parsed)) entries.set(key, entry); }, patchExpected: () => patchExpected });
// files the patch has already parsed, matched to this diff's entries. the patch // may have started before this list existed, so it is drained on mount and then // followed const fileByName = $derived( new Map( diff.files.flatMap((f) => [ [f.name, f], ...(f.oldName ? [[f.oldName, f] as const] : []) ]) ) );
const adoptBatch = (batch: FileDiffMetadata[]) => { const landed: Record<string, DiffEntry> = {}; for (const parsed of batch) { const file = fileByName.get(parsed.name) ?? (parsed.prevName ? fileByName.get(parsed.prevName) : undefined); if (file && !entries.has(file.key)) { landed[file.key] = parsed; } } if (Object.keys(landed).length) queue.adopt(landed); };
$effect(() => { const stream = patch; const url = patchUrl; const controller = new AbortController(); patchExpected = Boolean(stream ?? url); const settle = () => { patchExpected = false; queue.drain(); }; const fetchHere = () => { if (!url || controller.signal.aborted) { settle(); return; } queue .applyPatch(url, controller.signal) .catch(() => undefined) .finally(settle); }; if (stream) { adoptBatch([...stream.byName.values()]); const stop = stream.subscribe(adoptBatch); void stream.done.then((ok) => { if (ok) settle(); else fetchHere(); }); return () => { stop(); controller.abort(); queue.destroy(); }; } fetchHere(); return () => { controller.abort(); queue.destroy(); }; });
// the mirror spells an edit "changed" where the scroller (and pierre) say // "change": one mapping here beats a second spelling in the shared shape const viewFiles = $derived<CodeViewFile[]>( diff.files.map((file) => ({ ...file, kind: file.kind === "changed" ? "change" : file.kind })) );
let view: CodeViewApi | undefined = $state(); /** reports the offset the file landed on, so a deep link can wait for the diff to settle instead of missing it. exported for the file tree and the page's conflict list */ export const jumpToFile = (path: string): number | undefined => { const file = diff.files.find((candidate) => candidate.name === path); return file ? view?.scrollToFile(file.key) : undefined; }; export const resync = () => view?.resync(); jumpToAnchoredFile(jumpToFile);</script>
<!-- one scroller for the whole diff: the page itself never grows with the file count, which is the only way past the browser's layout coordinate ceiling on a diff of this size --><!-- fills whatever the sticky toolbar leaves of the column, so the scroller always reaches the bottom of the card --><div class="flex min-h-0 shrink grow basis-auto flex-col"> <DiffCodeView bind:this={view} files={viewFiles} entry={(key) => entries.get(key)} {blobUrl} {diffStyle} {wrap} {isOpen} onSetOpen={(key, open) => onSetOpen?.(key, open)} {dockTop} {onScrollExtent} onWant={(key) => queue.want(key)} onFocus={(key) => queue.focus(key)} /></div>