react-diff-viewer-continued 4.0.5 → 4.1.0

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.
@@ -3,10 +3,145 @@ import cn from "classnames";
3
3
  import * as React from "react";
4
4
  import memoize from "memoize-one";
5
5
  import { computeHiddenBlocks } from "./compute-hidden-blocks.js";
6
- import { DiffMethod, DiffType, computeLineInformation, } from "./compute-lines.js";
6
+ import { DiffMethod, DiffType, computeLineInformationWorker, computeDiff, } from "./compute-lines.js";
7
7
  import { Expand } from "./expand.js";
8
8
  import computeStyles from "./styles.js";
9
9
  import { Fold } from "./fold.js";
10
+ /**
11
+ * Applies diff styling (ins/del tags) to pre-highlighted HTML by walking through
12
+ * the HTML and wrapping text portions based on character positions in the diff.
13
+ */
14
+ function applyDiffToHighlightedHtml(html, diffArray, styles) {
15
+ const ranges = [];
16
+ let pos = 0;
17
+ for (const diff of diffArray) {
18
+ const value = typeof diff.value === "string" ? diff.value : "";
19
+ if (value.length > 0) {
20
+ ranges.push({ start: pos, end: pos + value.length, type: diff.type });
21
+ pos += value.length;
22
+ }
23
+ }
24
+ const segments = [];
25
+ let i = 0;
26
+ while (i < html.length) {
27
+ if (html[i] === "<") {
28
+ const tagEnd = html.indexOf(">", i);
29
+ if (tagEnd === -1) {
30
+ // Malformed HTML, treat rest as text
31
+ segments.push({ type: "text", content: html.slice(i) });
32
+ break;
33
+ }
34
+ segments.push({ type: "tag", content: html.slice(i, tagEnd + 1) });
35
+ i = tagEnd + 1;
36
+ }
37
+ else {
38
+ // Find the next tag or end of string
39
+ let textEnd = html.indexOf("<", i);
40
+ if (textEnd === -1)
41
+ textEnd = html.length;
42
+ segments.push({ type: "text", content: html.slice(i, textEnd) });
43
+ i = textEnd;
44
+ }
45
+ }
46
+ // Helper to decode HTML entities for character counting
47
+ function decodeEntities(text) {
48
+ return text
49
+ .replace(/&lt;/g, "<")
50
+ .replace(/&gt;/g, ">")
51
+ .replace(/&amp;/g, "&")
52
+ .replace(/&quot;/g, '"')
53
+ .replace(/&#39;/g, "'")
54
+ .replace(/&nbsp;/g, "\u00A0");
55
+ }
56
+ // Helper to get the wrapper tag for a diff type
57
+ function getWrapper(type) {
58
+ if (type === DiffType.ADDED) {
59
+ return {
60
+ open: `<ins class="${styles.wordDiff} ${styles.wordAdded}">`,
61
+ close: "</ins>",
62
+ };
63
+ }
64
+ if (type === DiffType.REMOVED) {
65
+ return {
66
+ open: `<del class="${styles.wordDiff} ${styles.wordRemoved}">`,
67
+ close: "</del>",
68
+ };
69
+ }
70
+ return {
71
+ open: `<span class="${styles.wordDiff}">`,
72
+ close: "</span>",
73
+ };
74
+ }
75
+ // Process segments, tracking text position
76
+ let textPos = 0;
77
+ let result = "";
78
+ for (const segment of segments) {
79
+ if (segment.type === "tag") {
80
+ result += segment.content;
81
+ }
82
+ else {
83
+ // Text segment - we need to split it according to diff ranges
84
+ const text = segment.content;
85
+ const decodedText = decodeEntities(text);
86
+ // Walk through the text, character by character (in decoded form)
87
+ // but output the original encoded form
88
+ let localDecodedPos = 0;
89
+ let localEncodedPos = 0;
90
+ while (localDecodedPos < decodedText.length) {
91
+ const globalPos = textPos + localDecodedPos;
92
+ // Find the range that covers this position
93
+ const range = ranges.find((r) => globalPos >= r.start && globalPos < r.end);
94
+ if (!range) {
95
+ // No range covers this position (shouldn't happen, but be safe)
96
+ // Just output the character
97
+ const char = text[localEncodedPos];
98
+ result += char;
99
+ localEncodedPos++;
100
+ localDecodedPos++;
101
+ continue;
102
+ }
103
+ // How many decoded characters until the end of this range?
104
+ const charsUntilRangeEnd = range.end - globalPos;
105
+ // How many decoded characters until the end of this text segment?
106
+ const charsUntilTextEnd = decodedText.length - localDecodedPos;
107
+ // Take the minimum
108
+ const charsToTake = Math.min(charsUntilRangeEnd, charsUntilTextEnd);
109
+ // Now we need to find the corresponding encoded substring
110
+ // Walk through encoded text, counting decoded characters
111
+ let encodedChunkEnd = localEncodedPos;
112
+ let decodedCount = 0;
113
+ while (decodedCount < charsToTake && encodedChunkEnd < text.length) {
114
+ if (text[encodedChunkEnd] === "&") {
115
+ // Find entity end
116
+ const entityEnd = text.indexOf(";", encodedChunkEnd);
117
+ if (entityEnd !== -1 && entityEnd - encodedChunkEnd < 10) {
118
+ encodedChunkEnd = entityEnd + 1;
119
+ }
120
+ else {
121
+ encodedChunkEnd++;
122
+ }
123
+ }
124
+ else {
125
+ encodedChunkEnd++;
126
+ }
127
+ decodedCount++;
128
+ }
129
+ const chunk = text.slice(localEncodedPos, encodedChunkEnd);
130
+ const wrapper = getWrapper(range.type);
131
+ if (wrapper) {
132
+ result += wrapper.open + chunk + wrapper.close;
133
+ }
134
+ else {
135
+ result += chunk;
136
+ }
137
+ localEncodedPos = encodedChunkEnd;
138
+ localDecodedPos += charsToTake;
139
+ }
140
+ textPos += decodedText.length;
141
+ }
142
+ }
143
+ return result;
144
+ }
10
145
  export var LineNumberPrefix;
11
146
  (function (LineNumberPrefix) {
12
147
  LineNumberPrefix["LEFT"] = "L";
@@ -14,6 +149,8 @@ export var LineNumberPrefix;
14
149
  })(LineNumberPrefix || (LineNumberPrefix = {}));
15
150
  class DiffViewer extends React.Component {
16
151
  styles;
152
+ // Cache for on-demand word diff computation
153
+ wordDiffCache = new Map();
17
154
  static defaultProps = {
18
155
  oldValue: "",
19
156
  newValue: "",
@@ -34,8 +171,42 @@ class DiffViewer extends React.Component {
34
171
  this.state = {
35
172
  expandedBlocks: [],
36
173
  noSelect: undefined,
174
+ scrollableContainerRef: React.createRef(),
175
+ computedDiffResult: {},
176
+ isLoading: false,
177
+ visibleStartRow: 0
37
178
  };
38
179
  }
180
+ /**
181
+ * Computes word diff on-demand for a line, with caching.
182
+ * This is used when word diff was deferred during initial computation.
183
+ */
184
+ getWordDiffValues = (left, right, lineIndex) => {
185
+ // Handle empty left/right
186
+ if (!left || !right) {
187
+ return { leftValue: left?.value, rightValue: right?.value };
188
+ }
189
+ // If no raw values, word diff was already computed or disabled
190
+ // Use explicit undefined check since empty string is a valid raw value
191
+ if (left.rawValue === undefined || right.rawValue === undefined) {
192
+ return { leftValue: left.value, rightValue: right.value };
193
+ }
194
+ // Check cache
195
+ const cacheKey = `${lineIndex}-${left.rawValue}-${right.rawValue}`;
196
+ let cached = this.wordDiffCache.get(cacheKey);
197
+ if (!cached) {
198
+ // Compute word diff on-demand
199
+ // Use CHARS method for on-demand computation since rawValue is always a string
200
+ // (JSON/YAML methods only work with objects, not the string lines we have here)
201
+ const compareMethod = (this.props.compareMethod === DiffMethod.JSON || this.props.compareMethod === DiffMethod.YAML)
202
+ ? DiffMethod.CHARS
203
+ : this.props.compareMethod;
204
+ const computed = computeDiff(left.rawValue, right.rawValue, compareMethod);
205
+ cached = { left: computed.left, right: computed.right };
206
+ this.wordDiffCache.set(cacheKey, cached);
207
+ }
208
+ return { leftValue: cached.left, rightValue: cached.right };
209
+ };
39
210
  /**
40
211
  * Resets code block expand to the initial stage. Will be exposed to the parent component via
41
212
  * refs.
@@ -79,6 +250,19 @@ class DiffViewer extends React.Component {
79
250
  }
80
251
  return () => { };
81
252
  };
253
+ /**
254
+ * Checks if the current compare method should show word-level highlighting.
255
+ * Character, word-level, JSON, and YAML diffs benefit from highlighting individual changes.
256
+ * JSON/YAML use CHARS internally for word-level diff, so they should be highlighted.
257
+ */
258
+ shouldHighlightWordDiff = () => {
259
+ const { compareMethod } = this.props;
260
+ return (compareMethod === DiffMethod.CHARS ||
261
+ compareMethod === DiffMethod.WORDS ||
262
+ compareMethod === DiffMethod.WORDS_WITH_SPACE ||
263
+ compareMethod === DiffMethod.JSON ||
264
+ compareMethod === DiffMethod.YAML);
265
+ };
82
266
  /**
83
267
  * Maps over the word diff and constructs the required React elements to show word diff.
84
268
  *
@@ -86,16 +270,59 @@ class DiffViewer extends React.Component {
86
270
  * @param renderer Optional renderer to format diff words. Useful for syntax highlighting.
87
271
  */
88
272
  renderWordDiff = (diffArray, renderer) => {
273
+ const showHighlight = this.shouldHighlightWordDiff();
274
+ const { compareMethod } = this.props;
275
+ // Don't apply syntax highlighting for JSON/YAML - their word diffs are computed
276
+ // on-demand from raw strings and syntax highlighting creates messy fragmented tokens.
277
+ const skipSyntaxHighlighting = compareMethod === DiffMethod.JSON || compareMethod === DiffMethod.YAML;
278
+ // Reconstruct the full line from diff chunks
279
+ const fullLine = diffArray
280
+ .map((d) => (typeof d.value === "string" ? d.value : ""))
281
+ .join("");
282
+ // For very long lines (>500 chars), skip fancy processing - just render plain text
283
+ // without word-level highlighting to avoid performance issues
284
+ const MAX_LINE_LENGTH = 500;
285
+ if (fullLine.length > MAX_LINE_LENGTH) {
286
+ return [_jsx("span", { children: fullLine }, "long-line")];
287
+ }
288
+ // If we have a renderer and syntax highlighting is enabled, try to highlight
289
+ // the full line first, then apply diff styling to preserve proper tokenization.
290
+ if (renderer && !skipSyntaxHighlighting) {
291
+ // Get the syntax-highlighted content
292
+ const highlighted = renderer(fullLine);
293
+ // Check if the renderer uses dangerouslySetInnerHTML (common with Prism, highlight.js, etc.)
294
+ const htmlContent = highlighted?.props?.dangerouslySetInnerHTML?.__html;
295
+ if (typeof htmlContent === "string") {
296
+ // Apply diff styling to the highlighted HTML
297
+ const styledHtml = applyDiffToHighlightedHtml(htmlContent, diffArray, {
298
+ wordDiff: this.styles.wordDiff,
299
+ wordAdded: showHighlight ? this.styles.wordAdded : "",
300
+ wordRemoved: showHighlight ? this.styles.wordRemoved : "",
301
+ });
302
+ // Clone the element with the modified HTML
303
+ return [
304
+ React.cloneElement(highlighted, {
305
+ key: "highlighted-diff",
306
+ dangerouslySetInnerHTML: { __html: styledHtml },
307
+ }),
308
+ ];
309
+ }
310
+ // Renderer doesn't use dangerouslySetInnerHTML - fall through to per-chunk rendering
311
+ }
312
+ // Fallback: render each chunk separately (used for JSON/YAML or non-HTML renderers)
89
313
  return diffArray.map((wordDiff, i) => {
90
- const content = renderer
91
- ? renderer(wordDiff.value)
92
- : wordDiff.value;
93
- if (typeof content !== "string")
94
- return;
314
+ let content;
315
+ if (typeof wordDiff.value === "string") {
316
+ content = wordDiff.value;
317
+ }
318
+ else {
319
+ // If wordDiff.value is DiffInformation[], we don't handle it. See c0c99f5712.
320
+ content = undefined;
321
+ }
95
322
  return wordDiff.type === DiffType.ADDED ? (_jsx("ins", { className: cn(this.styles.wordDiff, {
96
- [this.styles.wordAdded]: wordDiff.type === DiffType.ADDED,
323
+ [this.styles.wordAdded]: showHighlight,
97
324
  }), children: content }, i)) : wordDiff.type === DiffType.REMOVED ? (_jsx("del", { className: cn(this.styles.wordDiff, {
98
- [this.styles.wordRemoved]: wordDiff.type === DiffType.REMOVED,
325
+ [this.styles.wordRemoved]: showHighlight,
99
326
  }), children: content }, i)) : (_jsx("span", { className: cn(this.styles.wordDiff), children: content }, i));
100
327
  });
101
328
  };
@@ -196,7 +423,9 @@ class DiffViewer extends React.Component {
196
423
  * @param index React key for the lines.
197
424
  */
198
425
  renderSplitView = ({ left, right }, index) => {
199
- return (_jsxs("tr", { className: this.styles.line, children: [this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, left.value), this.renderLine(right.lineNumber, right.type, LineNumberPrefix.RIGHT, right.value)] }, index));
426
+ // Compute word diff on-demand if deferred
427
+ const { leftValue, rightValue } = this.getWordDiffValues(left, right, index);
428
+ 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));
200
429
  };
201
430
  /**
202
431
  * Generates lines for inline view.
@@ -207,18 +436,20 @@ class DiffViewer extends React.Component {
207
436
  * @param index React key for the lines.
208
437
  */
209
438
  renderInlineView = ({ left, right }, index) => {
439
+ // Compute word diff on-demand if deferred
440
+ const { leftValue, rightValue } = this.getWordDiffValues(left, right, index);
210
441
  let content;
211
442
  if (left.type === DiffType.REMOVED && right.type === DiffType.ADDED) {
212
- return (_jsxs(React.Fragment, { children: [_jsx("tr", { className: this.styles.line, children: this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, left.value, null) }), _jsx("tr", { className: this.styles.line, children: this.renderLine(null, right.type, LineNumberPrefix.RIGHT, right.value, right.lineNumber, LineNumberPrefix.RIGHT) })] }, index));
443
+ 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));
213
444
  }
