addons/isl/src/ComparisonView/SplitDiffView/SplitDiffHunk.tsxblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {ReactNode} from 'react';
b69ab319import type {Hunk, ParsedDiff} from 'shared/patch/types';
b69ab3110import type {Result} from '../../types';
b69ab3111import type {TokenizedDiffHunk, TokenizedHunk} from './syntaxHighlightingTypes';
b69ab3112import type {Context, OneIndexedLineNumber} from './types';
b69ab3113
b69ab3114import {diffChars} from 'diff';
b69ab3115import {ErrorNotice} from 'isl-components/ErrorNotice';
b69ab3116import {Icon} from 'isl-components/Icon';
b69ab3117import React, {useCallback, useEffect, useState} from 'react';
b69ab3118import {comparisonStringKey} from 'shared/Comparison';
b69ab3119import organizeLinesIntoGroups from 'shared/SplitDiffView/organizeLinesIntoGroups';
b69ab3120import {
b69ab3121 applyTokenizationToLine,
b69ab3122 createTokenizedIntralineDiff,
b69ab3123} from 'shared/createTokenizedIntralineDiff';
b69ab3124import SplitDiffRow, {BlankLineNumber} from './SplitDiffRow';
b69ab3125import {useTableColumnSelection} from './copyFromSelectedColumn';
b69ab3126import {useTokenizedContents, useTokenizedHunks} from './syntaxHighlighting';
b69ab3127
b69ab3128const MAX_INPUT_LENGTH_FOR_INTRALINE_DIFF = 300;
b69ab3129
b69ab3130export type SplitDiffTableProps = {
b69ab3131 ctx: Context;
b69ab3132 path: string;
b69ab3133 patch: ParsedDiff;
b69ab3134};
b69ab3135
b69ab3136export const SplitDiffTable = React.memo(
b69ab3137 ({ctx, path, patch}: SplitDiffTableProps): React.ReactElement => {
b69ab3138 const [deletedFileExpanded, setDeletedFileExpanded] = useState<boolean>(false);
b69ab3139 const [expandedSeparators, setExpandedSeparators] = useState<Readonly<Set<string>>>(
b69ab3140 () => new Set(),
b69ab3141 );
b69ab3142 const onExpand = useCallback(
b69ab3143 (key: string) => {
b69ab3144 const amendedSet = new Set(expandedSeparators);
b69ab3145 amendedSet.add(key);
b69ab3146 setExpandedSeparators(amendedSet);
b69ab3147 },
b69ab3148 [expandedSeparators, setExpandedSeparators],
b69ab3149 );
b69ab3150
b69ab3151 const t = ctx.t ?? (s => s);
b69ab3152 const tokenization = useTokenizedHunks(patch.newFileName ?? '', patch.hunks, ctx.useThemeHook);
b69ab3153
b69ab3154 const {className: tableSelectionClassName, ...tableSelectionProps} = useTableColumnSelection();
b69ab3155
b69ab3156 const isDeleted = patch.newFileName === '/dev/null';
b69ab3157 const isAdded = patch.type === 'Added';
b69ab3158
b69ab3159 const unified = ctx.display === 'unified';
b69ab3160 const displayLineNumbers = ctx.displayLineNumbers ?? true;
b69ab3161
b69ab3162 const {hunks} = patch;
b69ab3163 const lastHunkIndex = hunks.length - 1;
b69ab3164 const rows: React.ReactElement[] = [];
b69ab3165 if (!isDeleted || deletedFileExpanded) {
b69ab3166 hunks.forEach((hunk, index) => {
b69ab3167 // Show a separator before the first hunk if the file starts with a
b69ab3168 // section of unmodified lines that is hidden by default.
b69ab3169 if (index === 0 && (hunk.oldStart !== 1 || hunk.newStart !== 1)) {
b69ab3170 // TODO: test empty file that went from 644 to 755?
b69ab3171 const key = 's0';
b69ab3172 if (expandedSeparators.has(key)) {
b69ab3173 rows.push(
b69ab3174 <ExpandingSeparator
b69ab3175 key={key}
b69ab3176 ctx={ctx}
b69ab3177 path={path}
b69ab3178 start={1}
b69ab3179 numLines={hunk.oldStart - 1}
b69ab3180 beforeLineStart={1}
b69ab3181 afterLineStart={1}
b69ab3182 />,
b69ab3183 );
b69ab3184 } else if (ctx.fetchAdditionalLines != null) {
b69ab3185 const numLines = Math.max(hunk.oldStart, hunk.newStart) - 1;
b69ab3186 rows.push(
b69ab3187 <HunkSeparator key={key} numLines={numLines} onExpand={() => onExpand(key)} t={t} />,
b69ab3188 );
b69ab3189 }
b69ab3190 }
b69ab3191
b69ab3192 addRowsForHunk(
b69ab3193 unified,
b69ab3194 hunk,
b69ab3195 path,
b69ab3196 rows,
b69ab3197 tokenization?.[index],
b69ab3198 ctx.openFileToLine,
b69ab3199 displayLineNumbers,
b69ab31100 );
b69ab31101
b69ab31102 const isLast = index === lastHunkIndex;
b69ab31103 const nextHunk = hunks[index + 1];
b69ab31104 const key = `s${hunk.oldStart}`;
b69ab31105 const canExpand = !isLast || !(isAdded || isDeleted || isHunkProbablyAtEndOfFile(hunk)); // added and deleted files are already expanded
b69ab31106 if (canExpand) {
b69ab31107 if (expandedSeparators.has(key)) {
b69ab31108 const start = hunk.oldStart + hunk.oldLines;
b69ab31109 const MAX_LINES_FETCH = 10000; // We don't know the total number of lines, so for the last hunk we just request a lot of lines.
b69ab31110 const numLines = isLast ? MAX_LINES_FETCH : nextHunk.oldStart - start;
b69ab31111 rows.push(
b69ab31112 <ExpandingSeparator
b69ab31113 key={key}
b69ab31114 ctx={ctx}
b69ab31115 start={start}
b69ab31116 numLines={numLines}
b69ab31117 path={path}
b69ab31118 beforeLineStart={hunk.oldStart + hunk.oldLines}
b69ab31119 afterLineStart={hunk.newStart + hunk.newLines}
b69ab31120 />,
b69ab31121 );
b69ab31122 } else if (ctx.fetchAdditionalLines != null) {
b69ab31123 const numLines = isLast ? null : nextHunk.oldStart - hunk.oldLines - hunk.oldStart;
b69ab31124 rows.push(
b69ab31125 <HunkSeparator key={key} numLines={numLines} onExpand={() => onExpand(key)} t={t} />,
b69ab31126 );
b69ab31127 }
b69ab31128 }
b69ab31129 });
b69ab31130 } else {
b69ab31131 rows.push(
b69ab31132 <SeparatorRow>
b69ab31133 <InlineRowButton
b69ab31134 key={'show-deleted'}
b69ab31135 label={t('Show deleted file')}
b69ab31136 onClick={() => setDeletedFileExpanded(true)}
b69ab31137 />
b69ab31138 </SeparatorRow>,
b69ab31139 );
b69ab31140 }
b69ab31141
b69ab31142 if (unified) {
b69ab31143 return (
b69ab31144 <table
b69ab31145 className={
b69ab31146 'split-diff-view-hunk-table display-unified ' + (tableSelectionClassName ?? '')
b69ab31147 }
b69ab31148 {...tableSelectionProps}>
b69ab31149 <colgroup>
b69ab31150 {displayLineNumbers && <col width={50} />}
b69ab31151 {displayLineNumbers && <col width={50} />}
b69ab31152 <col width={'100%'} />
b69ab31153 </colgroup>
b69ab31154 <tbody>{rows}</tbody>
b69ab31155 </table>
b69ab31156 );
b69ab31157 }
b69ab31158 return (
b69ab31159 <table
b69ab31160 className={'split-diff-view-hunk-table display-split ' + (tableSelectionClassName ?? '')}
b69ab31161 {...tableSelectionProps}>
b69ab31162 <colgroup>
b69ab31163 {displayLineNumbers && <col width={50} />}
b69ab31164 <col width={'50%'} />
b69ab31165 {displayLineNumbers && <col width={50} />}
b69ab31166 <col width={'50%'} />
b69ab31167 </colgroup>
b69ab31168 <tbody>{rows}</tbody>
b69ab31169 </table>
b69ab31170 );
b69ab31171 },
b69ab31172);
b69ab31173
b69ab31174/**
b69ab31175 * If the last hunk of a file doesn't have as many context lines as it should,
b69ab31176 * it's because it's at the end of the file. This is a clue we can skip showing
b69ab31177 * the expander.
b69ab31178 * This util should only be called on the last hunk in the file.
b69ab31179 */
b69ab31180function isHunkProbablyAtEndOfFile(hunk: Hunk): boolean {
b69ab31181 // we could conceivably check if the initial context length matches the end length, but that's not true in short files.
b69ab31182 const CONTEXT_LENGTH = 4;
b69ab31183 return !hunk.lines.slice(-CONTEXT_LENGTH).every(line => line.startsWith(' '));
b69ab31184}
b69ab31185
b69ab31186/**
b69ab31187 * Adds new rows to the supplied `rows` array.
b69ab31188 */
b69ab31189function addRowsForHunk(
b69ab31190 unified: boolean,
b69ab31191 hunk: Hunk,
b69ab31192 path: string,
b69ab31193 rows: React.ReactElement[],
b69ab31194 tokenization: TokenizedDiffHunk | undefined,
b69ab31195 openFileToLine?: (line: OneIndexedLineNumber) => unknown,
b69ab31196 displayLineNumbers: boolean = true,
b69ab31197): void {
b69ab31198 const {oldStart, newStart, lines} = hunk;
b69ab31199 const groups = organizeLinesIntoGroups(lines);
b69ab31200 let beforeLineNumber = oldStart;
b69ab31201 let afterLineNumber = newStart;
b69ab31202
b69ab31203 let beforeTokenizedIndex = 0;
b69ab31204 let afterTokenizedIndex = 0;
b69ab31205
b69ab31206 groups.forEach(group => {
b69ab31207 const {common, removed, added} = group;
b69ab31208 addUnmodifiedRows(
b69ab31209 unified,
b69ab31210 common,
b69ab31211 path,
b69ab31212 'common',
b69ab31213 beforeLineNumber,
b69ab31214 afterLineNumber,
b69ab31215 rows,
b69ab31216 tokenization?.[0].slice(beforeTokenizedIndex),
b69ab31217 tokenization?.[1].slice(afterTokenizedIndex),
b69ab31218 openFileToLine,
b69ab31219 displayLineNumbers,
b69ab31220 );
b69ab31221 beforeLineNumber += common.length;
b69ab31222 afterLineNumber += common.length;
b69ab31223 beforeTokenizedIndex += common.length;
b69ab31224 afterTokenizedIndex += common.length;
b69ab31225
b69ab31226 // split content, or before lines when unified
b69ab31227 const linesA = [];
b69ab31228 // after lines when unified, or empty when using "split"
b69ab31229 const linesB = [];
b69ab31230
b69ab31231 const maxIndex = Math.max(removed.length, added.length);
b69ab31232 for (let index = 0; index < maxIndex; ++index) {
b69ab31233 const removedLine = removed[index];
b69ab31234 const addedLine = added[index];
b69ab31235 if (removedLine != null && addedLine != null) {
b69ab31236 let beforeAndAfter;
b69ab31237
b69ab31238 if (tokenization != null) {
b69ab31239 beforeAndAfter = createTokenizedIntralineDiff(
b69ab31240 removedLine,
b69ab31241 tokenization[0][beforeTokenizedIndex],
b69ab31242 addedLine,
b69ab31243 tokenization[1][afterTokenizedIndex],
b69ab31244 );
b69ab31245 } else {
b69ab31246 beforeAndAfter = createIntralineDiff(removedLine, addedLine);
b69ab31247 }
b69ab31248
b69ab31249 const [before, after] = beforeAndAfter;
b69ab31250 const [beforeLine, beforeChange, afterLine, afterChange] = SplitDiffRow({
b69ab31251 beforeLineNumber,
b69ab31252 before,
b69ab31253 afterLineNumber,
b69ab31254 after,
b69ab31255 rowType: 'modify',
b69ab31256 path,
b69ab31257 unified,
b69ab31258 openFileToLine,
b69ab31259 });
b69ab31260
b69ab31261 if (unified) {
b69ab31262 linesA.push(
b69ab31263 <tr key={`${beforeLineNumber}/${afterLineNumber}:b`}>
b69ab31264 {displayLineNumbers && beforeLine}
b69ab31265 {displayLineNumbers && <BlankLineNumber before />}
b69ab31266 {beforeChange}
b69ab31267 </tr>,
b69ab31268 );
b69ab31269 linesB.push(
b69ab31270 <tr key={`${beforeLineNumber}/${afterLineNumber}:a`}>
b69ab31271 {displayLineNumbers && <BlankLineNumber after />}
b69ab31272 {displayLineNumbers && afterLine}
b69ab31273 {afterChange}
b69ab31274 </tr>,
b69ab31275 );
b69ab31276 } else {
b69ab31277 linesA.push(
b69ab31278 <tr key={`${beforeLineNumber}/${afterLineNumber}`}>
b69ab31279 {displayLineNumbers && beforeLine}
b69ab31280 {beforeChange}
b69ab31281 {displayLineNumbers && afterLine}
b69ab31282 {afterChange}
b69ab31283 </tr>,
b69ab31284 );
b69ab31285 }
b69ab31286 ++beforeLineNumber;
b69ab31287 ++afterLineNumber;
b69ab31288 ++beforeTokenizedIndex;
b69ab31289 ++afterTokenizedIndex;
b69ab31290 } else if (removedLine != null) {
b69ab31291 const [beforeLine, beforeChange, afterLine, afterChange] = SplitDiffRow({
b69ab31292 beforeLineNumber,
b69ab31293 before:
b69ab31294 tokenization?.[0] == null
b69ab31295 ? removedLine
b69ab31296 : applyTokenizationToLine(removedLine, tokenization[0][beforeTokenizedIndex]),
b69ab31297 afterLineNumber: null,
b69ab31298 after: null,
b69ab31299 rowType: 'remove',
b69ab31300 path,
b69ab31301 unified,
b69ab31302 openFileToLine,
b69ab31303 });
b69ab31304
b69ab31305 if (unified) {
b69ab31306 linesA.push(
b69ab31307 <tr key={`${beforeLineNumber}/`}>
b69ab31308 {displayLineNumbers && beforeLine}
b69ab31309 {displayLineNumbers && <BlankLineNumber before />}
b69ab31310 {beforeChange}
b69ab31311 </tr>,
b69ab31312 );
b69ab31313 } else {
b69ab31314 linesA.push(
b69ab31315 <tr key={`${beforeLineNumber}/`}>
b69ab31316 {displayLineNumbers && beforeLine}
b69ab31317 {beforeChange}
b69ab31318 {displayLineNumbers && afterLine}
b69ab31319 {afterChange}
b69ab31320 </tr>,
b69ab31321 );
b69ab31322 }
b69ab31323 ++beforeLineNumber;
b69ab31324 ++beforeTokenizedIndex;
b69ab31325 } else {
b69ab31326 const [beforeLine, beforeChange, afterLine, afterChange] = SplitDiffRow({
b69ab31327 beforeLineNumber: null,
b69ab31328 before: null,
b69ab31329 afterLineNumber,
b69ab31330 after:
b69ab31331 tokenization?.[1] == null
b69ab31332 ? addedLine
b69ab31333 : applyTokenizationToLine(addedLine, tokenization[1][afterTokenizedIndex]),
b69ab31334 rowType: 'add',
b69ab31335 path,
b69ab31336 unified,
b69ab31337 openFileToLine,
b69ab31338 });
b69ab31339
b69ab31340 if (unified) {
b69ab31341 linesB.push(
b69ab31342 <tr key={`/${afterLineNumber}`}>
b69ab31343 {displayLineNumbers && <BlankLineNumber after />}
b69ab31344 {displayLineNumbers && afterLine}
b69ab31345 {afterChange}
b69ab31346 </tr>,
b69ab31347 );
b69ab31348 } else {
b69ab31349 linesA.push(
b69ab31350 <tr key={`/${afterLineNumber}`}>
b69ab31351 {displayLineNumbers && beforeLine}
b69ab31352 {beforeChange}
b69ab31353 {displayLineNumbers && afterLine}
b69ab31354 {afterChange}
b69ab31355 </tr>,
b69ab31356 );
b69ab31357 }
b69ab31358 ++afterLineNumber;
b69ab31359 ++afterTokenizedIndex;
b69ab31360 }
b69ab31361 }
b69ab31362
b69ab31363 rows.push(...linesA, ...linesB);
b69ab31364 });
b69ab31365}
b69ab31366
b69ab31367function InlineRowButton({onClick, label}: {onClick: () => unknown; label: ReactNode}) {
b69ab31368 return (
b69ab31369 // TODO: tabindex or make this a button for accessibility
b69ab31370 <div className="split-diff-view-inline-row-button" onClick={onClick}>
b69ab31371 <Icon icon="unfold" />
b69ab31372 <span className="inline-row-button-label">{label}</span>
b69ab31373 <Icon icon="unfold" />
b69ab31374 </div>
b69ab31375 );
b69ab31376}
b69ab31377
b69ab31378/**
b69ab31379 * Adds new rows to the supplied `rows` array.
b69ab31380 */
b69ab31381function addUnmodifiedRows(
b69ab31382 unified: boolean,
b69ab31383 lines: string[],
b69ab31384 path: string,
b69ab31385 rowType: 'common' | 'expanded',
b69ab31386 initialBeforeLineNumber: number,
b69ab31387 initialAfterLineNumber: number,
b69ab31388 rows: React.ReactElement[],
b69ab31389 tokenizationBefore?: TokenizedHunk | undefined,
b69ab31390 tokenizationAfter?: TokenizedHunk | undefined,
b69ab31391 openFileToLine?: (line: OneIndexedLineNumber) => unknown,
b69ab31392 displayLineNumbers: boolean = true,
b69ab31393): void {
b69ab31394 let beforeLineNumber = initialBeforeLineNumber;
b69ab31395 let afterLineNumber = initialAfterLineNumber;
b69ab31396 lines.forEach((lineContent, i) => {
b69ab31397 const [beforeLine, beforeChange, afterLine, afterChange] = SplitDiffRow({
b69ab31398 beforeLineNumber,
b69ab31399 before:
b69ab31400 tokenizationBefore?.[i] == null
b69ab31401 ? lineContent
b69ab31402 : applyTokenizationToLine(lineContent, tokenizationBefore[i]),
b69ab31403 afterLineNumber,
b69ab31404 after:
b69ab31405 tokenizationAfter?.[i] == null
b69ab31406 ? lineContent
b69ab31407 : applyTokenizationToLine(lineContent, tokenizationAfter[i]),
b69ab31408 rowType,
b69ab31409 path,
b69ab31410 unified,
b69ab31411 openFileToLine,
b69ab31412 });
b69ab31413 if (unified) {
b69ab31414 rows.push(
b69ab31415 <tr key={`${beforeLineNumber}/${afterLineNumber}`}>
b69ab31416 {displayLineNumbers && beforeLine}
b69ab31417 {displayLineNumbers && afterLine}
b69ab31418 {beforeChange}
b69ab31419 </tr>,
b69ab31420 );
b69ab31421 } else {
b69ab31422 rows.push(
b69ab31423 <tr key={`${beforeLineNumber}/${afterLineNumber}`}>
b69ab31424 {displayLineNumbers && beforeLine}
b69ab31425 {beforeChange}
b69ab31426 {displayLineNumbers && afterLine}
b69ab31427 {afterChange}
b69ab31428 </tr>,
b69ab31429 );
b69ab31430 }
b69ab31431 ++beforeLineNumber;
b69ab31432 ++afterLineNumber;
b69ab31433 });
b69ab31434}
b69ab31435
b69ab31436function createIntralineDiff(
b69ab31437 before: string,
b69ab31438 after: string,
b69ab31439): [React.ReactFragment, React.ReactFragment] {
b69ab31440 // For lines longer than this, diffChars() can get very expensive to compute
b69ab31441 // and is likely of little value to the user.
b69ab31442 if (before.length + after.length > MAX_INPUT_LENGTH_FOR_INTRALINE_DIFF) {
b69ab31443 return [before, after];
b69ab31444 }
b69ab31445
b69ab31446 const changes = diffChars(before, after);
b69ab31447 const beforeElements: React.ReactNode[] = [];
b69ab31448 const afterElements: React.ReactNode[] = [];
b69ab31449 changes.forEach((change, index) => {
b69ab31450 const {added, removed, value} = change;
b69ab31451 if (added) {
b69ab31452 afterElements.push(
b69ab31453 <span key={index} className="patch-add-word">
b69ab31454 {value}
b69ab31455 </span>,
b69ab31456 );
b69ab31457 } else if (removed) {
b69ab31458 beforeElements.push(
b69ab31459 <span key={index} className="patch-remove-word">
b69ab31460 {value}
b69ab31461 </span>,
b69ab31462 );
b69ab31463 } else {
b69ab31464 beforeElements.push(value);
b69ab31465 afterElements.push(value);
b69ab31466 }
b69ab31467 });
b69ab31468
b69ab31469 return [beforeElements, afterElements];
b69ab31470}
b69ab31471
b69ab31472/**
b69ab31473 * Visual element to delimit the discontinuity in a SplitDiffView.
b69ab31474 */
b69ab31475function HunkSeparator({
b69ab31476 numLines,
b69ab31477 onExpand,
b69ab31478 t,
b69ab31479}: {
b69ab31480 numLines: number | null;
b69ab31481 onExpand: () => unknown;
b69ab31482 t: (s: string) => string;
b69ab31483}): React.ReactElement | null {
b69ab31484 if (numLines === 0) {
b69ab31485 return null;
b69ab31486 }
b69ab31487 // TODO: Ensure numLines is never below a certain threshold: it takes up more
b69ab31488 // space to display the separator than it does to display the text (though
b69ab31489 // admittedly fetching the collapsed text is an async operation).
b69ab31490 const label =
b69ab31491 numLines == null
b69ab31492 ? // to expand the remaining lines at the end of the file, we don't know the size ahead of time,
b69ab31493 // just omit the amount to be expanded
b69ab31494 t('Expand lines')
b69ab31495 : numLines === 1
b69ab31496 ? t('Expand 1 line')
b69ab31497 : t(`Expand ${numLines} lines`);
b69ab31498 return (
b69ab31499 <SeparatorRow>
b69ab31500 <InlineRowButton label={label} onClick={onExpand} />
b69ab31501 </SeparatorRow>
b69ab31502 );
b69ab31503}
b69ab31504
b69ab31505/**
b69ab31506 * This replaces a <HunkSeparator> when the user clicks on it to expand the
b69ab31507 * hidden file contents.
b69ab31508 * By rendering this, additional lines are automatically fetched.
b69ab31509 */
b69ab31510function ExpandingSeparator({
b69ab31511 ctx,
b69ab31512 path,
b69ab31513 start,
b69ab31514 numLines,
b69ab31515 beforeLineStart,
b69ab31516 afterLineStart,
b69ab31517}: {
b69ab31518 ctx: Context;
b69ab31519 path: string;
b69ab31520 numLines: number;
b69ab31521 start: number;
b69ab31522 beforeLineStart: number;
b69ab31523 afterLineStart: number;
b69ab31524}): React.ReactElement {
b69ab31525 const result = useFetchLines(ctx, numLines, start);
b69ab31526 const t = ctx.t ?? (s => s);
b69ab31527
b69ab31528 const tokenization = useTokenizedContents(path, result?.value, ctx.useThemeHook);
b69ab31529 if (result == null) {
b69ab31530 return (
b69ab31531 <SeparatorRow>
b69ab31532 <div className="split-diff-view-loading-row">
b69ab31533 <Icon icon="loading" />
b69ab31534 <span>{t('Loading...')}</span>
b69ab31535 </div>
b69ab31536 </SeparatorRow>
b69ab31537 );
b69ab31538 }
b69ab31539 if (result.error) {
b69ab31540 return (
b69ab31541 <SeparatorRow>
b69ab31542 <div className="split-diff-view-error-row">
b69ab31543 <ErrorNotice error={result.error} title={t('Unable to fetch additional lines')} />
b69ab31544 </div>
b69ab31545 </SeparatorRow>
b69ab31546 );
b69ab31547 }
b69ab31548
b69ab31549 const rows: React.ReactElement[] = [];
b69ab31550 addUnmodifiedRows(
b69ab31551 ctx.display === 'unified',
b69ab31552 result.value,
b69ab31553 path,
b69ab31554 'expanded',
b69ab31555 beforeLineStart,
b69ab31556 afterLineStart,
b69ab31557 rows,
b69ab31558 tokenization,
b69ab31559 tokenization,
b69ab31560 ctx.openFileToLine,
b69ab31561 ctx.displayLineNumbers,
b69ab31562 );
b69ab31563 return <>{rows}</>;
b69ab31564}
b69ab31565
b69ab31566function SeparatorRow({children}: {children: React.ReactNode}): React.ReactElement {
b69ab31567 return (
b69ab31568 <tr className="separator-row">
b69ab31569 <td colSpan={4} className="separator">
b69ab31570 {children}
b69ab31571 </td>
b69ab31572 </tr>
b69ab31573 );
b69ab31574}
b69ab31575
b69ab31576/** Fetches context lines */
b69ab31577function useFetchLines(ctx: Context, numLines: number, start: number) {
b69ab31578 const [fetchedLines, setFetchedLines] = useState<Result<Array<string>> | undefined>(undefined);
b69ab31579
b69ab31580 // Use-case controlled key that allows invalidating the fetched lines.
b69ab31581 const invalidationKey = ctx.useComparisonInvalidationKeyHook?.();
b69ab31582
b69ab31583 const comparisonKey = comparisonStringKey(ctx.id.comparison);
b69ab31584 useEffect(() => {
b69ab31585 ctx.fetchAdditionalLines?.(ctx.id, start, numLines).then(result => {
b69ab31586 setFetchedLines(result);
b69ab31587 });
b69ab31588 // eslint-disable-next-line react-hooks/exhaustive-deps
b69ab31589 }, [invalidationKey, ctx.id.path, comparisonKey, numLines, start]);
b69ab31590
b69ab31591 return fetchedLines;
b69ab31592}