Skip to content

Commit d633a0c

Browse files
authored
Show diffstats (#204)
* thread through counts for diffstats * keyboard shortcut to toggle file list/dropdown * thread through filePair * style headers * fix test, delete then add * add changes
1 parent 7b5bd1a commit d633a0c

File tree

17 files changed

+193
-56
lines changed

17 files changed

+193
-56
lines changed

testdata/unified/manyfiles.txt

594 Bytes
Binary file not shown.

testdata/unified/rename+change.txt

87 Bytes
Binary file not shown.

tests/unified_diff_test.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ def test_add_replaces():
148148

149149

150150
def test_parse_raw_diff_many():
151-
# git diff --no-index --raw -z testdata/manyfiles/{left,right}
151+
# git diff --no-index --raw -z --numstat testdata/manyfiles/{left,right}
152152
diff = open('testdata/unified/manyfiles.txt').read()
153153
mod644 = ['100644', '100644', '0000000', '0000000']
154154
assert parse_raw_diff(diff) == [
@@ -161,20 +161,22 @@ def test_parse_raw_diff_many():
161161
'testdata/manyfiles/left/d.txt',
162162
score=100,
163163
dst_path='testdata/manyfiles/right/a.txt',
164+
num_add=0,
165+
num_delete=0,
164166
),
165-
RawDiffLine(*mod644, 'M', 'testdata/manyfiles/left/b.txt'),
166-
RawDiffLine(*mod644, 'M', 'testdata/manyfiles/left/c.txt'),
167-
RawDiffLine(*mod644, 'M', 'testdata/manyfiles/left/e.txt'),
168-
RawDiffLine(*mod644, 'M', 'testdata/manyfiles/left/f.txt'),
169-
RawDiffLine(*mod644, 'M', 'testdata/manyfiles/left/g.txt'),
170-
RawDiffLine(*mod644, 'M', 'testdata/manyfiles/left/h.txt'),
171-
RawDiffLine(*mod644, 'M', 'testdata/manyfiles/left/i.txt'),
172-
RawDiffLine(*mod644, 'M', 'testdata/manyfiles/left/j.txt'),
167+
RawDiffLine(*mod644, 'M', 'testdata/manyfiles/left/b.txt', num_add=0, num_delete=1),
168+
RawDiffLine(*mod644, 'M', 'testdata/manyfiles/left/c.txt', num_add=0, num_delete=1),
169+
RawDiffLine(*mod644, 'M', 'testdata/manyfiles/left/e.txt', num_add=0, num_delete=1),
170+
RawDiffLine(*mod644, 'M', 'testdata/manyfiles/left/f.txt', num_add=0, num_delete=1),
171+
RawDiffLine(*mod644, 'M', 'testdata/manyfiles/left/g.txt', num_add=0, num_delete=1),
172+
RawDiffLine(*mod644, 'M', 'testdata/manyfiles/left/h.txt', num_add=0, num_delete=1),
173+
RawDiffLine(*mod644, 'M', 'testdata/manyfiles/left/i.txt', num_add=0, num_delete=1),
174+
RawDiffLine(*mod644, 'M', 'testdata/manyfiles/left/j.txt', num_add=0, num_delete=1),
173175
]
174176

175177

176178
def test_parse_raw_diff_rename():
177-
# git diff --no-index --raw -z testdata/rename+change/{left,right}
179+
# git diff --no-index --raw -z --numstat testdata/rename+change/{left,right}
178180
diff = open('testdata/unified/rename+change.txt').read()
179181
assert parse_raw_diff(diff) == [
180182
RawDiffLine(
@@ -186,6 +188,8 @@ def test_parse_raw_diff_rename():
186188
'testdata/rename+change/left/huckfinn.txt',
187189
score=90,
188190
dst_path='testdata/rename+change/right/huckfinn.md',
191+
num_add=2,
192+
num_delete=2,
189193
),
190194
]
191195

ts/CodeDiffContainer.tsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ interface BaseFilePair {
1414
type: 'add' | 'delete' | 'move' | 'change'; // XXX check "change"
1515
/** Are there any changes to the file? Only set for "thick" diffs. */
1616
no_changes?: boolean;
17+
num_add: number | null;
18+
num_delete: number | null;
1719
}
1820

