| ... | @@ -105,10 +105,26 @@ export interface BuildNativeReviewDiffDataInput { | ... | @@ -105,10 +105,26 @@ export interface BuildNativeReviewDiffDataInput { |
| 105 | } | 105 | } |
| 106 | | 106 | |
| 107 | interface CachedNativeReviewDiffData { | 107 | interface CachedNativeReviewDiffData { |
| | 108 | readonly prepared: PreparedNativeReviewDiffData; |
| 108 | readonly commentsKey: string; | 109 | readonly commentsKey: string; |
| 109 | readonly data: NativeReviewDiffData; | 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 | const nativeReviewDiffDataCache = new WeakMap<ReviewParsedDiff, CachedNativeReviewDiffData>(); | 128 | const nativeReviewDiffDataCache = new WeakMap<ReviewParsedDiff, CachedNativeReviewDiffData>(); |
| 113 | | 129 | |
| 114 | function buildReviewCommentsCacheKey(comments: ReadonlyArray<ReviewInlineComment>): string { | 130 | function buildReviewCommentsCacheKey(comments: ReadonlyArray<ReviewInlineComment>): string { |
| ... | @@ -116,19 +132,18 @@ function buildReviewCommentsCacheKey(comments: ReadonlyArray<ReviewInlineComment | ... | @@ -116,19 +132,18 @@ function buildReviewCommentsCacheKey(comments: ReadonlyArray<ReviewInlineComment |
| 116 | return "none"; | 132 | return "none"; |
| 117 | } | 133 | } |
| 118 | | 134 | |
| 119 | return comments | 135 | return JSON.stringify( |
| 120 | .map((comment) => | 136 | comments.map((comment) => [ |
| 121 | [ | 137 | comment.id, |
| 122 | comment.id, | 138 | comment.sectionId, |
| 123 | comment.sectionId, | 139 | comment.sectionTitle, |
| 124 | comment.filePath, | 140 | comment.filePath, |
| 125 | comment.startIndex, | 141 | comment.startIndex, |
| 126 | comment.endIndex, | 142 | comment.endIndex, |
| 127 | comment.rangeLabel, | 143 | comment.rangeLabel, |
| 128 | comment.text, | 144 | comment.text, |
| 129 | ].join("\u001f"), | 145 | ]), |
| 130 | ) | 146 | ); |
| 131 | .join("\u001e"); | | |
| 132 | } | 147 | } |
| 133 | | 148 | |
| 134 | export function createNativeReviewDiffTheme( | 149 | export function createNativeReviewDiffTheme( |
| ... | @@ -378,12 +393,11 @@ function mapLineRow( | ... | @@ -378,12 +393,11 @@ function mapLineRow( |
| 378 | }; | 393 | }; |
| 379 | } | 394 | } |
| 380 | | 395 | |
| 381 | function mapFileRows( | 396 | function prepareFileRows( |
| 382 | file: ReviewRenderableFile, | 397 | file: ReviewRenderableFile, |
| 383 | comments: ReadonlyArray<ReviewInlineComment>, | | |
| 384 | commentTargetsByRowId: Map<string, NativeReviewDiffCommentTarget>, | 398 | commentTargetsByRowId: Map<string, NativeReviewDiffCommentTarget>, |
| 385 | rowIdByCommentLineId: Map<string, string>, | 399 | rowIdByCommentLineId: Map<string, string>, |
| 386 | ): ReadonlyArray<NativeReviewDiffRow> { | 400 | ): PreparedNativeReviewFileRows { |
| 387 | const rows: NativeReviewDiffRow[] = [ | 401 | const rows: NativeReviewDiffRow[] = [ |
| 388 | { | 402 | { |
| 389 | kind: "file", | 403 | kind: "file", |
| ... | @@ -398,22 +412,6 @@ function mapFileRows( | ... | @@ -398,22 +412,6 @@ function mapFileRows( |
| 398 | ]; | 412 | ]; |
| 399 | | 413 | |
| 400 | const lineRows = file.rows.filter((row): row is ReviewRenderableLineRow => row.kind === "line"); | 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 | let lineIndex = 0; | 415 | let lineIndex = 0; |
| 418 | file.rows.forEach((row, rowIndex) => { | 416 | file.rows.forEach((row, rowIndex) => { |
| 419 | if (row.kind === "hunk") { | 417 | if (row.kind === "hunk") { |
| ... | @@ -434,37 +432,70 @@ function mapFileRows( | ... | @@ -434,37 +432,70 @@ function mapFileRows( |
| 434 | lines: lineRows, | 432 | lines: lineRows, |
| 435 | lineIndex, | 433 | lineIndex, |
| 436 | }); | 434 | }); |
| 437 | const commentsForLine = commentsByEndIndex.get(lineIndex) ?? []; | 435 | lineIndex += 1; |
| 438 | for (const comment of commentsForLine) { | 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 | rows.push({ | 479 | rows.push({ |
| 440 | kind: "comment", | 480 | kind: "comment", |
| 441 | id: comment.id, | 481 | id: comment.id, |
| 442 | fileId: file.id, | 482 | fileId: file.fileId, |
| 443 | filePath: file.path, | 483 | filePath: file.filePath, |
| 444 | commentText: comment.text, | 484 | commentText: comment.text, |
| 445 | commentRangeLabel: comment.rangeLabel, | 485 | commentRangeLabel: comment.rangeLabel, |
| 446 | commentSectionTitle: comment.sectionTitle, | 486 | commentSectionTitle: comment.sectionTitle, |
| 447 | }); | 487 | }); |
| 448 | } | 488 | } |
| 449 | lineIndex += 1; | 489 | lineIndex += 1; |
| 450 | }); | 490 | } |
| 451 | | 491 | file.commentedRows = { commentsKey, rows }; |
| 452 | rows.push(...noticeRowsForFile(file)); | | |
| 453 | return rows; | 492 | return rows; |
| 454 | } | 493 | } |
| 455 | | 494 | |
| 456 | export function buildNativeReviewDiffData( | 495 | function prepareNativeReviewDiffData(parsedDiff: ReviewParsedDiff): PreparedNativeReviewDiffData { |
| 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 ?? []) : []; | | |
| 465 | if (parsedDiff.kind !== "files") { | 496 | if (parsedDiff.kind !== "files") { |
| 466 | return { | 497 | return { |
| 467 | rows: [], | 498 | fileRows: [], |
| 468 | files: [], | 499 | files: [], |
| 469 | commentTargetsByRowId: new Map(), | 500 | commentTargetsByRowId: new Map(), |
| 470 | rowIdByCommentLineId: new Map(), | 501 | rowIdByCommentLineId: new Map(), |
| ... | @@ -482,14 +513,12 @@ export function buildNativeReviewDiffData( | ... | @@ -482,14 +513,12 @@ export function buildNativeReviewDiffData( |
| 482 | })); | 513 | })); |
| 483 | const commentTargetsByRowId = new Map<string, NativeReviewDiffCommentTarget>(); | 514 | const commentTargetsByRowId = new Map<string, NativeReviewDiffCommentTarget>(); |
| 484 | const rowIdByCommentLineId = new Map<string, string>(); | 515 | const rowIdByCommentLineId = new Map<string, string>(); |
| 485 | const rows = addNativeWordDiffRanges( | 516 | const fileRows = parsedDiff.files.map((file) => |
| 486 | Arr.flatMap(parsedDiff.files, (file) => | 517 | prepareFileRows(file, commentTargetsByRowId, rowIdByCommentLineId), |
| 487 | mapFileRows(file, comments, commentTargetsByRowId, rowIdByCommentLineId), | | |
| 488 | ), | | |
| 489 | ); | 518 | ); |
| 490 | | 519 | |
| 491 | return { | 520 | return { |
| 492 | rows, | 521 | fileRows, |
| 493 | files, | 522 | files, |
| 494 | commentTargetsByRowId, | 523 | commentTargetsByRowId, |
| 495 | rowIdByCommentLineId, | 524 | rowIdByCommentLineId, |
| ... | @@ -498,10 +527,47 @@ export function buildNativeReviewDiffData( | ... | @@ -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 | 568 | * Prepares source rows once per parsed diff, including its section-specific IDs. |
| 503 | * render probes and unrelated draft updates. Only the latest comment version | 569 | * Comment edits reuse those rows, word ranges, and targets. Each file retains |
| 504 | * is retained for each parsed diff so editing a comment cannot grow the cache. | 570 | * only its latest comment overlay, and the weak key releases old parsed diffs. |
| 505 | */ | 571 | */ |
| 506 | export function getCachedNativeReviewDiffData( | 572 | export function getCachedNativeReviewDiffData( |
| 507 | input: BuildNativeReviewDiffDataInput, | 573 | input: BuildNativeReviewDiffDataInput, |
| ... | @@ -513,10 +579,8 @@ export function getCachedNativeReviewDiffData( | ... | @@ -513,10 +579,8 @@ export function getCachedNativeReviewDiffData( |
| 513 | return cached.data; | 579 | return cached.data; |
| 514 | } | 580 | } |
| 515 | | 581 | |
| 516 | const data = buildNativeReviewDiffData({ | 582 | const prepared = cached?.prepared ?? prepareNativeReviewDiffData(input.parsedDiff); |
| 517 | parsedDiff: input.parsedDiff, | 583 | const data = buildCommentedNativeReviewDiffData(prepared, comments); |
| 518 | comments, | 584 | nativeReviewDiffDataCache.set(input.parsedDiff, { prepared, commentsKey, data }); |
| 519 | }); | | |
| 520 | nativeReviewDiffDataCache.set(input.parsedDiff, { commentsKey, data }); | | |
| 521 | return data; | 585 | return data; |
| 522 | } | 586 | } |