From 1ab02f9830de39146eef1322f46d1eef177abee7 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Sat, 31 May 2025 14:19:11 -0400 Subject: [PATCH 01/12] z = normaliZe JSON --- ts/Root.tsx | 9 +++++++-- ts/codediff/KeyboardShortcuts.tsx | 3 +++ ts/codediff/NormalizeJSONOption.tsx | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/ts/Root.tsx b/ts/Root.tsx index ce05f34..899e87a 100644 --- a/ts/Root.tsx +++ b/ts/Root.tsx @@ -99,7 +99,10 @@ export function Root() { ); const setNormalizeJSON = React.useCallback( - (newNormalizeJSON: boolean) => { + (newNormalizeJSON?: boolean) => { + if (newNormalizeJSON === undefined) { + newNormalizeJSON = !options.normalizeJSON; + } setSearchParams(encodeOptions(options, maxDiffWidth, newNormalizeJSON)); }, [options, setSearchParams, maxDiffWidth], @@ -125,13 +128,15 @@ export function Root() { setShowKeyboardHelp(false); } else if (e.code === 'Period') { setShowOptions(val => !val); + } else if (e.code === 'KeyZ') { + setNormalizeJSON(); // toggles } }; document.addEventListener('keydown', handleKeydown); return () => { document.removeEventListener('keydown', handleKeydown); }; - }, [idx, selectIndex]); + }, [idx, selectIndex, setNormalizeJSON]); const inlineStyle = ` td.code { 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..6a41008 100644 --- a/ts/codediff/NormalizeJSONOption.tsx +++ b/ts/codediff/NormalizeJSONOption.tsx @@ -27,7 +27,7 @@ export function NormalizeJSONOption(props: NormalizeJSONOptionProps) { onChange={toggleNormalizeJSON} id="normalize-json" />{' '} - + ); } From be07779a5f7ae36fbfc5a9346c2d15087864bf52 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Sat, 31 May 2025 14:25:01 -0400 Subject: [PATCH 02/12] consistent style for JSON normalization control --- ts/ImageDiff.tsx | 2 +- ts/codediff/NormalizeJSONOption.tsx | 4 ++-- webdiff/static/css/style.css | 15 ++++++++------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/ts/ImageDiff.tsx b/ts/ImageDiff.tsx index 639a1df..83ef404 100644 --- a/ts/ImageDiff.tsx +++ b/ts/ImageDiff.tsx @@ -119,7 +119,7 @@ export function ImageDiff(props: Props) { return (
    -
    +
    +
    {' '} - +
    ); } diff --git a/webdiff/static/css/style.css b/webdiff/static/css/style.css index 92a49f6..fda6a2b 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; } From d36b4bd46ad538083c845c75a89177577dadae77 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Sat, 31 May 2025 14:42:57 -0400 Subject: [PATCH 03/12] refactor DiffOptions --- ts/CodeDiffContainer.tsx | 6 +-- ts/DiffOptions.tsx | 16 ++++---- ts/DiffView.tsx | 6 +-- ts/Root.tsx | 67 ++++++------------------------- ts/__tests__/diff-options.test.ts | 18 ++++----- ts/diff-options.ts | 9 +++-- ts/options.ts | 32 ++++++++++++++- 7 files changed, 71 insertions(+), 83 deletions(-) diff --git a/ts/CodeDiffContainer.tsx b/ts/CodeDiffContainer.tsx index 7aa8ac1..c4c5332 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'; @@ -84,7 +84,7 @@ async function getOrNull(side: string, path: string, normalizeJSON: boolean) { export interface CodeDiffContainerProps { filePair: FilePair; - diffOptions: Partial; + diffOptions: Partial; normalizeJSON: boolean; } @@ -104,7 +104,7 @@ export function CodeDiffContainer(props: CodeDiffContainerProps) { 'Content-Type': 'application/json', }, body: JSON.stringify({ - options: encodeDiffOptions(diffOptions), + options: gitDiffOptionsToFlags(diffOptions), normalize_json: normalizeJSON, }), }); diff --git a/ts/DiffOptions.tsx b/ts/DiffOptions.tsx index 5b5cd34..c8dae14 100644 --- a/ts/DiffOptions.tsx +++ b/ts/DiffOptions.tsx @@ -1,15 +1,14 @@ import React from 'react'; -import {DiffAlgorithm, DiffOptions, encodeDiffOptions} from './diff-options'; +import {DiffAlgorithm, gitDiffOptionsToFlags} from './diff-options'; import {PageCover} from './codediff/PageCover'; import {isLegitKeypress} from './file_diff'; +import { CombinedOptions } from './options'; export interface Props { - options: Partial; - setOptions: (newOptions: Partial) => void; + options: Partial; + setOptions: (newOptions: Partial) => void; defaultMaxDiffWidth: number; - maxDiffWidth: number; - setMaxDiffWidth: (maxDiffWidth: number) => void; isVisible: boolean; setIsVisible: (isVisible: boolean) => void; } @@ -52,7 +51,8 @@ const popupStyle: React.CSSProperties = { }; export function DiffOptionsControl(props: Props) { - const {options, setOptions, isVisible, setIsVisible, maxDiffWidth, setMaxDiffWidth} = props; + const {options, setOptions, isVisible, setIsVisible} = props; + const maxDiffWidth = options.maxDiffWidth ?? props.defaultMaxDiffWidth; const togglePopup = () => { setIsVisible(!isVisible); @@ -73,7 +73,7 @@ export function DiffOptionsControl(props: Props) { setOptions({...options, diffAlgorithm: e.currentTarget.value as DiffAlgorithm}); }; const changeMaxDiffWidth: React.ChangeEventHandler = e => { - setMaxDiffWidth(e.currentTarget.valueAsNumber); + setOptions({...options, maxDiffWidth: e.currentTarget.valueAsNumber}); }; React.useEffect(() => { @@ -91,7 +91,7 @@ export function DiffOptionsControl(props: Props) { }; }, [options, setOptions]); - const diffOptsStr = encodeDiffOptions(options).join(' '); + const diffOptsStr = gitDiffOptionsToFlags(options).join(' '); return ( <> 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/Root.tsx b/ts/Root.tsx index 899e87a..c43280f 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,43 +9,13 @@ import {ImageDiffMode} from './ImageDiffModeSelector'; import {filePairDisplayName} from './utils'; import {DiffOptionsControl} from './DiffOptions'; import {KeyboardShortcuts} from './codediff/KeyboardShortcuts'; -import {GitConfig} from './options'; +import {CombinedOptions, encodeOptions, GitConfig, parseOptions} 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() { @@ -81,31 +50,21 @@ 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)); + (newOptions: Partial) => { + setSearchParams(encodeOptions(newOptions)); }, - [maxDiffWidth, setSearchParams, normalizeJSON], + [setSearchParams], ); - const setMaxDiffWidth = React.useCallback( - (newMaxWidth: number) => { - setSearchParams(encodeOptions(options, newMaxWidth, normalizeJSON)); - }, - [options, setSearchParams, normalizeJSON], - ); - - const setNormalizeJSON = React.useCallback( - (newNormalizeJSON?: boolean) => { - if (newNormalizeJSON === undefined) { - newNormalizeJSON = !options.normalizeJSON; - } - setSearchParams(encodeOptions(options, maxDiffWidth, newNormalizeJSON)); - }, - [options, setSearchParams, maxDiffWidth], + const updateOptions = React.useCallback( + (updater: ((oldOptions: Partial) => Partial) | Partial) => { + setDiffOptions({...options, ...(typeof updater === 'function' ? updater(options) : updater)}); + }, [options, setDiffOptions] ); // TODO: switch to useKey() or some such @@ -129,14 +88,14 @@ export function Root() { } else if (e.code === 'Period') { setShowOptions(val => !val); } else if (e.code === 'KeyZ') { - setNormalizeJSON(); // toggles + updateOptions(o => ({normalizeJSON: !o.normalizeJSON})); } }; document.addEventListener('keydown', handleKeydown); return () => { document.removeEventListener('keydown', handleKeydown); }; - }, [idx, selectIndex, setNormalizeJSON]); + }, [idx, selectIndex, updateOptions]); const inlineStyle = ` td.code { @@ -150,8 +109,6 @@ 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..a9e4724 100644 --- a/ts/__tests__/diff-options.test.ts +++ b/ts/__tests__/diff-options.test.ts @@ -1,20 +1,20 @@ -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/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..9ebcb68 100644 --- a/ts/options.ts +++ b/ts/options.ts @@ -1,4 +1,4 @@ -import {DiffAlgorithm} from './diff-options'; +import {DiffAlgorithm, flagsToGitDiffOptions, GitDiffOptions, gitDiffOptionsToFlags} from './diff-options'; /** Type of global git_config object */ export interface GitConfig { @@ -48,3 +48,33 @@ export function injectStylesFromConfig() { `); } + +export interface CombinedOptions 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 !== GIT_CONFIG.webdiff.maxDiffWidth) { + params.set('width', String(maxDiffWidth)); + } + if (normalizeJSON) { + params.set('normalize_json', '1'); + } + return params; +} From 8a6cd2d6c3806e583d71b30f0c4e4aed4ccabb93 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Sat, 31 May 2025 14:49:56 -0400 Subject: [PATCH 04/12] pass down update function --- ts/DiffOptions.tsx | 24 ++++++++++++------------ ts/Root.tsx | 18 ++++++++++-------- ts/options.ts | 6 ++++++ 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/ts/DiffOptions.tsx b/ts/DiffOptions.tsx index c8dae14..91b5bb0 100644 --- a/ts/DiffOptions.tsx +++ b/ts/DiffOptions.tsx @@ -3,11 +3,11 @@ import React from 'react'; import {DiffAlgorithm, gitDiffOptionsToFlags} from './diff-options'; import {PageCover} from './codediff/PageCover'; import {isLegitKeypress} from './file_diff'; -import { CombinedOptions } from './options'; +import {CombinedOptions, UpdateOptionsFn} from './options'; export interface Props { options: Partial; - setOptions: (newOptions: Partial) => void; + updateOptions: UpdateOptionsFn; defaultMaxDiffWidth: number; isVisible: boolean; setIsVisible: (isVisible: boolean) => void; @@ -51,45 +51,45 @@ const popupStyle: React.CSSProperties = { }; export function DiffOptionsControl(props: Props) { - const {options, setOptions, isVisible, setIsVisible} = props; + const {options, updateOptions, isVisible, setIsVisible} = props; const maxDiffWidth = options.maxDiffWidth ?? props.defaultMaxDiffWidth; const togglePopup = () => { setIsVisible(!isVisible); }; const toggleIgnoreAllSpace = () => { - setOptions({...options, ignoreAllSpace: !options.ignoreAllSpace}); + updateOptions(options => ({ignoreAllSpace: !options.ignoreAllSpace})); }; const toggleIgnoreSpaceChange = () => { - setOptions({...options, ignoreSpaceChange: !options.ignoreSpaceChange}); + updateOptions(options => ({ignoreSpaceChange: !options.ignoreSpaceChange})); }; const toggleFunctionContext = () => { - setOptions({...options, functionContext: !options.functionContext}); + updateOptions(options => ({functionContext: !options.functionContext})); }; 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 => { - setOptions({...options, maxDiffWidth: 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 = gitDiffOptionsToFlags(options).join(' '); diff --git a/ts/Root.tsx b/ts/Root.tsx index c43280f..2933d7e 100644 --- a/ts/Root.tsx +++ b/ts/Root.tsx @@ -9,14 +9,13 @@ import {ImageDiffMode} from './ImageDiffModeSelector'; import {filePairDisplayName} from './utils'; import {DiffOptionsControl} from './DiffOptions'; import {KeyboardShortcuts} from './codediff/KeyboardShortcuts'; -import {CombinedOptions, encodeOptions, GitConfig, parseOptions} from './options'; +import {CombinedOptions, encodeOptions, GitConfig, parseOptions, UpdateOptionsFn} from './options'; import {NormalizeJSONOption} from './codediff/NormalizeJSONOption'; declare const pairs: FilePair[]; declare const initialIdx: number; declare const GIT_CONFIG: GitConfig; - // Webdiff application root. export function Root() { const [pdiffMode, setPDiffMode] = React.useState('off'); @@ -61,10 +60,11 @@ export function Root() { [setSearchParams], ); - const updateOptions = React.useCallback( - (updater: ((oldOptions: Partial) => Partial) | Partial) => { - setDiffOptions({...options, ...(typeof updater === 'function' ? updater(options) : updater)}); - }, [options, setDiffOptions] + const updateOptions = React.useCallback( + update => { + setDiffOptions({...options, ...(typeof update === 'function' ? update(options) : update)}); + }, + [options, setDiffOptions], ); // TODO: switch to useKey() or some such @@ -108,7 +108,7 @@ export function Root() {
    { updateOptions({normalizeJSON: v}); }} + setNormalizeJSON={v => { + updateOptions({normalizeJSON: v}); + }} filePair={filePair} /> {showKeyboardHelp ? ( diff --git a/ts/options.ts b/ts/options.ts index 9ebcb68..e077585 100644 --- a/ts/options.ts +++ b/ts/options.ts @@ -78,3 +78,9 @@ export function encodeOptions( } return params; } + +export type UpdateOptionsFn = ( + updater: + | ((oldOptions: Partial) => Partial) + | Partial, +) => void; From 1de7cf623fda808c3b344ac0c6c96f284934f520 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Sat, 31 May 2025 14:50:43 -0400 Subject: [PATCH 05/12] rename --- ts/DiffOptions.tsx | 4 ++-- ts/Root.tsx | 4 ++-- ts/options.ts | 19 ++++++++++--------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/ts/DiffOptions.tsx b/ts/DiffOptions.tsx index 91b5bb0..9903897 100644 --- a/ts/DiffOptions.tsx +++ b/ts/DiffOptions.tsx @@ -3,10 +3,10 @@ import React from 'react'; import {DiffAlgorithm, gitDiffOptionsToFlags} from './diff-options'; import {PageCover} from './codediff/PageCover'; import {isLegitKeypress} from './file_diff'; -import {CombinedOptions, UpdateOptionsFn} from './options'; +import {Options, UpdateOptionsFn} from './options'; export interface Props { - options: Partial; + options: Partial; updateOptions: UpdateOptionsFn; defaultMaxDiffWidth: number; isVisible: boolean; diff --git a/ts/Root.tsx b/ts/Root.tsx index 2933d7e..5e5ed6e 100644 --- a/ts/Root.tsx +++ b/ts/Root.tsx @@ -9,7 +9,7 @@ import {ImageDiffMode} from './ImageDiffModeSelector'; import {filePairDisplayName} from './utils'; import {DiffOptionsControl} from './DiffOptions'; import {KeyboardShortcuts} from './codediff/KeyboardShortcuts'; -import {CombinedOptions, encodeOptions, GitConfig, parseOptions, UpdateOptionsFn} from './options'; +import {Options, encodeOptions, GitConfig, parseOptions, UpdateOptionsFn} from './options'; import {NormalizeJSONOption} from './codediff/NormalizeJSONOption'; declare const pairs: FilePair[]; @@ -54,7 +54,7 @@ export function Root() { const normalizeJSON = !!options.normalizeJSON; const setDiffOptions = React.useCallback( - (newOptions: Partial) => { + (newOptions: Partial) => { setSearchParams(encodeOptions(newOptions)); }, [setSearchParams], diff --git a/ts/options.ts b/ts/options.ts index e077585..13d87ce 100644 --- a/ts/options.ts +++ b/ts/options.ts @@ -1,4 +1,9 @@ -import {DiffAlgorithm, flagsToGitDiffOptions, GitDiffOptions, gitDiffOptionsToFlags} from './diff-options'; +import { + DiffAlgorithm, + flagsToGitDiffOptions, + GitDiffOptions, + gitDiffOptionsToFlags, +} from './diff-options'; /** Type of global git_config object */ export interface GitConfig { @@ -49,12 +54,12 @@ export function injectStylesFromConfig() { `); } -export interface CombinedOptions extends GitDiffOptions { +export interface Options extends GitDiffOptions { maxDiffWidth: number; normalizeJSON?: boolean; } -export function parseOptions(query: URLSearchParams): Partial { +export function parseOptions(query: URLSearchParams): Partial { const flags = query.getAll('flag'); const gitDiffOptions = flagsToGitDiffOptions(flags); const maxWidthStr = query.get('width'); @@ -64,9 +69,7 @@ export function parseOptions(query: URLSearchParams): Partial { return {...gitDiffOptions, ...maxDiffWidth, ...normalizeJSON}; } -export function encodeOptions( - options: Partial, -) { +export function encodeOptions(options: Partial) { const {maxDiffWidth, normalizeJSON, ...diffOptions} = options; const flags = gitDiffOptionsToFlags(diffOptions); const params = new URLSearchParams(flags.map(f => ['flag', f])); @@ -80,7 +83,5 @@ export function encodeOptions( } export type UpdateOptionsFn = ( - updater: - | ((oldOptions: Partial) => Partial) - | Partial, + updater: ((oldOptions: Partial) => Partial) | Partial, ) => void; From d70cd5830e5b17216d26824a98871e9945569660 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Sat, 31 May 2025 14:56:32 -0400 Subject: [PATCH 06/12] confused myself; it works --- testdata/jsondiffs/right/tsconfig.json | 4 ++-- ts/__tests__/diff-options.test.ts | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) 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/__tests__/diff-options.test.ts b/ts/__tests__/diff-options.test.ts index a9e4724..cd262f3 100644 --- a/ts/__tests__/diff-options.test.ts +++ b/ts/__tests__/diff-options.test.ts @@ -6,7 +6,9 @@ describe('encodeDiffOptions', () => { }); it('should encode basic flags', () => { - expect(gitDiffOptionsToFlags({diffAlgorithm: 'patience'})).toEqual(['--diff-algorithm=patience']); + 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']); From dbe0d510c9c965b6355bbf1d84aa6db1afae511a Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Sat, 31 May 2025 15:11:54 -0400 Subject: [PATCH 07/12] Add a normalize toggle under the gear --- ts/DiffOptions.tsx | 32 ++++++++++++++++++++------------ ts/options.ts | 2 +- ts/utils.ts | 5 +++++ 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/ts/DiffOptions.tsx b/ts/DiffOptions.tsx index 9903897..b870cec 100644 --- a/ts/DiffOptions.tsx +++ b/ts/DiffOptions.tsx @@ -4,6 +4,7 @@ import {DiffAlgorithm, gitDiffOptionsToFlags} from './diff-options'; import {PageCover} from './codediff/PageCover'; import {isLegitKeypress} from './file_diff'; import {Options, UpdateOptionsFn} from './options'; +import {Unionize} from './utils'; export interface Props { options: Partial; @@ -50,6 +51,8 @@ const popupStyle: React.CSSProperties = { fontFamily: 'sans-serif', }; +type BooleanOptions = Extract, {v: boolean}>['k']; + export function DiffOptionsControl(props: Props) { const {options, updateOptions, isVisible, setIsVisible} = props; const maxDiffWidth = options.maxDiffWidth ?? props.defaultMaxDiffWidth; @@ -57,14 +60,8 @@ export function DiffOptionsControl(props: Props) { const togglePopup = () => { setIsVisible(!isVisible); }; - const toggleIgnoreAllSpace = () => { - updateOptions(options => ({ignoreAllSpace: !options.ignoreAllSpace})); - }; - const toggleIgnoreSpaceChange = () => { - updateOptions(options => ({ignoreSpaceChange: !options.ignoreSpaceChange})); - }; - const toggleFunctionContext = () => { - updateOptions(options => ({functionContext: !options.functionContext})); + const toggleField = (k: BooleanOptions) => () => { + updateOptions(options => ({[k]: !options[k]})); }; const setUnifiedContext: React.ChangeEventHandler = e => { updateOptions({unified: e.currentTarget.valueAsNumber}); @@ -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')} />{' '}
    + {' '} + +
    Context: @@ -157,7 +165,7 @@ export function DiffOptionsControl(props: Props) { type="checkbox" checked={!!options.functionContext} id="function-context" - onChange={toggleFunctionContext} + onChange={toggleField('functionContext')} />{' '}