| ... | @@ -1,4 +1,4 @@ | ... | @@ -1,4 +1,4 @@ |
| 1 | import { createHighlighterCore, type HighlighterCore } from "@shikijs/core"; | 1 | import { createHighlighterCore, type GrammarState, type HighlighterCore } from "@shikijs/core"; |
| 2 | import { createJavaScriptRegexEngine } from "@shikijs/engine-javascript"; | 2 | import { createJavaScriptRegexEngine } from "@shikijs/engine-javascript"; |
| 3 | import bashLanguage from "@shikijs/langs/bash"; | 3 | import bashLanguage from "@shikijs/langs/bash"; |
| 4 | import diffLanguage from "@shikijs/langs/diff"; | 4 | import diffLanguage from "@shikijs/langs/diff"; |
| ... | @@ -46,8 +46,12 @@ export interface NativeReviewDiffHighlighterHandle { | ... | @@ -46,8 +46,12 @@ export interface NativeReviewDiffHighlighterHandle { |
| 46 | readonly engine: NativeReviewDiffHighlightEngine; | 46 | readonly engine: NativeReviewDiffHighlightEngine; |
| 47 | readonly tokenize: ( | 47 | readonly tokenize: ( |
| 48 | code: string, | 48 | code: string, |
| 49 | options: { readonly lang: NativeReviewDiffLanguage; readonly theme: string }, | 49 | options: { |
| 50 | ) => ReadonlyArray<ReadonlyArray<NativeReviewDiffToken>>; | 50 | readonly lang: NativeReviewDiffLanguage; |
| | 51 | readonly theme: string; |
| | 52 | readonly signal?: AbortSignal; |
| | 53 | }, |
| | 54 | ) => Promise<ReadonlyArray<ReadonlyArray<NativeReviewDiffToken>>>; |
| 51 | } | 55 | } |
| 52 | | 56 | |
| 53 | interface NativeReviewDiffLineRow extends NativeReviewDiffRow { | 57 | interface NativeReviewDiffLineRow extends NativeReviewDiffRow { |
| ... | @@ -97,6 +101,8 @@ export interface HighlightNativeReviewDiffVisibleRowsInput { | ... | @@ -97,6 +101,8 @@ export interface HighlightNativeReviewDiffVisibleRowsInput { |
| 97 | const NATIVE_REVIEW_DIFF_HIGHLIGHT_CHUNK_SIZE = 500; | 101 | const NATIVE_REVIEW_DIFF_HIGHLIGHT_CHUNK_SIZE = 500; |
| 98 | const NATIVE_REVIEW_DIFF_VISIBLE_OVERSCAN_ROWS = 160; | 102 | const NATIVE_REVIEW_DIFF_VISIBLE_OVERSCAN_ROWS = 160; |
| 99 | const NATIVE_REVIEW_DIFF_VISIBLE_MAX_ROWS = 360; | 103 | const NATIVE_REVIEW_DIFF_VISIBLE_MAX_ROWS = 360; |
| | 104 | const NATIVE_REVIEW_DIFF_TOKENIZE_MAX_LINE_LENGTH = 1_000; |
| | 105 | const NATIVE_REVIEW_DIFF_TOKENIZE_MAX_CHARACTERS = 8_000; |
| 100 | | 106 | |
| 101 | const NATIVE_REVIEW_DIFF_THEME_NAME_BY_SCHEME = { | 107 | const NATIVE_REVIEW_DIFF_THEME_NAME_BY_SCHEME = { |
| 102 | dark: "t3-pierre-dark", | 108 | dark: "t3-pierre-dark", |
| ... | @@ -226,6 +232,63 @@ function normalizeTokens( | ... | @@ -226,6 +232,63 @@ function normalizeTokens( |
| 226 | ); | 232 | ); |
| 227 | } | 233 | } |
| 228 | | 234 | |
| | 235 | function createHighlighterHandle( |
| | 236 | highlighter: HighlighterCore, |
| | 237 | engine: NativeReviewDiffHighlightEngine, |
| | 238 | ): NativeReviewDiffHighlighterHandle { |
| | 239 | return { |
| | 240 | engine, |
| | 241 | async tokenize(code, { lang, theme, signal }) { |
| | 242 | const lines = code.split("\n"); |
| | 243 | const highlighted: Array<ReadonlyArray<NativeReviewDiffToken>> = []; |
| | 244 | let grammarState: GrammarState | undefined; |
| | 245 | let start = 0; |
| | 246 | |
| | 247 | while (start < lines.length) { |
| | 248 | if (signal?.aborted) return []; |
| | 249 | |
| | 250 | // Skipping this line leaves its ending grammar state unknown. Keep the |
| | 251 | // rest of this contiguous segment plain instead of guessing its syntax. |
| | 252 | if (lines[start]!.length > NATIVE_REVIEW_DIFF_TOKENIZE_MAX_LINE_LENGTH) { |
| | 253 | highlighted.push( |
| | 254 | ...lines |
| | 255 | .slice(start) |
| | 256 | .map((content) => [{ content: content || " ", color: null, fontStyle: null }]), |
| | 257 | ); |
| | 258 | break; |
| | 259 | } |
| | 260 | |
| | 261 | let end = start; |
| | 262 | let characters = 0; |
| | 263 | while (end < lines.length) { |
| | 264 | const length = lines[end]!.length; |
| | 265 | const nextCharacters = characters + length + (end > start ? 1 : 0); |
| | 266 | if ( |
| | 267 | length > NATIVE_REVIEW_DIFF_TOKENIZE_MAX_LINE_LENGTH || |
| | 268 | nextCharacters > NATIVE_REVIEW_DIFF_TOKENIZE_MAX_CHARACTERS |
| | 269 | ) { |
| | 270 | break; |
| | 271 | } |
| | 272 | characters = nextCharacters; |
| | 273 | end += 1; |
| | 274 | } |
| | 275 | |
| | 276 | const tokens = highlighter.codeToTokensBase(lines.slice(start, end).join("\n"), { |
| | 277 | lang, |
| | 278 | theme, |
| | 279 | grammarState, |
| | 280 | }); |
| | 281 | grammarState = highlighter.getLastGrammarState(tokens); |
| | 282 | highlighted.push(...normalizeTokens(tokens)); |
| | 283 | start = end; |
| | 284 | if (start < lines.length) await waitForNextFrame(); |
| | 285 | } |
| | 286 | |
| | 287 | return signal?.aborted ? [] : highlighted; |
| | 288 | }, |
| | 289 | }; |
| | 290 | } |
| | 291 | |
| 229 | async function createNativeReviewDiffHighlighter(): Promise<NativeReviewDiffHighlighterHandle> { | 292 | async function createNativeReviewDiffHighlighter(): Promise<NativeReviewDiffHighlighterHandle> { |
| 230 | const nativeEngineModule = await import("react-native-shiki-engine"); | 293 | const nativeEngineModule = await import("react-native-shiki-engine"); |
| 231 | if (!nativeEngineModule.isNativeEngineAvailable()) { | 294 | if (!nativeEngineModule.isNativeEngineAvailable()) { |
| ... | @@ -238,10 +301,7 @@ async function createNativeReviewDiffHighlighter(): Promise<NativeReviewDiffHigh | ... | @@ -238,10 +301,7 @@ async function createNativeReviewDiffHighlighter(): Promise<NativeReviewDiffHigh |
| 238 | engine: nativeEngineModule.createNativeEngine(), | 301 | engine: nativeEngineModule.createNativeEngine(), |
| 239 | }); | 302 | }); |
| 240 | | 303 | |
| 241 | return { | 304 | return createHighlighterHandle(highlighter, "native"); |
| 242 | engine: "native", | | |
| 243 | tokenize: (code, options) => normalizeTokens(highlighter.codeToTokensBase(code, options)), | | |
| 244 | }; | | |
| 245 | } | 305 | } |
| 246 | | 306 | |
| 247 | async function createJavascriptReviewDiffHighlighter(): Promise<NativeReviewDiffHighlighterHandle> { | 307 | async function createJavascriptReviewDiffHighlighter(): Promise<NativeReviewDiffHighlighterHandle> { |
| ... | @@ -251,10 +311,7 @@ async function createJavascriptReviewDiffHighlighter(): Promise<NativeReviewDiff | ... | @@ -251,10 +311,7 @@ async function createJavascriptReviewDiffHighlighter(): Promise<NativeReviewDiff |
| 251 | engine: createJavaScriptRegexEngine(), | 311 | engine: createJavaScriptRegexEngine(), |
| 252 | }); | 312 | }); |
| 253 | | 313 | |
| 254 | return { | 314 | return createHighlighterHandle(highlighter, "javascript"); |
| 255 | engine: "javascript", | | |
| 256 | tokenize: (code, options) => normalizeTokens(highlighter.codeToTokensBase(code, options)), | | |
| 257 | }; | | |
| 258 | } | 315 | } |
| 259 | | 316 | |
| 260 | export async function getNativeReviewDiffHighlighter( | 317 | export async function getNativeReviewDiffHighlighter( |
| ... | @@ -438,8 +495,9 @@ export async function highlightNativeReviewDiffVisibleRows( | ... | @@ -438,8 +495,9 @@ export async function highlightNativeReviewDiffVisibleRows( |
| 438 | const tokensByRowId: Record<string, ReadonlyArray<NativeReviewDiffToken>> = {}; | 495 | const tokensByRowId: Record<string, ReadonlyArray<NativeReviewDiffToken>> = {}; |
| 439 | let segmentRows: IndexedNativeReviewDiffLineRow[] = []; | 496 | let segmentRows: IndexedNativeReviewDiffLineRow[] = []; |
| 440 | let segmentFile: NativeReviewDiffFile | undefined; | 497 | let segmentFile: NativeReviewDiffFile | undefined; |
| | 498 | let charactersSinceYield = 0; |
| 441 | | 499 | |
| 442 | const flushSegment = () => { | 500 | const flushSegment = async () => { |
| 443 | if (!segmentFile || segmentRows.length === 0 || input.signal?.aborted) { | 501 | if (!segmentFile || segmentRows.length === 0 || input.signal?.aborted) { |
| 444 | segmentRows = []; | 502 | segmentRows = []; |
| 445 | segmentFile = undefined; | 503 | segmentFile = undefined; |
| ... | @@ -447,7 +505,20 @@ export async function highlightNativeReviewDiffVisibleRows( | ... | @@ -447,7 +505,20 @@ export async function highlightNativeReviewDiffVisibleRows( |
| 447 | } | 505 | } |
| 448 | | 506 | |
| 449 | const code = segmentRows.map(({ row }) => row.content).join("\n"); | 507 | const code = segmentRows.map(({ row }) => row.content).join("\n"); |
| 450 | const tokenLines = highlighter.tokenize(code, { lang: segmentFile.language, theme }); | 508 | if ( |
| | 509 | charactersSinceYield > 0 && |
| | 510 | charactersSinceYield + code.length > NATIVE_REVIEW_DIFF_TOKENIZE_MAX_CHARACTERS |
| | 511 | ) { |
| | 512 | await waitForNextFrame(); |
| | 513 | charactersSinceYield = 0; |
| | 514 | if (input.signal?.aborted) return; |
| | 515 | } |
| | 516 | const tokenLines = await highlighter.tokenize(code, { |
| | 517 | lang: segmentFile.language, |
| | 518 | theme, |
| | 519 | signal: input.signal, |
| | 520 | }); |
| | 521 | charactersSinceYield += code.length; |
| 451 | segmentRows.forEach(({ row }, rowIndex) => { | 522 | segmentRows.forEach(({ row }, rowIndex) => { |
| 452 | tokensByRowId[row.id] = tokenLines[rowIndex] ?? makePlainTokenFallback(row); | 523 | tokensByRowId[row.id] = tokenLines[rowIndex] ?? makePlainTokenFallback(row); |
| 453 | }); | 524 | }); |
| ... | @@ -456,6 +527,7 @@ export async function highlightNativeReviewDiffVisibleRows( | ... | @@ -456,6 +527,7 @@ export async function highlightNativeReviewDiffVisibleRows( |
| 456 | }; | 527 | }; |
| 457 | | 528 | |
| 458 | for (const selectedRow of selectedRows) { | 529 | for (const selectedRow of selectedRows) { |
| | 530 | if (input.signal?.aborted) break; |
| 459 | const { row } = selectedRow; | 531 | const { row } = selectedRow; |
| 460 | const file = fileMap.get(row.fileId); | 532 | const file = fileMap.get(row.fileId); |
| 461 | if (!file) { | 533 | if (!file) { |
| ... | @@ -469,13 +541,17 @@ export async function highlightNativeReviewDiffVisibleRows( | ... | @@ -469,13 +541,17 @@ export async function highlightNativeReviewDiffVisibleRows( |
| 469 | (previousRow !== undefined && | 541 | (previousRow !== undefined && |
| 470 | !canShareGrammarContext(previousRow, selectedRow, input.rows))) | 542 | !canShareGrammarContext(previousRow, selectedRow, input.rows))) |
| 471 | ) { | 543 | ) { |
| 472 | flushSegment(); | 544 | await flushSegment(); |
| 473 | } | 545 | } |
| 474 | | 546 | |
| 475 | segmentFile = file; | 547 | segmentFile = file; |
| 476 | segmentRows.push(selectedRow); | 548 | segmentRows.push(selectedRow); |
| 477 | } | 549 | } |
| 478 | flushSegment(); | 550 | await flushSegment(); |
| | 551 | |
| | 552 | if (input.signal?.aborted) { |
| | 553 | return { engine: highlighter.engine, tokensByRowId: {}, rowCount: 0, durationMs: 0 }; |
| | 554 | } |
| 479 | | 555 | |
| 480 | return { | 556 | return { |
| 481 | engine: highlighter.engine, | 557 | engine: highlighter.engine, |
| ... | @@ -504,7 +580,12 @@ export async function streamNativeReviewDiffTokens( | ... | @@ -504,7 +580,12 @@ export async function streamNativeReviewDiffTokens( |
| 504 | const startedAt = performance.now(); | 580 | const startedAt = performance.now(); |
| 505 | const chunkRows = fileRows.slice(startIndex, startIndex + chunkSize); | 581 | const chunkRows = fileRows.slice(startIndex, startIndex + chunkSize); |
| 506 | const code = chunkRows.map((row) => row.content).join("\n"); | 582 | const code = chunkRows.map((row) => row.content).join("\n"); |
| 507 | const tokenLines = highlighter.tokenize(code, { lang: file.language, theme }); | 583 | const tokenLines = await highlighter.tokenize(code, { |
| | 584 | lang: file.language, |
| | 585 | theme, |
| | 586 | signal: input.signal, |
| | 587 | }); |
| | 588 | if (input.signal?.aborted) return highlighter.engine; |
| 508 | const tokensByRowId: Record<string, ReadonlyArray<NativeReviewDiffToken>> = {}; | 589 | const tokensByRowId: Record<string, ReadonlyArray<NativeReviewDiffToken>> = {}; |
| 509 | | 590 | |
| 510 | chunkRows.forEach((row, rowIndex) => { | 591 | chunkRows.forEach((row, rowIndex) => { |