Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions web/src/lib/components/diff/ConciseDiffView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
type SearchSegment,
} from "$lib/components/diff/concise-diff-view.svelte";
import Spinner from "$lib/components/Spinner.svelte";
import { type StructuredPatch } from "diff";
import { onDestroy } from "svelte";
import { type MutableValue } from "$lib/util";
import { box } from "svelte-toolbelt";
Expand All @@ -33,7 +32,7 @@
cacheKey,
}: ConciseDiffViewProps<K> = $props();

const parsedPatch: Promise<StructuredPatch> = $derived.by(async () => {
const parsedPatch = $derived.by(() => {
if (rawPatchContent !== undefined) {
return parseSinglePatch(rawPatchContent);
} else if (patch !== undefined) {
Expand Down
17 changes: 15 additions & 2 deletions web/src/lib/components/diff/concise-diff-view.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,19 @@ async function makeHunk(
return { lines };
}

export function patchHeaderDiffOnly(patch: StructuredPatch): boolean {
if (patch.hunks.length === 0) {
return false;
}
let onlyHeaderChanges = true;
for (let j = 0; j < patch.hunks.length; j++) {
if (hasNonHeaderChanges(patch.hunks[j].lines)) {
onlyHeaderChanges = false;
}
}
return onlyHeaderChanges;
}

export function hasNonHeaderChanges(contentLines: string[]) {
for (const line of contentLines) {
if (lineHasNonHeaderChange(line)) {
Expand Down Expand Up @@ -1013,7 +1026,7 @@ export function parseSinglePatch(rawPatchContent: string): StructuredPatch {

export interface ConciseDiffViewProps<K> {
rawPatchContent?: string;
patch?: Promise<StructuredPatch>;
patch?: StructuredPatch;

syntaxHighlighting?: boolean;
syntaxHighlightingTheme?: BundledTheme;
Expand All @@ -1030,7 +1043,7 @@ export interface ConciseDiffViewProps<K> {
}

export type ConciseDiffViewStateProps<K> = ReadableBoxedValues<{
patch: Promise<StructuredPatch>;
patch: StructuredPatch;

syntaxHighlighting: boolean;
syntaxHighlightingTheme: BundledTheme | undefined;
Expand Down
67 changes: 16 additions & 51 deletions web/src/lib/diff-viewer-multi-file.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { fetchGithubCommitDiff, fetchGithubComparison, fetchGithubPRComparison, type FileStatus, getGithubToken, type GithubDiff } from "./github.svelte";
import { type StructuredPatch, parsePatch } from "diff";
import { type StructuredPatch } from "diff";
import {
ConciseDiffViewCachedState,
DEFAULT_THEME_DARK,
DEFAULT_THEME_LIGHT,
hasNonHeaderChanges,
isNoNewlineAtEofLine,
parseSinglePatch,
patchHeaderDiffOnly,
} from "$lib/components/diff/concise-diff-view.svelte";
import type { BundledTheme } from "shiki";
import { browser } from "$app/environment";
Expand Down Expand Up @@ -150,8 +150,8 @@ export type CommonFileDetails = {

export type TextFileDetails = CommonFileDetails & {
type: "text";
patchText: string;
structuredPatch: Promise<StructuredPatch>;
structuredPatch: StructuredPatch;
patchHeaderDiffOnly: boolean;
};

export type ImageFileDetails = CommonFileDetails & {
Expand All @@ -160,13 +160,14 @@ export type ImageFileDetails = CommonFileDetails & {
};

export function makeTextDetails(fromFile: string, toFile: string, status: FileStatus, patchText: string): TextFileDetails {
const patch = parseSinglePatch(patchText);
return {
type: "text",
fromFile,
toFile,
status,
patchText,
structuredPatch: (async () => parseSinglePatch(patchText))(),
structuredPatch: patch,
patchHeaderDiffOnly: patchHeaderDiffOnly(patch),
};
}

Expand Down Expand Up @@ -285,45 +286,6 @@ export function getFileStatusProps(status: FileStatus): FileStatusProps {
}
}

export function findHeaderChangeOnlyPatches(fileDetails: FileDetails[]) {
const patchStrings = fileDetails.map((details) => {
if (details.type === "text") {
return details.patchText;
}
return undefined;
});

const result: boolean[] = [];

for (let i = 0; i < patchStrings.length; i++) {
const patchString = patchStrings[i];
if (patchString === undefined || patchString.length === 0) {
result.push(false);
continue;
}
// TODO: Parsing twice is wasteful
const patches = parsePatch(patchString);
if (patches.length !== 1) {
result.push(false);
continue;
}
const patch = patches[0];
if (patch.hunks.length === 0) {
result.push(false);
continue;
}
let onlyHeaderChanges = true;
for (let j = 0; j < patch.hunks.length; j++) {
if (hasNonHeaderChanges(patch.hunks[j].lines)) {
onlyHeaderChanges = false;
}
}
result.push(onlyHeaderChanges);
}

return result;
}

export type ViewerStatistics = {
addedLines: number;
removedLines: number;
Expand Down Expand Up @@ -368,19 +330,22 @@ export class MultiFileDiffViewerState {

readonly fileTreeFilterDebounced = new Debounced(() => this.fileTreeFilter, 500);
readonly searchQueryDebounced = new Debounced(() => this.searchQuery, 500);
readonly stats: Promise<ViewerStatistics> = $derived(this.countStats());
readonly stats: ViewerStatistics = $derived(this.countStats());
readonly fileTreeRoots: TreeNode<FileTreeNodeData>[] = $derived(makeFileTree(this.fileDetails));
readonly filteredFileDetails: FileDetails[] = $derived(
this.fileTreeFilterDebounced.current ? this.fileDetails.filter((f) => this.filterFile(f)) : this.fileDetails,
);
readonly patchHeaderDiffOnly: boolean[] = $derived(findHeaderChangeOnlyPatches(this.fileDetails));
readonly searchResults: Promise<SearchResults> = $derived(this.findSearchResults());

private constructor() {
// Auto-check all patch header diff only diffs
$effect(() => {
for (let i = 0; i < this.patchHeaderDiffOnly.length; i++) {
if (this.patchHeaderDiffOnly[i] && this.checked[i] === undefined) {
for (let i = 0; i < this.fileDetails.length; i++) {
const details = this.fileDetails[i];
if (details.type !== "text") {
continue;
}
if (details.patchHeaderDiffOnly && this.checked[i] === undefined) {
this.checked[i] = true;
}
}
Expand Down Expand Up @@ -543,7 +508,7 @@ export class MultiFileDiffViewerState {
return false;
}

private async countStats(): Promise<ViewerStatistics> {
private countStats(): ViewerStatistics {
let addedLines = 0;
let removedLines = 0;
const fileAddedLines: number[] = [];
Expand All @@ -554,7 +519,7 @@ export class MultiFileDiffViewerState {
if (details.type !== "text") {
continue;
}
const diff = await details.structuredPatch;
const diff = details.structuredPatch;

for (let j = 0; j < diff.hunks.length; j++) {
const hunk = diff.hunks[j];
Expand Down
8 changes: 2 additions & 6 deletions web/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -283,11 +283,7 @@
</div>
<div class="flex flex-row items-center gap-2 px-3 pb-2">
<SidebarToggle class="data-[side=right]:order-10" />
{#await viewer.stats}
<DiffStats />
{:then stats}
<DiffStats add={stats.addedLines} remove={stats.removedLines} />
{/await}
<DiffStats add={viewer.stats.addedLines} remove={viewer.stats.removedLines} />
<DiffSearch />
</div>
<div class="flex flex-1 flex-col border-t">
Expand Down Expand Up @@ -325,7 +321,7 @@
{/if}
</div>
{/if}
{#if !viewer.collapsed[index] && value.type === "text" && (!viewer.patchHeaderDiffOnly[index] || !globalOptions.omitPatchHeaderOnlyHunks)}
{#if !viewer.collapsed[index] && value.type === "text" && (!value.patchHeaderDiffOnly || !globalOptions.omitPatchHeaderOnlyHunks)}
<div class="mb border-b">
<ConciseDiffView
patch={value.structuredPatch}
Expand Down
12 changes: 5 additions & 7 deletions web/src/routes/FileHeader.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
});
}
}

let patchHeaderDiffOnly = $derived(value.type === "text" && value.patchHeaderDiffOnly);
</script>

{#snippet fileName()}
Expand Down Expand Up @@ -97,19 +99,15 @@
onkeyup={(event) => event.key === "Enter" && viewer.scrollToFile(index, { autoExpand: false, smooth: true })}
>
{#if value.type === "text"}
{#await viewer.stats}
<DiffStats brief />
{:then stats}
<DiffStats brief add={stats.fileAddedLines[index]} remove={stats.fileRemovedLines[index]} />
{/await}
<DiffStats brief add={viewer.stats.fileAddedLines[index]} remove={viewer.stats.fileRemovedLines[index]} />
{/if}
{@render fileName()}
<div class="ms-0.5 ml-auto flex items-center gap-2">
{#if viewer.patchHeaderDiffOnly[index]}
{#if patchHeaderDiffOnly}
<span class="rounded-sm bg-neutral-3 px-1.5">Patch-header-only diff</span>
{/if}
{@render actionsPopover()}
{#if !viewer.patchHeaderDiffOnly[index] || !globalOptions.omitPatchHeaderOnlyHunks || value.type === "image"}
{#if !patchHeaderDiffOnly || !globalOptions.omitPatchHeaderOnlyHunks || value.type === "image"}
{@render collapseToggle()}
{/if}
</div>
Expand Down