214
445
  if (left.type === DiffType.REMOVED) {
215
- content = this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, left.value, null);
446
+ content = this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, leftValue, null);
216
447
  }
217
448
  if (left.type === DiffType.DEFAULT) {
218
- content = this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, left.value, right.lineNumber, LineNumberPrefix.RIGHT);
449
+ content = this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, leftValue, right.lineNumber, LineNumberPrefix.RIGHT);
219
450
  }
220
451
  if (right.type === DiffType.ADDED) {
221
- content = this.renderLine(null, right.type, LineNumberPrefix.RIGHT, right.value, right.lineNumber);
452
+ content = this.renderLine(null, right.type, LineNumberPrefix.RIGHT, rightValue, right.lineNumber);
222
453
  }
223
454
  return (_jsx("tr", { className: this.styles.line, children: content }, index));
224
455
  };
@@ -239,47 +470,217 @@ class DiffViewer extends React.Component {
239
470
  */
240
471
  renderSkippedLineIndicator = (num, blockNumber, leftBlockLineNumber, rightBlockLineNumber) => {
241
472
  const { hideLineNumbers, splitView } = this.props;
242
- const message = this.props.codeFoldMessageRenderer ? (this.props.codeFoldMessageRenderer(num, leftBlockLineNumber, rightBlockLineNumber)) : (_jsxs("span", { className: this.styles.codeFoldContent, children: ["Expand ", num, " lines ..."] }));
473
+ const message = this.props.codeFoldMessageRenderer ? (this.props.codeFoldMessageRenderer(num, leftBlockLineNumber, rightBlockLineNumber)) : (_jsxs("span", { className: this.styles.codeFoldContent, children: ["@@ -", leftBlockLineNumber - num, ",", num, " +", rightBlockLineNumber - num, ",", num, " @@"] }));
243
474
  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 }) }));
