| ... | ... | @@ -105,10 +105,26 @@ export interface BuildNativeReviewDiffDataInput { |
| 105 | 105 | } |
| 106 | 106 | |
| 107 | 107 | interface CachedNativeReviewDiffData { |
| 108 | readonly prepared: PreparedNativeReviewDiffData; |
| 108 | 109 | readonly commentsKey: string; |
| 109 | 110 | readonly data: NativeReviewDiffData; |
| 110 | 111 | } |
| 111 | 112 | |
| 113 | interface PreparedNativeReviewFileRows { |
| 114 | readonly fileId: string; |
| 115 | readonly filePath: string; |
| 116 | readonly lineCount: number; |
| 117 | readonly rows: ReadonlyArray<NativeReviewDiffRow>; |
| 118 | commentedRows: { |
| 119 | readonly commentsKey: string; |
| 120 | readonly rows: ReadonlyArray<NativeReviewDiffRow>; |
| 121 | } | null; |
| 122 | } |
| 123 | |
| 124 | interface PreparedNativeReviewDiffData extends Omit<NativeReviewDiffData, "rows"> { |
| 125 | readonly fileRows: ReadonlyArray<PreparedNativeReviewFileRows>; |
| 126 | } |
| 127 | |
| 112 | 128 | const nativeReviewDiffDataCache = new WeakMap<ReviewParsedDiff, CachedNativeReviewDiffData>(); |
| 113 | 129 | |
| 114 | 130 | function buildReviewCommentsCacheKey(comments: ReadonlyArray<ReviewInlineComment>): string { |
| ... | ... | @@ -116,19 +132,18 @@ function buildReviewCommentsCacheKey(comments: ReadonlyArray<ReviewInlineComment |
| 116 | 132 | return "none"; |
| 117 | 133 | } |
| 118 | 134 | |
| 119 | | return comments |
| 120 | | .map((comment) => |
| 121 | | [ |
| 122 | | comment.id, |
| 123 | | comment.sectionId, |
| 124 | | comment.filePath, |
| 125 | | comment.startIndex, |
| 126 | | comment.endIndex, |
| 127 | | comment.rangeLabel, |
| 128 | | comment.text, |
| 129 | | ].join("\u001f"), |
| 130 | | ) |
| 131 | | .join("\u001e"); |
| 135 | return JSON.stringify( |
| 136 | comments.map((comment) => [ |
| 137 | comment.id, |
| 138 | comment.sectionId, |
| 139 | comment.sectionTitle, |
| 140 | comment.filePath, |
| 141 | comment.startIndex, |
| 142 | comment.endIndex, |
| 143 | comment.rangeLabel, |
| 144 | comment.text, |
| 145 | ]), |
| 146 | ); |
| 132 | 147 | } |
| 133 | 148 | |
| 134 | 149 | export function createNativeReviewDiffTheme( |
| ... | ... | @@ -378,12 +393,11 @@ function mapLineRow( |
| 378 | 393 | }; |
| 379 | 394 | } |
| 380 | 395 | |
| 381 | | function mapFileRows( |
| 396 | function prepareFileRows( |
| 382 | 397 | file: ReviewRenderableFile, |
| 383 | | comments: ReadonlyArray<ReviewInlineComment>, |
| 384 | 398 | commentTargetsByRowId: Map<string, NativeReviewDiffCommentTarget>, |
| 385 | 399 | rowIdByCommentLineId: Map<string, string>, |
| 386 | | ): ReadonlyArray<NativeReviewDiffRow> { |
| 400 | ): PreparedNativeReviewFileRows { |
| 387 | 401 | const rows: NativeReviewDiffRow[] = [ |
| 388 | 402 | { |
| 389 | 403 | kind: "file", |
| ... | ... | @@ -398,22 +412,6 @@ function mapFileRows( |
| 398 | 412 | ]; |
| 399 | 413 | |
| 400 | 414 | const lineRows = file.rows.filter((row): row is ReviewRenderableLineRow => row.kind === "line"); |
| 401 | | const commentsByEndIndex = new Map<number, ReviewInlineComment[]>(); |
| 402 | | comments.forEach((comment) => { |
| 403 | | if (comment.filePath !== file.path) { |
| 404 | | return; |
| 405 | | } |
| 406 | | const endIndex = Math.min(comment.endIndex, lineRows.length - 1); |
| 407 | | if (endIndex < 0) { |
| 408 | | return; |
| 409 | | } |
| 410 | | const existing = commentsByEndIndex.get(endIndex); |
| 411 | | if (existing) { |
| 412 | | existing.push(comment); |
| 413 | | return; |
| 414 | | } |
| 415 | | commentsByEndIndex.set(endIndex, [comment]); |
| 416 | | }); |
| 417 | 415 | let lineIndex = 0; |
| 418 | 416 | file.rows.forEach((row, rowIndex) => { |
| 419 | 417 | if (row.kind === "hunk") { |
| ... | ... | @@ -434,37 +432,70 @@ function mapFileRows( |
| 434 | 432 | lines: lineRows, |
| 435 | 433 | lineIndex, |
| 436 | 434 | }); |
| 437 | | const commentsForLine = commentsByEndIndex.get(lineIndex) ?? []; |
| 438 | | for (const comment of commentsForLine) { |
| 435 | lineIndex += 1; |
| 436 | }); |
| 437 | |
| 438 | rows.push(...noticeRowsForFile(file)); |
| 439 | return { |
| 440 | fileId: file.id, |
| 441 | filePath: file.path, |
| 442 | lineCount: lineRows.length, |
| 443 | // Comments must not split the source deletion/addition runs used for word matching. |
| 444 | rows: addNativeWordDiffRanges(rows), |
| 445 | commentedRows: null, |
| 446 | }; |
| 447 | } |
| 448 | |
| 449 | function insertFileComments( |
| 450 | file: PreparedNativeReviewFileRows, |
| 451 | comments: ReadonlyArray<ReviewInlineComment>, |
| 452 | ): ReadonlyArray<NativeReviewDiffRow> { |
| 453 | if (comments.length === 0) { |
| 454 | file.commentedRows = null; |
| 455 | return file.rows; |
| 456 | } |
| 457 | const commentsKey = buildReviewCommentsCacheKey(comments); |
| 458 | if (file.commentedRows?.commentsKey === commentsKey) { |
| 459 | return file.commentedRows.rows; |
| 460 | } |
| 461 | |
| 462 | const commentsByEndIndex = new Map<number, ReviewInlineComment[]>(); |
| 463 | for (const comment of comments) { |
| 464 | const endIndex = Math.min(comment.endIndex, file.lineCount - 1); |
| 465 | if (endIndex < 0) continue; |
| 466 | const existing = commentsByEndIndex.get(endIndex); |
| 467 | if (existing) { |
| 468 | existing.push(comment); |
| 469 | } else { |
| 470 | commentsByEndIndex.set(endIndex, [comment]); |
| 471 | } |
| 472 | } |
| 473 | const rows: NativeReviewDiffRow[] = []; |
| 474 | let lineIndex = 0; |
| 475 | for (const row of file.rows) { |
| 476 | rows.push(row); |
| 477 | if (row.kind !== "line") continue; |
| 478 | for (const comment of commentsByEndIndex.get(lineIndex) ?? []) { |
| 439 | 479 | rows.push({ |
| 440 | 480 | kind: "comment", |
| 441 | 481 | id: comment.id, |
| 442 | | fileId: file.id, |
| 443 | | filePath: file.path, |
| 482 | fileId: file.fileId, |
| 483 | filePath: file.filePath, |
| 444 | 484 | commentText: comment.text, |
| 445 | 485 | commentRangeLabel: comment.rangeLabel, |
| 446 | 486 | commentSectionTitle: comment.sectionTitle, |
| 447 | 487 | }); |
| 448 | 488 | } |
| 449 | 489 | lineIndex += 1; |
| 450 | | }); |
| 451 | | |
| 452 | | rows.push(...noticeRowsForFile(file)); |
| 490 | } |
| 491 | file.commentedRows = { commentsKey, rows }; |
| 453 | 492 | return rows; |
| 454 | 493 | } |
| 455 | 494 | |
| 456 | | export function buildNativeReviewDiffData( |
| 457 | | input: BuildNativeReviewDiffDataInput, |
| 458 | | ): NativeReviewDiffData; |
| 459 | | export function buildNativeReviewDiffData(parsedDiff: ReviewParsedDiff): NativeReviewDiffData; |
| 460 | | export function buildNativeReviewDiffData( |
| 461 | | input: ReviewParsedDiff | BuildNativeReviewDiffDataInput, |
| 462 | | ): NativeReviewDiffData { |
| 463 | | const parsedDiff = "parsedDiff" in input ? input.parsedDiff : input; |
| 464 | | const comments = "parsedDiff" in input ? (input.comments ?? []) : []; |
| 495 | function prepareNativeReviewDiffData(parsedDiff: ReviewParsedDiff): PreparedNativeReviewDiffData { |
| 465 | 496 | if (parsedDiff.kind !== "files") { |
| 466 | 497 | return { |
| 467 | | rows: [], |
| 498 | fileRows: [], |
| 468 | 499 | files: [], |
| 469 | 500 | commentTargetsByRowId: new Map(), |
| 470 | 501 | rowIdByCommentLineId: new Map(), |
| ... | ... | @@ -482,14 +513,12 @@ export function buildNativeReviewDiffData( |
| 482 | 513 | })); |
| 483 | 514 | const commentTargetsByRowId = new Map<string, NativeReviewDiffCommentTarget>(); |
| 484 | 515 | const rowIdByCommentLineId = new Map<string, string>(); |
| 485 | | const rows = addNativeWordDiffRanges( |
| 486 | | Arr.flatMap(parsedDiff.files, (file) => |
| 487 | | mapFileRows(file, comments, commentTargetsByRowId, rowIdByCommentLineId), |
| 488 | | ), |
| 516 | const fileRows = parsedDiff.files.map((file) => |
| 517 | prepareFileRows(file, commentTargetsByRowId, rowIdByCommentLineId), |
| 489 | 518 | ); |
| 490 | 519 | |
| 491 | 520 | return { |
| 492 | | rows, |
| 521 | fileRows, |
| 493 | 522 | files, |
| 494 | 523 | commentTargetsByRowId, |
| 495 | 524 | rowIdByCommentLineId, |
| ... | ... | @@ -498,10 +527,47 @@ export function buildNativeReviewDiffData( |
| 498 | 527 | }; |
| 499 | 528 | } |
| 500 | 529 | |
| 530 | function buildCommentedNativeReviewDiffData( |
| 531 | prepared: PreparedNativeReviewDiffData, |
| 532 | comments: ReadonlyArray<ReviewInlineComment>, |
| 533 | ): NativeReviewDiffData { |
| 534 | const commentsByFilePath = new Map<string, ReviewInlineComment[]>(); |
| 535 | for (const comment of comments) { |
| 536 | const existing = commentsByFilePath.get(comment.filePath); |
| 537 | if (existing) { |
| 538 | existing.push(comment); |
| 539 | } else { |
| 540 | commentsByFilePath.set(comment.filePath, [comment]); |
| 541 | } |
| 542 | } |
| 543 | return { |
| 544 | rows: Arr.flatMap(prepared.fileRows, (file) => |
| 545 | insertFileComments(file, commentsByFilePath.get(file.filePath) ?? []), |
| 546 | ), |
| 547 | files: prepared.files, |
| 548 | commentTargetsByRowId: prepared.commentTargetsByRowId, |
| 549 | rowIdByCommentLineId: prepared.rowIdByCommentLineId, |
| 550 | additions: prepared.additions, |
| 551 | deletions: prepared.deletions, |
| 552 | }; |
| 553 | } |
| 554 | |
| 555 | export function buildNativeReviewDiffData( |
| 556 | input: BuildNativeReviewDiffDataInput, |
| 557 | ): NativeReviewDiffData; |
| 558 | export function buildNativeReviewDiffData(parsedDiff: ReviewParsedDiff): NativeReviewDiffData; |
| 559 | export function buildNativeReviewDiffData( |
| 560 | input: ReviewParsedDiff | BuildNativeReviewDiffDataInput, |
| 561 | ): NativeReviewDiffData { |
| 562 | const parsedDiff = "parsedDiff" in input ? input.parsedDiff : input; |
| 563 | const comments = "parsedDiff" in input ? (input.comments ?? []) : []; |
| 564 | return buildCommentedNativeReviewDiffData(prepareNativeReviewDiffData(parsedDiff), comments); |
| 565 | } |
| 566 | |
| 501 | 567 | /** |
| 502 | | * Reuses the expensive flattened native row model across React development |
| 503 | | * render probes and unrelated draft updates. Only the latest comment version |
| 504 | | * is retained for each parsed diff so editing a comment cannot grow the cache. |
| 568 | * Prepares source rows once per parsed diff, including its section-specific IDs. |
| 569 | * Comment edits reuse those rows, word ranges, and targets. Each file retains |
| 570 | * only its latest comment overlay, and the weak key releases old parsed diffs. |
| 505 | 571 | */ |
| 506 | 572 | export function getCachedNativeReviewDiffData( |
| 507 | 573 | input: BuildNativeReviewDiffDataInput, |
| ... | ... | @@ -513,10 +579,8 @@ export function getCachedNativeReviewDiffData( |
| 513 | 579 | return cached.data; |
| 514 | 580 | } |
| 515 | 581 | |
| 516 | | const data = buildNativeReviewDiffData({ |
| 517 | | parsedDiff: input.parsedDiff, |
| 518 | | comments, |
| 519 | | }); |
| 520 | | nativeReviewDiffDataCache.set(input.parsedDiff, { commentsKey, data }); |
| 582 | const prepared = cached?.prepared ?? prepareNativeReviewDiffData(input.parsedDiff); |
| 583 | const data = buildCommentedNativeReviewDiffData(prepared, comments); |
| 584 | nativeReviewDiffDataCache.set(input.parsedDiff, { prepared, commentsKey, data }); |
| 521 | 585 | return data; |
| 522 | 586 | } |