tree-view.ts
2686 bytes
1import type { BranchCandidate } from "./tree-model.ts";
2
3export interface PruneTreeItem {
4 readonly candidate: BranchCandidate;
5 readonly label: string | undefined;
6 readonly treePrefix: string;
7}
8
9export function buildPruneTreeItems(
10 candidates: readonly BranchCandidate[],
11 getLabel: (entryId: string) => string | undefined,
12): PruneTreeItem[] {
13 const parentByRootId = new Map<string, string>();
14
15 for (const candidate of candidates) {
16 const parent = findParentCandidate(candidate, candidates);
17 if (parent) parentByRootId.set(candidate.rootId, parent.rootId);
18 }
19
20 const childrenByRootId = new Map<string | undefined, BranchCandidate[]>();
21 for (const candidate of candidates) {
22 const parentRootId = parentByRootId.get(candidate.rootId);
23 const children = childrenByRootId.get(parentRootId) ?? [];
24 children.push(candidate);
25 childrenByRootId.set(parentRootId, children);
26 }
27
28 const items: PruneTreeItem[] = [];
29 const visit = (candidate: BranchCandidate, ancestorIsLast: readonly boolean[], isLast: boolean): void => {
30 const parentRootId = parentByRootId.get(candidate.rootId);
31 const treePrefix = parentRootId
32 ? `${ancestorIsLast.map((last) => (last ? " " : "│ ")).join("")}${isLast ? "└─ " : "├─ "}`
33 : "";
34
35 items.push({ candidate, label: getLabel(candidate.rootId), treePrefix });
36
37 const children = childrenByRootId.get(candidate.rootId) ?? [];
38 for (let index = 0; index < children.length; index += 1) {
39 const child = children[index];
40 if (!child) continue;
41 visit(child, [...ancestorIsLast, isLast], index === children.length - 1);
42 }
43 };
44
45 const roots = childrenByRootId.get(undefined) ?? [];
46 for (let index = 0; index < roots.length; index += 1) {
47 const root = roots[index];
48 if (!root) continue;
49 visit(root, [], index === roots.length - 1);
50 }
51
52 return items;
53}
54
55export function formatPruneTreeItem(item: PruneTreeItem): string {
56 const label = item.label?.replace(/\s+/g, " ").trim();
57 const preview = label ? `[${label}] ${item.candidate.preview}` : item.candidate.preview;
58 return `${preview} · ${formatEntryCount(item.candidate.entryCount)}`;
59}
60
61export function formatEntryCount(count: number): string {
62 return `${count} ${count === 1 ? "entry" : "entries"}`;
63}
64
65function findParentCandidate(
66 candidate: BranchCandidate,
67 candidates: readonly BranchCandidate[],
68): BranchCandidate | undefined {
69 return candidates
70 .filter(
71 (possibleParent) =>
72 possibleParent.rootId !== candidate.rootId &&
73 possibleParent.entryIds.includes(candidate.forkPointId),
74 )
75 .sort((left, right) => left.entryCount - right.entryCount)[0];
76}