react-diff-viewer-continued 4.1.0 → 4.1.2

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.
@@ -161,6 +161,11 @@ class DiffViewer extends React.Component {
161
161
  super(props);
162
162
  // Cache for on-demand word diff computation
163
163
  this.wordDiffCache = new Map();
164
+ // Refs for measuring content column width and character width
165
+ this.contentColumnRef = React.createRef();
166
+ this.charMeasureRef = React.createRef();
167
+ this.stickyHeaderRef = React.createRef();
168
+ this.resizeObserver = null;
164
169
  /**
165
170
  * Computes word diff on-demand for a line, with caching.
166
171
  * This is used when word diff was deferred during initial computation.
@@ -211,8 +216,28 @@ class DiffViewer extends React.Component {
211
216
  this.onBlockExpand = (id) => {
212
217
  const prevState = this.state.expandedBlocks.slice();
213
218
  prevState.push(id);
214
- this.setState({
215
- expandedBlocks: prevState,
219
+ this.setState({ expandedBlocks: prevState }, () => this.recalculateOffsets());
220
+ };
221
+ /**
222
+ * Recalculates cumulative offsets based on current measurements.
223
+ * Called on resize and when blocks are expanded/collapsed.
224
+ */
225
+ this.recalculateOffsets = () => {
226
+ var _a;
227
+ if (!this.props.infiniteLoading)
228
+ return;
229
+ const columnWidth = this.measureContentColumnWidth();
230
+ const charWidth = this.measureCharWidth();
231
+ if (!columnWidth)
232
+ return;
233
+ const cacheKey = this.getMemoisedKey();
234
+ const { lineInformation, lineBlocks, blocks } = (_a = this.state.computedDiffResult[cacheKey]) !== null && _a !== void 0 ? _a : {};
235
+ if (!lineInformation)
236
+ return;
237
+ const offsets = this.buildCumulativeOffsets(lineInformation, lineBlocks, blocks, this.state.expandedBlocks, this.props.showDiffOnly, charWidth, columnWidth, this.props.splitView);
238
+ this.setState({ cumulativeOffsets: offsets, contentColumnWidth: columnWidth, charWidth }, () => {
239
+ // Force a scroll position update to recalculate visible rows with new offsets
240
+ this.onScroll();
216
241
  });
217
242
  };
218
243
  /**
@@ -256,10 +281,6 @@ class DiffViewer extends React.Component {
256
281
  this.renderWordDiff = (diffArray, renderer) => {
257
282
  var _a, _b;
258
283
  const showHighlight = this.shouldHighlightWordDiff();
259
- const { compareMethod } = this.props;
260
- // Don't apply syntax highlighting for JSON/YAML - their word diffs are computed
261
- // on-demand from raw strings and syntax highlighting creates messy fragmented tokens.
262
- const skipSyntaxHighlighting = compareMethod === DiffMethod.JSON || compareMethod === DiffMethod.YAML;
263
284
  // Reconstruct the full line from diff chunks
264
285
  const fullLine = diffArray
265
286
  .map((d) => (typeof d.value === "string" ? d.value : ""))
@@ -270,9 +291,9 @@ class DiffViewer extends React.Component {
270
291
  if (fullLine.length > MAX_LINE_LENGTH) {
271
292
  return [_jsx("span", { children: fullLine }, "long-line")];
272
293
  }
273
- // If we have a renderer and syntax highlighting is enabled, try to highlight
274
- // the full line first, then apply diff styling to preserve proper tokenization.
275
- if (renderer && !skipSyntaxHighlighting) {
294
+ // If we have a renderer, try to highlight the full line first,
295
+ // then apply diff styling to preserve proper tokenization.
296
+ if (renderer) {
276
297
  // Get the syntax-highlighted content
277
298
  const highlighted = renderer(fullLine);
278
299
  // Check if the renderer uses dangerouslySetInnerHTML (common with Prism, highlight.js, etc.)
@@ -379,7 +400,7 @@ class DiffViewer extends React.Component {
379
400
  [this.styles.diffRemoved]: removed,
380
401
  [this.styles.diffChanged]: changed,
381
402
  [this.styles.highlightedLine]: highlightLine,
382
- }), children: _jsxs("pre", { children: [added && "+", removed && "-"] }) }), _jsx("td", { className: cn(this.styles.content, {
403
+ }), children: _jsxs("pre", { children: [added && "+", removed && "-"] }) }), _jsx("td", { ref: prefix === LineNumberPrefix.LEFT && !this.state.cumulativeOffsets ? this.contentColumnRef : undefined, className: cn(this.styles.content, {
383
404
  [this.styles.emptyLine]: !content,
384
405
  [this.styles.diffAdded]: added,
385
406
  [this.styles.diffRemoved]: removed,
@@ -517,7 +538,13 @@ class DiffViewer extends React.Component {
517
538
  : Math.round(this.props.extraLinesSurroundingDiff);
518
539
  const { lineBlocks, blocks } = computeHiddenBlocks(lineInformation, diffLines, extraLines);
519
540
  this.state.computedDiffResult[cacheKey] = { lineInformation, lineBlocks, blocks };
520
- this.setState((prev) => (Object.assign(Object.assign({}, prev), { computedDiffResult: this.state.computedDiffResult, isLoading: false })));
541
+ this.setState((prev) => (Object.assign(Object.assign({}, prev), { computedDiffResult: this.state.computedDiffResult, isLoading: false })), () => {
542
+ // Trigger offset recalculation after diff is computed and rendered
543
+ // Use requestAnimationFrame to ensure DOM is ready for measurement
544
+ if (this.props.infiniteLoading) {
545
+ requestAnimationFrame(() => this.recalculateOffsets());
546
+ }
547
+ });
521
548
  });
522
549
  /**
523
550
  * Handles scroll events on the scrollable container.
@@ -528,7 +555,13 @@ class DiffViewer extends React.Component {
528
555
  const container = this.state.scrollableContainerRef.current;
529
556
  if (!container || !this.props.infiniteLoading)
530
557
  return;
531
- const newStartRow = Math.floor(container.scrollTop / DiffViewer.ESTIMATED_ROW_HEIGHT);
558
+ // Account for sticky header height in scroll calculations
559
+ const headerHeight = this.getStickyHeaderHeight();
560
+ const contentScrollTop = Math.max(0, container.scrollTop - headerHeight);
561
+ const { cumulativeOffsets } = this.state;
562
+ const newStartRow = cumulativeOffsets
563
+ ? this.findLineAtOffset(contentScrollTop, cumulativeOffsets)
564
+ : Math.floor(contentScrollTop / DiffViewer.ESTIMATED_ROW_HEIGHT);
532
565
  // Only update state if the start row changed (avoid unnecessary re-renders)
533
566
  if (newStartRow !== this.state.visibleStartRow) {
534
567
  this.setState({ visibleStartRow: newStartRow });
@@ -538,9 +571,9 @@ class DiffViewer extends React.Component {
538
571
  * Generates the entire diff view with virtualization support.
539
572
  */
540
573
  this.renderDiff = () => {
541
- var _a;
574
+ var _a, _b, _c, _d, _e, _f;
542
575
  const { splitView, infiniteLoading, showDiffOnly } = this.props;
543
- const { computedDiffResult, expandedBlocks, visibleStartRow, scrollableContainerRef } = this.state;
576
+ const { computedDiffResult, expandedBlocks, visibleStartRow, scrollableContainerRef, cumulativeOffsets } = this.state;
544
577
  const cacheKey = this.getMemoisedKey();
545
578
  const { lineInformation = [], lineBlocks = [], blocks = [] } = (_a = computedDiffResult[cacheKey]) !== null && _a !== void 0 ? _a : {};
546
579
  // Calculate visible range for virtualization
@@ -549,9 +582,32 @@ class DiffViewer extends React.Component {
549
582
  const buffer = 5; // render extra rows above/below viewport
550
583
  if (infiniteLoading && scrollableContainerRef.current) {
551
584
  const container = scrollableContainerRef.current;
552
- const viewportRows = Math.ceil(container.clientHeight / DiffViewer.ESTIMATED_ROW_HEIGHT);
553
- visibleRowStart = Math.max(0, visibleStartRow - buffer);
554
- visibleRowEnd = visibleStartRow + viewportRows + buffer;
585
+ // Account for sticky header height in scroll calculations
586
+ const headerHeight = this.getStickyHeaderHeight();
587
+ const contentScrollTop = Math.max(0, container.scrollTop - headerHeight);
588
+ if (cumulativeOffsets) {
589
+ // Variable height mode: use binary search to find visible range
590
+ const totalHeight = cumulativeOffsets[cumulativeOffsets.length - 1] || 0;
591
+ const lastRowIndex = cumulativeOffsets.length - 2;
592
+ visibleRowStart = Math.max(0, this.findLineAtOffset(contentScrollTop, cumulativeOffsets) - buffer);
593
+ visibleRowEnd = this.findLineAtOffset(contentScrollTop + container.clientHeight, cumulativeOffsets) + buffer;
594
+ // IMPORTANT: The calculated offsets may overestimate row heights (based on char count),
595
+ // but actual CSS rendering might produce shorter rows. To prevent empty space,
596
+ // ensure we render at least enough rows to fill the viewport using ESTIMATED_ROW_HEIGHT
597
+ // as a conservative minimum.
598
+ const minRowsToFillViewport = Math.ceil(container.clientHeight / DiffViewer.ESTIMATED_ROW_HEIGHT);
599
+ visibleRowEnd = Math.max(visibleRowEnd, visibleRowStart + minRowsToFillViewport + buffer);
600
+ // Also ensure we render all rows when near the bottom
601
+ if (contentScrollTop + container.clientHeight >= totalHeight - buffer * DiffViewer.ESTIMATED_ROW_HEIGHT) {
602
+ visibleRowEnd = lastRowIndex + buffer;
603
+ }
604
+ }
605
+ else {
606
+ // Fixed height fallback
607
+ const viewportRows = Math.ceil(container.clientHeight / DiffViewer.ESTIMATED_ROW_HEIGHT);
608
+ visibleRowStart = Math.max(0, visibleStartRow - buffer);
609
+ visibleRowEnd = visibleStartRow + viewportRows + buffer;
610
+ }
555
611
  }
556
612
  // First pass: build a map of lineIndex -> renderedRowIndex
557
613
  // This accounts for code folding where some lines don't render or render as fold indicators
@@ -589,6 +645,7 @@ class DiffViewer extends React.Component {
589
645
  const diffNodes = [];
590
646
  let topPadding = 0;
591
647
  let firstVisibleFound = false;
648
+ let lastRenderedRowIndex = -1;
592
649
  seenBlocks.clear();
593
650
  for (let lineIndex = 0; lineIndex < lineInformation.length; lineIndex++) {
594
651
  const line = lineInformation[lineIndex];
@@ -606,9 +663,13 @@ class DiffViewer extends React.Component {
606
663
  }
607
664
  // Calculate top padding from the first visible row
608
665
  if (!firstVisibleFound) {
609
- topPadding = rowIndex * DiffViewer.ESTIMATED_ROW_HEIGHT;
666
+ topPadding = cumulativeOffsets
667
+ ? cumulativeOffsets[rowIndex] || 0
668
+ : rowIndex * DiffViewer.ESTIMATED_ROW_HEIGHT;
610
669
  firstVisibleFound = true;
611
670
  }
671
+ // Track the last rendered row for bottom padding calculation
672
+ lastRenderedRowIndex = rowIndex;
612
673
  // Render the line
613
674
  if (showDiffOnly) {
614
675
  const blockIndex = lineBlocks[lineIndex];
@@ -628,15 +689,41 @@ class DiffViewer extends React.Component {
628
689
  ? this.renderSplitView(line, lineIndex)
629
690
  : this.renderInlineView(line, lineIndex));
630
691
  }
692
+ // Calculate total content height
693
+ const totalContentHeight = cumulativeOffsets
694
+ ? cumulativeOffsets[cumulativeOffsets.length - 1] || 0
695
+ : totalRenderedRows * DiffViewer.ESTIMATED_ROW_HEIGHT;
696
+ // Calculate bottom padding: space after the last rendered row
697
+ const bottomPadding = cumulativeOffsets && lastRenderedRowIndex >= 0
698
+ ? totalContentHeight - (cumulativeOffsets[lastRenderedRowIndex + 1] || totalContentHeight)
699
+ : 0;
631
700
  return {
632
701
  diffNodes,
633
702
  blocks,
634
703
  lineInformation,
635
704
  totalRenderedRows,
636
705
  topPadding,
706
+ bottomPadding,
707
+ totalContentHeight,
708
+ renderedCount: diffNodes.length,
709
+ // Debug info
710
+ debug: {
711
+ visibleRowStart,
712
+ visibleRowEnd,
713
+ totalRows: totalRenderedRows,
714
+ offsetsLength: (_b = cumulativeOffsets === null || cumulativeOffsets === void 0 ? void 0 : cumulativeOffsets.length) !== null && _b !== void 0 ? _b : 0,
715
+ renderedCount: diffNodes.length,
716
+ scrollTop: (_d = (_c = scrollableContainerRef.current) === null || _c === void 0 ? void 0 : _c.scrollTop) !== null && _d !== void 0 ? _d : 0,
717
+ headerHeight: this.getStickyHeaderHeight(),
718
+ contentScrollTop: scrollableContainerRef.current
719
+ ? Math.max(0, scrollableContainerRef.current.scrollTop - this.getStickyHeaderHeight())
720
+ : 0,
721
+ clientHeight: (_f = (_e = scrollableContainerRef.current) === null || _e === void 0 ? void 0 : _e.clientHeight) !== null && _f !== void 0 ? _f : 0,
722
+ }
637
723
  };
638
724
  };
639
725
  this.render = () => {
726
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
640
727
  const { oldValue, newValue, useDarkTheme, leftTitle, rightTitle, splitView, compareMethod, hideLineNumbers, nonce, } = this.props;
641
728
  if (typeof compareMethod === "string" &&
642
729
  compareMethod !== DiffMethod.JSON) {
@@ -690,9 +777,12 @@ class DiffViewer extends React.Component {
690
777
  overflowX: 'hidden',
691
778
  height: this.props.infiniteLoading.containerHeight
692
779
  } : {};
693
- const totalContentHeight = nodes.totalRenderedRows * DiffViewer.ESTIMATED_ROW_HEIGHT;
780
+ // Only apply noWrap when infiniteLoading is enabled but we don't have cumulative offsets yet
781
+ // Once offsets are calculated, we enable pre-wrap for proper text wrapping
782
+ const shouldNoWrap = !!this.props.infiniteLoading && !this.state.cumulativeOffsets;
694
783
  const tableElement = (_jsxs("table", { className: cn(this.styles.diffContainer, {
695
784
  [this.styles.splitView]: splitView,
785
+ [this.styles.noWrap]: shouldNoWrap,
696
786
  }), onMouseUp: () => {
697
787
  const elements = document.getElementsByClassName("right");
698
788
  for (let i = 0; i < elements.length; i++) {
@@ -705,13 +795,42 @@ class DiffViewer extends React.Component {
705
795
  element.classList.remove(this.styles.noSelect);
706
796
  }
707
797
  }, 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 })] }));
708
- return (_jsxs("div", { style: Object.assign(Object.assign({}, scrollDivStyle), { position: 'relative' }), onScroll: this.onScroll, ref: this.state.scrollableContainerRef, children: [(!this.props.hideSummary || leftTitle || rightTitle) && (_jsxs("div", { 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: () => {
798
+ return (_jsxs("div", { style: Object.assign(Object.assign({}, scrollDivStyle), { position: 'relative' }), onScroll: this.onScroll, ref: this.state.scrollableContainerRef, children: [(!this.props.hideSummary || leftTitle || rightTitle) && (_jsxs("div", { ref: this.stickyHeaderRef, className: this.styles.stickyHeader, children: [!this.props.hideSummary && (_jsxs("div", { className: this.styles.summary, role: "banner", children: [_jsx("button", { type: "button", className: this.styles.allExpandButton, onClick: () => {
709
799
  this.setState({
710
800
  expandedBlocks: allExpanded
711
801
  ? []
712
802
  : nodes.blocks.map((b) => b.index),
713
- });
714
- }, 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)] }));
803
+ }, () => this.recalculateOffsets());
804
+ }, 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: {
805
+ height: nodes.totalContentHeight,
806
+ position: 'relative',
807
+ }, children: _jsx("div", { style: {
808
+ position: 'absolute',
809
+ top: nodes.topPadding,
810
+ left: 0,
811
+ right: 0,
812
+ }, children: tableElement }) })) : (tableElement), _jsx("span", { ref: this.charMeasureRef, style: {
813
+ position: 'absolute',
814
+ top: 0,
815
+ left: '-9999px',
816
+ visibility: 'hidden',
817
+ whiteSpace: 'pre',
818
+ fontFamily: 'monospace',
819
+ fontSize: 12,
820
+ }, "aria-hidden": "true", children: "M" }), this.props.infiniteLoading && this.props.showDebugInfo && (_jsxs("div", { style: {
821
+ position: 'fixed',
822
+ top: 10,
823
+ right: 10,
824
+ background: 'rgba(0,0,0,0.85)',
825
+ color: '#0f0',
826
+ padding: '10px',
827
+ fontFamily: 'monospace',
828
+ fontSize: '11px',
829
+ zIndex: 9999,
830
+ borderRadius: '4px',
831
+ maxWidth: '300px',
832
+ lineHeight: 1.4,
833
+ }, children: [_jsx("div", { style: { fontWeight: 'bold', marginBottom: '5px', color: '#fff' }, children: "Debug Info" }), _jsxs("div", { children: ["scrollTop: ", nodes.debug.scrollTop] }), _jsxs("div", { children: ["headerHeight: ", nodes.debug.headerHeight] }), _jsxs("div", { children: ["contentScrollTop: ", nodes.debug.contentScrollTop] }), _jsxs("div", { children: ["clientHeight: ", nodes.debug.clientHeight] }), _jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px' }, children: [_jsxs("div", { children: ["visibleRowStart: ", nodes.debug.visibleRowStart] }), _jsxs("div", { children: ["visibleRowEnd: ", nodes.debug.visibleRowEnd] })] }), _jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px' }, children: [_jsxs("div", { children: ["totalRows: ", nodes.debug.totalRows] }), _jsxs("div", { children: ["offsetsLength: ", nodes.debug.offsetsLength] }), _jsxs("div", { children: ["renderedCount: ", nodes.debug.renderedCount] })] }), _jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px' }, children: [_jsxs("div", { children: ["topPadding: ", nodes.topPadding.toFixed(0)] }), _jsxs("div", { children: ["bottomPadding: ", nodes.bottomPadding.toFixed(0)] }), _jsxs("div", { children: ["totalContentHeight: ", nodes.totalContentHeight.toFixed(0)] })] }), _jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px', color: '#ff0' }, children: [_jsxs("div", { children: ["cumulativeOffsets: ", this.state.cumulativeOffsets ? 'SET' : 'NULL'] }), _jsxs("div", { children: ["columnWidth: ", (_b = (_a = this.state.contentColumnWidth) === null || _a === void 0 ? void 0 : _a.toFixed(0)) !== null && _b !== void 0 ? _b : 'N/A', "px"] }), _jsxs("div", { children: ["charWidth: ", (_d = (_c = this.state.charWidth) === null || _c === void 0 ? void 0 : _c.toFixed(2)) !== null && _d !== void 0 ? _d : 'N/A', "px"] }), _jsxs("div", { children: ["charsPerRow: ", this.state.contentColumnWidth && this.state.charWidth ? Math.floor(this.state.contentColumnWidth / this.state.charWidth) : 'N/A'] })] }), this.state.cumulativeOffsets && (_jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px', color: '#0ff', fontSize: '10px' }, children: [_jsxs("div", { children: ["offsets[", nodes.debug.visibleRowEnd, "]: ", (_f = (_e = this.state.cumulativeOffsets[nodes.debug.visibleRowEnd]) === null || _e === void 0 ? void 0 : _e.toFixed(0)) !== null && _f !== void 0 ? _f : 'N/A'] }), _jsxs("div", { children: ["offsets[", nodes.debug.totalRows - 1, "]: ", (_h = (_g = this.state.cumulativeOffsets[nodes.debug.totalRows - 1]) === null || _g === void 0 ? void 0 : _g.toFixed(0)) !== null && _h !== void 0 ? _h : 'N/A'] }), _jsxs("div", { children: ["offsets[", nodes.debug.totalRows, "]: ", (_k = (_j = this.state.cumulativeOffsets[nodes.debug.totalRows]) === null || _j === void 0 ? void 0 : _j.toFixed(0)) !== null && _k !== void 0 ? _k : 'N/A'] }), _jsxs("div", { style: { marginTop: '3px' }, children: ["viewportEnd: ", (nodes.debug.contentScrollTop + nodes.debug.clientHeight).toFixed(0)] }), _jsxs("div", { style: { marginTop: '3px', color: '#f0f' }, children: ["scrollHeight: ", (_m = (_l = this.state.scrollableContainerRef.current) === null || _l === void 0 ? void 0 : _l.scrollHeight) !== null && _m !== void 0 ? _m : 'N/A'] }), _jsxs("div", { children: ["maxScrollTop: ", ((_p = (_o = this.state.scrollableContainerRef.current) === null || _o === void 0 ? void 0 : _o.scrollHeight) !== null && _p !== void 0 ? _p : 0) - nodes.debug.clientHeight] })] }))] }))] }));
715
834
  };
716
835
  this.state = {
717
836
  expandedBlocks: [],
@@ -719,9 +838,117 @@ class DiffViewer extends React.Component {
719
838
  scrollableContainerRef: React.createRef(),
720
839
  computedDiffResult: {},
721
840
  isLoading: false,
722
- visibleStartRow: 0
841
+ visibleStartRow: 0,
842
+ contentColumnWidth: null,
843
+ charWidth: null,
844
+ cumulativeOffsets: null,
723
845
  };
724
846
  }
847
+ /**
848
+ * Gets the height of the sticky header, if present.
849
+ */
850
+ getStickyHeaderHeight() {
851
+ var _a;
852
+ return ((_a = this.stickyHeaderRef.current) === null || _a === void 0 ? void 0 : _a.offsetHeight) || 0;
853
+ }
854
+ /**
855
+ * Measures the width of a single character in the monospace font.
856
+ * Falls back to 7.2px if measurement fails.
857
+ */
858
+ measureCharWidth() {
859
+ const span = this.charMeasureRef.current;
860
+ if (!span)
861
+ return 7.2; // fallback
862
+ return span.getBoundingClientRect().width || 7.2;
863
+ }
864
+ /**
865
+ * Measures the available width for content in a content column.
866
+ * Falls back to estimating from container width if direct measurement fails.
867
+ */
868
+ measureContentColumnWidth() {
869
+ // Try direct measurement first
870
+ const cell = this.contentColumnRef.current;
871
+ if (cell && cell.clientWidth > 0) {
872
+ const style = window.getComputedStyle(cell);
873
+ const padding = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
874
+ const width = cell.clientWidth - padding;
875
+ if (width > 0)
876
+ return width;
877
+ }
878
+ // Fallback: estimate from container width
879
+ // In split view: container has 2 content columns + gutters (50px each) + markers (28px each)
880
+ // In unified view: 1 content column + 2 gutters + 1 marker
881
+ const container = this.state.scrollableContainerRef.current;
882
+ if (!container || container.clientWidth <= 0)
883
+ return null;
884
+ const containerWidth = container.clientWidth;
885
+ const gutterWidth = this.props.hideLineNumbers ? 0 : 50;
886
+ const markerWidth = 28;
887
+ const gutterCount = this.props.splitView ? 2 : 2; // left gutter(s)
888
+ const markerCount = this.props.splitView ? 2 : 1;
889
+ const contentColumns = this.props.splitView ? 2 : 1;
890
+ const fixedWidth = gutterCount * gutterWidth + markerCount * markerWidth;
891
+ const availableWidth = containerWidth - fixedWidth;
892
+ return Math.max(100, availableWidth / contentColumns); // minimum 100px
893
+ }
894
+ /**
895
+ * Gets the text length from a value that may be a string or DiffInformation array.
896
+ */
897
+ getTextLength(value) {
898
+ if (!value)
899
+ return 0;
900
+ if (typeof value === 'string')
901
+ return value.length;
902
+ return value.reduce((sum, d) => sum + (typeof d.value === 'string' ? d.value.length : 0), 0);
903
+ }
904
+ /**
905
+ * Builds cumulative vertical offsets for each line based on character count and column width.
906
+ * This allows accurate scroll position calculations with variable row heights.
907
+ */
908
+ buildCumulativeOffsets(lineInformation, lineBlocks, blocks, expandedBlocks, showDiffOnly, charWidth, columnWidth, splitView) {
909
+ var _a, _b;
910
+ const offsets = [0];
911
+ const seenBlocks = new Set();
912
+ for (let i = 0; i < lineInformation.length; i++) {
913
+ const line = lineInformation[i];
914
+ if (showDiffOnly) {
915
+ const blockIndex = lineBlocks[i];
916
+ if (blockIndex !== undefined && !expandedBlocks.includes(blockIndex)) {
917
+ const isLastLine = blocks[blockIndex].endLine === i;
918
+ if (!seenBlocks.has(blockIndex) && isLastLine) {
919
+ seenBlocks.add(blockIndex);
920
+ offsets.push(offsets[offsets.length - 1] + DiffViewer.ESTIMATED_ROW_HEIGHT);
921
+ }
922
+ continue;
923
+ }
924
+ }
925
+ // Calculate visual rows for this line
926
+ const leftLen = ((_a = line.left) === null || _a === void 0 ? void 0 : _a.value) ? this.getTextLength(line.left.value) : 0;
927
+ const rightLen = ((_b = line.right) === null || _b === void 0 ? void 0 : _b.value) ? this.getTextLength(line.right.value) : 0;
928
+ const maxLen = splitView ? Math.max(leftLen, rightLen) : (leftLen || rightLen);
929
+ const charsPerRow = Math.floor(columnWidth / charWidth);
930
+ const visualRows = charsPerRow > 0 ? Math.max(1, Math.ceil(maxLen / charsPerRow)) : 1;
931
+ offsets.push(offsets[offsets.length - 1] + visualRows * DiffViewer.ESTIMATED_ROW_HEIGHT);
932
+ }
933
+ return offsets;
934
+ }
935
+ /**
936
+ * Binary search to find the line index at a given scroll offset.
937
+ */
938
+ findLineAtOffset(scrollTop, offsets) {
939
+ let low = 0;
940
+ let high = offsets.length - 2;
941
+ while (low < high) {
942
+ const mid = Math.floor((low + high + 1) / 2);
943
+ if (offsets[mid] <= scrollTop) {
944
+ low = mid;
945
+ }
946
+ else {
947
+ high = mid - 1;
948
+ }
949
+ }
950
+ return low;
951
+ }
725
952
  componentDidUpdate(prevProps) {
726
953
  if (prevProps.oldValue !== this.props.oldValue ||
727
954
  prevProps.newValue !== this.props.newValue ||
@@ -730,13 +957,32 @@ class DiffViewer extends React.Component {
730
957
  prevProps.linesOffset !== this.props.linesOffset) {
731
958
  // Clear word diff cache when diff changes
732
959
  this.wordDiffCache.clear();
733
- this.setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: true, visibleStartRow: 0 })));
960
+ // Reset scroll position to top
961
+ const container = this.state.scrollableContainerRef.current;
962
+ if (container) {
963
+ container.scrollTop = 0;
964
+ }
965
+ this.setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: true, visibleStartRow: 0, cumulativeOffsets: null })));
734
966
  this.memoisedCompute();
735
967
  }
736
968
  }
737
969
  componentDidMount() {
738
970
  this.setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: true })));
739
971
  this.memoisedCompute();
972
+ // Set up ResizeObserver for recalculating offsets on container resize
973
+ if (typeof ResizeObserver !== 'undefined' && this.props.infiniteLoading) {
974
+ this.resizeObserver = new ResizeObserver(() => {
975
+ requestAnimationFrame(() => this.recalculateOffsets());
976
+ });
977
+ const container = this.state.scrollableContainerRef.current;
978
+ if (container) {
979
+ this.resizeObserver.observe(container);
980
+ }
981
+ }
982
+ }
983
+ componentWillUnmount() {
984
+ var _a;
985
+ (_a = this.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
740
986
  }
741
987
  }
742
988
  DiffViewer.defaultProps = {
@@ -28,6 +28,7 @@ export interface ReactDiffViewerStyles {
28
28
  content?: string;
29
29
  column?: string;
30
30
  noSelect?: string;
31
+ noWrap?: string;
31
32
  splitView?: string;
32
33
  allExpandButton?: string;
33
34
  [key: string]: string | undefined;
@@ -72,6 +72,7 @@ export default (styleOverride, useDarkTheme = false, nonce = "") => {
72
72
  const { css, cx } = createEmotion({ key: "react-diff", nonce });
73
73
  const content = css({
74
74
  width: "auto",
75
+ overflow: "hidden",
75
76
  label: "content",
76
77
  });
77
78
  const splitView = css({
@@ -113,6 +114,9 @@ export default (styleOverride, useDarkTheme = false, nonce = "") => {
113
114
  },
114
115
  label: "diff-container",
115
116
  borderCollapse: "collapse",
117
+ "@media (max-width: 768px)": {
118
+ minWidth: "unset",
119
+ },
116
120
  });
117
121
  const lineContent = css({
118
122
  overflow: "hidden",
@@ -130,6 +134,16 @@ export default (styleOverride, useDarkTheme = false, nonce = "") => {
130
134
  userSelect: "none",
131
135
  label: "unselectable",
132
136
  });
137
+ const noWrap = css({
138
+ label: "no-wrap",
139
+ pre: {
140
+ whiteSpace: "pre",
141
+ },
142
+ [`.${contentText}`]: {
143
+ whiteSpace: "pre",
144
+ lineBreak: "auto",
145
+ },
146
+ });
133
147
  const allExpandButton = css({
134
148
  background: "transparent",
135
149
  border: "none",
@@ -376,6 +390,7 @@ export default (styleOverride, useDarkTheme = false, nonce = "") => {
376
390
  blockDeletion,
377
391
  wordRemoved,
378
392
  noSelect: unselectable,
393
+ noWrap,
379
394
  codeFoldGutter,
380
395
  codeFoldExpandButton,
381
396
  codeFoldContentContainer,