~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
71 lines · 2.5 KB Raw History
<script lang="ts">
	import type { FileDiffView } from '#lib/lexicons/dev/knotless/defs.ts';
	import { pathOf } from '#lib/diffs.ts';
	import Icon from '#lib/atoms/Icon.svelte';
	/**
	 * A diff's files as a tree of their directories, each a link to its
	 * file's diff (`href` of its index). Each directory lists what it holds
	 * in the order the diffs come, so the tree read down is the diffs read
	 * down. A directory with one directory in it, and nothing else, goes by
	 * both their names. A file with comments on its lines says how many, as
	 * `comments` counts them. `onpick` hears which file was clicked.
	 */
	let {
		files,
		href,
		onpick,
		comments
	}: {
		files: FileDiffView[];
		href: (i: number) => string;
		onpick?: (i: number) => void;
		comments?: (f: FileDiffView) => number;
	} = $props();
	type File = { name: string; i: number; f: FileDiffView };
	type Dir = { name: string; entries: (Dir | File)[] };
	const isDir = (e: Dir | File): e is Dir => 'entries' in e;
	const tree = $derived.by(() => {
		const root: Dir = { name: '', entries: [] };
		for (const [i, f] of files.entries()) {
			const parts = pathOf(f).split('/');
			const name = parts.pop() ?? '';
			let at = root;
			for (const part of parts) {
				let next = at.entries.find((e): e is Dir => isDir(e) && e.name === part);
				if (!next) at.entries.push((next = { name: part, entries: [] }));
				at = next;
			}
			at.entries.push({ name, i, f });
		}
		const folded = (e: Dir | File): Dir | File => {
			if (!isDir(e)) return e;
			let d = e;
			while (d.entries.length === 1 && isDir(d.entries[0]))
				d = { ...d.entries[0], name: `${d.name}/${d.entries[0].name}` };
			return { ...d, entries: d.entries.map(folded) };
		};
		return { ...root, entries: root.entries.map(folded) };
	});
</script>

{#snippet dir(d: Dir)}
	{#each d.entries as e (isDir(e) ? `d:${e.name}` : e.i)}
		<li>
			{#if isDir(e)}
				<details open>
					<summary><Icon name="dir" /><span class="name">{e.name}</span></summary>
					<ul>{@render dir(e)}</ul>
				</details>
			{:else}
				<a href={href(e.i)} title={pathOf(e.f)} onclick={() => onpick?.(e.i)}
					><Icon name="file" /><span class="name">{e.name}</span>{#if comments?.(e.f)}{@const n =
							comments(e.f)}<span class="said" aria-label="{n} {n === 1 ? 'comment' : 'comments'}"
							><Icon name="chat" />{n}</span
						>{/if}<span class="add">+{e.f.added}</span><span class="rem">−{e.f.removed}</span></a
				>
			{/if}
		</li>
	{/each}
{/snippet}

<nav class="file-tree" aria-label="Changed files"><ul>{@render dir(tree)}</ul></nav>