From 8b674348befbdc6a86346ca4d47bfb092c55f485 Mon Sep 17 00:00:00 2001 From: Guillaume Sabran Date: Mon, 23 Mar 2026 11:55:25 -0700 Subject: [PATCH 1/2] hide whitespace changes --- packages/diffs/src/components/FileDiff.ts | 8 +++++-- .../src/components/VirtualizedFileDiff.ts | 6 +---- packages/diffs/src/ssr/preloadDiffs.ts | 2 +- packages/diffs/src/types.ts | 18 +++++++++++++- packages/diffs/src/utils/areOptionsEqual.ts | 24 ++++++++++++++++--- packages/diffs/test/parseDiffFromFile.test.ts | 19 +++++++++++++++ 6 files changed, 65 insertions(+), 12 deletions(-) diff --git a/packages/diffs/src/components/FileDiff.ts b/packages/diffs/src/components/FileDiff.ts index 6e8b47b40..fddc3ca54 100644 --- a/packages/diffs/src/components/FileDiff.ts +++ b/packages/diffs/src/components/FileDiff.ts @@ -531,7 +531,7 @@ export class FileDiff { this.fileDiff = fileDiff ?? (oldFile != null && newFile != null - ? parseDiffFromFile(oldFile, newFile) + ? parseDiffFromFile(oldFile, newFile, this.options.diffOptions) : undefined); this.hunksRenderer.hydrate(this.fileDiff); @@ -649,7 +649,11 @@ export class FileDiff { this.fileDiff = fileDiff; } else if (oldFile != null && newFile != null && filesDidChange) { diffDidChange = true; - this.fileDiff = parseDiffFromFile(oldFile, newFile); + this.fileDiff = parseDiffFromFile( + oldFile, + newFile, + this.options.diffOptions + ); } if (lineAnnotations != null) { diff --git a/packages/diffs/src/components/VirtualizedFileDiff.ts b/packages/diffs/src/components/VirtualizedFileDiff.ts index e4046f7cb..db985275f 100644 --- a/packages/diffs/src/components/VirtualizedFileDiff.ts +++ b/packages/diffs/src/components/VirtualizedFileDiff.ts @@ -347,11 +347,7 @@ export class VirtualizedFileDiff< this.fileDiff ??= fileDiff ?? (oldFile != null && newFile != null - ? // NOTE(amadeus): We might be forcing ourselves to double up the - // computation of fileDiff (in the super.render() call), so we might want - // to figure out a way to avoid that. That also could be just as simple as - // passing through fileDiff though... so maybe we good? - parseDiffFromFile(oldFile, newFile) + ? parseDiffFromFile(oldFile, newFile, this.options.diffOptions) : undefined); fileContainer = this.getOrCreateFileContainer(fileContainer); diff --git a/packages/diffs/src/ssr/preloadDiffs.ts b/packages/diffs/src/ssr/preloadDiffs.ts index 5e4243e86..a353ccd45 100644 --- a/packages/diffs/src/ssr/preloadDiffs.ts +++ b/packages/diffs/src/ssr/preloadDiffs.ts @@ -36,7 +36,7 @@ export async function preloadDiffHTML({ annotations, }: PreloadDiffOptions): Promise { if (fileDiff == null && oldFile != null && newFile != null) { - fileDiff = parseDiffFromFile(oldFile, newFile); + fileDiff = parseDiffFromFile(oldFile, newFile, options?.diffOptions); } if (fileDiff == null) { throw new Error( diff --git a/packages/diffs/src/types.ts b/packages/diffs/src/types.ts index 1fa4406e6..96245ce00 100644 --- a/packages/diffs/src/types.ts +++ b/packages/diffs/src/types.ts @@ -1,3 +1,4 @@ +import type { CreatePatchOptionsNonabortable } from 'diff'; import type { ElementContent } from 'hast'; import type { BundledLanguage, @@ -393,10 +394,25 @@ export interface BaseDiffOptions extends BaseCodeOptions { // How many lines to expand per click expansionLineCount?: number; // 100 is default + + /** + * Options forwarded to the underlying diff algorithm when computing diffs + * from file contents (oldFile/newFile). Has no effect on pre-parsed patches. + * + * Supported options: + * - `ignoreWhitespace`: treat lines differing only in whitespace as unchanged + * - `stripTrailingCr`: strip `\r` before diffing (useful for UNIX vs Windows) + */ + diffOptions?: DiffComputeOptions; } +export type DiffComputeOptions = Pick< + CreatePatchOptionsNonabortable, + 'ignoreWhitespace' | 'stripTrailingCr' +>; + export type BaseDiffOptionsWithDefaults = Required< - Omit + Omit >; export type CustomPreProperties = Record; diff --git a/packages/diffs/src/utils/areOptionsEqual.ts b/packages/diffs/src/utils/areOptionsEqual.ts index 3a3df4984..c36f0be14 100644 --- a/packages/diffs/src/utils/areOptionsEqual.ts +++ b/packages/diffs/src/utils/areOptionsEqual.ts @@ -1,17 +1,35 @@ import type { FileDiffOptions } from '../components/FileDiff'; import { DEFAULT_THEMES } from '../constants'; import type { FileOptions } from '../react'; +import type { DiffComputeOptions } from '../types'; import { areObjectsEqual } from './areObjectsEqual'; import { areThemesEqual } from './areThemesEqual'; +type AnyOptions = FileOptions | FileDiffOptions | undefined; + export function areOptionsEqual( - optionsA: FileOptions | FileDiffOptions | undefined, - optionsB: FileOptions | FileDiffOptions | undefined + optionsA: AnyOptions, + optionsB: AnyOptions ): boolean { const themeA = optionsA?.theme ?? DEFAULT_THEMES; const themeB = optionsB?.theme ?? DEFAULT_THEMES; + const diffOptsA = getDiffOptions(optionsA); + const diffOptsB = getDiffOptions(optionsB); return ( areThemesEqual(themeA, themeB) && - areObjectsEqual(optionsA, optionsB, ['theme']) + areObjectsEqual(optionsA, optionsB, [ + 'theme', + 'diffOptions' as keyof typeof optionsA, + ]) && + areObjectsEqual(diffOptsA ?? {}, diffOptsB ?? {}) ); } + +function getDiffOptions( + options: AnyOptions +): DiffComputeOptions | undefined { + if (options != null && 'diffOptions' in options) { + return options.diffOptions; + } + return undefined; +} diff --git a/packages/diffs/test/parseDiffFromFile.test.ts b/packages/diffs/test/parseDiffFromFile.test.ts index c7d280bde..368c8f633 100644 --- a/packages/diffs/test/parseDiffFromFile.test.ts +++ b/packages/diffs/test/parseDiffFromFile.test.ts @@ -35,4 +35,23 @@ describe('parseDiffFromFile', () => { const expectedNewLineCount = fileNew.split(/(?<=\n)/).length; expect(result.additionLines.length).toBe(expectedNewLineCount); }); + + test('ignoreWhitespace hides leading/trailing whitespace changes', () => { + const oldFile = { + name: 'test.txt', + contents: 'hello world\nfoo bar\n', + }; + const newFile = { + name: 'test.txt', + contents: ' hello world\nfoo bar\n', + }; + + const withWhitespace = parseDiffFromFile(oldFile, newFile); + expect(withWhitespace.hunks.length).toBeGreaterThan(0); + + const withoutWhitespace = parseDiffFromFile(oldFile, newFile, { + ignoreWhitespace: true, + }); + expect(withoutWhitespace.hunks).toHaveLength(0); + }); }); From b1568e132b07178a0dbfa3fea249930850f021b2 Mon Sep 17 00:00:00 2001 From: Guillaume Sabran Date: Mon, 30 Mar 2026 11:12:13 -0700 Subject: [PATCH 2/2] Rename diffOptions to parseDiffOptions and use full CreatePatchOptionsNonabortable type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR review feedback: - Rename diffOptions → parseDiffOptions to align with parseDiffFromFile - Expose full CreatePatchOptionsNonabortable instead of a two-field subset - Remove unnecessary ?? {} coercion in areOptionsEqual - Pass parseDiffOptions through in FileDiff base class calls --- packages/diffs/src/components/FileDiff.ts | 8 ++++++-- .../src/components/VirtualizedFileDiff.ts | 2 +- packages/diffs/src/ssr/preloadDiffs.ts | 2 +- packages/diffs/src/types.ts | 16 +++++----------- packages/diffs/src/utils/areOptionsEqual.ts | 19 ++++++++++--------- 5 files changed, 23 insertions(+), 24 deletions(-) diff --git a/packages/diffs/src/components/FileDiff.ts b/packages/diffs/src/components/FileDiff.ts index 2cbc508bb..7f3a1a110 100644 --- a/packages/diffs/src/components/FileDiff.ts +++ b/packages/diffs/src/components/FileDiff.ts @@ -588,7 +588,7 @@ export class FileDiff { this.fileDiff = fileDiff ?? (oldFile != null && newFile != null - ? parseDiffFromFile(oldFile, newFile) + ? parseDiffFromFile(oldFile, newFile, this.options.parseDiffOptions) : undefined); if (this.pre == null) { @@ -706,7 +706,11 @@ export class FileDiff { this.fileDiff = fileDiff; } else if (oldFile != null && newFile != null && filesDidChange) { diffDidChange = true; - this.fileDiff = parseDiffFromFile(oldFile, newFile); + this.fileDiff = parseDiffFromFile( + oldFile, + newFile, + this.options.parseDiffOptions + ); } if (lineAnnotations != null) { diff --git a/packages/diffs/src/components/VirtualizedFileDiff.ts b/packages/diffs/src/components/VirtualizedFileDiff.ts index db985275f..0c98df628 100644 --- a/packages/diffs/src/components/VirtualizedFileDiff.ts +++ b/packages/diffs/src/components/VirtualizedFileDiff.ts @@ -347,7 +347,7 @@ export class VirtualizedFileDiff< this.fileDiff ??= fileDiff ?? (oldFile != null && newFile != null - ? parseDiffFromFile(oldFile, newFile, this.options.diffOptions) + ? parseDiffFromFile(oldFile, newFile, this.options.parseDiffOptions) : undefined); fileContainer = this.getOrCreateFileContainer(fileContainer); diff --git a/packages/diffs/src/ssr/preloadDiffs.ts b/packages/diffs/src/ssr/preloadDiffs.ts index 9d6d95e3e..4ee40b75b 100644 --- a/packages/diffs/src/ssr/preloadDiffs.ts +++ b/packages/diffs/src/ssr/preloadDiffs.ts @@ -40,7 +40,7 @@ export async function preloadDiffHTML({ annotations, }: PreloadDiffOptions): Promise { if (fileDiff == null && oldFile != null && newFile != null) { - fileDiff = parseDiffFromFile(oldFile, newFile, options?.diffOptions); + fileDiff = parseDiffFromFile(oldFile, newFile, options?.parseDiffOptions); } if (fileDiff == null) { throw new Error( diff --git a/packages/diffs/src/types.ts b/packages/diffs/src/types.ts index a357ef697..6fe6a809e 100644 --- a/packages/diffs/src/types.ts +++ b/packages/diffs/src/types.ts @@ -398,21 +398,15 @@ export interface BaseDiffOptions extends BaseCodeOptions { /** * Options forwarded to the underlying diff algorithm when computing diffs * from file contents (oldFile/newFile). Has no effect on pre-parsed patches. - * - * Supported options: - * - `ignoreWhitespace`: treat lines differing only in whitespace as unchanged - * - `stripTrailingCr`: strip `\r` before diffing (useful for UNIX vs Windows) */ - diffOptions?: DiffComputeOptions; + parseDiffOptions?: CreatePatchOptionsNonabortable; } -export type DiffComputeOptions = Pick< - CreatePatchOptionsNonabortable, - 'ignoreWhitespace' | 'stripTrailingCr' ->; - export type BaseDiffOptionsWithDefaults = Required< - Omit + Omit< + BaseDiffOptions, + 'unsafeCSS' | 'preferredHighlighter' | 'parseDiffOptions' + > >; export type CustomPreProperties = Record; diff --git a/packages/diffs/src/utils/areOptionsEqual.ts b/packages/diffs/src/utils/areOptionsEqual.ts index c36f0be14..edb0bf81c 100644 --- a/packages/diffs/src/utils/areOptionsEqual.ts +++ b/packages/diffs/src/utils/areOptionsEqual.ts @@ -1,7 +1,8 @@ +import type { CreatePatchOptionsNonabortable } from 'diff'; + import type { FileDiffOptions } from '../components/FileDiff'; import { DEFAULT_THEMES } from '../constants'; import type { FileOptions } from '../react'; -import type { DiffComputeOptions } from '../types'; import { areObjectsEqual } from './areObjectsEqual'; import { areThemesEqual } from './areThemesEqual'; @@ -13,23 +14,23 @@ export function areOptionsEqual( ): boolean { const themeA = optionsA?.theme ?? DEFAULT_THEMES; const themeB = optionsB?.theme ?? DEFAULT_THEMES; - const diffOptsA = getDiffOptions(optionsA); - const diffOptsB = getDiffOptions(optionsB); + const diffOptsA = getParseDiffOptions(optionsA); + const diffOptsB = getParseDiffOptions(optionsB); return ( areThemesEqual(themeA, themeB) && areObjectsEqual(optionsA, optionsB, [ 'theme', - 'diffOptions' as keyof typeof optionsA, + 'parseDiffOptions' as keyof typeof optionsA, ]) && - areObjectsEqual(diffOptsA ?? {}, diffOptsB ?? {}) + areObjectsEqual(diffOptsA, diffOptsB) ); } -function getDiffOptions( +function getParseDiffOptions( options: AnyOptions -): DiffComputeOptions | undefined { - if (options != null && 'diffOptions' in options) { - return options.diffOptions; +): CreatePatchOptionsNonabortable | undefined { + if (options != null && 'parseDiffOptions' in options) { + return options.parseDiffOptions; } return undefined; }