react-diff-viewer-continued 4.0.6 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +21 -0
- package/.github/workflows/publish.yml +48 -0
- package/.github/workflows/release.yml +23 -8
- package/CHANGELOG.md +20 -0
- package/README.md +147 -29
- package/lib/cjs/src/compute-hidden-blocks.js +1 -4
- package/lib/cjs/src/compute-lines.d.ts +27 -3
- package/lib/cjs/src/compute-lines.js +330 -51
- package/lib/cjs/src/computeWorker.d.ts +1 -0
- package/lib/cjs/src/computeWorker.js +10 -0
- package/lib/cjs/src/expand.js +3 -6
- package/lib/cjs/src/fold.js +3 -6
- package/lib/cjs/src/index.d.ts +111 -2
- package/lib/cjs/src/index.js +737 -130
- package/lib/cjs/src/styles.d.ts +3 -0
- package/lib/cjs/src/styles.js +68 -29
- package/lib/esm/src/compute-lines.js +325 -10
- package/lib/esm/src/computeWorker.js +10 -0
- package/lib/esm/src/index.js +696 -52
- package/lib/esm/src/styles.js +65 -21
- package/package.json +32 -32
- package/playwright-report/index.html +76 -0
- package/publish-examples.sh +0 -0
- package/test-results/.last-run.json +4 -0
- package/tsconfig.json +1 -1
package/lib/cjs/src/index.js
CHANGED
|
@@ -1,60 +1,201 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
11
|
+
import cn from "classnames";
|
|
12
|
+
import * as React from "react";
|
|
13
|
+
import memoize from "memoize-one";
|
|
14
|
+
import { computeHiddenBlocks } from "./compute-hidden-blocks.js";
|
|
15
|
+
import { DiffMethod, DiffType, computeLineInformationWorker, computeDiff, } from "./compute-lines.js";
|
|
16
|
+
import { Expand } from "./expand.js";
|
|
17
|
+
import computeStyles from "./styles.js";
|
|
18
|
+
import { Fold } from "./fold.js";
|
|
19
|
+
/**
|
|
20
|
+
* Applies diff styling (ins/del tags) to pre-highlighted HTML by walking through
|
|
21
|
+
* the HTML and wrapping text portions based on character positions in the diff.
|
|
22
|
+
*/
|
|
23
|
+
function applyDiffToHighlightedHtml(html, diffArray, styles) {
|
|
24
|
+
const ranges = [];
|
|
25
|
+
let pos = 0;
|
|
26
|
+
for (const diff of diffArray) {
|
|
27
|
+
const value = typeof diff.value === "string" ? diff.value : "";
|
|
28
|
+
if (value.length > 0) {
|
|
29
|
+
ranges.push({ start: pos, end: pos + value.length, type: diff.type });
|
|
30
|
+
pos += value.length;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const segments = [];
|
|
34
|
+
let i = 0;
|
|
35
|
+
while (i < html.length) {
|
|
36
|
+
if (html[i] === "<") {
|
|
37
|
+
const tagEnd = html.indexOf(">", i);
|
|
38
|
+
if (tagEnd === -1) {
|
|
39
|
+
// Malformed HTML, treat rest as text
|
|
40
|
+
segments.push({ type: "text", content: html.slice(i) });
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
segments.push({ type: "tag", content: html.slice(i, tagEnd + 1) });
|
|
44
|
+
i = tagEnd + 1;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// Find the next tag or end of string
|
|
48
|
+
let textEnd = html.indexOf("<", i);
|
|
49
|
+
if (textEnd === -1)
|
|
50
|
+
textEnd = html.length;
|
|
51
|
+
segments.push({ type: "text", content: html.slice(i, textEnd) });
|
|
52
|
+
i = textEnd;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Helper to decode HTML entities for character counting
|
|
56
|
+
function decodeEntities(text) {
|
|
57
|
+
return text
|
|
58
|
+
.replace(/</g, "<")
|
|
59
|
+
.replace(/>/g, ">")
|
|
60
|
+
.replace(/&/g, "&")
|
|
61
|
+
.replace(/"/g, '"')
|
|
62
|
+
.replace(/'/g, "'")
|
|
63
|
+
.replace(/ /g, "\u00A0");
|
|
7
64
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
65
|
+
// Helper to get the wrapper tag for a diff type
|
|
66
|
+
function getWrapper(type) {
|
|
67
|
+
if (type === DiffType.ADDED) {
|
|
68
|
+
return {
|
|
69
|
+
open: `<ins class="${styles.wordDiff} ${styles.wordAdded}">`,
|
|
70
|
+
close: "</ins>",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (type === DiffType.REMOVED) {
|
|
74
|
+
return {
|
|
75
|
+
open: `<del class="${styles.wordDiff} ${styles.wordRemoved}">`,
|
|
76
|
+
close: "</del>",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
open: `<span class="${styles.wordDiff}">`,
|
|
81
|
+
close: "</span>",
|
|
24
82
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
83
|
+
}
|
|
84
|
+
// Process segments, tracking text position
|
|
85
|
+
let textPos = 0;
|
|
86
|
+
let result = "";
|
|
87
|
+
for (const segment of segments) {
|
|
88
|
+
if (segment.type === "tag") {
|
|
89
|
+
result += segment.content;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// Text segment - we need to split it according to diff ranges
|
|
93
|
+
const text = segment.content;
|
|
94
|
+
const decodedText = decodeEntities(text);
|
|
95
|
+
// Walk through the text, character by character (in decoded form)
|
|
96
|
+
// but output the original encoded form
|
|
97
|
+
let localDecodedPos = 0;
|
|
98
|
+
let localEncodedPos = 0;
|
|
99
|
+
while (localDecodedPos < decodedText.length) {
|
|
100
|
+
const globalPos = textPos + localDecodedPos;
|
|
101
|
+
// Find the range that covers this position
|
|
102
|
+
const range = ranges.find((r) => globalPos >= r.start && globalPos < r.end);
|
|
103
|
+
if (!range) {
|
|
104
|
+
// No range covers this position (shouldn't happen, but be safe)
|
|
105
|
+
// Just output the character
|
|
106
|
+
const char = text[localEncodedPos];
|
|
107
|
+
result += char;
|
|
108
|
+
localEncodedPos++;
|
|
109
|
+
localDecodedPos++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// How many decoded characters until the end of this range?
|
|
113
|
+
const charsUntilRangeEnd = range.end - globalPos;
|
|
114
|
+
// How many decoded characters until the end of this text segment?
|
|
115
|
+
const charsUntilTextEnd = decodedText.length - localDecodedPos;
|
|
116
|
+
// Take the minimum
|
|
117
|
+
const charsToTake = Math.min(charsUntilRangeEnd, charsUntilTextEnd);
|
|
118
|
+
// Now we need to find the corresponding encoded substring
|
|
119
|
+
// Walk through encoded text, counting decoded characters
|
|
120
|
+
let encodedChunkEnd = localEncodedPos;
|
|
121
|
+
let decodedCount = 0;
|
|
122
|
+
while (decodedCount < charsToTake && encodedChunkEnd < text.length) {
|
|
123
|
+
if (text[encodedChunkEnd] === "&") {
|
|
124
|
+
// Find entity end
|
|
125
|
+
const entityEnd = text.indexOf(";", encodedChunkEnd);
|
|
126
|
+
if (entityEnd !== -1 && entityEnd - encodedChunkEnd < 10) {
|
|
127
|
+
encodedChunkEnd = entityEnd + 1;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
encodedChunkEnd++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
encodedChunkEnd++;
|
|
135
|
+
}
|
|
136
|
+
decodedCount++;
|
|
137
|
+
}
|
|
138
|
+
const chunk = text.slice(localEncodedPos, encodedChunkEnd);
|
|
139
|
+
const wrapper = getWrapper(range.type);
|
|
140
|
+
if (wrapper) {
|
|
141
|
+
result += wrapper.open + chunk + wrapper.close;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
result += chunk;
|
|
145
|
+
}
|
|
146
|
+
localEncodedPos = encodedChunkEnd;
|
|
147
|
+
localDecodedPos += charsToTake;
|
|
148
|
+
}
|
|
149
|
+
textPos += decodedText.length;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
export var LineNumberPrefix;
|
|
51
155
|
(function (LineNumberPrefix) {
|
|
52
156
|
LineNumberPrefix["LEFT"] = "L";
|
|
53
157
|
LineNumberPrefix["RIGHT"] = "R";
|
|
54
|
-
})(LineNumberPrefix || (
|
|
158
|
+
})(LineNumberPrefix || (LineNumberPrefix = {}));
|
|
55
159
|
class DiffViewer extends React.Component {
|
|
56
160
|
constructor(props) {
|
|
57
161
|
super(props);
|
|
162
|
+
// Cache for on-demand word diff computation
|
|
163
|
+
this.wordDiffCache = new Map();
|
|
164
|
+
// Refs for measuring content column width and character width
|
|
165
|
+
this.contentColumnRef = React.createRef();
|
|
166
|
+
this.charMeasureRef = React.createRef();
|
|
167
|
+
this.stickyHeaderRef = React.createRef();
|
|
168
|
+
this.resizeObserver = null;
|
|
169
|
+
/**
|
|
170
|
+
* Computes word diff on-demand for a line, with caching.
|
|
171
|
+
* This is used when word diff was deferred during initial computation.
|
|
172
|
+
*/
|
|
173
|
+
this.getWordDiffValues = (left, right, lineIndex) => {
|
|
174
|
+
// Handle empty left/right
|
|
175
|
+
if (!left || !right) {
|
|
176
|
+
return { leftValue: left === null || left === void 0 ? void 0 : left.value, rightValue: right === null || right === void 0 ? void 0 : right.value };
|
|
177
|
+
}
|
|
178
|
+
// If no raw values, word diff was already computed or disabled
|
|
179
|
+
// Use explicit undefined check since empty string is a valid raw value
|
|
180
|
+
if (left.rawValue === undefined || right.rawValue === undefined) {
|
|
181
|
+
return { leftValue: left.value, rightValue: right.value };
|
|
182
|
+
}
|
|
183
|
+
// Check cache
|
|
184
|
+
const cacheKey = `${lineIndex}-${left.rawValue}-${right.rawValue}`;
|
|
185
|
+
let cached = this.wordDiffCache.get(cacheKey);
|
|
186
|
+
if (!cached) {
|
|
187
|
+
// Compute word diff on-demand
|
|
188
|
+
// Use CHARS method for on-demand computation since rawValue is always a string
|
|
189
|
+
// (JSON/YAML methods only work with objects, not the string lines we have here)
|
|
190
|
+
const compareMethod = (this.props.compareMethod === DiffMethod.JSON || this.props.compareMethod === DiffMethod.YAML)
|
|
191
|
+
? DiffMethod.CHARS
|
|
192
|
+
: this.props.compareMethod;
|
|
193
|
+
const computed = computeDiff(left.rawValue, right.rawValue, compareMethod);
|
|
194
|
+
cached = { left: computed.left, right: computed.right };
|
|
195
|
+
this.wordDiffCache.set(cacheKey, cached);
|
|
196
|
+
}
|
|
197
|
+
return { leftValue: cached.left, rightValue: cached.right };
|
|
198
|
+
};
|
|
58
199
|
/**
|
|
59
200
|
* Resets code block expand to the initial stage. Will be exposed to the parent component via
|
|
60
201
|
* refs.
|
|
@@ -75,8 +216,28 @@ class DiffViewer extends React.Component {
|
|
|
75
216
|
this.onBlockExpand = (id) => {
|
|
76
217
|
const prevState = this.state.expandedBlocks.slice();
|
|
77
218
|
prevState.push(id);
|
|
78
|
-
this.setState({
|
|
79
|
-
|
|
219
|
+
this.setState({ expandedBlocks: prevState }, () => this.recalculateOffsets());
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* Recalculates cumulative offsets based on current measurements.
|
|
223
|
+
* Called on resize and when blocks are expanded/collapsed.
|
|
224
|
+
*/
|
|
225
|
+
this.recalculateOffsets = () => {
|
|
226
|
+
var _a;
|
|
227
|
+
if (!this.props.infiniteLoading)
|
|
228
|
+
return;
|
|
229
|
+
const columnWidth = this.measureContentColumnWidth();
|
|
230
|
+
const charWidth = this.measureCharWidth();
|
|
231
|
+
if (!columnWidth)
|
|
232
|
+
return;
|
|
233
|
+
const cacheKey = this.getMemoisedKey();
|
|
234
|
+
const { lineInformation, lineBlocks, blocks } = (_a = this.state.computedDiffResult[cacheKey]) !== null && _a !== void 0 ? _a : {};
|
|
235
|
+
if (!lineInformation)
|
|
236
|
+
return;
|
|
237
|
+
const offsets = this.buildCumulativeOffsets(lineInformation, lineBlocks, blocks, this.state.expandedBlocks, this.props.showDiffOnly, charWidth, columnWidth, this.props.splitView);
|
|
238
|
+
this.setState({ cumulativeOffsets: offsets, contentColumnWidth: columnWidth, charWidth }, () => {
|
|
239
|
+
// Force a scroll position update to recalculate visible rows with new offsets
|
|
240
|
+
this.onScroll();
|
|
80
241
|
});
|
|
81
242
|
};
|
|
82
243
|
/**
|
|
@@ -85,7 +246,7 @@ class DiffViewer extends React.Component {
|
|
|
85
246
|
*
|
|
86
247
|
* @param styles User supplied style overrides.
|
|
87
248
|
*/
|
|
88
|
-
this.computeStyles = (
|
|
249
|
+
this.computeStyles = memoize(computeStyles);
|
|
89
250
|
/**
|
|
90
251
|
* Returns a function with clicked line number in the closure. Returns an no-op function when no
|
|
91
252
|
* onLineNumberClick handler is supplied.
|
|
@@ -98,6 +259,19 @@ class DiffViewer extends React.Component {
|
|
|
98
259
|
}
|
|
99
260
|
return () => { };
|
|
100
261
|
};
|
|
262
|
+
/**
|
|
263
|
+
* Checks if the current compare method should show word-level highlighting.
|
|
264
|
+
* Character, word-level, JSON, and YAML diffs benefit from highlighting individual changes.
|
|
265
|
+
* JSON/YAML use CHARS internally for word-level diff, so they should be highlighted.
|
|
266
|
+
*/
|
|
267
|
+
this.shouldHighlightWordDiff = () => {
|
|
268
|
+
const { compareMethod } = this.props;
|
|
269
|
+
return (compareMethod === DiffMethod.CHARS ||
|
|
270
|
+
compareMethod === DiffMethod.WORDS ||
|
|
271
|
+
compareMethod === DiffMethod.WORDS_WITH_SPACE ||
|
|
272
|
+
compareMethod === DiffMethod.JSON ||
|
|
273
|
+
compareMethod === DiffMethod.YAML);
|
|
274
|
+
};
|
|
101
275
|
/**
|
|
102
276
|
* Maps over the word diff and constructs the required React elements to show word diff.
|
|
103
277
|
*
|
|
@@ -105,18 +279,61 @@ class DiffViewer extends React.Component {
|
|
|
105
279
|
* @param renderer Optional renderer to format diff words. Useful for syntax highlighting.
|
|
106
280
|
*/
|
|
107
281
|
this.renderWordDiff = (diffArray, renderer) => {
|
|
282
|
+
var _a, _b;
|
|
283
|
+
const showHighlight = this.shouldHighlightWordDiff();
|
|
284
|
+
const { compareMethod } = this.props;
|
|
285
|
+
// Don't apply syntax highlighting for JSON/YAML - their word diffs are computed
|
|
286
|
+
// on-demand from raw strings and syntax highlighting creates messy fragmented tokens.
|
|
287
|
+
const skipSyntaxHighlighting = compareMethod === DiffMethod.JSON || compareMethod === DiffMethod.YAML;
|
|
288
|
+
// Reconstruct the full line from diff chunks
|
|
289
|
+
const fullLine = diffArray
|
|
290
|
+
.map((d) => (typeof d.value === "string" ? d.value : ""))
|
|
291
|
+
.join("");
|
|
292
|
+
// For very long lines (>500 chars), skip fancy processing - just render plain text
|
|
293
|
+
// without word-level highlighting to avoid performance issues
|
|
294
|
+
const MAX_LINE_LENGTH = 500;
|
|
295
|
+
if (fullLine.length > MAX_LINE_LENGTH) {
|
|
296
|
+
return [_jsx("span", { children: fullLine }, "long-line")];
|
|
297
|
+
}
|
|
298
|
+
// If we have a renderer and syntax highlighting is enabled, try to highlight
|
|
299
|
+
// the full line first, then apply diff styling to preserve proper tokenization.
|
|
300
|
+
if (renderer && !skipSyntaxHighlighting) {
|
|
301
|
+
// Get the syntax-highlighted content
|
|
302
|
+
const highlighted = renderer(fullLine);
|
|
303
|
+
// Check if the renderer uses dangerouslySetInnerHTML (common with Prism, highlight.js, etc.)
|
|
304
|
+
const htmlContent = (_b = (_a = highlighted === null || highlighted === void 0 ? void 0 : highlighted.props) === null || _a === void 0 ? void 0 : _a.dangerouslySetInnerHTML) === null || _b === void 0 ? void 0 : _b.__html;
|
|
305
|
+
if (typeof htmlContent === "string") {
|
|
306
|
+
// Apply diff styling to the highlighted HTML
|
|
307
|
+
const styledHtml = applyDiffToHighlightedHtml(htmlContent, diffArray, {
|
|
308
|
+
wordDiff: this.styles.wordDiff,
|
|
309
|
+
wordAdded: showHighlight ? this.styles.wordAdded : "",
|
|
310
|
+
wordRemoved: showHighlight ? this.styles.wordRemoved : "",
|
|
311
|
+
});
|
|
312
|
+
// Clone the element with the modified HTML
|
|
313
|
+
return [
|
|
314
|
+
React.cloneElement(highlighted, {
|
|
315
|
+
key: "highlighted-diff",
|
|
316
|
+
dangerouslySetInnerHTML: { __html: styledHtml },
|
|
317
|
+
}),
|
|
318
|
+
];
|
|
319
|
+
}
|
|
320
|
+
// Renderer doesn't use dangerouslySetInnerHTML - fall through to per-chunk rendering
|
|
321
|
+
}
|
|
322
|
+
// Fallback: render each chunk separately (used for JSON/YAML or non-HTML renderers)
|
|
108
323
|
return diffArray.map((wordDiff, i) => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
[this.styles.
|
|
119
|
-
}), children: content }, i)) :
|
|
324
|
+
let content;
|
|
325
|
+
if (typeof wordDiff.value === "string") {
|
|
326
|
+
content = wordDiff.value;
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
// If wordDiff.value is DiffInformation[], we don't handle it. See c0c99f5712.
|
|
330
|
+
content = undefined;
|
|
331
|
+
}
|
|
332
|
+
return wordDiff.type === DiffType.ADDED ? (_jsx("ins", { className: cn(this.styles.wordDiff, {
|
|
333
|
+
[this.styles.wordAdded]: showHighlight,
|
|
334
|
+
}), children: content }, i)) : wordDiff.type === DiffType.REMOVED ? (_jsx("del", { className: cn(this.styles.wordDiff, {
|
|
335
|
+
[this.styles.wordRemoved]: showHighlight,
|
|
336
|
+
}), children: content }, i)) : (_jsx("span", { className: cn(this.styles.wordDiff), children: content }, i));
|
|
120
337
|
});
|
|
121
338
|
};
|
|
122
339
|
/**
|
|
@@ -137,9 +354,9 @@ class DiffViewer extends React.Component {
|
|
|
137
354
|
const additionalLineNumberTemplate = `${additionalPrefix}-${additionalLineNumber}`;
|
|
138
355
|
const highlightLine = this.props.highlightLines.includes(lineNumberTemplate) ||
|
|
139
356
|
this.props.highlightLines.includes(additionalLineNumberTemplate);
|
|
140
|
-
const added = type ===
|
|
141
|
-
const removed = type ===
|
|
142
|
-
const changed = type ===
|
|
357
|
+
const added = type === DiffType.ADDED;
|
|
358
|
+
const removed = type === DiffType.REMOVED;
|
|
359
|
+
const changed = type === DiffType.CHANGED;
|
|
143
360
|
let content;
|
|
144
361
|
const hasWordDiff = Array.isArray(value);
|
|
145
362
|
if (hasWordDiff) {
|
|
@@ -158,20 +375,20 @@ class DiffViewer extends React.Component {
|
|
|
158
375
|
else if (removed && !hasWordDiff) {
|
|
159
376
|
ElementType = "del";
|
|
160
377
|
}
|
|
161
|
-
return ((
|
|
378
|
+
return (_jsxs(_Fragment, { children: [!this.props.hideLineNumbers && (_jsx("td", { onClick: lineNumber && this.onLineNumberClickProxy(lineNumberTemplate), className: cn(this.styles.gutter, {
|
|
162
379
|
[this.styles.emptyGutter]: !lineNumber,
|
|
163
380
|
[this.styles.diffAdded]: added,
|
|
164
381
|
[this.styles.diffRemoved]: removed,
|
|
165
382
|
[this.styles.diffChanged]: changed,
|
|
166
383
|
[this.styles.highlightedGutter]: highlightLine,
|
|
167
|
-
}), children: (
|
|
168
|
-
this.onLineNumberClickProxy(additionalLineNumberTemplate), className: (
|
|
384
|
+
}), children: _jsx("pre", { className: this.styles.lineNumber, children: lineNumber }) })), !this.props.splitView && !this.props.hideLineNumbers && (_jsx("td", { onClick: additionalLineNumber &&
|
|
385
|
+
this.onLineNumberClickProxy(additionalLineNumberTemplate), className: cn(this.styles.gutter, {
|
|
169
386
|
[this.styles.emptyGutter]: !additionalLineNumber,
|
|
170
387
|
[this.styles.diffAdded]: added,
|
|
171
388
|
[this.styles.diffRemoved]: removed,
|
|
172
389
|
[this.styles.diffChanged]: changed,
|
|
173
390
|
[this.styles.highlightedGutter]: highlightLine,
|
|
174
|
-
}), children: (
|
|
391
|
+
}), children: _jsx("pre", { className: this.styles.lineNumber, children: additionalLineNumber }) })), this.props.renderGutter
|
|
175
392
|
? this.props.renderGutter({
|
|
176
393
|
lineNumber,
|
|
177
394
|
type,
|
|
@@ -181,13 +398,13 @@ class DiffViewer extends React.Component {
|
|
|
181
398
|
additionalPrefix,
|
|
182
399
|
styles: this.styles,
|
|
183
400
|
})
|
|
184
|
-
: null, (
|
|
401
|
+
: null, _jsx("td", { className: cn(this.styles.marker, {
|
|
185
402
|
[this.styles.emptyLine]: !content,
|
|
186
403
|
[this.styles.diffAdded]: added,
|
|
187
404
|
[this.styles.diffRemoved]: removed,
|
|
188
405
|
[this.styles.diffChanged]: changed,
|
|
189
406
|
[this.styles.highlightedLine]: highlightLine,
|
|
190
|
-
}), children: (
|
|
407
|
+
}), children: _jsxs("pre", { children: [added && "+", removed && "-"] }) }), _jsx("td", { ref: prefix === LineNumberPrefix.LEFT && !this.state.cumulativeOffsets ? this.contentColumnRef : undefined, className: cn(this.styles.content, {
|
|
191
408
|
[this.styles.emptyLine]: !content,
|
|
192
409
|
[this.styles.diffAdded]: added,
|
|
193
410
|
[this.styles.diffRemoved]: removed,
|
|
@@ -205,7 +422,7 @@ class DiffViewer extends React.Component {
|
|
|
205
422
|
? "Added line"
|
|
206
423
|
: removed && !hasWordDiff
|
|
207
424
|
? "Removed line"
|
|
208
|
-
: undefined, children: (
|
|
425
|
+
: undefined, children: _jsx(ElementType, { className: this.styles.contentText, children: content }) })] }));
|
|
209
426
|
};
|
|
210
427
|
/**
|
|
211
428
|
* Generates lines for split view.
|
|
@@ -216,7 +433,9 @@ class DiffViewer extends React.Component {
|
|
|
216
433
|
* @param index React key for the lines.
|
|
217
434
|
*/
|
|
218
435
|
this.renderSplitView = ({ left, right }, index) => {
|
|
219
|
-
|
|
436
|
+
// Compute word diff on-demand if deferred
|
|
437
|
+
const { leftValue, rightValue } = this.getWordDiffValues(left, right, index);
|
|
438
|
+
return (_jsxs("tr", { className: this.styles.line, children: [this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, leftValue), this.renderLine(right.lineNumber, right.type, LineNumberPrefix.RIGHT, rightValue)] }, index));
|
|
220
439
|
};
|
|
221
440
|
/**
|
|
222
441
|
* Generates lines for inline view.
|
|
@@ -227,20 +446,22 @@ class DiffViewer extends React.Component {
|
|
|
227
446
|
* @param index React key for the lines.
|
|
228
447
|
*/
|
|
229
448
|
this.renderInlineView = ({ left, right }, index) => {
|
|
449
|
+
// Compute word diff on-demand if deferred
|
|
450
|
+
const { leftValue, rightValue } = this.getWordDiffValues(left, right, index);
|
|
230
451
|
let content;
|
|
231
|
-
if (left.type ===
|
|
232
|
-
return ((
|
|
452
|
+
if (left.type === DiffType.REMOVED && right.type === DiffType.ADDED) {
|
|
453
|
+
return (_jsxs(React.Fragment, { children: [_jsx("tr", { className: this.styles.line, children: this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, leftValue, null) }), _jsx("tr", { className: this.styles.line, children: this.renderLine(null, right.type, LineNumberPrefix.RIGHT, rightValue, right.lineNumber, LineNumberPrefix.RIGHT) })] }, index));
|
|
233
454
|
}
|
|
234
|
-
if (left.type ===
|
|
235
|
-
content = this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT,
|
|
455
|
+
if (left.type === DiffType.REMOVED) {
|
|
456
|
+
content = this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, leftValue, null);
|
|
236
457
|
}
|
|
237
|
-
if (left.type ===
|
|
238
|
-
content = this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT,
|
|
458
|
+
if (left.type === DiffType.DEFAULT) {
|
|
459
|
+
content = this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, leftValue, right.lineNumber, LineNumberPrefix.RIGHT);
|
|
239
460
|
}
|
|
240
|
-
if (right.type ===
|
|
241
|
-
content = this.renderLine(null, right.type, LineNumberPrefix.RIGHT,
|
|
461
|
+
if (right.type === DiffType.ADDED) {
|
|
462
|
+
content = this.renderLine(null, right.type, LineNumberPrefix.RIGHT, rightValue, right.lineNumber);
|
|
242
463
|
}
|
|
243
|
-
return ((
|
|
464
|
+
return (_jsx("tr", { className: this.styles.line, children: content }, index));
|
|
244
465
|
};
|
|
245
466
|
/**
|
|
246
467
|
* Returns a function with clicked block number in the closure.
|
|
@@ -259,51 +480,257 @@ class DiffViewer extends React.Component {
|
|
|
259
480
|
*/
|
|
260
481
|
this.renderSkippedLineIndicator = (num, blockNumber, leftBlockLineNumber, rightBlockLineNumber) => {
|
|
261
482
|
const { hideLineNumbers, splitView } = this.props;
|
|
262
|
-
const message = this.props.codeFoldMessageRenderer ? (this.props.codeFoldMessageRenderer(num, leftBlockLineNumber, rightBlockLineNumber)) : ((
|
|
263
|
-
const content = ((
|
|
483
|
+
const message = this.props.codeFoldMessageRenderer ? (this.props.codeFoldMessageRenderer(num, leftBlockLineNumber, rightBlockLineNumber)) : (_jsxs("span", { className: this.styles.codeFoldContent, children: ["@@ -", leftBlockLineNumber - num, ",", num, " +", rightBlockLineNumber - num, ",", num, " @@"] }));
|
|
484
|
+
const content = (_jsx("td", { className: this.styles.codeFoldContentContainer, children: _jsx("button", { type: "button", className: this.styles.codeFoldExpandButton, onClick: this.onBlockClickProxy(blockNumber), tabIndex: 0, children: message }) }));
|
|
264
485
|
const isUnifiedViewWithoutLineNumbers = !splitView && !hideLineNumbers;
|
|
265
|
-
|
|
486
|
+
const expandGutter = (_jsx("td", { className: this.styles.codeFoldGutter, children: _jsx(Expand, {}) }));
|
|
487
|
+
return (_jsxs("tr", { className: this.styles.codeFold, onClick: this.onBlockClickProxy(blockNumber), role: "button", tabIndex: 0, children: [!hideLineNumbers && expandGutter, this.props.renderGutter ? (_jsx("td", { className: this.styles.codeFoldGutter })) : null, _jsx("td", { className: cn({
|
|
266
488
|
[this.styles.codeFoldGutter]: isUnifiedViewWithoutLineNumbers,
|
|
267
|
-
}) }), isUnifiedViewWithoutLineNumbers ? ((
|
|
489
|
+
}) }), isUnifiedViewWithoutLineNumbers ? (_jsxs(React.Fragment, { children: [_jsx("td", {}), content] })) : (_jsxs(React.Fragment, { children: [content, this.props.renderGutter ? _jsx("td", {}) : null, _jsx("td", {}), _jsx("td", {}), !hideLineNumbers ? _jsx("td", {}) : null] }))] }, `${leftBlockLineNumber}-${rightBlockLineNumber}`));
|
|
268
490
|
};
|
|
269
491
|
/**
|
|
270
|
-
*
|
|
492
|
+
*
|
|
493
|
+
* Generates a unique cache key based on the current props used in diff computation.
|
|
494
|
+
*
|
|
495
|
+
* This key is used to memoize results and avoid recomputation for the same inputs.
|
|
496
|
+
* @returns A stringified JSON key representing the current diff settings and input values.
|
|
497
|
+
*
|
|
271
498
|
*/
|
|
272
|
-
this.
|
|
273
|
-
const { oldValue, newValue,
|
|
274
|
-
|
|
499
|
+
this.getMemoisedKey = () => {
|
|
500
|
+
const { oldValue, newValue, disableWordDiff, compareMethod, linesOffset, alwaysShowLines, extraLinesSurroundingDiff, } = this.props;
|
|
501
|
+
return JSON.stringify({
|
|
502
|
+
oldValue,
|
|
503
|
+
newValue,
|
|
504
|
+
disableWordDiff,
|
|
505
|
+
compareMethod,
|
|
506
|
+
linesOffset,
|
|
507
|
+
alwaysShowLines,
|
|
508
|
+
extraLinesSurroundingDiff,
|
|
509
|
+
});
|
|
510
|
+
};
|
|
511
|
+
/**
|
|
512
|
+
* Computes and memoizes the diff result between `oldValue` and `newValue`.
|
|
513
|
+
*
|
|
514
|
+
* If a memoized result exists for the current input configuration, it uses that.
|
|
515
|
+
* Otherwise, it runs the diff logic in a Web Worker to avoid blocking the UI.
|
|
516
|
+
* It also computes hidden line blocks for collapsing unchanged sections,
|
|
517
|
+
* and stores the result in the local component state.
|
|
518
|
+
*/
|
|
519
|
+
this.memoisedCompute = () => __awaiter(this, void 0, void 0, function* () {
|
|
520
|
+
var _a;
|
|
521
|
+
const { oldValue, newValue, disableWordDiff, compareMethod, linesOffset } = this.props;
|
|
522
|
+
const cacheKey = this.getMemoisedKey();
|
|
523
|
+
if (!!this.state.computedDiffResult[cacheKey]) {
|
|
524
|
+
this.setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: false })));
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
// Defer word diff computation when using infinite loading with reasonable container height
|
|
528
|
+
// This significantly improves initial render time for large diffs
|
|
529
|
+
const containerHeight = (_a = this.props.infiniteLoading) === null || _a === void 0 ? void 0 : _a.containerHeight;
|
|
530
|
+
const containerHeightPx = containerHeight
|
|
531
|
+
? typeof containerHeight === 'number'
|
|
532
|
+
? containerHeight
|
|
533
|
+
: parseInt(containerHeight, 10) || 0
|
|
534
|
+
: 0;
|
|
535
|
+
const shouldDeferWordDiff = !disableWordDiff &&
|
|
536
|
+
!!this.props.infiniteLoading &&
|
|
537
|
+
containerHeightPx > 0 &&
|
|
538
|
+
containerHeightPx < 2000;
|
|
539
|
+
const { lineInformation, diffLines } = yield computeLineInformationWorker(oldValue, newValue, disableWordDiff, compareMethod, linesOffset, this.props.alwaysShowLines, shouldDeferWordDiff);
|
|
275
540
|
const extraLines = this.props.extraLinesSurroundingDiff < 0
|
|
276
541
|
? 0
|
|
277
542
|
: Math.round(this.props.extraLinesSurroundingDiff);
|
|
278
|
-
const { lineBlocks, blocks } =
|
|
279
|
-
|
|
280
|
-
|
|
543
|
+
const { lineBlocks, blocks } = computeHiddenBlocks(lineInformation, diffLines, extraLines);
|
|
544
|
+
this.state.computedDiffResult[cacheKey] = { lineInformation, lineBlocks, blocks };
|
|
545
|
+
this.setState((prev) => (Object.assign(Object.assign({}, prev), { computedDiffResult: this.state.computedDiffResult, isLoading: false })), () => {
|
|
546
|
+
// Trigger offset recalculation after diff is computed and rendered
|
|
547
|
+
// Use requestAnimationFrame to ensure DOM is ready for measurement
|
|
548
|
+
if (this.props.infiniteLoading) {
|
|
549
|
+
requestAnimationFrame(() => this.recalculateOffsets());
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
/**
|
|
554
|
+
* Handles scroll events on the scrollable container.
|
|
555
|
+
*
|
|
556
|
+
* Updates the visible start row for virtualization.
|
|
557
|
+
*/
|
|
558
|
+
this.onScroll = () => {
|
|
559
|
+
const container = this.state.scrollableContainerRef.current;
|
|
560
|
+
if (!container || !this.props.infiniteLoading)
|
|
561
|
+
return;
|
|
562
|
+
// Account for sticky header height in scroll calculations
|
|
563
|
+
const headerHeight = this.getStickyHeaderHeight();
|
|
564
|
+
const contentScrollTop = Math.max(0, container.scrollTop - headerHeight);
|
|
565
|
+
const { cumulativeOffsets } = this.state;
|
|
566
|
+
const newStartRow = cumulativeOffsets
|
|
567
|
+
? this.findLineAtOffset(contentScrollTop, cumulativeOffsets)
|
|
568
|
+
: Math.floor(contentScrollTop / DiffViewer.ESTIMATED_ROW_HEIGHT);
|
|
569
|
+
// Only update state if the start row changed (avoid unnecessary re-renders)
|
|
570
|
+
if (newStartRow !== this.state.visibleStartRow) {
|
|
571
|
+
this.setState({ visibleStartRow: newStartRow });
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
/**
|
|
575
|
+
* Generates the entire diff view with virtualization support.
|
|
576
|
+
*/
|
|
577
|
+
this.renderDiff = () => {
|
|
578
|
+
var _a, _b, _c, _d, _e, _f;
|
|
579
|
+
const { splitView, infiniteLoading, showDiffOnly } = this.props;
|
|
580
|
+
const { computedDiffResult, expandedBlocks, visibleStartRow, scrollableContainerRef, cumulativeOffsets } = this.state;
|
|
581
|
+
const cacheKey = this.getMemoisedKey();
|
|
582
|
+
const { lineInformation = [], lineBlocks = [], blocks = [] } = (_a = computedDiffResult[cacheKey]) !== null && _a !== void 0 ? _a : {};
|
|
583
|
+
// Calculate visible range for virtualization
|
|
584
|
+
let visibleRowStart = 0;
|
|
585
|
+
let visibleRowEnd = Infinity;
|
|
586
|
+
const buffer = 5; // render extra rows above/below viewport
|
|
587
|
+
if (infiniteLoading && scrollableContainerRef.current) {
|
|
588
|
+
const container = scrollableContainerRef.current;
|
|
589
|
+
// Account for sticky header height in scroll calculations
|
|
590
|
+
const headerHeight = this.getStickyHeaderHeight();
|
|
591
|
+
const contentScrollTop = Math.max(0, container.scrollTop - headerHeight);
|
|
592
|
+
if (cumulativeOffsets) {
|
|
593
|
+
// Variable height mode: use binary search to find visible range
|
|
594
|
+
const totalHeight = cumulativeOffsets[cumulativeOffsets.length - 1] || 0;
|
|
595
|
+
const lastRowIndex = cumulativeOffsets.length - 2;
|
|
596
|
+
visibleRowStart = Math.max(0, this.findLineAtOffset(contentScrollTop, cumulativeOffsets) - buffer);
|
|
597
|
+
visibleRowEnd = this.findLineAtOffset(contentScrollTop + container.clientHeight, cumulativeOffsets) + buffer;
|
|
598
|
+
// IMPORTANT: The calculated offsets may overestimate row heights (based on char count),
|
|
599
|
+
// but actual CSS rendering might produce shorter rows. To prevent empty space,
|
|
600
|
+
// ensure we render at least enough rows to fill the viewport using ESTIMATED_ROW_HEIGHT
|
|
601
|
+
// as a conservative minimum.
|
|
602
|
+
const minRowsToFillViewport = Math.ceil(container.clientHeight / DiffViewer.ESTIMATED_ROW_HEIGHT);
|
|
603
|
+
visibleRowEnd = Math.max(visibleRowEnd, visibleRowStart + minRowsToFillViewport + buffer);
|
|
604
|
+
// Also ensure we render all rows when near the bottom
|
|
605
|
+
if (contentScrollTop + container.clientHeight >= totalHeight - buffer * DiffViewer.ESTIMATED_ROW_HEIGHT) {
|
|
606
|
+
visibleRowEnd = lastRowIndex + buffer;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
// Fixed height fallback
|
|
611
|
+
const viewportRows = Math.ceil(container.clientHeight / DiffViewer.ESTIMATED_ROW_HEIGHT);
|
|
612
|
+
visibleRowStart = Math.max(0, visibleStartRow - buffer);
|
|
613
|
+
visibleRowEnd = visibleStartRow + viewportRows + buffer;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
// First pass: build a map of lineIndex -> renderedRowIndex
|
|
617
|
+
// This accounts for code folding where some lines don't render or render as fold indicators
|
|
618
|
+
const lineToRowMap = new Map();
|
|
619
|
+
const seenBlocks = new Set();
|
|
620
|
+
let currentRow = 0;
|
|
621
|
+
for (let i = 0; i < lineInformation.length; i++) {
|
|
622
|
+
const blockIndex = lineBlocks[i];
|
|
623
|
+
if (showDiffOnly && blockIndex !== undefined) {
|
|
624
|
+
if (!expandedBlocks.includes(blockIndex)) {
|
|
625
|
+
// Line is in a collapsed block
|
|
626
|
+
const lastLineOfBlock = blocks[blockIndex].endLine === i;
|
|
627
|
+
if (!seenBlocks.has(blockIndex) && lastLineOfBlock) {
|
|
628
|
+
// This line renders as a fold indicator
|
|
629
|
+
seenBlocks.add(blockIndex);
|
|
630
|
+
lineToRowMap.set(i, currentRow);
|
|
631
|
+
currentRow++;
|
|
632
|
+
}
|
|
633
|
+
// Other lines in collapsed block don't render
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
// Block is expanded, line renders normally
|
|
637
|
+
lineToRowMap.set(i, currentRow);
|
|
638
|
+
currentRow++;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
// Not in a block or showDiffOnly is false, line renders normally
|
|
643
|
+
lineToRowMap.set(i, currentRow);
|
|
644
|
+
currentRow++;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
const totalRenderedRows = currentRow;
|
|
648
|
+
// Second pass: render only lines in the visible range
|
|
649
|
+
const diffNodes = [];
|
|
650
|
+
let topPadding = 0;
|
|
651
|
+
let firstVisibleFound = false;
|
|
652
|
+
let lastRenderedRowIndex = -1;
|
|
653
|
+
seenBlocks.clear();
|
|
654
|
+
for (let lineIndex = 0; lineIndex < lineInformation.length; lineIndex++) {
|
|
655
|
+
const line = lineInformation[lineIndex];
|
|
656
|
+
const rowIndex = lineToRowMap.get(lineIndex);
|
|
657
|
+
// Skip lines that don't render (hidden in collapsed blocks)
|
|
658
|
+
if (rowIndex === undefined)
|
|
659
|
+
continue;
|
|
660
|
+
// Skip lines before visible range
|
|
661
|
+
if (rowIndex < visibleRowStart) {
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
// Stop after visible range
|
|
665
|
+
if (rowIndex > visibleRowEnd) {
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
// Calculate top padding from the first visible row
|
|
669
|
+
if (!firstVisibleFound) {
|
|
670
|
+
topPadding = cumulativeOffsets
|
|
671
|
+
? cumulativeOffsets[rowIndex] || 0
|
|
672
|
+
: rowIndex * DiffViewer.ESTIMATED_ROW_HEIGHT;
|
|
673
|
+
firstVisibleFound = true;
|
|
674
|
+
}
|
|
675
|
+
// Track the last rendered row for bottom padding calculation
|
|
676
|
+
lastRenderedRowIndex = rowIndex;
|
|
677
|
+
// Render the line
|
|
678
|
+
if (showDiffOnly) {
|
|
281
679
|
const blockIndex = lineBlocks[lineIndex];
|
|
282
680
|
if (blockIndex !== undefined) {
|
|
283
681
|
const lastLineOfBlock = blocks[blockIndex].endLine === lineIndex;
|
|
284
|
-
if (!
|
|
682
|
+
if (!expandedBlocks.includes(blockIndex) &&
|
|
285
683
|
lastLineOfBlock) {
|
|
286
|
-
|
|
684
|
+
diffNodes.push(_jsx(React.Fragment, { children: this.renderSkippedLineIndicator(blocks[blockIndex].lines, blockIndex, line.left.lineNumber, line.right.lineNumber) }, lineIndex));
|
|
685
|
+
continue;
|
|
287
686
|
}
|
|
288
|
-
if (!
|
|
289
|
-
|
|
687
|
+
if (!expandedBlocks.includes(blockIndex)) {
|
|
688
|
+
continue;
|
|
290
689
|
}
|
|
291
690
|
}
|
|
292
691
|
}
|
|
293
|
-
|
|
692
|
+
diffNodes.push(splitView
|
|
294
693
|
? this.renderSplitView(line, lineIndex)
|
|
295
|
-
: this.renderInlineView(line, lineIndex);
|
|
296
|
-
}
|
|
694
|
+
: this.renderInlineView(line, lineIndex));
|
|
695
|
+
}
|
|
696
|
+
// Calculate total content height
|
|
697
|
+
const totalContentHeight = cumulativeOffsets
|
|
698
|
+
? cumulativeOffsets[cumulativeOffsets.length - 1] || 0
|
|
699
|
+
: totalRenderedRows * DiffViewer.ESTIMATED_ROW_HEIGHT;
|
|
700
|
+
// Calculate bottom padding: space after the last rendered row
|
|
701
|
+
const bottomPadding = cumulativeOffsets && lastRenderedRowIndex >= 0
|
|
702
|
+
? totalContentHeight - (cumulativeOffsets[lastRenderedRowIndex + 1] || totalContentHeight)
|
|
703
|
+
: 0;
|
|
297
704
|
return {
|
|
298
705
|
diffNodes,
|
|
299
706
|
blocks,
|
|
300
707
|
lineInformation,
|
|
708
|
+
totalRenderedRows,
|
|
709
|
+
topPadding,
|
|
710
|
+
bottomPadding,
|
|
711
|
+
totalContentHeight,
|
|
712
|
+
renderedCount: diffNodes.length,
|
|
713
|
+
// Debug info
|
|
714
|
+
debug: {
|
|
715
|
+
visibleRowStart,
|
|
716
|
+
visibleRowEnd,
|
|
717
|
+
totalRows: totalRenderedRows,
|
|
718
|
+
offsetsLength: (_b = cumulativeOffsets === null || cumulativeOffsets === void 0 ? void 0 : cumulativeOffsets.length) !== null && _b !== void 0 ? _b : 0,
|
|
719
|
+
renderedCount: diffNodes.length,
|
|
720
|
+
scrollTop: (_d = (_c = scrollableContainerRef.current) === null || _c === void 0 ? void 0 : _c.scrollTop) !== null && _d !== void 0 ? _d : 0,
|
|
721
|
+
headerHeight: this.getStickyHeaderHeight(),
|
|
722
|
+
contentScrollTop: scrollableContainerRef.current
|
|
723
|
+
? Math.max(0, scrollableContainerRef.current.scrollTop - this.getStickyHeaderHeight())
|
|
724
|
+
: 0,
|
|
725
|
+
clientHeight: (_f = (_e = scrollableContainerRef.current) === null || _e === void 0 ? void 0 : _e.clientHeight) !== null && _f !== void 0 ? _f : 0,
|
|
726
|
+
}
|
|
301
727
|
};
|
|
302
728
|
};
|
|
303
729
|
this.render = () => {
|
|
730
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
|
|
304
731
|
const { oldValue, newValue, useDarkTheme, leftTitle, rightTitle, splitView, compareMethod, hideLineNumbers, nonce, } = this.props;
|
|
305
732
|
if (typeof compareMethod === "string" &&
|
|
306
|
-
compareMethod !==
|
|
733
|
+
compareMethod !== DiffMethod.JSON) {
|
|
307
734
|
if (typeof oldValue !== "string" || typeof newValue !== "string") {
|
|
308
735
|
throw Error('"oldValue" and "newValue" should be strings');
|
|
309
736
|
}
|
|
@@ -323,16 +750,16 @@ class DiffViewer extends React.Component {
|
|
|
323
750
|
let deletions = 0;
|
|
324
751
|
let additions = 0;
|
|
325
752
|
for (const l of nodes.lineInformation) {
|
|
326
|
-
if (l.left.type ===
|
|
753
|
+
if (l.left.type === DiffType.ADDED) {
|
|
327
754
|
additions++;
|
|
328
755
|
}
|
|
329
|
-
if (l.right.type ===
|
|
756
|
+
if (l.right.type === DiffType.ADDED) {
|
|
330
757
|
additions++;
|
|
331
758
|
}
|
|
332
|
-
if (l.left.type ===
|
|
759
|
+
if (l.left.type === DiffType.REMOVED) {
|
|
333
760
|
deletions++;
|
|
334
761
|
}
|
|
335
|
-
if (l.right.type ===
|
|
762
|
+
if (l.right.type === DiffType.REMOVED) {
|
|
336
763
|
deletions++;
|
|
337
764
|
}
|
|
338
765
|
}
|
|
@@ -341,39 +768,216 @@ class DiffViewer extends React.Component {
|
|
|
341
768
|
const blocks = [];
|
|
342
769
|
for (let i = 0; i < 5; i++) {
|
|
343
770
|
if (percentageAddition > i * 20) {
|
|
344
|
-
blocks.push((
|
|
771
|
+
blocks.push(_jsx("span", { className: cn(this.styles.block, this.styles.blockAddition) }, i));
|
|
345
772
|
}
|
|
346
773
|
else {
|
|
347
|
-
blocks.push((
|
|
774
|
+
blocks.push(_jsx("span", { className: cn(this.styles.block, this.styles.blockDeletion) }, i));
|
|
348
775
|
}
|
|
349
776
|
}
|
|
350
777
|
const allExpanded = this.state.expandedBlocks.length === nodes.blocks.length;
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
778
|
+
const LoadingElement = this.props.loadingElement;
|
|
779
|
+
const scrollDivStyle = this.props.infiniteLoading ? {
|
|
780
|
+
overflowY: 'scroll',
|
|
781
|
+
overflowX: 'hidden',
|
|
782
|
+
height: this.props.infiniteLoading.containerHeight
|
|
783
|
+
} : {};
|
|
784
|
+
// Only apply noWrap when infiniteLoading is enabled but we don't have cumulative offsets yet
|
|
785
|
+
// Once offsets are calculated, we enable pre-wrap for proper text wrapping
|
|
786
|
+
const shouldNoWrap = !!this.props.infiniteLoading && !this.state.cumulativeOffsets;
|
|
787
|
+
const tableElement = (_jsxs("table", { className: cn(this.styles.diffContainer, {
|
|
788
|
+
[this.styles.splitView]: splitView,
|
|
789
|
+
[this.styles.noWrap]: shouldNoWrap,
|
|
790
|
+
}), onMouseUp: () => {
|
|
791
|
+
const elements = document.getElementsByClassName("right");
|
|
792
|
+
for (let i = 0; i < elements.length; i++) {
|
|
793
|
+
const element = elements.item(i);
|
|
794
|
+
element.classList.remove(this.styles.noSelect);
|
|
795
|
+
}
|
|
796
|
+
const elementsLeft = document.getElementsByClassName("left");
|
|
797
|
+
for (let i = 0; i < elementsLeft.length; i++) {
|
|
798
|
+
const element = elementsLeft.item(i);
|
|
799
|
+
element.classList.remove(this.styles.noSelect);
|
|
800
|
+
}
|
|
801
|
+
}, children: [_jsxs("colgroup", { children: [!this.props.hideLineNumbers && _jsx("col", { width: "50px" }), !splitView && !this.props.hideLineNumbers && _jsx("col", { width: "50px" }), this.props.renderGutter && _jsx("col", { width: "50px" }), _jsx("col", { width: "28px" }), _jsx("col", { width: "auto" }), splitView && (_jsxs(_Fragment, { children: [!this.props.hideLineNumbers && _jsx("col", { width: "50px" }), this.props.renderGutter && _jsx("col", { width: "50px" }), _jsx("col", { width: "28px" }), _jsx("col", { width: "auto" })] }))] }), _jsx("tbody", { children: nodes.diffNodes })] }));
|
|
802
|
+
return (_jsxs("div", { style: Object.assign(Object.assign({}, scrollDivStyle), { position: 'relative' }), onScroll: this.onScroll, ref: this.state.scrollableContainerRef, children: [(!this.props.hideSummary || leftTitle || rightTitle) && (_jsxs("div", { ref: this.stickyHeaderRef, className: this.styles.stickyHeader, children: [!this.props.hideSummary && (_jsxs("div", { className: this.styles.summary, role: "banner", children: [_jsx("button", { type: "button", className: this.styles.allExpandButton, onClick: () => {
|
|
803
|
+
this.setState({
|
|
804
|
+
expandedBlocks: allExpanded
|
|
805
|
+
? []
|
|
806
|
+
: nodes.blocks.map((b) => b.index),
|
|
807
|
+
}, () => this.recalculateOffsets());
|
|
808
|
+
}, children: allExpanded ? _jsx(Fold, {}) : _jsx(Expand, {}) }), " ", totalChanges, _jsx("div", { style: { display: "flex", gap: "1px" }, children: blocks }), this.props.summary ? _jsx("span", { children: this.props.summary }) : null] })), (leftTitle || rightTitle) && (_jsxs("div", { className: this.styles.columnHeaders, children: [_jsx("div", { className: this.styles.titleBlock, children: leftTitle ? (_jsx("pre", { className: this.styles.contentText, children: leftTitle })) : null }), splitView && (_jsx("div", { className: this.styles.titleBlock, children: rightTitle ? (_jsx("pre", { className: this.styles.contentText, children: rightTitle })) : null }))] }))] })), this.state.isLoading && LoadingElement && _jsx(LoadingElement, {}), this.props.infiniteLoading ? (_jsx("div", { style: {
|
|
809
|
+
paddingTop: nodes.topPadding,
|
|
810
|
+
paddingBottom: nodes.bottomPadding,
|
|
811
|
+
}, children: tableElement })) : (tableElement), _jsx("span", { ref: this.charMeasureRef, style: {
|
|
812
|
+
position: 'absolute',
|
|
813
|
+
top: 0,
|
|
814
|
+
left: '-9999px',
|
|
815
|
+
visibility: 'hidden',
|
|
816
|
+
whiteSpace: 'pre',
|
|
817
|
+
fontFamily: 'monospace',
|
|
818
|
+
fontSize: 12,
|
|
819
|
+
}, "aria-hidden": "true", children: "M" }), this.props.infiniteLoading && this.props.showDebugInfo && (_jsxs("div", { style: {
|
|
820
|
+
position: 'fixed',
|
|
821
|
+
top: 10,
|
|
822
|
+
right: 10,
|
|
823
|
+
background: 'rgba(0,0,0,0.85)',
|
|
824
|
+
color: '#0f0',
|
|
825
|
+
padding: '10px',
|
|
826
|
+
fontFamily: 'monospace',
|
|
827
|
+
fontSize: '11px',
|
|
828
|
+
zIndex: 9999,
|
|
829
|
+
borderRadius: '4px',
|
|
830
|
+
maxWidth: '300px',
|
|
831
|
+
lineHeight: 1.4,
|
|
832
|
+
}, children: [_jsx("div", { style: { fontWeight: 'bold', marginBottom: '5px', color: '#fff' }, children: "Debug Info" }), _jsxs("div", { children: ["scrollTop: ", nodes.debug.scrollTop] }), _jsxs("div", { children: ["headerHeight: ", nodes.debug.headerHeight] }), _jsxs("div", { children: ["contentScrollTop: ", nodes.debug.contentScrollTop] }), _jsxs("div", { children: ["clientHeight: ", nodes.debug.clientHeight] }), _jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px' }, children: [_jsxs("div", { children: ["visibleRowStart: ", nodes.debug.visibleRowStart] }), _jsxs("div", { children: ["visibleRowEnd: ", nodes.debug.visibleRowEnd] })] }), _jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px' }, children: [_jsxs("div", { children: ["totalRows: ", nodes.debug.totalRows] }), _jsxs("div", { children: ["offsetsLength: ", nodes.debug.offsetsLength] }), _jsxs("div", { children: ["renderedCount: ", nodes.debug.renderedCount] })] }), _jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px' }, children: [_jsxs("div", { children: ["topPadding: ", nodes.topPadding.toFixed(0)] }), _jsxs("div", { children: ["bottomPadding: ", nodes.bottomPadding.toFixed(0)] }), _jsxs("div", { children: ["totalContentHeight: ", nodes.totalContentHeight.toFixed(0)] })] }), _jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px', color: '#ff0' }, children: [_jsxs("div", { children: ["cumulativeOffsets: ", this.state.cumulativeOffsets ? 'SET' : 'NULL'] }), _jsxs("div", { children: ["columnWidth: ", (_b = (_a = this.state.contentColumnWidth) === null || _a === void 0 ? void 0 : _a.toFixed(0)) !== null && _b !== void 0 ? _b : 'N/A', "px"] }), _jsxs("div", { children: ["charWidth: ", (_d = (_c = this.state.charWidth) === null || _c === void 0 ? void 0 : _c.toFixed(2)) !== null && _d !== void 0 ? _d : 'N/A', "px"] }), _jsxs("div", { children: ["charsPerRow: ", this.state.contentColumnWidth && this.state.charWidth ? Math.floor(this.state.contentColumnWidth / this.state.charWidth) : 'N/A'] })] }), this.state.cumulativeOffsets && (_jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px', color: '#0ff', fontSize: '10px' }, children: [_jsxs("div", { children: ["offsets[", nodes.debug.visibleRowEnd, "]: ", (_f = (_e = this.state.cumulativeOffsets[nodes.debug.visibleRowEnd]) === null || _e === void 0 ? void 0 : _e.toFixed(0)) !== null && _f !== void 0 ? _f : 'N/A'] }), _jsxs("div", { children: ["offsets[", nodes.debug.totalRows - 1, "]: ", (_h = (_g = this.state.cumulativeOffsets[nodes.debug.totalRows - 1]) === null || _g === void 0 ? void 0 : _g.toFixed(0)) !== null && _h !== void 0 ? _h : 'N/A'] }), _jsxs("div", { children: ["offsets[", nodes.debug.totalRows, "]: ", (_k = (_j = this.state.cumulativeOffsets[nodes.debug.totalRows]) === null || _j === void 0 ? void 0 : _j.toFixed(0)) !== null && _k !== void 0 ? _k : 'N/A'] }), _jsxs("div", { style: { marginTop: '3px' }, children: ["viewportEnd: ", (nodes.debug.contentScrollTop + nodes.debug.clientHeight).toFixed(0)] }), _jsxs("div", { style: { marginTop: '3px', color: '#f0f' }, children: ["scrollHeight: ", (_m = (_l = this.state.scrollableContainerRef.current) === null || _l === void 0 ? void 0 : _l.scrollHeight) !== null && _m !== void 0 ? _m : 'N/A'] }), _jsxs("div", { children: ["maxScrollTop: ", ((_p = (_o = this.state.scrollableContainerRef.current) === null || _o === void 0 ? void 0 : _o.scrollHeight) !== null && _p !== void 0 ? _p : 0) - nodes.debug.clientHeight] })] }))] }))] }));
|
|
371
833
|
};
|
|
372
834
|
this.state = {
|
|
373
835
|
expandedBlocks: [],
|
|
374
836
|
noSelect: undefined,
|
|
837
|
+
scrollableContainerRef: React.createRef(),
|
|
838
|
+
computedDiffResult: {},
|
|
839
|
+
isLoading: false,
|
|
840
|
+
visibleStartRow: 0,
|
|
841
|
+
contentColumnWidth: null,
|
|
842
|
+
charWidth: null,
|
|
843
|
+
cumulativeOffsets: null,
|
|
375
844
|
};
|
|
376
845
|
}
|
|
846
|
+
/**
|
|
847
|
+
* Gets the height of the sticky header, if present.
|
|
848
|
+
*/
|
|
849
|
+
getStickyHeaderHeight() {
|
|
850
|
+
var _a;
|
|
851
|
+
return ((_a = this.stickyHeaderRef.current) === null || _a === void 0 ? void 0 : _a.offsetHeight) || 0;
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Measures the width of a single character in the monospace font.
|
|
855
|
+
* Falls back to 7.2px if measurement fails.
|
|
856
|
+
*/
|
|
857
|
+
measureCharWidth() {
|
|
858
|
+
const span = this.charMeasureRef.current;
|
|
859
|
+
if (!span)
|
|
860
|
+
return 7.2; // fallback
|
|
861
|
+
return span.getBoundingClientRect().width || 7.2;
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Measures the available width for content in a content column.
|
|
865
|
+
* Falls back to estimating from container width if direct measurement fails.
|
|
866
|
+
*/
|
|
867
|
+
measureContentColumnWidth() {
|
|
868
|
+
// Try direct measurement first
|
|
869
|
+
const cell = this.contentColumnRef.current;
|
|
870
|
+
if (cell && cell.clientWidth > 0) {
|
|
871
|
+
const style = window.getComputedStyle(cell);
|
|
872
|
+
const padding = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
|
|
873
|
+
const width = cell.clientWidth - padding;
|
|
874
|
+
if (width > 0)
|
|
875
|
+
return width;
|
|
876
|
+
}
|
|
877
|
+
// Fallback: estimate from container width
|
|
878
|
+
// In split view: container has 2 content columns + gutters (50px each) + markers (28px each)
|
|
879
|
+
// In unified view: 1 content column + 2 gutters + 1 marker
|
|
880
|
+
const container = this.state.scrollableContainerRef.current;
|
|
881
|
+
if (!container || container.clientWidth <= 0)
|
|
882
|
+
return null;
|
|
883
|
+
const containerWidth = container.clientWidth;
|
|
884
|
+
const gutterWidth = this.props.hideLineNumbers ? 0 : 50;
|
|
885
|
+
const markerWidth = 28;
|
|
886
|
+
const gutterCount = this.props.splitView ? 2 : 2; // left gutter(s)
|
|
887
|
+
const markerCount = this.props.splitView ? 2 : 1;
|
|
888
|
+
const contentColumns = this.props.splitView ? 2 : 1;
|
|
889
|
+
const fixedWidth = gutterCount * gutterWidth + markerCount * markerWidth;
|
|
890
|
+
const availableWidth = containerWidth - fixedWidth;
|
|
891
|
+
return Math.max(100, availableWidth / contentColumns); // minimum 100px
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Gets the text length from a value that may be a string or DiffInformation array.
|
|
895
|
+
*/
|
|
896
|
+
getTextLength(value) {
|
|
897
|
+
if (!value)
|
|
898
|
+
return 0;
|
|
899
|
+
if (typeof value === 'string')
|
|
900
|
+
return value.length;
|
|
901
|
+
return value.reduce((sum, d) => sum + (typeof d.value === 'string' ? d.value.length : 0), 0);
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Builds cumulative vertical offsets for each line based on character count and column width.
|
|
905
|
+
* This allows accurate scroll position calculations with variable row heights.
|
|
906
|
+
*/
|
|
907
|
+
buildCumulativeOffsets(lineInformation, lineBlocks, blocks, expandedBlocks, showDiffOnly, charWidth, columnWidth, splitView) {
|
|
908
|
+
var _a, _b;
|
|
909
|
+
const offsets = [0];
|
|
910
|
+
const seenBlocks = new Set();
|
|
911
|
+
for (let i = 0; i < lineInformation.length; i++) {
|
|
912
|
+
const line = lineInformation[i];
|
|
913
|
+
if (showDiffOnly) {
|
|
914
|
+
const blockIndex = lineBlocks[i];
|
|
915
|
+
if (blockIndex !== undefined && !expandedBlocks.includes(blockIndex)) {
|
|
916
|
+
const isLastLine = blocks[blockIndex].endLine === i;
|
|
917
|
+
if (!seenBlocks.has(blockIndex) && isLastLine) {
|
|
918
|
+
seenBlocks.add(blockIndex);
|
|
919
|
+
offsets.push(offsets[offsets.length - 1] + DiffViewer.ESTIMATED_ROW_HEIGHT);
|
|
920
|
+
}
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
// Calculate visual rows for this line
|
|
925
|
+
const leftLen = ((_a = line.left) === null || _a === void 0 ? void 0 : _a.value) ? this.getTextLength(line.left.value) : 0;
|
|
926
|
+
const rightLen = ((_b = line.right) === null || _b === void 0 ? void 0 : _b.value) ? this.getTextLength(line.right.value) : 0;
|
|
927
|
+
const maxLen = splitView ? Math.max(leftLen, rightLen) : (leftLen || rightLen);
|
|
928
|
+
const charsPerRow = Math.floor(columnWidth / charWidth);
|
|
929
|
+
const visualRows = charsPerRow > 0 ? Math.max(1, Math.ceil(maxLen / charsPerRow)) : 1;
|
|
930
|
+
offsets.push(offsets[offsets.length - 1] + visualRows * DiffViewer.ESTIMATED_ROW_HEIGHT);
|
|
931
|
+
}
|
|
932
|
+
return offsets;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Binary search to find the line index at a given scroll offset.
|
|
936
|
+
*/
|
|
937
|
+
findLineAtOffset(scrollTop, offsets) {
|
|
938
|
+
let low = 0;
|
|
939
|
+
let high = offsets.length - 2;
|
|
940
|
+
while (low < high) {
|
|
941
|
+
const mid = Math.floor((low + high + 1) / 2);
|
|
942
|
+
if (offsets[mid] <= scrollTop) {
|
|
943
|
+
low = mid;
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
high = mid - 1;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return low;
|
|
950
|
+
}
|
|
951
|
+
componentDidUpdate(prevProps) {
|
|
952
|
+
if (prevProps.oldValue !== this.props.oldValue ||
|
|
953
|
+
prevProps.newValue !== this.props.newValue ||
|
|
954
|
+
prevProps.compareMethod !== this.props.compareMethod ||
|
|
955
|
+
prevProps.disableWordDiff !== this.props.disableWordDiff ||
|
|
956
|
+
prevProps.linesOffset !== this.props.linesOffset) {
|
|
957
|
+
// Clear word diff cache when diff changes
|
|
958
|
+
this.wordDiffCache.clear();
|
|
959
|
+
this.setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: true, visibleStartRow: 0, cumulativeOffsets: null })));
|
|
960
|
+
this.memoisedCompute();
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
componentDidMount() {
|
|
964
|
+
this.setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: true })));
|
|
965
|
+
this.memoisedCompute();
|
|
966
|
+
// Set up ResizeObserver for recalculating offsets on container resize
|
|
967
|
+
if (typeof ResizeObserver !== 'undefined' && this.props.infiniteLoading) {
|
|
968
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
969
|
+
requestAnimationFrame(() => this.recalculateOffsets());
|
|
970
|
+
});
|
|
971
|
+
const container = this.state.scrollableContainerRef.current;
|
|
972
|
+
if (container) {
|
|
973
|
+
this.resizeObserver.observe(container);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
componentWillUnmount() {
|
|
978
|
+
var _a;
|
|
979
|
+
(_a = this.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
980
|
+
}
|
|
377
981
|
}
|
|
378
982
|
DiffViewer.defaultProps = {
|
|
379
983
|
oldValue: "",
|
|
@@ -381,7 +985,7 @@ DiffViewer.defaultProps = {
|
|
|
381
985
|
splitView: true,
|
|
382
986
|
highlightLines: [],
|
|
383
987
|
disableWordDiff: false,
|
|
384
|
-
compareMethod:
|
|
988
|
+
compareMethod: DiffMethod.CHARS,
|
|
385
989
|
styles: {},
|
|
386
990
|
hideLineNumbers: false,
|
|
387
991
|
extraLinesSurroundingDiff: 3,
|
|
@@ -390,4 +994,7 @@ DiffViewer.defaultProps = {
|
|
|
390
994
|
linesOffset: 0,
|
|
391
995
|
nonce: "",
|
|
392
996
|
};
|
|
393
|
-
|
|
997
|
+
// Estimated row height based on lineHeight: 1.6em with 12px base font
|
|
998
|
+
DiffViewer.ESTIMATED_ROW_HEIGHT = 19;
|
|
999
|
+
export default DiffViewer;
|
|
1000
|
+
export { DiffMethod };
|