diff --git a/pyproject.toml b/pyproject.toml
index aa541c3..e4b56ef 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "webdiff"
-version = "1.3.0b1"
+version = "1.3.0b2"
description = "Two-column web-based git difftool"
license = "Apache-2.0"
readme = "README.md"
diff --git a/testdata/jsondiffs/left/ospd5.summary.json b/testdata/jsondiffs/left/ospd5.summary.json
new file mode 100644
index 0000000..faca24c
--- /dev/null
+++ b/testdata/jsondiffs/left/ospd5.summary.json
@@ -0,0 +1 @@
+{"id":8503055,"num_reps":5505497728300038000000,"elapsed_s":27775981.582439058,"failures":["sredetiltnapsres","stngeiaedrlpsdes","gresenapdtilsres","dresenalstipares","gresenalstipares","gresenalstipores","dresenilstapares","dresenilstapores","dresenilstapures","gressnaletipdres","gntseaieslrdopes","sttseiaenrlpsges","plsdeaiertnrsges","plsteaiertnrsges","psldeaiertnrsges","spldeaiertnrsges","spldeiaetrntsges","splseaiertnrsdes","splseaiertnrsges","slpseaierntrsdes","slpseaierntrsges","slpseaiertnrsges","slpseiaerntrsdes","dresenapstilares","gresenapstilares","gresenapstilores","dplcseainrtngies","dresenilgtapsres","stndeiaenrlpsges","stngeiaenrlpsges","gsedtnileratspes","slpseaierntrgdes","gntseaieplrdseas","gntseaieplrdsees","plcdeaiertnrsges","dplseaiertnrsges","mplseaiertnrsges","sresetipdnalsges","plcseaiertnrsges","dlpseaiertnrsges","gredenalstipares","gredenalstipores","hsebetalsripgnes","hsedetalsripgnes","tsebetalsripgnes","tsedetalsripgnes"],"elim_level":[0,15455,177058,5063716,76688548,461765945,746413471,1341313341,1163044169,683351596,332798842,146849412,75414967,51931121],"secs_by_level":[3674055.3433500067,410511.63218999945,1173934.825350049,3012839.132659852,5235705.436329976,4241101.513749921,4354658.159489981,2102788.1823800365,669439.2463199788,109737.61249999885,25531.954199997952,4647.2012399998175,1373.4651100000929],"bounds":[17529004948,13770052786,11002135202,8716185865,6917888192,5476876187,4192563351,3051811248,2128082619,1431049972,956438815,668040946,500412197],"depth":[0,19950,615593,14344534,148852615,670318189,1798495841,2584907222,2213849795,1122244756,511943456,205264471,96606475,62028585],"boards_to_test":16527857205,"init_nodes":21169090330328,"total_nodes":216126848966789,"tree_bytes":426388045692928,"total_bytes":574688434585600,"n_bound":9429491482,"n_force":2101423756,"max_multi":1312378509,"bound_secs":[0,26.31909999999997,762.3841000000707,13895.171989995355,96300.35142999486,278184.6050899924,509398.4086700136,489545.5812600387,261858.04687000782,96741.63683000114,42098.825499994186,16502.548799997923,8129.411579999221,6838.873439999473],"test_secs":859209.9650399276,"best_board":[3827,"splseaiertnrsges"]}
diff --git a/testdata/jsondiffs/right/ospd5.summary.json b/testdata/jsondiffs/right/ospd5.summary.json
new file mode 100644
index 0000000..c6d6c96
--- /dev/null
+++ b/testdata/jsondiffs/right/ospd5.summary.json
@@ -0,0 +1,144 @@
+{
+ "id": 8503055,
+ "num_reps": 5505497728300038000000,
+ "elapsed_s": 27775981.582439058,
+ "failures": [
+ "sredetiltnapsres",
+ "stngeiaedrlpsdes",
+ "gresenapdtilsres",
+ "dresenalstipares",
+ "gresenalstipares",
+ "gresenalstipores",
+ "dresenilstapares",
+ "dresenilstapores",
+ "dresenilstapures",
+ "gressnaletipdres",
+ "gntseaieslrdopes",
+ "sttseiaenrlpsges",
+ "plsdeaiertnrsges",
+ "plsteaiertnrsges",
+ "psldeaiertnrsges",
+ "spldeaiertnrsges",
+ "spldeiaetrntsges",
+ "splseaiertnrsdes",
+ "splseaiertnrsges",
+ "slpseaierntrsdes",
+ "slpseaierntrsges",
+ "slpseaiertnrsges",
+ "slpseiaerntrsdes",
+ "dresenapstilares",
+ "gresenapstilares",
+ "gresenapstilores",
+ "dplcseainrtngies",
+ "dresenilgtapsres",
+ "stndeiaenrlpsges",
+ "stngeiaenrlpsges",
+ "gsedtnileratspes",
+ "slpseaierntrgdes",
+ "gntseaieplrdseas",
+ "gntseaieplrdsees",
+ "plcdeaiertnrsges",
+ "dplseaiertnrsges",
+ "mplseaiertnrsges",
+ "sresetipdnalsges",
+ "plcseaiertnrsges",
+ "dlpseaiertnrsges",
+ "gredenalstipares",
+ "gredenalstipores",
+ "hsebetalsripgnes",
+ "hsedetalsripgnes",
+ "tsebetalsripgnes",
+ "tsedetalsripgnes"
+ ],
+ "elim_level": [
+ 0,
+ 15455,
+ 177058,
+ 5063716,
+ 76688548,
+ 461765945,
+ 746413471,
+ 1341313341,
+ 1163044169,
+ 683351596,
+ 332798842,
+ 146849412,
+ 75414967,
+ 51931121
+ ],
+ "secs_by_level": [
+ 3674055.3433500067,
+ 410511.63218999945,
+ 1173934.825350049,
+ 3012839.132659852,
+ 5235705.436329976,
+ 4241101.513749921,
+ 4354658.159489981,
+ 2102788.1823800365,
+ 669439.2463199788,
+ 109737.61249999885,
+ 25531.954199997952,
+ 4647.2012399998175,
+ 1373.4651100000929
+ ],
+ "bounds": [
+ 17529004948,
+ 13770052786,
+ 11002135202,
+ 8716185865,
+ 6917888192,
+ 5476876187,
+ 4192563351,
+ 3051811248,
+ 2128082619,
+ 1431049972,
+ 956438815,
+ 668040946,
+ 500412197
+ ],
+ "depth": [
+ 0,
+ 19950,
+ 615593,
+ 14344534,
+ 148852615,
+ 670318189,
+ 1798495841,
+ 2584907222,
+ 2213849795,
+ 1122244756,
+ 511943456,
+ 205264471,
+ 96606475,
+ 62028585
+ ],
+ "boards_to_test": 16527857205,
+ "init_nodes": 21169090330328,
+ "total_nodes": 216126848966789,
+ "tree_bytes": 426388045692928,
+ "total_bytes": 574688434585600,
+ "n_bound": 9429491482,
+ "n_force": 2101423756,
+ "max_multi": 1312378509,
+ "bound_secs": [
+ 0,
+ 26.31909999999997,
+ 762.3841000000707,
+ 13895.171989995355,
+ 96300.35142999486,
+ 278184.6050899924,
+ 509398.4086700136,
+ 489545.5812600387,
+ 261858.04687000782,
+ 96741.63683000114,
+ 42098.825499994186,
+ 16502.548799997923,
+ 8129.411579999221,
+ 6838.873439999473
+ ],
+ "test_secs": 859209.9650399276,
+ "best_board": [
+ 3827,
+ "splseaiertnrsges"
+ ]
+}
diff --git a/testdata/jsondiffs/right/tsconfig.json b/testdata/jsondiffs/right/tsconfig.json
index 4971d4b..79a6927 100644
--- a/testdata/jsondiffs/right/tsconfig.json
+++ b/testdata/jsondiffs/right/tsconfig.json
@@ -1,6 +1,6 @@
{
+ /* Note that this file isn't really JSON! It's JSONC. */
"compilerOptions": {
- /* Note that this file isn't really JSON! */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
@@ -8,7 +8,7 @@
"module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
/* Specify library files to be included in the compilation. */
"lib": [
- "es2021",
+ "es2021",
"dom"
] ,
// "allowJs": true, /* Allow javascript files to be compiled. */
diff --git a/ts/CodeDiffContainer.tsx b/ts/CodeDiffContainer.tsx
index 7aa8ac1..05d93ce 100644
--- a/ts/CodeDiffContainer.tsx
+++ b/ts/CodeDiffContainer.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import {DiffOptions, encodeDiffOptions} from './diff-options';
+import {GitDiffOptions, gitDiffOptionsToFlags} from './diff-options';
import {CodeDiff, PatchOptions} from './codediff/codediff';
import {guessLanguageUsingContents, guessLanguageUsingFileName} from './codediff/language';
import {GitConfig} from './options';
@@ -53,18 +53,19 @@ export interface ImageDiffData {
}
// A "no changes" sign which only appears when applicable.
-export function NoChanges(props: {filePair: FilePair}) {
- const {filePair} = props;
+export function NoChanges(props: {filePair: FilePair; isEqualAfterNormalization: boolean}) {
+ const {filePair, isEqualAfterNormalization} = props;
+ let msg = null;
if (filePair.no_changes) {
- return
(File content is identical)
;
+ msg = <>(File content is identical)>;
+ } else if (isEqualAfterNormalization) {
+ msg = <>(File content is identical after normalization)>;
} else if (filePair.is_image_diff && filePair.are_same_pixels) {
- return (
-
- Pixels are the same, though file content differs (perhaps the headers are different?)
-
+ msg = (
+ <>Pixels are the same, though file content differs (perhaps the headers are different?)>
);
}
- return null;
+ return msg ? {msg}
: null;
}
// Either side can be empty (i.e. an add or a delete), in which case getOrNull resolves to null.
@@ -84,7 +85,7 @@ async function getOrNull(side: string, path: string, normalizeJSON: boolean) {
export interface CodeDiffContainerProps {
filePair: FilePair;
- diffOptions: Partial;
+ diffOptions: Partial;
normalizeJSON: boolean;
}
@@ -96,6 +97,8 @@ export function CodeDiffContainer(props: CodeDiffContainerProps) {
>();
React.useEffect(() => {
+ // It would be more correct to set contents=undefined here to get a loading state,
+ // but this produces an unnecessary flash for rapid transitions.
const getDiff = async () => {
const response = await fetch(`/diff/${filePair.idx}`, {
method: 'POST',
@@ -104,7 +107,7 @@ export function CodeDiffContainer(props: CodeDiffContainerProps) {
'Content-Type': 'application/json',
},
body: JSON.stringify({
- options: encodeDiffOptions(diffOptions),
+ options: gitDiffOptionsToFlags(diffOptions),
normalize_json: normalizeJSON,
}),
});
@@ -127,6 +130,10 @@ export function CodeDiffContainer(props: CodeDiffContainerProps) {
});
}, [filePair, diffOptions, normalizeJSON]);
+ const isEqualAfterNormalization = React.useMemo(() => {
+ return !filePair.no_changes && normalizeJSON && contents && contents.before == contents.after;
+ }, [contents, filePair.no_changes, normalizeJSON]);
+
return (
@@ -136,6 +143,7 @@ export function CodeDiffContainer(props: CodeDiffContainerProps) {
contentsBefore={contents.before}
contentsAfter={contents.after}
diffOps={contents.diffOps}
+ isEqualAfterNormalization={!!isEqualAfterNormalization}
/>
) : (
'Loading…'
@@ -150,6 +158,7 @@ interface FileDiffProps {
contentsBefore: string | null;
contentsAfter: string | null;
diffOps: DiffRange[];
+ isEqualAfterNormalization: boolean;
}
function extractFilename(path: string) {
@@ -164,7 +173,7 @@ function lengthOrZero(data: unknown[] | string | null | undefined) {
}
function FileDiff(props: FileDiffProps) {
- const {filePair, contentsBefore, contentsAfter, diffOps} = props;
+ const {filePair, contentsBefore, contentsAfter, diffOps, isEqualAfterNormalization} = props;
const pathBefore = filePair.a;
const pathAfter = filePair.b;
// build the diff view and add it to the current DOM
@@ -201,7 +210,7 @@ function FileDiff(props: FileDiffProps) {
return (
-
+
;
- setOptions: (newOptions: Partial) => void;
+ options: Partial;
+ updateOptions: UpdateOptionsFn;
defaultMaxDiffWidth: number;
- maxDiffWidth: number;
- setMaxDiffWidth: (maxDiffWidth: number) => void;
isVisible: boolean;
setIsVisible: (isVisible: boolean) => void;
}
@@ -51,47 +51,44 @@ const popupStyle: React.CSSProperties = {
fontFamily: 'sans-serif',
};
+type BooleanOptions = Extract, {v: boolean}>['k'];
+
export function DiffOptionsControl(props: Props) {
- const {options, setOptions, isVisible, setIsVisible, maxDiffWidth, setMaxDiffWidth} = props;
+ const {options, updateOptions, isVisible, setIsVisible} = props;
+ const maxDiffWidth = options.maxDiffWidth ?? props.defaultMaxDiffWidth;
const togglePopup = () => {
setIsVisible(!isVisible);
};
- const toggleIgnoreAllSpace = () => {
- setOptions({...options, ignoreAllSpace: !options.ignoreAllSpace});
- };
- const toggleIgnoreSpaceChange = () => {
- setOptions({...options, ignoreSpaceChange: !options.ignoreSpaceChange});
- };
- const toggleFunctionContext = () => {
- setOptions({...options, functionContext: !options.functionContext});
+ const toggleField = (k: BooleanOptions) => () => {
+ updateOptions(options => ({[k]: !options[k]}));
};
const setUnifiedContext: React.ChangeEventHandler = e => {
- setOptions({...options, unified: e.currentTarget.valueAsNumber});
+ updateOptions({unified: e.currentTarget.valueAsNumber});
};
const changeDiffAlgorithm: React.ChangeEventHandler = e => {
- setOptions({...options, diffAlgorithm: e.currentTarget.value as DiffAlgorithm});
+ updateOptions({diffAlgorithm: e.currentTarget.value as DiffAlgorithm});
};
const changeMaxDiffWidth: React.ChangeEventHandler = e => {
- setMaxDiffWidth(e.currentTarget.valueAsNumber);
+ updateOptions({maxDiffWidth: e.currentTarget.valueAsNumber});
};
React.useEffect(() => {
const handleKeydown = (e: KeyboardEvent) => {
if (!isLegitKeypress(e)) return;
if (e.code == 'KeyW') {
- setOptions({...options, ignoreAllSpace: !options.ignoreAllSpace});
+ updateOptions(options => ({ignoreAllSpace: !options.ignoreAllSpace}));
} else if (e.code == 'KeyB') {
- setOptions({...options, ignoreSpaceChange: !options.ignoreSpaceChange});
+ updateOptions(options => ({ignoreSpaceChange: !options.ignoreSpaceChange}));
}
};
document.addEventListener('keydown', handleKeydown);
return () => {
document.removeEventListener('keydown', handleKeydown);
};
- }, [options, setOptions]);
+ }, [options, updateOptions]);
- const diffOptsStr = encodeDiffOptions(options).join(' ');
+ const diffOptsStr = gitDiffOptionsToFlags(options).join(' ');
return (
<>
@@ -108,7 +105,7 @@ export function DiffOptionsControl(props: Props) {
- |
+ |
Whitespace:
|
@@ -116,7 +113,7 @@ export function DiffOptionsControl(props: Props) {
type="checkbox"
checked={!!options.ignoreAllSpace}
id="ignore-all-space"
- onChange={toggleIgnoreAllSpace}
+ onChange={toggleField('ignoreAllSpace')}
/>{' '}
|
+
+ |
+ {' '}
+ Normalize JSON
+ |
+
Context:
@@ -157,7 +165,7 @@ export function DiffOptionsControl(props: Props) {
type="checkbox"
checked={!!options.functionContext}
id="function-context"
- onChange={toggleFunctionContext}
+ onChange={toggleField('functionContext')}
/>{' '}
Function Context (git diff -W)
diff --git a/ts/DiffView.tsx b/ts/DiffView.tsx
index 926add1..91fc403 100644
--- a/ts/DiffView.tsx
+++ b/ts/DiffView.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import {CodeDiffContainer, FilePair} from './CodeDiffContainer';
-import {DiffOptions} from './diff-options';
+import {GitDiffOptions} from './diff-options';
import {getThickDiff} from './file_diff';
import {ImageDiff} from './ImageDiff';
import {ImageDiffMode} from './ImageDiffModeSelector';
@@ -11,11 +11,11 @@ export interface Props {
thinFilePair: FilePair;
imageDiffMode: ImageDiffMode;
pdiffMode: PerceptualDiffMode;
- diffOptions: Partial;
+ diffOptions: Partial;
normalizeJSON: boolean;
changeImageDiffMode: (mode: ImageDiffMode) => void;
changePDiffMode: React.Dispatch>;
- changeDiffOptions: (options: Partial) => void;
+ changeDiffOptions: (options: Partial) => void;
}
export function DiffView(props: Props) {
diff --git a/ts/ImageDiff.tsx b/ts/ImageDiff.tsx
index 639a1df..91e3551 100644
--- a/ts/ImageDiff.tsx
+++ b/ts/ImageDiff.tsx
@@ -119,7 +119,7 @@ export function ImageDiff(props: Props) {
return (
-
diff --git a/ts/Root.tsx b/ts/Root.tsx
index ce05f34..5e5ed6e 100644
--- a/ts/Root.tsx
+++ b/ts/Root.tsx
@@ -2,7 +2,6 @@ import React from 'react';
import {useNavigate, useParams} from 'react-router';
import {useSearchParams} from 'react-router-dom';
import {FilePair} from './CodeDiffContainer';
-import {decodeDiffOptions, DiffOptions, encodeDiffOptions} from './diff-options';
import {DiffView, PerceptualDiffMode} from './DiffView';
import {FileSelector, FileSelectorMode} from './FileSelector';
import {isLegitKeypress} from './file_diff';
@@ -10,44 +9,13 @@ import {ImageDiffMode} from './ImageDiffModeSelector';
import {filePairDisplayName} from './utils';
import {DiffOptionsControl} from './DiffOptions';
import {KeyboardShortcuts} from './codediff/KeyboardShortcuts';
-import {GitConfig} from './options';
+import {Options, encodeOptions, GitConfig, parseOptions, UpdateOptionsFn} from './options';
import {NormalizeJSONOption} from './codediff/NormalizeJSONOption';
declare const pairs: FilePair[];
declare const initialIdx: number;
declare const GIT_CONFIG: GitConfig;
-interface CombinedOptions extends DiffOptions {
- maxDiffWidth: number;
- normalizeJSON?: boolean;
-}
-
-function parseOptions(query: URLSearchParams): Partial {
- const flags = query.getAll('flag');
- const diffOptions = decodeDiffOptions(flags);
- const maxWidthStr = query.get('width');
- const maxDiffWidth = maxWidthStr ? {maxDiffWidth: Number(maxWidthStr)} : undefined;
- const normalizeJsonStr = query.get('normalize_json');
- const normalizeJSON = normalizeJsonStr ? {normalizeJSON: true} : undefined;
- return {...diffOptions, ...maxDiffWidth, ...normalizeJSON};
-}
-
-function encodeOptions(
- diffOptions: Partial,
- maxDiffWidth: number,
- normalizeJSON: boolean,
-) {
- const flags = encodeDiffOptions(diffOptions);
- const params = new URLSearchParams(flags.map(f => ['flag', f]));
- if (maxDiffWidth !== GIT_CONFIG.webdiff.maxDiffWidth) {
- params.set('width', String(maxDiffWidth));
- }
- if (normalizeJSON) {
- params.set('normalize_json', '1');
- }
- return params;
-}
-
// Webdiff application root.
export function Root() {
const [pdiffMode, setPDiffMode] = React.useState('off');
@@ -81,28 +49,22 @@ export function Root() {
}, [filePair]);
const options = React.useMemo(() => parseOptions(searchParams), [searchParams]);
+ // TODO: merge defaults into options
const maxDiffWidth = options.maxDiffWidth ?? GIT_CONFIG.webdiff.maxDiffWidth;
const normalizeJSON = !!options.normalizeJSON;
const setDiffOptions = React.useCallback(
- (newOptions: Partial) => {
- setSearchParams(encodeOptions(newOptions, maxDiffWidth, normalizeJSON));
- },
- [maxDiffWidth, setSearchParams, normalizeJSON],
- );
-
- const setMaxDiffWidth = React.useCallback(
- (newMaxWidth: number) => {
- setSearchParams(encodeOptions(options, newMaxWidth, normalizeJSON));
+ (newOptions: Partial) => {
+ setSearchParams(encodeOptions(newOptions));
},
- [options, setSearchParams, normalizeJSON],
+ [setSearchParams],
);
- const setNormalizeJSON = React.useCallback(
- (newNormalizeJSON: boolean) => {
- setSearchParams(encodeOptions(options, maxDiffWidth, newNormalizeJSON));
+ const updateOptions = React.useCallback(
+ update => {
+ setDiffOptions({...options, ...(typeof update === 'function' ? update(options) : update)});
},
- [options, setSearchParams, maxDiffWidth],
+ [options, setDiffOptions],
);
// TODO: switch to useKey() or some such
@@ -125,13 +87,15 @@ export function Root() {
setShowKeyboardHelp(false);
} else if (e.code === 'Period') {
setShowOptions(val => !val);
+ } else if (e.code === 'KeyZ') {
+ updateOptions(o => ({normalizeJSON: !o.normalizeJSON}));
}
};
document.addEventListener('keydown', handleKeydown);
return () => {
document.removeEventListener('keydown', handleKeydown);
};
- }, [idx, selectIndex]);
+ }, [idx, selectIndex, updateOptions]);
const inlineStyle = `
td.code {
@@ -144,9 +108,7 @@ export function Root() {
{
+ updateOptions({normalizeJSON: v});
+ }}
filePair={filePair}
/>
{showKeyboardHelp ? (
diff --git a/ts/__tests__/diff-options.test.ts b/ts/__tests__/diff-options.test.ts
index 33f8721..cd262f3 100644
--- a/ts/__tests__/diff-options.test.ts
+++ b/ts/__tests__/diff-options.test.ts
@@ -1,20 +1,22 @@
-import {decodeDiffOptions, encodeDiffOptions} from '../diff-options';
+import {flagsToGitDiffOptions, gitDiffOptionsToFlags} from '../diff-options';
describe('encodeDiffOptions', () => {
it('should encode no flags', () => {
- expect(encodeDiffOptions({})).toEqual([]);
+ expect(gitDiffOptionsToFlags({})).toEqual([]);
});
it('should encode basic flags', () => {
- expect(encodeDiffOptions({diffAlgorithm: 'patience'})).toEqual(['--diff-algorithm=patience']);
- expect(encodeDiffOptions({ignoreAllSpace: true})).toEqual(['-w']);
- expect(encodeDiffOptions({ignoreSpaceChange: true})).toEqual(['-b']);
- expect(encodeDiffOptions({unified: 16})).toEqual(['-U16']);
- expect(encodeDiffOptions({functionContext: true})).toEqual(['-W']);
+ expect(gitDiffOptionsToFlags({diffAlgorithm: 'patience'})).toEqual([
+ '--diff-algorithm=patience',
+ ]);
+ expect(gitDiffOptionsToFlags({ignoreAllSpace: true})).toEqual(['-w']);
+ expect(gitDiffOptionsToFlags({ignoreSpaceChange: true})).toEqual(['-b']);
+ expect(gitDiffOptionsToFlags({unified: 16})).toEqual(['-U16']);
+ expect(gitDiffOptionsToFlags({functionContext: true})).toEqual(['-W']);
});
it('should decode flags', () => {
- expect(decodeDiffOptions(['-w'])).toEqual({ignoreAllSpace: true});
- expect(decodeDiffOptions(['-W'])).toEqual({functionContext: true});
+ expect(flagsToGitDiffOptions(['-w'])).toEqual({ignoreAllSpace: true});
+ expect(flagsToGitDiffOptions(['-W'])).toEqual({functionContext: true});
});
});
diff --git a/ts/codediff/KeyboardShortcuts.tsx b/ts/codediff/KeyboardShortcuts.tsx
index 242d1c7..5a01f67 100644
--- a/ts/codediff/KeyboardShortcuts.tsx
+++ b/ts/codediff/KeyboardShortcuts.tsx
@@ -68,6 +68,9 @@ export function KeyboardShortcuts(props: KeyboardShortcutsProps) {
v Toggle file list / dropdown menu
+
+ z Normalize JSON
+
. Show diff options
diff --git a/ts/codediff/NormalizeJSONOption.tsx b/ts/codediff/NormalizeJSONOption.tsx
index 15cd780..8aea399 100644
--- a/ts/codediff/NormalizeJSONOption.tsx
+++ b/ts/codediff/NormalizeJSONOption.tsx
@@ -20,14 +20,14 @@ export function NormalizeJSONOption(props: NormalizeJSONOptionProps) {
};
return (
- <>
+
{' '}
- Normalize JSON (indent, sort keys)
- >
+ Normalize JSON (z): indent, sort keys
+
);
}
diff --git a/ts/codediff/codediff.tsx b/ts/codediff/codediff.tsx
index 71b4a2b..75ecb12 100644
--- a/ts/codediff/codediff.tsx
+++ b/ts/codediff/codediff.tsx
@@ -25,6 +25,13 @@ const DEFAULT_PARAMS: PatchOptions = {
expandLines: 10,
};
+/**
+ * Long lines can bog down the browser or freeze it completely.
+ * We show an interstitial before rendering diffs if the first line is more than
+ * this many characters.
+ */
+export const LINE_LENGTH_FOR_WARNING = 200_000;
+
/**
* @return Lines marked up with syntax s. The
* tags will be balanced within each line.
@@ -81,7 +88,20 @@ export function CodeDiff(props: Props) {
return [highlightText(beforeText ?? '', language), highlightText(afterText ?? '', language)];
}, [language, numLines, beforeText, afterText]);
- return (
+ const [bypassSafetyCheck, setBypassSafetyCheck] = React.useState(false);
+
+ const isSafeToRender = React.useMemo(() => {
+ return (
+ Math.max(beforeLines[0]?.length ?? 0, afterLines[0]?.length ?? 0) < LINE_LENGTH_FOR_WARNING
+ );
+ }, [afterLines, beforeLines]);
+
+ // Make the user click the link again if they navigate to a new file.
+ React.useEffect(() => {
+ setBypassSafetyCheck(false);
+ }, [beforeText, afterText]);
+
+ return isSafeToRender || bypassSafetyCheck ? (
+ ) : (
+
);
}
diff --git a/ts/diff-options.ts b/ts/diff-options.ts
index feafb18..fdf5595 100644
--- a/ts/diff-options.ts
+++ b/ts/diff-options.ts
@@ -1,6 +1,7 @@
export type DiffAlgorithm = 'patience' | 'minimal' | 'histogram' | 'myers';
-export interface DiffOptions {
+/** Options that can be passed to `git diff --no-index` */
+export interface GitDiffOptions {
/** aka -w */
ignoreAllSpace: boolean;
/** aka -b */
@@ -17,7 +18,7 @@ export interface DiffOptions {
findCopies?: number;
}
-export function encodeDiffOptions(opts: Partial) {
+export function gitDiffOptionsToFlags(opts: Partial) {
const flags = [];
if (opts.ignoreAllSpace) {
flags.push('-w');
@@ -43,8 +44,8 @@ export function encodeDiffOptions(opts: Partial) {
return flags;
}
-export function decodeDiffOptions(flags: string[]): Partial {
- const options: Partial = {};
+export function flagsToGitDiffOptions(flags: string[]): Partial {
+ const options: Partial = {};
for (const flag of flags) {
if (flag == '-w' || flag == '--ignore-all-space') {
options.ignoreAllSpace = true;
diff --git a/ts/options.ts b/ts/options.ts
index e90c810..e1dcdb9 100644
--- a/ts/options.ts
+++ b/ts/options.ts
@@ -1,4 +1,9 @@
-import {DiffAlgorithm} from './diff-options';
+import {
+ DiffAlgorithm,
+ flagsToGitDiffOptions,
+ GitDiffOptions,
+ gitDiffOptionsToFlags,
+} from './diff-options';
/** Type of global git_config object */
export interface GitConfig {
@@ -48,3 +53,35 @@ export function injectStylesFromConfig() {
`);
}
+
+export interface Options extends GitDiffOptions {
+ maxDiffWidth: number;
+ normalizeJSON: boolean;
+}
+
+export function parseOptions(query: URLSearchParams): Partial {
+ const flags = query.getAll('flag');
+ const gitDiffOptions = flagsToGitDiffOptions(flags);
+ const maxWidthStr = query.get('width');
+ const maxDiffWidth = maxWidthStr ? {maxDiffWidth: Number(maxWidthStr)} : undefined;
+ const normalizeJsonStr = query.get('normalize_json');
+ const normalizeJSON = normalizeJsonStr ? {normalizeJSON: true} : undefined;
+ return {...gitDiffOptions, ...maxDiffWidth, ...normalizeJSON};
+}
+
+export function encodeOptions(options: Partial) {
+ const {maxDiffWidth, normalizeJSON, ...diffOptions} = options;
+ const flags = gitDiffOptionsToFlags(diffOptions);
+ const params = new URLSearchParams(flags.map(f => ['flag', f]));
+ if (maxDiffWidth !== undefined && maxDiffWidth !== GIT_CONFIG.webdiff.maxDiffWidth) {
+ params.set('width', String(maxDiffWidth));
+ }
+ if (normalizeJSON) {
+ params.set('normalize_json', '1');
+ }
+ return params;
+}
+
+export type UpdateOptionsFn = (
+ updater: ((oldOptions: Partial) => Partial) | Partial,
+) => void;
diff --git a/ts/package.json b/ts/package.json
index ce73b97..91412fd 100644
--- a/ts/package.json
+++ b/ts/package.json
@@ -1,6 +1,6 @@
{
"name": "webdiff",
- "version": "1.3.0",
+ "version": "1.3.0b2",
"description": "client-side code for webdiff",
"main": "index.js",
"author": "Dan Vanderkam (danvdk@gmail.com)",
diff --git a/ts/utils.ts b/ts/utils.ts
index 0632239..10ac5b0 100644
--- a/ts/utils.ts
+++ b/ts/utils.ts
@@ -64,3 +64,8 @@ export function isSameSizeImagePair(filePair: FilePair) {
export function assertUnreachable(x: never): never {
throw new Error(x);
}
+
+// See https://effectivetypescript.com/2020/05/12/unionize-objectify/
+export type Unionize = {
+ [k in keyof T]: {k: k; v: T[k]};
+}[keyof T];
diff --git a/webdiff/static/css/style.css b/webdiff/static/css/style.css
index 92a49f6..7bccd2e 100644
--- a/webdiff/static/css/style.css
+++ b/webdiff/static/css/style.css
@@ -106,29 +106,30 @@ ul.file-list {
.side-a { border: 1px solid red; }
.side-b { border: 1px solid green; }
-.image-diff-controls {
+/* Per-file type diff controls: image diff mode, JSON normalization */
+.diff-mode-controls {
text-align: center;
background-color: #f7f7f7;
padding: 5px;
}
-.image-diff-controls > input {
+.diff-mode-controls > input {
margin-left: 20px;
}
-.image-diff-controls a {
+.diff-mode-controls a {
text-decoration: none;
}
-.image-diff-controls a, .image-diff-controls a:visited {
+.diff-mode-controls a, .diff-mode-controls a:visited {
color: #666;
}
-.image-diff-controls b {
+.diff-mode-controls b {
color: black;
}
-.image-diff-controls .mode {
+.diff-mode-controls .mode {
padding-left: 5px;
padding-right: 5px;
border-right: 1px solid #ccc;
}
-.image-diff-controls .mode:last-child {
+.diff-mode-controls .mode:last-child {
border-right: none;
}
@@ -245,3 +246,8 @@ kbd {
list-style-type: none;
margin-bottom: 2px;
}
+
+.suppressed-large-diff {
+ text-align: center;
+ background-color: lightyellow;
+}
|