Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210<script lang="ts"> import type { FileDiffMetadata, SelectedLineRange } from "@pierre/diffs"; import { untrack } from "svelte"; import { goto } from "$app/navigation"; import { page } from "$app/state"; import { SvelteMap } from "svelte/reactivity"; import DiffCodeView, { type CodeViewApi, type CodeViewFile } from "#lib/components/repo/DiffCodeView.svelte"; import { fileAnchor, jumpToAnchoredFile } from "#lib/components/repo/fileAnchor.svelte.ts"; import type { PatchStream } from "#lib/api/patchStream.ts"; import type { RepoDiff, RepoDiffDeps } from "#lib/api/repoDiff.ts"; import type { DiffStyle } from "#lib/components/repo/pierre.ts"; import type { DiffEntry } from "#lib/components/repo/fileDiff.ts"; import { createDiffQueue } from "#lib/components/repo/diffQueue.ts";
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; linkLines?: boolean; onQuoteLines?: (path: string, range: SelectedLineRange) => void; }
let { deps, diff, blobUrl, diffStyle = "unified", wrap = false, isOpen = () => true, onSetOpen, patchUrl, patch, dockTop, onScrollExtent, linkLines = false, onQuoteLines }: 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(untrack(() => 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, range?: SelectedLineRange | null ): number | undefined => { const file = diff.files.find((candidate) => candidate.name === path); return file ? view?.scrollToFile(file.key, range) : undefined; };
const fileByKey = (key: string) => diff.files.find((candidate) => candidate.key === key);
const selectLines = (key: string, range: SelectedLineRange | null): void => { const file = fileByKey(key); if (!file) return; const hash = range ? fileAnchor(file.name, range) : ""; // eslint-disable-next-line svelte/no-navigation-without-resolve void goto(`${location.pathname}${location.search}${hash}`, { shallow: true, replace: true, state: page.state }); };
const quoteLines = (key: string, range: SelectedLineRange): void => { const file = fileByKey(key); if (file) onQuoteLines?.(file.name, range); };
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)} onLineSelected={linkLines ? selectLines : undefined} onWantWhole={(key) => queue.whole(key)} onQuoteLines={onQuoteLines && quoteLines} /></div>