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.
@@ -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,13 @@ 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();
154
+ // Refs for measuring content column width and character width
155
+ contentColumnRef = React.createRef();
156
+ charMeasureRef = React.createRef();
157
+ stickyHeaderRef = React.createRef();
158
+ resizeObserver = null;
17
159
  static defaultProps = {
18
160
  oldValue: "",
19
161
  newValue: "",
@@ -34,8 +176,45 @@ class DiffViewer extends React.Component {
34
176
  this.state = {
35
177
  expandedBlocks: [],
36
178
  noSelect: undefined,
179
+ scrollableContainerRef: React.createRef(),
180
+ computedDiffResult: {},
181
+ isLoading: false,
182
+ visibleStartRow: 0,
183
+ contentColumnWidth: null,
184
+ charWidth: null,
185
+ cumulativeOffsets: null,
37
186
  };
38
187
  }
188
+ /**
189
+ * Computes word diff on-demand for a line, with caching.
190
+ * This is used when word diff was deferred during initial computation.
191
+ */
192
+ getWordDiffValues = (left, right, lineIndex) => {
193
+ // Handle empty left/right
194
+ if (!left || !right) {
195
+ return { leftValue: left?.value, rightValue: right?.value };
196
+ }
197
+ // If no raw values, word diff was already computed or disabled
198
+ // Use explicit undefined check since empty string is a valid raw value
199
+ if (left.rawValue === undefined || right.rawValue === undefined) {
200
+ return { leftValue: left.value, rightValue: right.value };
201
+ }
202
+ // Check cache
203
+ const cacheKey = `${lineIndex}-${left.rawValue}-${right.rawValue}`;
204
+ let cached = this.wordDiffCache.get(cacheKey);
205
+ if (!cached) {
206
+ // Compute word diff on-demand
207
+ // Use CHARS method for on-demand computation since rawValue is always a string
208
+ // (JSON/YAML methods only work with objects, not the string lines we have here)
209
+ const compareMethod = (this.props.compareMethod === DiffMethod.JSON || this.props.compareMethod === DiffMethod.YAML)
210
+ ? DiffMethod.CHARS
211
+ : this.props.compareMethod;
212
+ const computed = computeDiff(left.rawValue, right.rawValue, compareMethod);
213
+ cached = { left: computed.left, right: computed.right };
214
+ this.wordDiffCache.set(cacheKey, cached);
215
+ }
216
+ return { leftValue: cached.left, rightValue: cached.right };
217
+ };
39
218
  /**
40
219
  * Resets code block expand to the initial stage. Will be exposed to the parent component via
41
220
  * refs.
@@ -56,8 +235,130 @@ class DiffViewer extends React.Component {
56
235
  onBlockExpand = (id) => {
57
236
  const prevState = this.state.expandedBlocks.slice();
58
237
  prevState.push(id);
59
- this.setState({
60
- expandedBlocks: prevState,
238
+ this.setState({ expandedBlocks: prevState }, () => this.recalculateOffsets());
239
+ };
240
+ /**
241
+ * Gets the height of the sticky header, if present.
242
+ */
243
+ getStickyHeaderHeight() {
244
+ return this.stickyHeaderRef.current?.offsetHeight || 0;
245
+ }
246
+ /**
247
+ * Measures the width of a single character in the monospace font.
248
+ * Falls back to 7.2px if measurement fails.
249
+ */
250
+ measureCharWidth() {
251
+ const span = this.charMeasureRef.current;
252
+ if (!span)
253
+ return 7.2; // fallback
254
+ return span.getBoundingClientRect().width || 7.2;
255
+ }
256
+ /**
257
+ * Measures the available width for content in a content column.
258
+ * Falls back to estimating from container width if direct measurement fails.
259
+ */
260
+ measureContentColumnWidth() {
261
+ // Try direct measurement first
262
+ const cell = this.contentColumnRef.current;
263
+ if (cell && cell.clientWidth > 0) {
264
+ const style = window.getComputedStyle(cell);
265
+ const padding = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
266
+ const width = cell.clientWidth - padding;
267
+ if (width > 0)
268
+ return width;
269
+ }
270
+ // Fallback: estimate from container width
271
+ // In split view: container has 2 content columns + gutters (50px each) + markers (28px each)
272
+ // In unified view: 1 content column + 2 gutters + 1 marker
273
+ const container = this.state.scrollableContainerRef.current;
274
+ if (!container || container.clientWidth <= 0)
275
+ return null;
276
+ const containerWidth = container.clientWidth;
277
+ const gutterWidth = this.props.hideLineNumbers ? 0 : 50;
278
+ const markerWidth = 28;
279
+ const gutterCount = this.props.splitView ? 2 : 2; // left gutter(s)
280
+ const markerCount = this.props.splitView ? 2 : 1;
281
+ const contentColumns = this.props.splitView ? 2 : 1;
282
+ const fixedWidth = gutterCount * gutterWidth + markerCount * markerWidth;
283
+ const availableWidth = containerWidth - fixedWidth;
284
+ return Math.max(100, availableWidth / contentColumns); // minimum 100px
285
+ }
286
+ /**
287
+ * Gets the text length from a value that may be a string or DiffInformation array.
288
+ */
289
+ getTextLength(value) {
290
+ if (!value)
291
+ return 0;
292
+ if (typeof value === 'string')
293
+ return value.length;
294
+ return value.reduce((sum, d) => sum + (typeof d.value === 'string' ? d.value.length : 0), 0);
295
+ }
296
+ /**
297
+ * Builds cumulative vertical offsets for each line based on character count and column width.
298
+ * This allows accurate scroll position calculations with variable row heights.
299
+ */
300
+ buildCumulativeOffsets(lineInformation, lineBlocks, blocks, expandedBlocks, showDiffOnly, charWidth, columnWidth, splitView) {
301
+ const offsets = [0];
302
+ const seenBlocks = new Set();
303
+ for (let i = 0; i < lineInformation.length; i++) {
304
+ const line = lineInformation[i];
305
+ if (showDiffOnly) {
306
+ const blockIndex = lineBlocks[i];
307
+ if (blockIndex !== undefined && !expandedBlocks.includes(blockIndex)) {
308
+ const isLastLine = blocks[blockIndex].endLine === i;
309
+ if (!seenBlocks.has(blockIndex) && isLastLine) {
310
+ seenBlocks.add(blockIndex);
311
+ offsets.push(offsets[offsets.length - 1] + DiffViewer.ESTIMATED_ROW_HEIGHT);
312
+ }
313
+ continue;
314
+ }
315
+ }
316
+ // Calculate visual rows for this line
317
+ const leftLen = line.left?.value ? this.getTextLength(line.left.value) : 0;
318
+ const rightLen = line.right?.value ? this.getTextLength(line.right.value) : 0;
319
+ const maxLen = splitView ? Math.max(leftLen, rightLen) : (leftLen || rightLen);
320
+ const charsPerRow = Math.floor(columnWidth / charWidth);
321
+ const visualRows = charsPerRow > 0 ? Math.max(1, Math.ceil(maxLen / charsPerRow)) : 1;
322
+ offsets.push(offsets[offsets.length - 1] + visualRows * DiffViewer.ESTIMATED_ROW_HEIGHT);
323
+ }
324
+ return offsets;
325
+ }
326
+ /**
327
+ * Binary search to find the line index at a given scroll offset.
328
+ */
329
+ findLineAtOffset(scrollTop, offsets) {
330
+ let low = 0;
331
+ let high = offsets.length - 2;
332
+ while (low < high) {
333
+ const mid = Math.floor((low + high + 1) / 2);
334
+ if (offsets[mid] <= scrollTop) {
335
+ low = mid;
336
+ }
337
+ else {
338
+ high = mid - 1;
339
+ }
340
+ }
341
+ return low;
342
+ }
343
+ /**
344
+ * Recalculates cumulative offsets based on current measurements.
345
+ * Called on resize and when blocks are expanded/collapsed.
346
+ */
347
+ recalculateOffsets = () => {
348
+ if (!this.props.infiniteLoading)
349
+ return;
350
+ const columnWidth = this.measureContentColumnWidth();
351
+ const charWidth = this.measureCharWidth();
352
+ if (!columnWidth)
353
+ return;
354
+ const cacheKey = this.getMemoisedKey();
355
+ const { lineInformation, lineBlocks, blocks } = this.state.computedDiffResult[cacheKey] ?? {};
356
+ if (!lineInformation)
357
+ return;
358
+ const offsets = this.buildCumulativeOffsets(lineInformation, lineBlocks, blocks, this.state.expandedBlocks, this.props.showDiffOnly, charWidth, columnWidth, this.props.splitView);
359
+ this.setState({ cumulativeOffsets: offsets, contentColumnWidth: columnWidth, charWidth }, () => {
360
+ // Force a scroll position update to recalculate visible rows with new offsets
361
+ this.onScroll();
61
362
  });
62
363
  };
63
364
  /**
@@ -79,6 +380,19 @@ class DiffViewer extends React.Component {
79
380
  }
80
381
  return () => { };
81
382
  };
383
+ /**
384
+ * Checks if the current compare method should show word-level highlighting.
385
+ * Character, word-level, JSON, and YAML diffs benefit from highlighting individual changes.
386
+ * JSON/YAML use CHARS internally for word-level diff, so they should be highlighted.
387
+ */
388
+ shouldHighlightWordDiff = () => {
389
+ const { compareMethod } = this.props;
390
+ return (compareMethod === DiffMethod.CHARS ||
391
+ compareMethod === DiffMethod.WORDS ||
392
+ compareMethod === DiffMethod.WORDS_WITH_SPACE ||
393
+ compareMethod === DiffMethod.JSON ||
394
+ compareMethod === DiffMethod.YAML);
395
+ };
82
396
  /**
83
397
  * Maps over the word diff and constructs the required React elements to show word diff.
84
398
  *
@@ -86,17 +400,59 @@ class DiffViewer extends React.Component {
86
400
  * @param renderer Optional renderer to format diff words. Useful for syntax highlighting.
87
401
  */
88
402
  renderWordDiff = (diffArray, renderer) => {
403
+ const showHighlight = this.shouldHighlightWordDiff();
404
+ const { compareMethod } = this.props;
405
+ // Don't apply syntax highlighting for JSON/YAML - their word diffs are computed
406
+ // on-demand from raw strings and syntax highlighting creates messy fragmented tokens.
407
+ const skipSyntaxHighlighting = compareMethod === DiffMethod.JSON || compareMethod === DiffMethod.YAML;
408
+ // Reconstruct the full line from diff chunks
409
+ const fullLine = diffArray
410
+ .map((d) => (typeof d.value === "string" ? d.value : ""))
411
+ .join("");
412
+ // For very long lines (>500 chars), skip fancy processing - just render plain text
413
+ // without word-level highlighting to avoid performance issues
414
+ const MAX_LINE_LENGTH = 500;
415
+ if (fullLine.length > MAX_LINE_LENGTH) {
416
+ return [_jsx("span", { children: fullLine }, "long-line")];
417
+ }
418
+ // If we have a renderer and syntax highlighting is enabled, try to highlight
419
+ // the full line first, then apply diff styling to preserve proper tokenization.
420
+ if (renderer && !skipSyntaxHighlighting) {
421
+ // Get the syntax-highlighted content
422
+ const highlighted = renderer(fullLine);
423
+ // Check if the renderer uses dangerouslySetInnerHTML (common with Prism, highlight.js, etc.)
424
+ const htmlContent = highlighted?.props?.dangerouslySetInnerHTML?.__html;
425
+ if (typeof htmlContent === "string") {
426
+ // Apply diff styling to the highlighted HTML
427
+ const styledHtml = applyDiffToHighlightedHtml(htmlContent, diffArray, {
428
+ wordDiff: this.styles.wordDiff,
429
+ wordAdded: showHighlight ? this.styles.wordAdded : "",
430
+ wordRemoved: showHighlight ? this.styles.wordRemoved : "",
431
+ });
432
+ // Clone the element with the modified HTML
433
+ return [
434
+ React.cloneElement(highlighted, {
435
+ key: "highlighted-diff",
436
+ dangerouslySetInnerHTML: { __html: styledHtml },
437
+ }),
438
+ ];
439
+ }
440
+ // Renderer doesn't use dangerouslySetInnerHTML - fall through to per-chunk rendering
441
+ }
442
+ // Fallback: render each chunk separately (used for JSON/YAML or non-HTML renderers)
89
443
  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);
444
+ let content;
445
+ if (typeof wordDiff.value === "string") {
446
+ content = wordDiff.value;
447
+ }
448
+ else {
449
+ // If wordDiff.value is DiffInformation[], we don't handle it. See c0c99f5712.
450
+ content = undefined;
451
+ }
96
452
  return wordDiff.type === DiffType.ADDED ? (_jsx("ins", { className: cn(this.styles.wordDiff, {
97
- [this.styles.wordAdded]: wordDiff.type === DiffType.ADDED,
453
+ [this.styles.wordAdded]: showHighlight,
98
454
  }), children: content }, i)) : wordDiff.type === DiffType.REMOVED ? (_jsx("del", { className: cn(this.styles.wordDiff, {
99
- [this.styles.wordRemoved]: wordDiff.type === DiffType.REMOVED,
455
+ [this.styles.wordRemoved]: showHighlight,
100
456
  }), children: content }, i)) : (_jsx("span", { className: cn(this.styles.wordDiff), children: content }, i));
101
457
  });