244
475
  const isUnifiedViewWithoutLineNumbers = !splitView && !hideLineNumbers;
245
- return (_jsxs("tr", { className: this.styles.codeFold, children: [!hideLineNumbers && _jsx("td", { className: this.styles.codeFoldGutter }), this.props.renderGutter ? (_jsx("td", { className: this.styles.codeFoldGutter })) : null, _jsx("td", { className: cn({
476
+ const expandGutter = (_jsx("td", { className: this.styles.codeFoldGutter, children: _jsx(Expand, {}) }));
477
+ 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({
246
478
  [this.styles.codeFoldGutter]: isUnifiedViewWithoutLineNumbers,
247
479
  }) }), 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}`));
248
480
  };
249
481
  /**
250
- * Generates the entire diff view.
482
+ *
483
+ * Generates a unique cache key based on the current props used in diff computation.
484
+ *
485
+ * This key is used to memoize results and avoid recomputation for the same inputs.
486
+ * @returns A stringified JSON key representing the current diff settings and input values.
487
+ *
251
488
  */
252
- renderDiff = () => {
253
- const { oldValue, newValue, splitView, disableWordDiff, compareMethod, linesOffset, } = this.props;
254
- const { lineInformation, diffLines } = computeLineInformation(oldValue, newValue, disableWordDiff, compareMethod, linesOffset, this.props.alwaysShowLines);
489
+ getMemoisedKey = () => {
490
+ const { oldValue, newValue, disableWordDiff, compareMethod, linesOffset, alwaysShowLines, extraLinesSurroundingDiff, } = this.props;
491
+ return JSON.stringify({
492
+ oldValue,
493
+ newValue,
494
+ disableWordDiff,
495
+ compareMethod,
496
+ linesOffset,
497
+ alwaysShowLines,
498
+ extraLinesSurroundingDiff,
499
+ });
500
+ };
501
+ /**
502
+ * Computes and memoizes the diff result between `oldValue` and `newValue`.
503
+ *
504
+ * If a memoized result exists for the current input configuration, it uses that.
505
+ * Otherwise, it runs the diff logic in a Web Worker to avoid blocking the UI.
506
+ * It also computes hidden line blocks for collapsing unchanged sections,
507
+ * and stores the result in the local component state.
508
+ */
509
+ memoisedCompute = async () => {
510
+ const { oldValue, newValue, disableWordDiff, compareMethod, linesOffset } = this.props;
511
+ const cacheKey = this.getMemoisedKey();
512
+ if (!!this.state.computedDiffResult[cacheKey]) {
513
+ this.setState((prev) => ({
514
+ ...prev,
515
+ isLoading: false
516
+ }));
517
+ return;
518
+ }
519
+ // Defer word diff computation when using infinite loading with reasonable container height
520
+ // This significantly improves initial render time for large diffs
521
+ const containerHeight = this.props.infiniteLoading?.containerHeight;
522
+ const containerHeightPx = containerHeight
523
+ ? typeof containerHeight === 'number'
524
+ ? containerHeight
525
+ : parseInt(containerHeight, 10) || 0
526
+ : 0;
527
+ const shouldDeferWordDiff = !disableWordDiff &&
528
+ !!this.props.infiniteLoading &&
529
+ containerHeightPx > 0 &&
530
+ containerHeightPx < 2000;
531
+ const { lineInformation, diffLines } = await computeLineInformationWorker(oldValue, newValue, disableWordDiff, compareMethod, linesOffset, this.props.alwaysShowLines, shouldDeferWordDiff);
255
532
  const extraLines = this.props.extraLinesSurroundingDiff < 0
256
533
  ? 0
257
534
  : Math.round(this.props.extraLinesSurroundingDiff);
258
535
  const { lineBlocks, blocks } = computeHiddenBlocks(lineInformation, diffLines, extraLines);
259
- const diffNodes = lineInformation.map((line, lineIndex) => {
260
- if (this.props.showDiffOnly) {
536
+ this.state.computedDiffResult[cacheKey] = { lineInformation, lineBlocks, blocks };
537
+ this.setState((prev) => ({
538
+ ...prev,
539
+ computedDiffResult: this.state.computedDiffResult,
540
+ isLoading: false,
541
+ }));
542
+ };
543
+ // Estimated row height based on lineHeight: 1.6em with 12px base font
544
+ static ESTIMATED_ROW_HEIGHT = 19;
545
+ /**
546
+ * Handles scroll events on the scrollable container.
547
+ *
548
+ * Updates the visible start row for virtualization.
549
+ */
550
+ onScroll = () => {
551
+ const container = this.state.scrollableContainerRef.current;
552
+ if (!container || !this.props.infiniteLoading)
553
+ return;
554
+ const newStartRow = Math.floor(container.scrollTop / DiffViewer.ESTIMATED_ROW_HEIGHT);
555
+ // Only update state if the start row changed (avoid unnecessary re-renders)
556
+ if (newStartRow !== this.state.visibleStartRow) {
557
+ this.setState({ visibleStartRow: newStartRow });
558
+ }
559
+ };
560
+ /**
561
+ * Generates the entire diff view with virtualization support.
562
+ */
563
+ renderDiff = () => {
564
+ const { splitView, infiniteLoading, showDiffOnly } = this.props;
565
+ const { computedDiffResult, expandedBlocks, visibleStartRow, scrollableContainerRef } = this.state;
566
+ const cacheKey = this.getMemoisedKey();
567
+ const { lineInformation = [], lineBlocks = [], blocks = [] } = computedDiffResult[cacheKey] ?? {};
568
+ // Calculate visible range for virtualization
569
+ let visibleRowStart = 0;
570
+ let visibleRowEnd = Infinity;
571
+ const buffer = 5; // render extra rows above/below viewport
572
+ if (infiniteLoading && scrollableContainerRef.current) {
573
+ const container = scrollableContainerRef.current;
574
+ const viewportRows = Math.ceil(container.clientHeight / DiffViewer.ESTIMATED_ROW_HEIGHT);
575
+ visibleRowStart = Math.max(0, visibleStartRow - buffer);
576
+ visibleRowEnd = visibleStartRow + viewportRows + buffer;
577
+ }
578
+ // First pass: build a map of lineIndex -> renderedRowIndex
579
+ // This accounts for code folding where some lines don't render or render as fold indicators
580
+ const lineToRowMap = new Map();
581
+ const seenBlocks = new Set();
582
+ let currentRow = 0;
583
+ for (let i = 0; i < lineInformation.length; i++) {
584
+ const blockIndex = lineBlocks[i];
585
+ if (showDiffOnly && blockIndex !== undefined) {
586
+ if (!expandedBlocks.includes(blockIndex)) {
587
+ // Line is in a collapsed block
588
+ const lastLineOfBlock = blocks[blockIndex].endLine === i;
589
+ if (!seenBlocks.has(blockIndex) && lastLineOfBlock) {
590
+ // This line renders as a fold indicator
591
+ seenBlocks.add(blockIndex);
592
+ lineToRowMap.set(i, currentRow);
593
+ currentRow++;
594
+ }
595
+ // Other lines in collapsed block don't render
596
+ }
597
+ else {
598
+ // Block is expanded, line renders normally
599
+ lineToRowMap.set(i, currentRow);
600
+ currentRow++;
601
+ }
602
+ }
603
+ else {
604
+ // Not in a block or showDiffOnly is false, line renders normally
605
+ lineToRowMap.set(i, currentRow);
606
+ currentRow++;
607
+ }
608
+ }
609
+ const totalRenderedRows = currentRow;
610
+ // Second pass: render only lines in the visible range
611
+ const diffNodes = [];
612
+ let topPadding = 0;
613
+ let firstVisibleFound = false;
614
+ seenBlocks.clear();
615
+ for (let lineIndex = 0; lineIndex < lineInformation.length; lineIndex++) {
616
+ const line = lineInformation[lineIndex];
617
+ const rowIndex = lineToRowMap.get(lineIndex);
618
+ // Skip lines that don't render (hidden in collapsed blocks)
619
+ if (rowIndex === undefined)
620
+ continue;
621
+ // Skip lines before visible range
622
+ if (rowIndex < visibleRowStart) {
623
+ continue;
624
+ }
625
+ // Stop after visible range
626
+ if (rowIndex > visibleRowEnd) {
627
+ break;
628
+ }
629
+ // Calculate top padding from the first visible row
630
+ if (!firstVisibleFound) {
631
+ topPadding = rowIndex * DiffViewer.ESTIMATED_ROW_HEIGHT;
632
+ firstVisibleFound = true;
633
+ }
634
+ // Render the line
635
+ if (showDiffOnly) {
261
636
  const blockIndex = lineBlocks[lineIndex];
262
637
  if (blockIndex !== undefined) {
263
638
  const lastLineOfBlock = blocks[blockIndex].endLine === lineIndex;
264
- if (!this.state.expandedBlocks.includes(blockIndex) &&
639
+ if (!expandedBlocks.includes(blockIndex) &&
265
640
  lastLineOfBlock) {
266
- return (_jsx(React.Fragment, { children: this.renderSkippedLineIndicator(blocks[blockIndex].lines, blockIndex, line.left.lineNumber, line.right.lineNumber) }, lineIndex));
641
+ diffNodes.push(_jsx(React.Fragment, { children: this.renderSkippedLineIndicator(blocks[blockIndex].lines, blockIndex, line.left.lineNumber, line.right.lineNumber) }, lineIndex));
642
+ continue;
267
643
  }
268
- if (!this.state.expandedBlocks.includes(blockIndex)) {
269
- return null;
644
+ if (!expandedBlocks.includes(blockIndex)) {
645
+ continue;
270
646
  }
271
647
  }
272
648
  }
273
- return splitView
649
+ diffNodes.push(splitView
274
650
  ? this.renderSplitView(line, lineIndex)
275
- : this.renderInlineView(line, lineIndex);
276
- });
651
+ : this.renderInlineView(line, lineIndex));
652
+ }
277
653
  return {
278
654
  diffNodes,
279
655
  blocks,
280
656
  lineInformation,
657
+ totalRenderedRows,
658
+ topPadding,
281
659
  };
282
660
  };
661
+ componentDidUpdate(prevProps) {
662
+ if (prevProps.oldValue !== this.props.oldValue ||
663
+ prevProps.newValue !== this.props.newValue ||
664
+ prevProps.compareMethod !== this.props.compareMethod ||
665
+ prevProps.disableWordDiff !== this.props.disableWordDiff ||
666
+ prevProps.linesOffset !== this.props.linesOffset) {
667
+ // Clear word diff cache when diff changes
668
+ this.wordDiffCache.clear();
669
+ this.setState((prev) => ({
670
+ ...prev,
671
+ isLoading: true,
672
+ visibleStartRow: 0
673
+ }));
674
+ this.memoisedCompute();
675
+ }
676
+ }
677
+ componentDidMount() {
678
+ this.setState((prev) => ({
679
+ ...prev,
680
+ isLoading: true
681
+ }));
682
+ this.memoisedCompute();
683
+ }
283
684
  render = () => {
284
685
  const { oldValue, newValue, useDarkTheme, leftTitle, rightTitle, splitView, compareMethod, hideLineNumbers, nonce, } = this.props;
285
686
  if (typeof compareMethod === "string" &&
@@ -328,26 +729,34 @@ class DiffViewer extends React.Component {
328
729
  }
329
730
  }
330
731
  const allExpanded = this.state.expandedBlocks.length === nodes.blocks.length;
331
- return (_jsxs("div", { children: [_jsxs("div", { className: this.styles.summary, role: "banner", children: [_jsx("button", { type: "button", className: this.styles.allExpandButton, onClick: () => {
332
- this.setState({
333
- expandedBlocks: allExpanded
334
- ? []
335
- : nodes.blocks.map((b) => b.index),
336
- });
337
- }, 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] }), _jsx("table", { className: cn(this.styles.diffContainer, {
338
- [this.styles.splitView]: splitView,
339
- }), onMouseUp: () => {
340
- const elements = document.getElementsByClassName("right");
341
- for (let i = 0; i < elements.length; i++) {
342
- const element = elements.item(i);
343
- element.classList.remove(this.styles.noSelect);
344
- }
345
- const elementsLeft = document.getElementsByClassName("left");
346
- for (let i = 0; i < elementsLeft.length; i++) {
347
- const element = elementsLeft.item(i);
348
- element.classList.remove(this.styles.noSelect);
349
- }
350
- }, children: _jsxs("tbody", { children: [_jsxs("tr", { children: [!this.props.hideLineNumbers ? _jsx("td", { width: "50px" }) : null, !splitView && !this.props.hideLineNumbers ? (_jsx("td", { width: "50px" })) : null, this.props.renderGutter ? _jsx("td", { width: "50px" }) : null, _jsx("td", { width: "28px" }), _jsx("td", { width: "100%" }), splitView ? (_jsxs(_Fragment, { children: [!this.props.hideLineNumbers ? _jsx("td", { width: "50px" }) : null, this.props.renderGutter ? _jsx("td", { width: "50px" }) : null, _jsx("td", { width: "28px" }), _jsx("td", { width: "100%" })] })) : null] }), leftTitle || rightTitle ? (_jsxs("tr", { children: [_jsx("th", { colSpan: splitView ? colSpanOnSplitView : colSpanOnInlineView, className: cn(this.styles.titleBlock, this.styles.column), children: leftTitle ? (_jsx("pre", { className: this.styles.contentText, children: leftTitle })) : null }), splitView ? (_jsx("th", { colSpan: colSpanOnSplitView, className: cn(this.styles.titleBlock, this.styles.column), children: rightTitle ? (_jsx("pre", { className: this.styles.contentText, children: rightTitle })) : null })) : null] })) : null, nodes.diffNodes] }) })] }));
732
+ const LoadingElement = this.props.loadingElement;
733
+ const scrollDivStyle = this.props.infiniteLoading ? {
734
+ overflowY: 'scroll',
735
+ overflowX: 'hidden',
736
+ height: this.props.infiniteLoading.containerHeight
737
+ } : {};
738
+ const totalContentHeight = nodes.totalRenderedRows * DiffViewer.ESTIMATED_ROW_HEIGHT;
739
+ const tableElement = (_jsxs("table", { className: cn(this.styles.diffContainer, {
740
+ [this.styles.splitView]: splitView,
741
+ }), onMouseUp: () => {
742
+ const elements = document.getElementsByClassName("right");
743
+ for (let i = 0; i < elements.length; i++) {
744
+ const element = elements.item(i);
745
+ element.classList.remove(this.styles.noSelect);
746
+ }
747
+ const elementsLeft = document.getElementsByClassName("left");
748
+ for (let i = 0; i < elementsLeft.length; i++) {
749
+ const element = elementsLeft.item(i);
750
+ element.classList.remove(this.styles.noSelect);
751
+ }
752
+ }, 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 })] }));
753
+ return (_jsxs("div", { style: { ...scrollDivStyle, position: 'relative' }, onScroll: this.onScroll, ref: this.state.scrollableContainerRef, children: [(!this.props.hideSummary || leftTitle || rightTitle) && (_jsxs("div", { 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: () => {
754
+ this.setState({
755
+ expandedBlocks: allExpanded
756
+ ? []
757
+ : nodes.blocks.map((b) => b.index),
758
+ });
759
+ }, 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: { minHeight: totalContentHeight, paddingTop: nodes.topPadding }, children: tableElement })) : (tableElement)] }));
351
760
  };
352
761
  }
353
762
  export default DiffViewer;