react-diff-viewer-continued 4.0.6 → 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,17 +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
- : (typeof wordDiff.value === 'string'
93
- ? wordDiff.value
94
- // If wordDiff.value is DiffInformation, we don't handle it, unclear why. See c0c99f5712.
95
- : undefined);
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
+ }
96
322
  return wordDiff.type === DiffType.ADDED ? (_jsx("ins", { className: cn(this.styles.wordDiff, {
97
- [this.styles.wordAdded]: wordDiff.type === DiffType.ADDED,
323
+ [this.styles.wordAdded]: showHighlight,
98
324
  }), children: content }, i)) : wordDiff.type === DiffType.REMOVED ? (_jsx("del", { className: cn(this.styles.wordDiff, {
99
- [this.styles.wordRemoved]: wordDiff.type === DiffType.REMOVED,
325
+ [this.styles.wordRemoved]: showHighlight,
100
326
  }), children: content }, i)) : (_jsx("span", { className: cn(this.styles.wordDiff), children: content }, i));
101
327
  });
102
328
  };
@@ -197,7 +423,9 @@ class DiffViewer extends React.Component {
197
423
  * @param index React key for the lines.
198
424
  */
199
425
  renderSplitView = ({ left, right }, index) => {
200
- 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));
201
429
  };
202
430
  /**
203
431
  * Generates lines for inline view.
@@ -208,18 +436,20 @@ class DiffViewer extends React.Component {
208
436
  * @param index React key for the lines.
209
437
  */
210
438
  renderInlineView = ({ left, right }, index) => {
439
+ // Compute word diff on-demand if deferred
440
+ const { leftValue, rightValue } = this.getWordDiffValues(left, right, index);
211
441
  let content;
212
442
  if (left.type === DiffType.REMOVED && right.type === DiffType.ADDED) {
213
- 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));
214
444
  }
215
445
  if (left.type === DiffType.REMOVED) {
216
- content = this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, left.value, null);
446
+ content = this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, leftValue, null);
217
447
  }
218
448
  if (left.type === DiffType.DEFAULT) {
219
- 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);
220
450
  }
221
451
  if (right.type === DiffType.ADDED) {
222
- content = this.renderLine(null, right.type, LineNumberPrefix.RIGHT, right.value, right.lineNumber);
452
+ content = this.renderLine(null, right.type, LineNumberPrefix.RIGHT, rightValue, right.lineNumber);
223
453
  }
224
454
  return (_jsx("tr", { className: this.styles.line, children: content }, index));
225
455
  };
@@ -240,47 +470,217 @@ class DiffViewer extends React.Component {
240
470
  */
241
471
  renderSkippedLineIndicator = (num, blockNumber, leftBlockLineNumber, rightBlockLineNumber) => {
242
472
  const { hideLineNumbers, splitView } = this.props;
243
- 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, " @@"] }));
244
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 }) }));
245
475
  const isUnifiedViewWithoutLineNumbers = !splitView && !hideLineNumbers;
246
- 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({
247
478
  [this.styles.codeFoldGutter]: isUnifiedViewWithoutLineNumbers,
248
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}`));
249
480
  };
250
481
  /**
251
- * 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
+ *
252
488
  */
253
- renderDiff = () => {
254
- const { oldValue, newValue, splitView, disableWordDiff, compareMethod, linesOffset, } = this.props;
255
- 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);
256
532
  const extraLines = this.props.extraLinesSurroundingDiff < 0
257
533
  ? 0
258
534
  : Math.round(this.props.extraLinesSurroundingDiff);
259
535
  const { lineBlocks, blocks } = computeHiddenBlocks(lineInformation, diffLines, extraLines);
260
- const diffNodes = lineInformation.map((line, lineIndex) => {
261
- 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) {
262
636
  const blockIndex = lineBlocks[lineIndex];
263
637
  if (blockIndex !== undefined) {
264
638
  const lastLineOfBlock = blocks[blockIndex].endLine === lineIndex;
265
- if (!this.state.expandedBlocks.includes(blockIndex) &&
639
+ if (!expandedBlocks.includes(blockIndex) &&
266
640
  lastLineOfBlock) {
267
- 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;
268
643
  }
269
- if (!this.state.expandedBlocks.includes(blockIndex)) {
270
- return null;
644
+ if (!expandedBlocks.includes(blockIndex)) {
645
+ continue;
271
646
  }
272
647
  }
273
648
  }
274
- return splitView
649
+ diffNodes.push(splitView
275
650
  ? this.renderSplitView(line, lineIndex)
276
- : this.renderInlineView(line, lineIndex);
277
- });
651
+ : this.renderInlineView(line, lineIndex));
652
+ }
278
653
  return {
279
654
  diffNodes,
280
655
  blocks,
281
656
  lineInformation,
657
+ totalRenderedRows,
658
+ topPadding,
282
659
  };
283
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
+ }
284
684
  render = () => {
285
685
  const { oldValue, newValue, useDarkTheme, leftTitle, rightTitle, splitView, compareMethod, hideLineNumbers, nonce, } = this.props;
286
686
  if (typeof compareMethod === "string" &&
@@ -329,26 +729,34 @@ class DiffViewer extends React.Component {
329
729
  }
330
730
  }
331
731
  const allExpanded = this.state.expandedBlocks.length === nodes.blocks.length;
332
- return (_jsxs("div", { children: [_jsxs("div", { className: this.styles.summary, role: "banner", children: [_jsx("button", { type: "button", className: this.styles.allExpandButton, onClick: () => {
333
- this.setState({
334
- expandedBlocks: allExpanded
335
- ? []
336
- : nodes.blocks.map((b) => b.index),
337
- });
338
- }, 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, {
339
- [this.styles.splitView]: splitView,
340
- }), onMouseUp: () => {
341
- const elements = document.getElementsByClassName("right");
342
- for (let i = 0; i < elements.length; i++) {
343
- const element = elements.item(i);
344
- element.classList.remove(this.styles.noSelect);
345
- }
346
- const elementsLeft = document.getElementsByClassName("left");
347
- for (let i = 0; i < elementsLeft.length; i++) {
348
- const element = elementsLeft.item(i);
349
- element.classList.remove(this.styles.noSelect);
350
- }
351
- }, 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)] }));
352
760
  };
353
761
  }
354
762
  export default DiffViewer;