import { type View, type ViewContext } from "@berry/api/web.ts"; import { editing } from "./edit-view.tsx"; import { editable } from "./eligibility.ts"; import { fileKey, lineCounts } from "./model.ts"; import { type Layout, rendering } from "./render.tsx"; import type { FileDiff, Id, Reply, Revision, Shown } from "./types.ts"; const LAYOUT = "berry.diff.layout"; const VIEWED = "berry.diff.viewed"; /** How many files' viewed marks are remembered, the oldest forgotten first. */ const REMEMBERED = 5000; const PAGE = 100; // lockfiles and the like: shown folded, since nobody reads them line by line const GENERATED = /(?:^|\/)(?:[\w.-]+\.lock|package-lock\.json|pnpm-lock\.yaml|go\.sum)$/; // thousands of marks, which every file reads as it draws: parsed again only once another tab has changed them let parsed: { raw: string | null; marks: Record } | null = null; function viewedMarks(): Record { const raw = localStorage.getItem(VIEWED); if (parsed?.raw !== raw) { let marks = {}; try { marks = JSON.parse(raw ?? "{}"); } catch { // a mangled store forgets its marks } parsed = { raw, marks }; } return parsed!.marks; } function markViewed(key: string, viewed: boolean) { const marks = { ...viewedMarks() }; if (viewed) marks[key] = Date.now(); else delete marks[key]; const kept = Object.entries(marks).sort(([, a], [, b]) => b - a).slice(0, REMEMBERED); localStorage.setItem(VIEWED, JSON.stringify(Object.fromEntries(kept))); } const relative = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" }); const UNITS: [Intl.RelativeTimeFormatUnit, number][] = [ ["year", 31536000], ["month", 2592000], ["week", 604800], ["day", 86400], ["hour", 3600], ["minute", 60], ]; function ago(timestamp: string) { const seconds = (new Date(timestamp).getTime() - Date.now()) / 1000; for (const [unit, size] of UNITS) { if (Math.abs(seconds) >= size) return relative.format(Math.trunc(seconds / size), unit); } return relative.format(0, "second"); } const idOf = (id: Id, className: string, full = false) => ( {id.prefix ? {id.prefix} : []} {full ? id.full.slice(id.prefix.length) : id.rest} ); function ids(revision: Revision) { if (revision.commit === null) return [working tree]; return [ ...(revision.change ? [idOf(revision.change, "change")] : []), idOf(revision.commit, "commit"), ]; } const glyph = (revision: Revision) => revision.workingCopy ? "@" : revision.conflict ? "×" : revision.immutable ? "◆" : "○"; const summaryOf = (revision: Revision) => revision.description.split("\n")[0] || (revision.commit === null ? "uncommitted changes" : "(no description set)"); type FileView = { file: FileDiff; key: string; section: HTMLElement; /** Which folds are open, by their place among the file's pieces. */ unfolded: Set; draw(): void; dispose(): void; }; /** A revision's changes beside the history it is in: the working copy at first. */ export function diffView(): View { const log =