102
458
  };
@@ -168,7 +524,7 @@ class DiffViewer extends React.Component {
168
524
  [this.styles.diffRemoved]: removed,
169
525
  [this.styles.diffChanged]: changed,
170
526
  [this.styles.highlightedLine]: highlightLine,
171
- }), children: _jsxs("pre", { children: [added && "+", removed && "-"] }) }), _jsx("td", { className: cn(this.styles.content, {
527
+ }), children: _jsxs("pre", { children: [added && "+", removed && "-"] }) }), _jsx("td", { ref: prefix === LineNumberPrefix.LEFT && !this.state.cumulativeOffsets ? this.contentColumnRef : undefined, className: cn(this.styles.content, {
172
528
  [this.styles.emptyLine]: !content,
173
529
  [this.styles.diffAdded]: added,
174
530
  [this.styles.diffRemoved]: removed,
@@ -197,7 +553,9 @@ class DiffViewer extends React.Component {
197
553
  * @param index React key for the lines.
198
554
  */
199
555
  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));
556
+ // Compute word diff on-demand if deferred
557
+ const { leftValue, rightValue } = this.getWordDiffValues(left, right, index);
558
+ 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
559
  };
202
560
  /**
203
561
  * Generates lines for inline view.
@@ -208,18 +566,20 @@ class DiffViewer extends React.Component {
208
566
  * @param index React key for the lines.
209
567
  */
210
568
  renderInlineView = ({ left, right }, index) => {
569
+ // Compute word diff on-demand if deferred
570
+ const { leftValue, rightValue } = this.getWordDiffValues(left, right, index);
211
571
  let content;
212
572
  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));
573
+ 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
574
  }