1921
interface TextFilePair extends BaseFilePair {
@@ -121,8 +123,7 @@ export function CodeDiffContainer(props: {filePair: FilePair; diffOptions: Parti
121123
<div ref={codediffRef} key={filePair.idx}>
122124
{contents ? (
123125
<FileDiff
124-
pathBefore={filePair.a}
125-
pathAfter={filePair.b}
126+
filePair={filePair}
126127
contentsBefore={contents.before}
127128
contentsAfter={contents.after}
128129
diffOps={contents.diffOps}
@@ -136,8 +137,7 @@ export function CodeDiffContainer(props: {filePair: FilePair; diffOptions: Parti
136137
}
137138

138139
interface FileDiffProps {
139-
pathBefore: string;
140-
pathAfter: string;
140+
filePair: FilePair;
141141
contentsBefore: string | null;
142142
contentsAfter: string | null;
143143
diffOps: DiffRange[];
@@ -155,7 +155,9 @@ function lengthOrZero(data: unknown[] | string | null | undefined) {
155155
}
156156

157157
function FileDiff(props: FileDiffProps) {
158-
const {pathBefore, pathAfter, contentsBefore, contentsAfter, diffOps} = props;
158+
const {filePair, contentsBefore, contentsAfter, diffOps} = props;
159+
const pathBefore = filePair.a;
160+
const pathAfter = filePair.b;
159161
// build the diff view and add it to the current DOM
160162

161163
const lastOp = diffOps[diffOps.length - 1];
@@ -182,18 +184,21 @@ function FileDiff(props: FileDiffProps) {
182184

183185
const opts = React.useMemo(
184186
(): Partial<PatchOptions> => ({
185-
// set the display titles for each resource
186-
beforeName: pathBefore || '(none)',
187-
afterName: pathAfter || '(none)',
188187
language,
189188
// TODO: thread through minJumpSize
190189
}),
191-
[language, pathAfter, pathBefore],
190+
[language],
192191
);
193192

194193
return (
195194
<div className="diff">
196-
<CodeDiff beforeText={contentsBefore} afterText={contentsAfter} ops={diffOps} params={opts} />
195+
<CodeDiff
196+
beforeText={contentsBefore}
197+
afterText={contentsAfter}
198+
filePair={filePair}
199+
ops={diffOps}
200+
params={opts}
201+
/>
197202
</div>
198203
);
199204
}

ts/FileList.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ export interface Props {
1515
export function FileList(props: Props) {
1616
const {filePairs, selectedIndex, fileChangeHandler} = props;
1717

18+
const maxDelta = React.useMemo(() => {
19+
return Math.max(1, ...filePairs.map(fp => (fp.num_add ?? 0) + (fp.num_delete ?? 0)));
20+
}, [filePairs]);
21+
1822
const lis = filePairs.map((filePair, idx) => {
1923
const displayName = filePairDisplayName(filePair);
2024
const content =
@@ -31,10 +35,36 @@ export function FileList(props: Props) {
3135
);
3236
return (
3337
<li key={idx}>
38+
<SparkChart maxDelta={maxDelta} numAdd={filePair.num_add} numDelete={filePair.num_delete} />
3439
<span title={filePair.type} className={`diff ${filePair.type}`} />
3540
{content}
3641
</li>
3742
);
3843
});
3944
return <ul className="file-list">{lis}</ul>;
4045
}
46+
47+
interface SparkChartProps {
48+
maxDelta: number;
49+
numAdd: number | null;
50+
numDelete: number | null;
51+
}
52+
53+
function SparkChart(props: SparkChartProps) {
54+
const {numAdd, numDelete, maxDelta} = props;
55+
if (numAdd === null || numDelete === null) {
56+
return null;
57+
}
58+
return (
59+
<div className="spark">
60+
{numDelete > 0 && (
61+
<div
62+
className="delete"
63+
style={{width: `${Math.round((100 * numDelete) / maxDelta)}%`}}></div>
64+
)}
65+
{numAdd > 0 && (
66+
<div className="add" style={{width: `${Math.round((100 * numAdd) / maxDelta)}%`}}></div>
67+
)}
68+
</div>
69+
);
70+
}

ts/FileModeSelector.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import React from 'react';
2+
import {FileSelectorMode} from './FileSelector';
23

34
export interface Props {
4-
mode: 'list' | 'dropdown';
5-
changeHandler: (mode: 'list' | 'dropdown') => void;
5+
mode: FileSelectorMode;
6+
changeHandler: (mode: FileSelectorMode) => void;
67
}
78

89
/** A widget for toggling between file selection modes. */

ts/FileSelector.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,15 @@ export interface Props {
88
filePairs: FilePair[];
99
selectedFileIndex: number;
1010
fileChangeHandler: (newIndex: number) => void;
11+
mode: FileSelectorMode;
12+
onChangeMode: (mode: FileSelectorMode) => void;
1113
}
1214

13-
type Mode = 'list' | 'dropdown';
15+
export type FileSelectorMode = 'list' | 'dropdown';
1416

1517
/** Shows a list of files in one of two possible modes (list or dropdown). */
1618
export function FileSelector(props: Props) {
17-
const {filePairs, selectedFileIndex, fileChangeHandler} = props;
18-
19-
// An explicit list is better, unless there are a ton of files.
20-
const [mode, setMode] = React.useState<Mode>(filePairs.length <= 6 ? 'list' : 'dropdown');
19+
const {filePairs, selectedFileIndex, fileChangeHandler, mode, onChangeMode} = props;
2120

2221
// For single file diffs, a file selector is a waste of space.
2322
if (filePairs.length === 1) {
@@ -46,7 +45,7 @@ export function FileSelector(props: Props) {
4645
return (
4746
<div className="file-selector">
4847
{selector}
49-
{filePairs.length > 3 ? <FileModeSelector mode={mode} changeHandler={setMode} /> : null}
48+
{filePairs.length > 3 ? <FileModeSelector mode={mode} changeHandler={onChangeMode} /> : null}
5049
</div>
5150
);
5251
}

ts/Root.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {RouteComponentProps, useHistory} from 'react-router';
33
import {FilePair} from './CodeDiffContainer';
44
import {DiffOptions} from './diff-options';
55
import {DiffView, PerceptualDiffMode} from './DiffView';
6-
import {FileSelector} from './FileSelector';
6+
import {FileSelector, FileSelectorMode} from './FileSelector';
77
import {isLegitKeypress} from './file_diff';
88
import {ImageDiffMode} from './ImageDiffModeSelector';
99
import {filePairDisplayName} from './utils';
@@ -22,6 +22,10 @@ export function Root(props: Props) {
2222
const [diffOptions, setDiffOptions] = React.useState<Partial<DiffOptions>>({});
2323
const [showKeyboardHelp, setShowKeyboardHelp] = React.useState(false);
2424
const [showOptions, setShowOptions] = React.useState(false);
25+
// An explicit list is better, unless there are a ton of files.
26+
const [fileSelectorMode, setFileSelectorMode] = React.useState<FileSelectorMode>(
27+
pairs.length <= 6 ? 'list' : 'dropdown',
28+
);
2529

2630
const history = useHistory();
2731
const selectIndex = React.useCallback(
@@ -49,6 +53,8 @@ export function Root(props: Props) {
4953
if (idx < pairs.length - 1) {
5054
selectIndex(idx + 1);
5155
}
56+
} else if (e.code == 'KeyV') {
57+
setFileSelectorMode(mode => (mode === 'dropdown' ? 'list' : 'dropdown'));
5258
} else if (e.code === 'Slash' && e.shiftKey) {
5359
setShowKeyboardHelp(val => !val);
5460
} else if (e.code === 'Escape') {
@@ -71,7 +77,13 @@ export function Root(props: Props) {
7177
isVisible={showOptions}
7278
setIsVisible={setShowOptions}
7379
/>
74-
<FileSelector selectedFileIndex={idx} filePairs={pairs} fileChangeHandler={selectIndex} />
80+
<FileSelector
81+
selectedFileIndex={idx}
82+
filePairs={pairs}
83+
fileChangeHandler={selectIndex}
84+
mode={fileSelectorMode}
85+
onChangeMode={setFileSelectorMode}
86+
/>
7587
{showKeyboardHelp ? (
7688
<KeyboardShortcuts
7789
onClose={() => {

ts/__tests__/util.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ test('util.filePairDisplayName', () => {
88
type: 'delete',
99
a: 'dir/file.json',
1010
b: '',
11+
num_add: 0,
12+
num_delete: 0,
1113
}),
1214
).toEqual('dir/file.json');
1315

1416
const rename = (a: string, b: string) => {
15-
return filePairDisplayName({...props, type: 'move', a, b});
17+
return filePairDisplayName({...props, type: 'move', a, b, num_add: 0, num_delete: 0});
1618
};
1719
expect(rename('file.json', 'renamed.json')).toEqual('{file → renamed}.json');
1820
expect(rename('dir/file.json', 'dir/renamed.json')).toEqual('dir/{file → renamed}.json');

ts/codediff/KeyboardShortcuts.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ export function KeyboardShortcuts(props: KeyboardShortcutsProps) {
6565
<li>
6666
<kbd>p</kbd> Previous Diffhunk
6767
</li>
68+
<li>
69+
<kbd>v</kbd> Toggle file list / dropdown menu
70+
</li>
6871
<li>
6972
<kbd>.</kbd> Show diff options
7073
</li>

0 commit comments

Comments
 (0)