| ... | @@ -56,6 +56,11 @@ interface NativeReviewDiffLineRow extends NativeReviewDiffRow { | ... | @@ -56,6 +56,11 @@ interface NativeReviewDiffLineRow extends NativeReviewDiffRow { |
| 56 | readonly content: string; | 56 | readonly content: string; |
| 57 | } | 57 | } |
| 58 | | 58 | |
| | 59 | interface IndexedNativeReviewDiffLineRow { |
| | 60 | readonly row: NativeReviewDiffLineRow; |
| | 61 | readonly rowIndex: number; |
| | 62 | } |
| | 63 | |
| 59 | export interface NativeReviewDiffTokenChunk { | 64 | export interface NativeReviewDiffTokenChunk { |
| 60 | readonly chunkIndex: number; | 65 | readonly chunkIndex: number; |
| 61 | readonly fileId: string; | 66 | readonly fileId: string; |
| ... | @@ -308,6 +313,58 @@ function isHighlightableLineRow(row: NativeReviewDiffRow): row is NativeReviewDi | ... | @@ -308,6 +313,58 @@ function isHighlightableLineRow(row: NativeReviewDiffRow): row is NativeReviewDi |
| 308 | return row.kind === "line" && typeof row.fileId === "string" && typeof row.content === "string"; | 313 | return row.kind === "line" && typeof row.fileId === "string" && typeof row.content === "string"; |
| 309 | } | 314 | } |
| 310 | | 315 | |
| | 316 | function hasConsecutiveLineNumbers( |
| | 317 | previous: number | null | undefined, |
| | 318 | next: number | null | undefined, |
| | 319 | ): boolean { |
| | 320 | return typeof previous === "number" && typeof next === "number" && next === previous + 1; |
| | 321 | } |
| | 322 | |
| | 323 | function hasOnlyCommentRowsBetween( |
| | 324 | rows: ReadonlyArray<NativeReviewDiffRow>, |
| | 325 | previousRowIndex: number, |
| | 326 | nextRowIndex: number, |
| | 327 | ): boolean { |
| | 328 | for (let rowIndex = previousRowIndex + 1; rowIndex < nextRowIndex; rowIndex += 1) { |
| | 329 | if (rows[rowIndex]?.kind !== "comment") { |
| | 330 | return false; |
| | 331 | } |
| | 332 | } |
| | 333 | return true; |
| | 334 | } |
| | 335 | |
| | 336 | function canShareGrammarContext( |
| | 337 | previous: IndexedNativeReviewDiffLineRow, |
| | 338 | next: IndexedNativeReviewDiffLineRow, |
| | 339 | rows: ReadonlyArray<NativeReviewDiffRow>, |
| | 340 | ): boolean { |
| | 341 | if ( |
| | 342 | next.row.fileId !== previous.row.fileId || |
| | 343 | !hasOnlyCommentRowsBetween(rows, previous.rowIndex, next.rowIndex) |
| | 344 | ) { |
| | 345 | return false; |
| | 346 | } |
| | 347 | |
| | 348 | if (previous.row.change === "delete" || next.row.change === "delete") { |
| | 349 | return ( |
| | 350 | previous.row.change !== "add" && |
| | 351 | next.row.change !== "add" && |
| | 352 | hasConsecutiveLineNumbers(previous.row.oldLineNumber, next.row.oldLineNumber) |
| | 353 | ); |
| | 354 | } |
| | 355 | |
| | 356 | if (previous.row.change === "add" || next.row.change === "add") { |
| | 357 | return hasConsecutiveLineNumbers(previous.row.newLineNumber, next.row.newLineNumber); |
| | 358 | } |
| | 359 | |
| | 360 | return ( |
| | 361 | previous.row.change === "context" && |
| | 362 | next.row.change === "context" && |
| | 363 | hasConsecutiveLineNumbers(previous.row.oldLineNumber, next.row.oldLineNumber) && |
| | 364 | hasConsecutiveLineNumbers(previous.row.newLineNumber, next.row.newLineNumber) |
| | 365 | ); |
| | 366 | } |
| | 367 | |
| 311 | function groupLineRowsByFileId(rows: ReadonlyArray<NativeReviewDiffRow>) { | 368 | function groupLineRowsByFileId(rows: ReadonlyArray<NativeReviewDiffRow>) { |
| 312 | const rowsByFileId = new Map<string, NativeReviewDiffLineRow[]>(); | 369 | const rowsByFileId = new Map<string, NativeReviewDiffLineRow[]>(); |
| 313 | for (const row of rows) { | 370 | for (const row of rows) { |
| ... | @@ -360,7 +417,7 @@ export async function highlightNativeReviewDiffVisibleRows( | ... | @@ -360,7 +417,7 @@ export async function highlightNativeReviewDiffVisibleRows( |
| 360 | const maxRows = input.maxRows ?? NATIVE_REVIEW_DIFF_VISIBLE_MAX_ROWS; | 417 | const maxRows = input.maxRows ?? NATIVE_REVIEW_DIFF_VISIBLE_MAX_ROWS; |
| 361 | const startIndex = clampRowIndex(input.firstRowIndex - overscanRows, input.rows); | 418 | const startIndex = clampRowIndex(input.firstRowIndex - overscanRows, input.rows); |
| 362 | const endIndex = clampRowIndex(input.lastRowIndex + overscanRows, input.rows); | 419 | const endIndex = clampRowIndex(input.lastRowIndex + overscanRows, input.rows); |
| 363 | const selectedRows: NativeReviewDiffLineRow[] = []; | 420 | const selectedRows: IndexedNativeReviewDiffLineRow[] = []; |
| 364 | | 421 | |
| 365 | for ( | 422 | for ( |
| 366 | let rowIndex = startIndex; | 423 | let rowIndex = startIndex; |
| ... | @@ -374,12 +431,12 @@ export async function highlightNativeReviewDiffVisibleRows( | ... | @@ -374,12 +431,12 @@ export async function highlightNativeReviewDiffVisibleRows( |
| 374 | !input.alreadyHighlightedRowIds?.has(row.id) && | 431 | !input.alreadyHighlightedRowIds?.has(row.id) && |
| 375 | fileMap.has(row.fileId) | 432 | fileMap.has(row.fileId) |
| 376 | ) { | 433 | ) { |
| 377 | selectedRows.push(row); | 434 | selectedRows.push({ row, rowIndex }); |
| 378 | } | 435 | } |
| 379 | } | 436 | } |
| 380 | | 437 | |
| 381 | const tokensByRowId: Record<string, ReadonlyArray<NativeReviewDiffToken>> = {}; | 438 | const tokensByRowId: Record<string, ReadonlyArray<NativeReviewDiffToken>> = {}; |
| 382 | let segmentRows: NativeReviewDiffLineRow[] = []; | 439 | let segmentRows: IndexedNativeReviewDiffLineRow[] = []; |
| 383 | let segmentFile: NativeReviewDiffFile | undefined; | 440 | let segmentFile: NativeReviewDiffFile | undefined; |
| 384 | | 441 | |
| 385 | const flushSegment = () => { | 442 | const flushSegment = () => { |
| ... | @@ -389,27 +446,34 @@ export async function highlightNativeReviewDiffVisibleRows( | ... | @@ -389,27 +446,34 @@ export async function highlightNativeReviewDiffVisibleRows( |
| 389 | return; | 446 | return; |
| 390 | } | 447 | } |
| 391 | | 448 | |
| 392 | const code = segmentRows.map((row) => row.content).join("\n"); | 449 | const code = segmentRows.map(({ row }) => row.content).join("\n"); |
| 393 | const tokenLines = highlighter.tokenize(code, { lang: segmentFile.language, theme }); | 450 | const tokenLines = highlighter.tokenize(code, { lang: segmentFile.language, theme }); |
| 394 | segmentRows.forEach((row, rowIndex) => { | 451 | segmentRows.forEach(({ row }, rowIndex) => { |
| 395 | tokensByRowId[row.id] = tokenLines[rowIndex] ?? makePlainTokenFallback(row); | 452 | tokensByRowId[row.id] = tokenLines[rowIndex] ?? makePlainTokenFallback(row); |
| 396 | }); | 453 | }); |
| 397 | segmentRows = []; | 454 | segmentRows = []; |
| 398 | segmentFile = undefined; | 455 | segmentFile = undefined; |
| 399 | }; | 456 | }; |
| 400 | | 457 | |
| 401 | for (const row of selectedRows) { | 458 | for (const selectedRow of selectedRows) { |
| | 459 | const { row } = selectedRow; |
| 402 | const file = fileMap.get(row.fileId); | 460 | const file = fileMap.get(row.fileId); |
| 403 | if (!file) { | 461 | if (!file) { |
| 404 | continue; | 462 | continue; |
| 405 | } | 463 | } |
| 406 | | 464 | |
| 407 | if (segmentFile && segmentFile.id !== file.id) { | 465 | const previousRow = segmentRows.at(-1); |
| | 466 | if ( |
| | 467 | segmentFile && |
| | 468 | (segmentFile.id !== file.id || |
| | 469 | (previousRow !== undefined && |
| | 470 | !canShareGrammarContext(previousRow, selectedRow, input.rows))) |
| | 471 | ) { |
| 408 | flushSegment(); | 472 | flushSegment(); |
| 409 | } | 473 | } |
| 410 | | 474 | |
| 411 | segmentFile = file; | 475 | segmentFile = file; |
| 412 | segmentRows.push(row); | 476 | segmentRows.push(selectedRow); |
| 413 | } | 477 | } |
| 414 | flushSegment(); | 478 | flushSegment(); |
| 415 | | 479 | |