215
575
  if (left.type === DiffType.REMOVED) {
216
- content = this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, left.value, null);
576
+ content = this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, leftValue, null);
217
577
  }
218
578
  if (left.type === DiffType.DEFAULT) {
219
- content = this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, left.value, right.lineNumber, LineNumberPrefix.RIGHT);
579
+ content = this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, leftValue, right.lineNumber, LineNumberPrefix.RIGHT);
220
580
  }
221
581
  if (right.type === DiffType.ADDED) {
222
- content = this.renderLine(null, right.type, LineNumberPrefix.RIGHT, right.value, right.lineNumber);
582
+ content = this.renderLine(null, right.type, LineNumberPrefix.RIGHT, rightValue, right.lineNumber);
223
583
  }
224
584
  return (_jsx("tr", { className: this.styles.line, children: content }, index));
225
585
  };
@@ -240,47 +600,296 @@ class DiffViewer extends React.Component {
240
600
  */
241
601
  renderSkippedLineIndicator = (num, blockNumber, leftBlockLineNumber, rightBlockLineNumber) => {
242
602
  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 ..."] }));
603
+ 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
604
  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
605
  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({
606
+ const expandGutter = (_jsx("td", { className: this.styles.codeFoldGutter, children: _jsx(Expand, {}) }));
607
+ 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
608
  [this.styles.codeFoldGutter]: isUnifiedViewWithoutLineNumbers,
248
609
  }) }), 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
