change to dark mode
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { ArrowRight, X } from "@lucide/svelte";
|
||||
import { ArrowRight, FileCode, X } from "@lucide/svelte";
|
||||
import type { GitCommitComparison, GitDiffFile, FileStatusKind } from "../types";
|
||||
|
||||
type DiffLineKind = "meta" | "hunk" | "add" | "del" | "context";
|
||||
type SplitRow =
|
||||
| { type: "span"; kind: "meta" | "hunk"; text: string }
|
||||
| {
|
||||
type: "pair";
|
||||
leftNum?: number; leftText?: string; leftKind: "del" | "context" | "empty";
|
||||
rightNum?: number; rightText?: string; rightKind: "add" | "context" | "empty";
|
||||
};
|
||||
|
||||
interface Props {
|
||||
comparison: GitCommitComparison;
|
||||
@@ -20,69 +26,115 @@
|
||||
onSelectFile = () => {},
|
||||
}: Props = $props();
|
||||
|
||||
function statusLabel(kind: FileStatusKind): string {
|
||||
return kind;
|
||||
}
|
||||
|
||||
function displayDiffFile(file: GitDiffFile): string {
|
||||
return file.old_path ? `${file.old_path} -> ${file.path}` : file.path;
|
||||
}
|
||||
|
||||
function diffLineKind(line: string): DiffLineKind {
|
||||
if (line.startsWith("diff ") || line.startsWith("index ") || line.startsWith("--- ") ||
|
||||
line.startsWith("+++ ") || line.startsWith("new file") || line.startsWith("deleted file") ||
|
||||
line.startsWith("rename ") || line.startsWith("similarity ")) return "meta";
|
||||
if (line.startsWith("@@")) return "hunk";
|
||||
if (line.startsWith("+")) return "add";
|
||||
if (line.startsWith("-")) return "del";
|
||||
return "context";
|
||||
}
|
||||
|
||||
function diffLines(patch: string): { kind: DiffLineKind; text: string }[] {
|
||||
return patch.replace(/\n$/, "").split("\n").map((text) => ({ kind: diffLineKind(text), text }));
|
||||
}
|
||||
|
||||
function stripDiffPathPrefix(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed === "/dev/null") return trimmed;
|
||||
if (trimmed.startsWith("a/") || trimmed.startsWith("b/")) return trimmed.slice(2);
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function patchFilePath(segment: string): string {
|
||||
let plusPath = "";
|
||||
let minusPath = "";
|
||||
for (const line of segment.split("\n")) {
|
||||
if (line.startsWith("+++ ")) plusPath = stripDiffPathPrefix(line.slice(4));
|
||||
else if (line.startsWith("--- ")) minusPath = stripDiffPathPrefix(line.slice(4));
|
||||
else if (line.startsWith("@@")) break;
|
||||
}
|
||||
return (plusPath && plusPath !== "/dev/null") ? plusPath : minusPath;
|
||||
return file.old_path ? `${file.old_path} → ${file.path}` : file.path;
|
||||
}
|
||||
|
||||
function buildDiffByPath(patch: string): Map<string, string> {
|
||||
const map = new Map<string, string>();
|
||||
if (!patch.trim()) return map;
|
||||
const segments: string[] = [];
|
||||
let current: string[] = [];
|
||||
for (const line of patch.split("\n")) {
|
||||
if (line.startsWith("diff --git ") && current.length > 0) {
|
||||
segments.push(current.join("\n"));
|
||||
const seg = current.join("\n");
|
||||
const p = segmentPath(seg);
|
||||
if (p) map.set(p, seg);
|
||||
current = [];
|
||||
}
|
||||
current.push(line);
|
||||
}
|
||||
if (current.length > 0) segments.push(current.join("\n"));
|
||||
for (const segment of segments) {
|
||||
const path = patchFilePath(segment);
|
||||
if (path) map.set(path, segment);
|
||||
if (current.length > 0) {
|
||||
const seg = current.join("\n");
|
||||
const p = segmentPath(seg);
|
||||
if (p) map.set(p, seg);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
let diffByPath = $derived(buildDiffByPath(comparison.patch));
|
||||
let selectedDiffFile = $derived(comparison.files.find((f) => f.path === selectedDiffPath) ?? null);
|
||||
let selectedDiffPatch = $derived(selectedDiffFile ? (diffByPath.get(selectedDiffFile.path) ?? "") : "");
|
||||
function segmentPath(seg: string): string {
|
||||
let plus = "";
|
||||
let minus = "";
|
||||
for (const line of seg.split("\n")) {
|
||||
if (line.startsWith("+++ ")) {
|
||||
const r = line.slice(4).trim();
|
||||
plus = r.startsWith("b/") ? r.slice(2) : r;
|
||||
} else if (line.startsWith("--- ")) {
|
||||
const r = line.slice(4).trim();
|
||||
minus = r.startsWith("a/") ? r.slice(2) : r;
|
||||
} else if (line.startsWith("@@")) break;
|
||||
}
|
||||
return (plus && plus !== "/dev/null") ? plus : minus;
|
||||
}
|
||||
|
||||
function buildSplitRows(patch: string): SplitRow[] {
|
||||
if (!patch.trim()) return [];
|
||||
const lines = patch.replace(/\n$/, "").split("\n");
|
||||
const rows: SplitRow[] = [];
|
||||
let leftNum = 0;
|
||||
let rightNum = 0;
|
||||
const dels: string[] = [];
|
||||
const adds: string[] = [];
|
||||
|
||||
function flush() {
|
||||
if (dels.length === 0 && adds.length === 0) return;
|
||||
const count = Math.max(dels.length, adds.length);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const hasDel = i < dels.length;
|
||||
const hasAdd = i < adds.length;
|
||||
if (hasDel) leftNum++;
|
||||
if (hasAdd) rightNum++;
|
||||
rows.push({
|
||||
type: "pair",
|
||||
leftNum: hasDel ? leftNum : undefined,
|
||||
leftText: hasDel ? dels[i].slice(1) : undefined,
|
||||
leftKind: hasDel ? "del" : "empty",
|
||||
rightNum: hasAdd ? rightNum : undefined,
|
||||
rightText: hasAdd ? adds[i].slice(1) : undefined,
|
||||
rightKind: hasAdd ? "add" : "empty",
|
||||
});
|
||||
}
|
||||
dels.length = 0;
|
||||
adds.length = 0;
|
||||
}
|
||||
|
||||
for (const line of lines) {
|
||||
const isMeta =
|
||||
line.startsWith("diff ") || line.startsWith("index ") ||
|
||||
line.startsWith("--- ") || line.startsWith("+++ ") ||
|
||||
line.startsWith("new file") || line.startsWith("deleted file") ||
|
||||
line.startsWith("rename ") || line.startsWith("similarity ");
|
||||
|
||||
if (isMeta) {
|
||||
flush();
|
||||
rows.push({ type: "span", kind: "meta", text: line });
|
||||
} else if (line.startsWith("@@")) {
|
||||
flush();
|
||||
const m = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
||||
if (m) { leftNum = parseInt(m[1]) - 1; rightNum = parseInt(m[2]) - 1; }
|
||||
rows.push({ type: "span", kind: "hunk", text: line });
|
||||
} else if (line.startsWith("-")) {
|
||||
dels.push(line);
|
||||
} else if (line.startsWith("+")) {
|
||||
adds.push(line);
|
||||
} else {
|
||||
flush();
|
||||
leftNum++;
|
||||
rightNum++;
|
||||
rows.push({
|
||||
type: "pair",
|
||||
leftNum, leftText: line.slice(1), leftKind: "context",
|
||||
rightNum, rightText: line.slice(1), rightKind: "context",
|
||||
});
|
||||
}
|
||||
}
|
||||
flush();
|
||||
return rows;
|
||||
}
|
||||
|
||||
let diffByPath = $derived(buildDiffByPath(comparison.patch));
|
||||
let selectedFile = $derived(comparison.files.find((f) => f.path === selectedDiffPath) ?? null);
|
||||
let selectedPatch = $derived(selectedFile ? (diffByPath.get(selectedFile.path) ?? "") : "");
|
||||
let splitRows = $derived(buildSplitRows(selectedPatch));
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -91,12 +143,13 @@
|
||||
onclick={(e) => { if (e.target === e.currentTarget) onClose(); }}
|
||||
>
|
||||
<div class="dialog" role="dialog" aria-modal="true" aria-label="Commit comparison" tabindex="-1">
|
||||
|
||||
<header class="dialog-header">
|
||||
<div>
|
||||
<span class="eyebrow">Compare</span>
|
||||
<h2 class="dialog-range">
|
||||
<span class="hash">{comparison.from_short}</span>
|
||||
<ArrowRight size={16} aria-hidden="true" />
|
||||
<ArrowRight size={14} aria-hidden="true" />
|
||||
<span class="hash">{comparison.to_short}</span>
|
||||
</h2>
|
||||
</div>
|
||||
@@ -106,9 +159,11 @@
|
||||
</header>
|
||||
|
||||
{#if comparison.files.length === 0}
|
||||
<div class="blank-state">No differences to show — these versions are identical.</div>
|
||||
<div class="blank-state">No differences — these versions are identical.</div>
|
||||
{:else}
|
||||
<div class="dialog-body">
|
||||
|
||||
<!-- File list -->
|
||||
<aside class="dialog-files" aria-label="Changed files">
|
||||
{#each comparison.files as file (`${file.old_path ?? ""}:${file.path}`)}
|
||||
<button
|
||||
@@ -118,7 +173,7 @@
|
||||
onclick={() => onSelectFile(file)}
|
||||
title={displayDiffFile(file)}
|
||||
>
|
||||
<span class={`status-badge ${file.status}`}>{statusLabel(file.status)}</span>
|
||||
<span class={`status-badge ${file.status}`}>{file.status}</span>
|
||||
<strong>{displayDiffFile(file)}</strong>
|
||||
<span class="diff-counts">
|
||||
<span class="adds">+{file.additions}</span>
|
||||
@@ -128,15 +183,52 @@
|
||||
{/each}
|
||||
</aside>
|
||||
|
||||
<div class="dialog-diff" aria-label="File diff">
|
||||
{#if !selectedDiffFile}
|
||||
<!-- Diff pane -->
|
||||
<div class="dialog-diff">
|
||||
{#if !selectedFile}
|
||||
<div class="blank-state">Select a file to see its changes.</div>
|
||||
{:else if selectedDiffPatch.trim().length === 0}
|
||||
{:else if splitRows.length === 0}
|
||||
<div class="blank-state">No textual changes for this file.</div>
|
||||
{:else}
|
||||
<pre class="diff-view" aria-label="Unified diff">{#each diffLines(selectedDiffPatch) as line}<span class={`diff-line ${line.kind}`}>{line.text || " "}</span>{/each}</pre>
|
||||
<!-- Path bar -->
|
||||
<div class="diff-header">
|
||||
<FileCode size={13} aria-hidden="true" />
|
||||
<span>{displayDiffFile(selectedFile)}</span>
|
||||
<span class="diff-counts" style="margin-left: auto; flex-shrink: 0;">
|
||||
<span class="adds">+{selectedFile.additions}</span>
|
||||
<span class="dels">-{selectedFile.deletions}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Column headers -->
|
||||
<div class="split-col-headers">
|
||||
<div class="split-col-label">
|
||||
<span>Before</span>
|
||||
<span class="split-col-hash">{comparison.from_short}</span>
|
||||
</div>
|
||||
<div class="split-col-label">
|
||||
<span>After</span>
|
||||
<span class="split-col-hash">{comparison.to_short}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Split diff grid -->
|
||||
<div class="split-diff" role="table" aria-label="Side-by-side diff">
|
||||
{#each splitRows as row, i (i)}
|
||||
{#if row.type === "span"}
|
||||
<div class="split-span split-{row.kind}">{row.text}</div>
|
||||
{:else}
|
||||
<div class="split-num" class:del={row.leftKind === "del"} class:empty={row.leftKind === "empty"}>{row.leftNum ?? ""}</div>
|
||||
<div class="split-cell" class:del={row.leftKind === "del"} class:empty={row.leftKind === "empty"}>{row.leftText ?? " "}</div>
|
||||
<div class="split-divider"></div>
|
||||
<div class="split-num" class:add={row.rightKind === "add"} class:empty={row.rightKind === "empty"}>{row.rightNum ?? ""}</div>
|
||||
<div class="split-cell" class:add={row.rightKind === "add"} class:empty={row.rightKind === "empty"}>{row.rightText ?? " "}</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user