authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-05 00:15:36-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-05 00:15:36-07:00
log50027bb3ad2912f9127304392a2c85ad5ab9a4ba
tree4cf4b0444d0a6ff7cb23a276fee7469f99e0c6ce
parent3e1333319aacff96856a3c161b8787ff6b0ddd6b
signature Signed by PGP key B5690EEEBB952194

fix(mobile): keep highlighting review diffs after a long line (#9911)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

2 files changed, 38 insertions(+), 15 deletions(-)

apps/mobile/src/features/diffs/nativeReviewDiffHighlighter.test.ts+29-7
......@@ -257,13 +257,13 @@ describe.each(["native", "javascript"] as const)("%s highlighting budgets", (eng
257257 expect(result.tokensByRowId["line-1"]?.some((token) => token.color !== null)).toBe(true);
258258 });
259259
260 it("keeps long lines and unknown following syntax plain until the next hunk", async () => {
260 it("keeps only the long line plain and resumes highlighting after it", async () => {
261261 const longLine = `${"x".repeat(1_001)} /*`;
262262 const rows = [
263263 line(1, "export const before = 1;"),
264264 line(2, longLine),
265265 { kind: "comment", id: "note", commentText: "Check this", fileId: TYPESCRIPT_FILE.id },
266 line(3, "inside the comment */"),
266 line(3, "export const inside = 'x';"),
267267 makeHunk("next-hunk"),
268268 line(100, "export const after = 2;"),
269269 ] satisfies ReadonlyArray<NativeReviewDiffRow>;
......@@ -274,14 +274,36 @@ describe.each(["native", "javascript"] as const)("%s highlighting budgets", (eng
274274 expect(result.tokensByRowId["line-2"]).toEqual([
275275 { content: longLine, color: null, fontStyle: null },
276276 ]);
277 expect(result.tokensByRowId["line-3"]).toEqual([
278 { content: "inside the comment */", color: null, fontStyle: null },
279 ]);
280 expect(result.tokensByRowId["line-1"]?.some((token) => token.color !== null)).toBe(true);
281 expect(result.tokensByRowId["line-100"]?.some((token) => token.color !== null)).toBe(true);
277 for (const id of ["line-1", "line-3", "line-100"]) {
278 expect(result.tokensByRowId[id]?.some((token) => token.color !== null)).toBe(true);
279 }
282280 expect(tokenization.calls.some((code) => code.includes(longLine))).toBe(false);
283281 });
284282
283 it("highlights the rows after a long line the same regardless of the first window", async () => {
284 const rows = [
285 line(1, "export const before = 1;"),
286 line(2, `const data = "${"x".repeat(1_050)}";`),
287 line(3, "export const inside = 'x';"),
288 line(4, "export const after = 2;"),
289 ];
290 const spanning = await highlightRows(rows);
291 const afterLongLine = await highlightNativeReviewDiffVisibleRows({
292 rows,
293 files: [TYPESCRIPT_FILE],
294 scheme: "dark",
295 engine,
296 firstRowIndex: 2,
297 lastRowIndex: 3,
298 overscanRows: 0,
299 });
300
301 for (const id of ["line-3", "line-4"]) {
302 expect(spanning.tokensByRowId[id]?.some((token) => token.color !== null)).toBe(true);
303 expect(afterLongLine.tokensByRowId[id]).toEqual(spanning.tokensByRowId[id]);
304 }
305 });
306
285307 it("preserves multiline grammar and row mapping across character-limited batches", async () => {
286308 const opening = line(1, "const message = `open");
287309 const body = Array.from({ length: 40 }, (_, index) => line(index + 2, "inside ".repeat(45)));
apps/mobile/src/features/diffs/nativeReviewDiffHighlighter.ts+9-8
......@@ -247,15 +247,16 @@ function createHighlighterHandle(
247247 while (start < lines.length) {
248248 if (signal?.aborted) return [];
249249
250 // Skipping this line leaves its ending grammar state unknown. Keep the
251 // rest of this contiguous segment plain instead of guessing its syntax.
250 // Skipping this line leaves its ending grammar state unknown. Resume
251 // from a fresh state rather than leaving the rest of the segment plain:
252 // highlighted rows are cached for the sheet's lifetime, so a plain tail
253 // would stick, and which rows it covered would depend on where the
254 // first visible window happened to start.
252255 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;
256 highlighted.push([{ content: lines[start] || " ", color: null, fontStyle: null }]);
257 grammarState = undefined;
258 start += 1;
259 continue;
259260 }
260261
261262 let end = start;