610
  };
250
611
  /**
251
- * Generates the entire diff view.
612
+ *
613
+ * Generates a unique cache key based on the current props used in diff computation.
614
+ *
615
+ * This key is used to memoize results and avoid recomputation for the same inputs.
616
+ * @returns A stringified JSON key representing the current diff settings and input values.
617
+ *
252
618
  */
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);
619
+ getMemoisedKey = () => {
620
+ const { oldValue, newValue, disableWordDiff, compareMethod, linesOffset, alwaysShowLines, extraLinesSurroundingDiff, } = this.props;
621
+ return JSON.stringify({
622
+ oldValue,
623
+ newValue,
624
+ disableWordDiff,
625
+ compareMethod,
626
+ linesOffset,
627
+ alwaysShowLines,
628
+ extraLinesSurroundingDiff,
629
+ });
630
+ };
631
+ /**
632
+ * Computes and memoizes the diff result between `oldValue` and `newValue`.
633
+ *
634
+ * If a memoized result exists for the current input configuration, it uses that.
635
+ * Otherwise, it runs the diff logic in a Web Worker to avoid blocking the UI.
636
+ * It also computes hidden line blocks for collapsing unchanged sections,
637
+ * and stores the result in the local component state.
638
+ */
639
+ memoisedCompute = async () => {
640
+ const { oldValue, newValue, disableWordDiff, compareMethod, linesOffset } = this.props;
641
+ const cacheKey = this.getMemoisedKey();
642
+ if (!!this.state.computedDiffResult[cacheKey]) {
643
+ this.setState((prev) => ({
644
+ ...prev,
645
+ isLoading: false
646
+ }));
647
+ return;
648
+ }
649
+ // Defer word diff computation when using infinite loading with reasonable container height
650
+ // This significantly improves initial render time for large diffs
651
+ const containerHeight = this.props.infiniteLoading?.containerHeight;
652
+ const containerHeightPx = containerHeight
653
+ ? typeof containerHeight === 'number'
654
+ ? containerHeight
655
+ : parseInt(containerHeight, 10) || 0
656
+ : 0;
657
+ const shouldDeferWordDiff = !disableWordDiff &&
658
+ !!this.props.infiniteLoading &&
659
+ containerHeightPx > 0 &&
660
+ containerHeightPx < 2000;
661
+ const { lineInformation, diffLines } = await computeLineInformationWorker(oldValue, newValue, disableWordDiff, compareMethod, linesOffset, this.props.alwaysShowLines, shouldDeferWordDiff);
256
662
  const extraLines = this.props.extraLinesSurroundingDiff < 0
257
663
  ? 0
258
664
  : Math.round(this.props.extraLinesSurroundingDiff);
259
665
  const { lineBlocks, blocks } = computeHiddenBlocks(lineInformation, diffLines, extraLines);
260
- const diffNodes = lineInformation.map((line, lineIndex) => {
261
- if (this.props.showDiffOnly) {
666
+ this.state.computedDiffResult[cacheKey] = { lineInformation, lineBlocks, blocks };
667
+ this.setState((prev) => ({
668
+ ...prev,
669
+ computedDiffResult: this.state.computedDiffResult,
670
+ isLoading: false,
671
+ }), () => {
672
+ // Trigger offset recalculation after diff is computed and rendered
673
+ // Use requestAnimationFrame to ensure DOM is ready for measurement
674
+ if (this.props.infiniteLoading) {
675
+ requestAnimationFrame(() => this.recalculateOffsets());
676
+ }
677
+ });
678
+ };
679
+ // Estimated row height based on lineHeight: 1.6em with 12px base font
680
+ static ESTIMATED_ROW_HEIGHT = 19;
681
+ /**
682
+ * Handles scroll events on the scrollable container.
683
+ *
684
+ * Updates the visible start row for virtualization.
685
+ */
686
+ onScroll = () => {
687
+ const container = this.state.scrollableContainerRef.current;
688
+ if (!container || !this.props.infiniteLoading)
689
+ return;
690
+ // Account for sticky header height in scroll calculations
691
+ const headerHeight = this.getStickyHeaderHeight();
692
+ const contentScrollTop = Math.max(0, container.scrollTop - headerHeight);
693
+ const { cumulativeOffsets } = this.state;
694
+ const newStartRow = cumulativeOffsets
695
+ ? this.findLineAtOffset(contentScrollTop, cumulativeOffsets)
696
+ : Math.floor(contentScrollTop / DiffViewer.ESTIMATED_ROW_HEIGHT);
697
+ // Only update state if the start row changed (avoid unnecessary re-renders)
698
+ if (newStartRow !== this.state.visibleStartRow) {
699
+ this.setState({ visibleStartRow: newStartRow });
700
+ }
701
+ };
702
+ /**
703
+ * Generates the entire diff view with virtualization support.
704
+ */
705
+ renderDiff = () => {
706
+ const { splitView, infiniteLoading, showDiffOnly } = this.props;
707
+ const { computedDiffResult, expandedBlocks, visibleStartRow, scrollableContainerRef, cumulativeOffsets } = this.state;
708
+ const cacheKey = this.getMemoisedKey();
709
+ const { lineInformation = [], lineBlocks = [], blocks = [] } = computedDiffResult[cacheKey] ?? {};
710
+ // Calculate visible range for virtualization
711
+ let visibleRowStart = 0;
712
+ let visibleRowEnd = Infinity;
713
+ const buffer = 5; // render extra rows above/below viewport
714
+ if (infiniteLoading && scrollableContainerRef.current) {
715
+ const container = scrollableContainerRef.current;
716
+ // Account for sticky header height in scroll calculations
717
+ const headerHeight = this.getStickyHeaderHeight();
718
+ const contentScrollTop = Math.max(0, container.scrollTop - headerHeight);
719
+ if (cumulativeOffsets) {
720
+ // Variable height mode: use binary search to find visible range
721
+ const totalHeight = cumulativeOffsets[cumulativeOffsets.length - 1] || 0;
722
+ const lastRowIndex = cumulativeOffsets.length - 2;
723
+ visibleRowStart = Math.max(0, this.findLineAtOffset(contentScrollTop, cumulativeOffsets) - buffer);
724
+ visibleRowEnd = this.findLineAtOffset(contentScrollTop + container.clientHeight, cumulativeOffsets) + buffer;
725
+ // IMPORTANT: The calculated offsets may overestimate row heights (based on char count),
726
+ // but actual CSS rendering might produce shorter rows. To prevent empty space,
727
+ // ensure we render at least enough rows to fill the viewport using ESTIMATED_ROW_HEIGHT
728
+ // as a conservative minimum.
729
+ const minRowsToFillViewport = Math.ceil(container.clientHeight / DiffViewer.ESTIMATED_ROW_HEIGHT);
730
+ visibleRowEnd = Math.max(visibleRowEnd, visibleRowStart + minRowsToFillViewport + buffer);
731
+ // Also ensure we render all rows when near the bottom
732
+ if (contentScrollTop + container.clientHeight >= totalHeight - buffer * DiffViewer.ESTIMATED_ROW_HEIGHT) {
733
+ visibleRowEnd = lastRowIndex + buffer;
734
+ }
735
+ }
736
+ else {
737
+ // Fixed height fallback
738
+ const viewportRows = Math.ceil(container.clientHeight / DiffViewer.ESTIMATED_ROW_HEIGHT);
739
+ visibleRowStart = Math.max(0, visibleStartRow - buffer);
740
+ visibleRowEnd = visibleStartRow + viewportRows + buffer;
741
+ }
742
+ }
743
+ // First pass: build a map of lineIndex -> renderedRowIndex
744
+ // This accounts for code folding where some lines don't render or render as fold indicators
745
+ const lineToRowMap = new Map();
746
+ const seenBlocks = new Set();
747
+ let currentRow = 0;
748
+ for (let i = 0; i < lineInformation.length; i++) {
749
+ const blockIndex = lineBlocks[i];
750
+ if (showDiffOnly && blockIndex !== undefined) {
751
+ if (!expandedBlocks.includes(blockIndex)) {
752
+ // Line is in a collapsed block
753
+ const lastLineOfBlock = blocks[blockIndex].endLine === i;
754
+ if (!seenBlocks.has(blockIndex) && lastLineOfBlock) {
755
+ // This line renders as a fold indicator
756
+ seenBlocks.add(blockIndex);
757
+ lineToRowMap.set(i, currentRow);
758
+ currentRow++;
759
+ }
760
+ // Other lines in collapsed block don't render
761
+ }
762
+ else {
763
+ // Block is expanded, line renders normally
764
+ lineToRowMap.set(i, currentRow);
765
+ currentRow++;
766
+ }
767
+ }
768
+ else {
769
+ // Not in a block or showDiffOnly is false, line renders normally
770
+ lineToRowMap.set(i, currentRow);
771
+ currentRow++;
772
+ }
773
+ }
774
+ const totalRenderedRows = currentRow;
775
+ // Second pass: render only lines in the visible range
776
+ const diffNodes = [];
777
+ let topPadding = 0;
778
+ let firstVisibleFound = false;
779
+ let lastRenderedRowIndex = -1;
780
+ seenBlocks.clear();
781
+ for (let lineIndex = 0; lineIndex < lineInformation.length; lineIndex++) {
782
+ const line = lineInformation[lineIndex];
783
+ const rowIndex = lineToRowMap.get(lineIndex);
784
+ // Skip lines that don't render (hidden in collapsed blocks)
785
+ if (rowIndex === undefined)
786
+ continue;
787
+ // Skip lines before visible range
788
+ if (rowIndex < visibleRowStart) {
789
+ continue;
790
+ }
791
+ // Stop after visible range
792
+ if (rowIndex > visibleRowEnd) {
793
+ break;
794
+ }
795
+ // Calculate top padding from the first visible row
796
+ if (!firstVisibleFound) {
797
+ topPadding = cumulativeOffsets
798
+ ? cumulativeOffsets[rowIndex] || 0
799
+ : rowIndex * DiffViewer.ESTIMATED_ROW_HEIGHT;
800
+ firstVisibleFound = true;
801
+ }
802
+ // Track the last rendered row for bottom padding calculation
803
+ lastRenderedRowIndex = rowIndex;
804
+ // Render the line
805
+ if (showDiffOnly) {
262
806
  const blockIndex = lineBlocks[lineIndex];
263
807
  if (blockIndex !== undefined) {
264
808
  const lastLineOfBlock = blocks[blockIndex].endLine === lineIndex;
265
- if (!this.state.expandedBlocks.includes(blockIndex) &&
809
+ if (!expandedBlocks.includes(blockIndex) &&
266
810
  lastLineOfBlock) {
267
- return (_jsx(React.Fragment, { children: this.renderSkippedLineIndicator(blocks[blockIndex].lines, blockIndex, line.left.lineNumber, line.right.lineNumber) }, lineIndex));
811
+ diffNodes.push(_jsx(React.Fragment, { children: this.renderSkippedLineIndicator(blocks[blockIndex].lines, blockIndex, line.left.lineNumber, line.right.lineNumber) }, lineIndex));
812
+ continue;
268
813
  }
269
- if (!this.state.expandedBlocks.includes(blockIndex)) {
270
- return null;
814
+ if (!expandedBlocks.includes(blockIndex)) {
815
+ continue;
271
816
  }
272
817
  }
273
818
  }
274
- return splitView
819
+ diffNodes.push(splitView
275
820
  ? this.renderSplitView(line, lineIndex)
276
- : this.renderInlineView(line, lineIndex);
277
- });
821
+ : this.renderInlineView(line, lineIndex));
822
+ }
823
+ // Calculate total content height
824
+ const totalContentHeight = cumulativeOffsets
825
+ ? cumulativeOffsets[cumulativeOffsets.length - 1] || 0
826
+ : totalRenderedRows * DiffViewer.ESTIMATED_ROW_HEIGHT;
827
+ // Calculate bottom padding: space after the last rendered row
828
+ const bottomPadding = cumulativeOffsets && lastRenderedRowIndex >= 0
829
+ ? totalContentHeight - (cumulativeOffsets[lastRenderedRowIndex + 1] || totalContentHeight)
830
+ : 0;
278
831
  return {
279
832
  diffNodes,
280
833
  blocks,
281
834
  lineInformation,
835
+ totalRenderedRows,
836
+ topPadding,
837
+ bottomPadding,
838
+ totalContentHeight,
839
+ renderedCount: diffNodes.length,
840
+ // Debug info
841
+ debug: {
842
+ visibleRowStart,
843
+ visibleRowEnd,
844
+ totalRows: totalRenderedRows,
845
+ offsetsLength: cumulativeOffsets?.length ?? 0,
846
+ renderedCount: diffNodes.length,
847
+ scrollTop: scrollableContainerRef.current?.scrollTop ?? 0,
848
+ headerHeight: this.getStickyHeaderHeight(),
849
+ contentScrollTop: scrollableContainerRef.current
850
+ ? Math.max(0, scrollableContainerRef.current.scrollTop - this.getStickyHeaderHeight())
851
+ : 0,
852
+ clientHeight: scrollableContainerRef.current?.clientHeight ?? 0,
853
+ }
282
854
  };
283
855
  };
856
+ componentDidUpdate(prevProps) {
857
+ if (prevProps.oldValue !== this.props.oldValue ||
858
+ prevProps.newValue !== this.props.newValue ||
859
+ prevProps.compareMethod !== this.props.compareMethod ||
860
+ prevProps.disableWordDiff !== this.props.disableWordDiff ||
861
+ prevProps.linesOffset !== this.props.linesOffset) {
862
+ // Clear word diff cache when diff changes
863
+ this.wordDiffCache.clear();
864
+ this.setState((prev) => ({
865
+ ...prev,
866
+ isLoading: true,
867
+ visibleStartRow: 0,
868
+ cumulativeOffsets: null,
869
+ }));
870
+ this.memoisedCompute();
871
+ }
872
+ }
873
+ componentDidMount() {
874
+ this.setState((prev) => ({
875
+ ...prev,
876
+ isLoading: true
877
+ }));
878
+ this.memoisedCompute();
879
+ // Set up ResizeObserver for recalculating offsets on container resize
880
+ if (typeof ResizeObserver !== 'undefined' && this.props.infiniteLoading) {
881
+ this.resizeObserver = new ResizeObserver(() => {
882
+ requestAnimationFrame(() => this.recalculateOffsets());
883
+ });
884
+ const container = this.state.scrollableContainerRef.current;
885
+ if (container) {
886
+ this.resizeObserver.observe(container);
887
+ }
888
+ }
889
+ }
890
+ componentWillUnmount() {
891
+ this.resizeObserver?.disconnect();
892
+ }
284
893
  render = () => {
285
894
  const { oldValue, newValue, useDarkTheme, leftTitle, rightTitle, splitView, compareMethod, hideLineNumbers, nonce, } = this.props;
286
895
  if (typeof compareMethod === "string" &&
@@ -329,26 +938,61 @@ class DiffViewer extends React.Component {
329
938
  }
330
939
  }
331
940
  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] }) })] }));
