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); + }); });