Skip to content

Commit 60a95f5

Browse files
authored
Merge pull request #25 from utkuakyuz/feature/difference-line-count
Feature: Add Line Count and Object Count Statistics Features
2 parents 18809ca + 81bcf78 commit 60a95f5

File tree

13 files changed

+599
-25
lines changed

13 files changed

+599
-25
lines changed

demo/src/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export default function App() {
2525
miniMapWidth: 20,
2626
hideSearch: false,
2727
height: 380,
28+
showLineCount: true,
29+
showObjectCountStats: false,
2830

2931
// Differ Configuration
3032
detectCircular: true,
@@ -219,6 +221,8 @@ export default function App() {
219221
height={config.height}
220222
miniMapWidth={config.miniMapWidth}
221223
hideSearch={config.hideSearch}
224+
showLineCount={config.showLineCount}
225+
showObjectCountStats={config.showObjectCountStats}
222226
inlineDiffOptions={{ mode: config.inlineDiffMode }}
223227
oldValue={parsedOldValue}
224228
newValue={parsedNewValue}

demo/src/components/Sidebar.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,32 @@ function Sidebar(props: Props) {
9494
</label>
9595
</div>
9696

97+
<div className="form-group">
98+
<label className="checkbox-label">
99+
<input
100+
type="checkbox"
101+
className="form-checkbox"
102+
checked={config.showLineCount}
103+
onChange={e => updateConfig("showLineCount", e.target.checked)}
104+
/>
105+
Show Line Count
106+
</label>
107+
<p className="form-hint">Display statistics for added, removed, and modified lines</p>
108+
</div>
109+
110+
<div className="form-group">
111+
<label className="checkbox-label">
112+
<input
113+
type="checkbox"
114+
className="form-checkbox"
115+
checked={config.showObjectCountStats}
116+
onChange={e => updateConfig("showObjectCountStats", e.target.checked)}
117+
/>
118+
Show Object Count Stats
119+
</label>
120+
<p className="form-hint">Display object statistics when using compare-key method</p>
121+
</div>
122+
97123
<div className="form-group">
98124
<label className="form-label">
99125
CSS Class Name

demo/src/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export type Config = {
88
miniMapWidth: number;
99
hideSearch: boolean;
1010
height: number;
11+
showLineCount: boolean;
12+
showObjectCountStats: boolean;
1113

1214
// Differ Configuration
1315
detectCircular: boolean;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React from "react";
2+
3+
import type { LineCountStats } from "../types";
4+
5+
type LineCountDisplayProps = {
6+
stats: LineCountStats;
7+
};
8+
9+
export const LineCountDisplay: React.FC<LineCountDisplayProps> = ({ stats }) => {
10+
if (stats.total === 0) {
11+
return (
12+
<div className="line-count-display">
13+
<span className="line-count-item no-changes">No changes</span>
14+
</div>
15+
);
16+
}
17+
18+
return (
19+
<div className="line-count-display">
20+
<div className="line-count-item-sub-holder">
21+
{stats.added > 0 && (
22+
<span className="line-count-item added">
23+
+
24+
{stats.added}
25+
{" "}
26+
added
27+
</span>
28+
)}
29+
30+
{stats.removed > 0 && (
31+
<span className="line-count-item removed">
32+
-
33+
{stats.removed}
34+
{" "}
35+
removed
36+
</span>
37+
)}
38+
</div>
39+
<div className="line-count-item-sub-holder">
40+
{stats.modified > 0 && (
41+
<span className="line-count-item modified">
42+
~
43+
{stats.modified}
44+
{" "}
45+
modified
46+
</span>
47+
)}
48+
<span className="line-count-item total">
49+
{stats.total}
50+
{" "}
51+
total changes
52+
</span>
53+
</div>
54+
</div>
55+
);
56+
};
57+
58+
export default LineCountDisplay;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from "react";
2+
3+
import type { ObjectCountStats } from "../types";
4+
5+
type ObjectCountDisplayProps = {
6+
stats: ObjectCountStats;
7+
};
8+
9+
export const ObjectCountDisplay: React.FC<ObjectCountDisplayProps> = ({ stats }) => {
10+
if (!stats || typeof stats !== "object") {
11+
return null;
12+
}
13+
14+
const { added = 0, removed = 0, modified = 0, total = 0 } = stats;
15+
16+
if (total === 0) {
17+
return (
18+
<div className="object-count-display">
19+
<span className="object-count-item no-changes">No object changes</span>
20+
</div>
21+
);
22+
}
23+
24+
return (
25+
<div className="object-count-display">
26+
<div className="object-count-item-sub-holder">
27+
{added > 0 && (
28+
<span className="object-count-item added">
29+
+
30+
{added}
31+
{" "}
32+
added objects
33+
</span>
34+
)}
35+
36+
{removed > 0 && (
37+
<span className="object-count-item removed">
38+
-
39+
{removed}
40+
{" "}
41+
removed objects
42+
</span>
43+
)}
44+
</div>
45+
<div className="object-count-item-sub-holder">
46+
{modified > 0 && (
47+
<span className="object-count-item modified">
48+
~
49+
{modified}
50+
{" "}
51+
modified objects
52+
</span>
53+
)}
54+
<span className="object-count-item total">
55+
{total}
56+
{" "}
57+
total object changes
58+
</span>
59+
</div>
60+
</div>
61+
);
62+
};
63+
64+
export default ObjectCountDisplay;

src/components/DiffViewer/components/VirtualizedDiffViewer.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@ import type { VariableSizeList as List } from "react-window";
44
import { Differ } from "json-diff-kit";
55
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
66

7-
import type { DiffRowOrCollapsed, SegmentItem, VirtualizedDiffViewerProps } from "../types";
7+
import type { DiffRowOrCollapsed, LineCountStats, ObjectCountStats, SegmentItem, VirtualizedDiffViewerProps } from "../types";
88

99
import "../styles/JsonDiffCustomTheme.css";
1010
import { useSearch } from "../hooks/useSearch";
1111
import { fastHash } from "../utils/json-diff/diff-hash";
1212
import { expandSegment, hasExpandedSegments, hideAllSegments } from "../utils/json-diff/segment-util";
13+
import { calculateLineCountStats } from "../utils/lineCountUtils";
14+
import { calculateObjectCountStats } from "../utils/objectCountUtils";
1315
import { buildViewFromSegments, generateSegments } from "../utils/preprocessDiff";
1416
import { DiffMinimap } from "./DiffMinimap";
17+
import LineCountDisplay from "./LineCountDisplay";
18+
import ObjectCountDisplay from "./ObjectCountDisplay";
1519
import SearchboxHolder from "./SearchboxHolder";
1620
import VirtualDiffGrid from "./VirtualDiffGrid";
1721

@@ -32,6 +36,8 @@ export const VirtualizedDiffViewer: React.FC<VirtualizedDiffViewerProps> = ({
3236
miniMapWidth,
3337
inlineDiffOptions,
3438
overScanCount,
39+
showLineCount = false,
40+
showObjectCountStats = false,
3541
}) => {
3642
const listRef = useRef<List>(null);
3743
const getDiffDataRef = useRef<typeof getDiffData>();
@@ -58,6 +64,28 @@ export const VirtualizedDiffViewer: React.FC<VirtualizedDiffViewerProps> = ({
5864
return differ.diff(oldValue, newValue);
5965
}, [oldValue, newValue, differ]);
6066

67+
const lineCountStats = useMemo((): LineCountStats => {
68+
if (!diffData || (diffData[0].length === 0 && diffData[1].length === 0)) {
69+
return { added: 0, removed: 0, modified: 0, total: 0 };
70+
}
71+
return calculateLineCountStats(diffData as [DiffResult[], DiffResult[]]);
72+
}, [diffData]);
73+
74+
const objectCountStats = useMemo((): ObjectCountStats => {
75+
// Only calculate object counts when using compare-key method
76+
if (!differOptions?.arrayDiffMethod || differOptions.arrayDiffMethod !== "compare-key" || !differOptions.compareKey) {
77+
return { added: 0, removed: 0, modified: 0, total: 0 };
78+
}
79+
80+
try {
81+
return calculateObjectCountStats(oldValue, newValue, differOptions.compareKey);
82+
}
83+
catch (error) {
84+
console.warn("Error calculating object count stats:", error);
85+
return { added: 0, removed: 0, modified: 0, total: 0 };
86+
}
87+
}, [oldValue, newValue, differOptions]);
88+
6189
const [scrollTop, setScrollTop] = useState(0);
6290
const [segments, setSegments] = useState<SegmentItem[]>([]);
6391
const [rawLeftDiff, rawRightDiff] = diffData;
@@ -138,6 +166,12 @@ export const VirtualizedDiffViewer: React.FC<VirtualizedDiffViewerProps> = ({
138166
<div><span>{leftTitle}</span></div>
139167
<div><span>{rightTitle}</span></div>
140168
</div>
169+
{showLineCount && (
170+
<LineCountDisplay stats={lineCountStats} />
171+
)}
172+
{showObjectCountStats && differOptions?.arrayDiffMethod === "compare-key" && differOptions?.compareKey && (
173+
<ObjectCountDisplay stats={objectCountStats} />
174+
)}
141175
</div>
142176

143177
{/* List & Minimap */}

src/components/DiffViewer/styles/JsonDiffCustomTheme.css

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,116 @@
4242
background: rgba(182, 180, 67, 0.08);
4343
}
4444

45+
/* LINE COUNT DISPLAY */
46+
.line-count-display {
47+
display: flex;
48+
gap: 12px;
49+
margin-top: 5px;
50+
align-items: center;
51+
font-size: 11px;
52+
color: #f8f8f2;
53+
margin-left: auto;
54+
justify-content: space-between;
55+
}
56+
57+
.line-count-item-sub-holder {
58+
display: flex;
59+
align-items: center;
60+
gap: 4px;
61+
}
62+
63+
.line-count-item {
64+
padding: 2px 6px;
65+
border-radius: 3px;
66+
font-weight: 500;
67+
white-space: nowrap;
68+
}
69+
70+
.line-count-item.added {
71+
background: rgba(100, 182, 67, 0.2);
72+
color: #a5ff99;
73+
border: 1px solid rgba(100, 182, 67, 0.3);
74+
}
75+
76+
.line-count-item.removed {
77+
background: rgba(160, 128, 100, 0.2);
78+
color: #ffaa99;
79+
border: 1px solid rgba(160, 128, 100, 0.3);
80+
}
81+
82+
.line-count-item.modified {
83+
background: rgba(182, 180, 67, 0.2);
84+
color: #ecff99;
85+
border: 1px solid rgba(182, 180, 67, 0.3);
86+
}
87+
88+
.line-count-item.total {
89+
background: rgba(69, 96, 248, 0.2);
90+
color: #4560f8;
91+
border: 1px solid rgba(69, 96, 248, 0.3);
92+
}
93+
94+
.line-count-item.no-changes {
95+
background: rgba(248, 248, 242, 0.1);
96+
color: #f8f8f2;
97+
border: 1px solid rgba(248, 248, 242, 0.2);
98+
}
99+
100+
/* OBJECT COUNT DISPLAY */
101+
.object-count-display {
102+
display: flex;
103+
gap: 12px;
104+
margin-top: 5px;
105+
align-items: center;
106+
font-size: 11px;
107+
color: #f8f8f2;
108+
margin-left: auto;
109+
justify-content: space-between;
110+
}
111+
112+
.object-count-item-sub-holder {
113+
display: flex;
114+
align-items: center;
115+
gap: 4px;
116+
}
117+
118+
.object-count-item {
119+
padding: 2px 6px;
120+
border-radius: 3px;
121+
font-weight: 500;
122+
white-space: nowrap;
123+
}
124+
125+
.object-count-item.added {
126+
background: rgba(100, 182, 67, 0.2);
127+
color: #a5ff99;
128+
border: 1px solid rgba(100, 182, 67, 0.3);
129+
}
130+
131+
.object-count-item.removed {
132+
background: rgba(160, 128, 100, 0.2);
133+
color: #ffaa99;
134+
border: 1px solid rgba(160, 128, 100, 0.3);
135+
}
136+
137+
.object-count-item.modified {
138+
background: rgba(182, 180, 67, 0.2);
139+
color: #ecff99;
140+
border: 1px solid rgba(182, 180, 67, 0.3);
141+
}
142+
143+
.object-count-item.total {
144+
background: rgba(69, 96, 248, 0.2);
145+
color: #4560f8;
146+
border: 1px solid rgba(69, 96, 248, 0.3);
147+
}
148+
149+
.object-count-item.no-changes {
150+
background: rgba(248, 248, 242, 0.1);
151+
color: #f8f8f2;
152+
border: 1px solid rgba(248, 248, 242, 0.2);
153+
}
154+
45155
.json-diff-viewer.json-diff-viewer-theme-custom .empty-equal-cell {
46156
opacity: 0.4;
47157
background: repeating-linear-gradient(-53deg, rgb(69, 69, 70), rgb(69, 69, 70) 1.5px, #282a36 1.5px, #282a36 4px);

0 commit comments

Comments
 (0)