941
+ const LoadingElement = this.props.loadingElement;
942
+ const scrollDivStyle = this.props.infiniteLoading ? {
943
+ overflowY: 'scroll',
944
+ overflowX: 'hidden',
945
+ height: this.props.infiniteLoading.containerHeight
946
+ } : {};
947
+ // Only apply noWrap when infiniteLoading is enabled but we don't have cumulative offsets yet
948
+ // Once offsets are calculated, we enable pre-wrap for proper text wrapping
949
+ const shouldNoWrap = !!this.props.infiniteLoading && !this.state.cumulativeOffsets;
950
+ const tableElement = (_jsxs("table", { className: cn(this.styles.diffContainer, {
951
+ [this.styles.splitView]: splitView,
952
+ [this.styles.noWrap]: shouldNoWrap,
953
+ }), onMouseUp: () => {
954
+ const elements = document.getElementsByClassName("right");
955
+ for (let i = 0; i < elements.length; i++) {
956
+ const element = elements.item(i);
957
+ element.classList.remove(this.styles.noSelect);
958
+ }
959
+ const elementsLeft = document.getElementsByClassName("left");
960
+ for (let i = 0; i < elementsLeft.length; i++) {
961
+ const element = elementsLeft.item(i);
962
+ element.classList.remove(this.styles.noSelect);
963
+ }
964
+ }, 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 })] }));
965
+ return (_jsxs("div", { style: { ...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: () => {
966
+ this.setState({
967
+ expandedBlocks: allExpanded
968
+ ? []
969
+ : nodes.blocks.map((b) => b.index),
970
+ }, () => this.recalculateOffsets());
971
+ }, 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: {
972
+ paddingTop: nodes.topPadding,
973
+ paddingBottom: nodes.bottomPadding,
974
+ }, children: tableElement })) : (tableElement), _jsx("span", { ref: this.charMeasureRef, style: {
975
+ position: 'absolute',
976
+ top: 0,
977
+ left: '-9999px',
978
+ visibility: 'hidden',
979
+ whiteSpace: 'pre',
980
+ fontFamily: 'monospace',
981
+ fontSize: 12,
982
+ }, "aria-hidden": "true", children: "M" }), this.props.infiniteLoading && this.props.showDebugInfo && (_jsxs("div", { style: {
983
+ position: 'fixed',
984
+ top: 10,
985
+ right: 10,
986
+ background: 'rgba(0,0,0,0.85)',
987
+ color: '#0f0',
988
+ padding: '10px',
989
+ fontFamily: 'monospace',
990
+ fontSize: '11px',
991
+ zIndex: 9999,
992
+ borderRadius: '4px',
993
+ maxWidth: '300px',
994
+ lineHeight: 1.4,
995
+ }, 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: ", this.state.contentColumnWidth?.toFixed(0) ?? 'N/A', "px"] }), _jsxs("div", { children: ["charWidth: ", this.state.charWidth?.toFixed(2) ?? '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, "]: ", this.state.cumulativeOffsets[nodes.debug.visibleRowEnd]?.toFixed(0) ?? 'N/A'] }), _jsxs("div", { children: ["offsets[", nodes.debug.totalRows - 1, "]: ", this.state.cumulativeOffsets[nodes.debug.totalRows - 1]?.toFixed(0) ?? 'N/A'] }), _jsxs("div", { children: ["offsets[", nodes.debug.totalRows, "]: ", this.state.cumulativeOffsets[nodes.debug.totalRows]?.toFixed(0) ?? '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: ", this.state.scrollableContainerRef.current?.scrollHeight ?? 'N/A'] }), _jsxs("div", { children: ["maxScrollTop: ", (this.state.scrollableContainerRef.current?.scrollHeight ?? 0) - nodes.debug.clientHeight] })] }))] }))] }));
352
996
  };
353
997
  }
354
998
  export default DiffViewer;