Add support for asymmetric skip chunks in GrDiff

Instead of a single number, `skip` chunks can now alternatively contain
an object with explicit skip lengths for the left and right side of the
diff.

This allows supporting diffs where a different number of lines were
skipped in the left and right file which allows for more optimized
truncation.

A new utility function `normalizeSkipInfo` is introduced to easily
retrieve skip lengths for a given side, regardless of whether the
symmetric or asymmetric representation is used.

go/grdiff-asymmetric-skip-chunks

PiperOrigin-RevId: 828376161
Release-Notes: skip
Change-Id: Ibef576f8fc19b0b27f8301263db065bbdd0ad1e2
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index e5d0af7..474ba56 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -122,6 +122,13 @@
   | 'COPIED'
   | 'REWRITE';
 
+export declare type SkipObject = {
+  left: number;
+  right: number;
+};
+
+export declare type SkipInfo = number | SkipObject;
+
 /**
  * The DiffContent entity contains information about the content differences in
  * a file.
@@ -158,7 +165,7 @@
    * Count of lines skipped on both sides when the file is too large to include
    * all common lines.
    */
-  skip?: number;
+  skip?: SkipInfo;
   /**
    * Set to true if the region is common according to the requested
    * ignore-whitespace parameter, but a and b contain differing amounts of
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
index 89bb49e..4e9e225 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
@@ -11,6 +11,7 @@
 } from '../gr-diff/gr-diff-group';
 import {DiffContent, DiffRangesToFocus} from '../../../types/diff';
 import {Side} from '../../../constants/constants';
+import {normalizeSkipInfo} from '../../../utils/diff-util';
 import {getStringLength} from '../gr-diff-highlight/gr-annotation';
 import {GrDiffLineType, LineNumber} from '../../../api/diff';
 import {FULL_CONTEXT, KeyLocations} from '../gr-diff/gr-diff-utils';
@@ -310,7 +311,9 @@
   }
 
   private chunkLength(chunk: DiffContent, side: Side) {
-    if (chunk.skip || chunk.common || chunk.ab) {
+    if (chunk.skip) {
+      return normalizeSkipInfo(chunk.skip)[side];
+    } else if (chunk.common || chunk.ab) {
       return this.commonChunkLength(chunk);
     } else if (side === Side.LEFT) {
       return this.linesLeft(chunk).length;
@@ -320,9 +323,6 @@
   }
 
   private commonChunkLength(chunk: DiffContent) {
-    if (chunk.skip) {
-      return chunk.skip;
-    }
     console.assert(!!chunk.ab || !!chunk.common);
 
     console.assert(
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
index ccd208c..38c8d1c 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
@@ -4,9 +4,16 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {BLANK_LINE, GrDiffLine} from './gr-diff-line';
-import {GrDiffLineType, LineNumber, LineRange, Side} from '../../../api/diff';
+import {
+  GrDiffLineType,
+  LineNumber,
+  LineRange,
+  Side,
+  SkipInfo,
+} from '../../../api/diff';
 import {assert, assertIsDefined} from '../../../utils/common-util';
 import {isDefined} from '../../../types/types';
+import {normalizeSkipInfo} from '../../../utils/diff-util';
 
 export enum GrDiffGroupType {
   /** A group of unchanged diff lines. */
@@ -270,7 +277,7 @@
       | {
           type: GrDiffGroupType.BOTH | GrDiffGroupType.DELTA;
           lines?: undefined;
-          skip: number;
+          skip: SkipInfo;
           offsetLeft: number;
           offsetRight: number;
           moveDetails?: GrMoveDetails;
@@ -296,14 +303,15 @@
         }
         this.skip = options.skip;
         if (options.skip !== undefined) {
+          const skip = normalizeSkipInfo(options.skip);
           this.lineRange = {
             left: {
               start_line: options.offsetLeft,
-              end_line: options.offsetLeft + options.skip - 1,
+              end_line: options.offsetLeft + skip.left - 1,
             },
             right: {
               start_line: options.offsetRight,
-              end_line: options.offsetRight + options.skip - 1,
+              end_line: options.offsetRight + skip.right - 1,
             },
           };
         } else {
@@ -368,7 +376,7 @@
    */
   readonly contextGroups: GrDiffGroup[] = [];
 
-  readonly skip?: number;
+  readonly skip?: SkipInfo;
 
   /** Both start and end line are inclusive. */
   readonly lineRange: {[side in Side]: LineRange} = {
diff --git a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
index 9e3e087..449a252 100644
--- a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
+++ b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
@@ -15,6 +15,7 @@
 import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
 import {GrDiffLineType} from '../../../api/diff';
 import {assert} from '../../../utils/common-util';
+import {normalizeSkipInfo} from '../../../utils/diff-util';
 
 const LANGUAGE_MAP = new Map<string, string>([
   ['application/dart', 'dart'],
@@ -271,10 +272,10 @@
       for (const line of b) {
         rightContent += line + '\n';
       }
-      const skip = chunk.skip ?? 0;
-      if (skip > 0) {
-        leftContent += '\n'.repeat(skip);
-        rightContent += '\n'.repeat(skip);
+      if (chunk.skip) {
+        const skip = normalizeSkipInfo(chunk.skip);
+        leftContent += '\n'.repeat(skip.left);
+        rightContent += '\n'.repeat(skip.right);
       }
     }
     leftContent = leftContent.trimEnd();
diff --git a/polygerrit-ui/app/types/diff.ts b/polygerrit-ui/app/types/diff.ts
index 47a96e4..b3880fb 100644
--- a/polygerrit-ui/app/types/diff.ts
+++ b/polygerrit-ui/app/types/diff.ts
@@ -22,7 +22,9 @@
   IgnoreWhitespaceType,
   MarkLength,
   MoveDetails,
+  SkipInfo,
   SkipLength,
+  SkipObject,
 } from '../api/diff';
 
 export type {
@@ -34,6 +36,8 @@
   MarkLength,
   MoveDetails,
   SkipLength,
+  SkipInfo,
+  SkipObject,
   WebLinkInfo,
 };
 
diff --git a/polygerrit-ui/app/utils/diff-util.ts b/polygerrit-ui/app/utils/diff-util.ts
index f6b00d4..b6b51a0 100644
--- a/polygerrit-ui/app/utils/diff-util.ts
+++ b/polygerrit-ui/app/utils/diff-util.ts
@@ -4,7 +4,12 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {Side} from '../constants/constants';
-import {DiffInfo} from '../types/diff';
+import {DiffInfo, SkipInfo, SkipObject} from '../types/diff';
+
+export function normalizeSkipInfo(skip: SkipInfo | undefined): SkipObject {
+  if (!skip) return {left: 0, right: 0};
+  return typeof skip === 'number' ? {left: skip, right: skip} : skip;
+}
 
 export function otherSide(side: Side) {
   return side === Side.LEFT ? Side.RIGHT : Side.LEFT;
@@ -14,7 +19,12 @@
   if (!diff?.content || !side) return 0;
   return diff.content.reduce((sum, chunk) => {
     const sideChunk = side === Side.LEFT ? chunk.a : chunk.b;
-    return sum + (sideChunk?.length ?? chunk.ab?.length ?? chunk.skip ?? 0);
+    return (
+      sum +
+      (sideChunk?.length ??
+        chunk.ab?.length ??
+        normalizeSkipInfo(chunk.skip)[side])
+    );
   }, 0);
 }
 
@@ -27,7 +37,7 @@
   let currentLine = 0;
   for (const chunk of diff.content) {
     if (chunk.skip) {
-      currentLine += chunk.skip;
+      currentLine += normalizeSkipInfo(chunk.skip)[side];
       if (currentLine >= line) return false;
     } else if (chunk.ab) {
       currentLine += chunk.ab.length;
@@ -51,7 +61,8 @@
   let lines: string[] = [];
   for (const chunk of diff.content) {
     if (chunk.skip) {
-      lines = lines.concat(Array(chunk.skip).fill(''));
+      const skip = normalizeSkipInfo(chunk.skip);
+      lines = lines.concat(Array(skip[side]).fill(''));
     } else if (chunk.ab) {
       lines = lines.concat(chunk.ab);
     } else if (side === Side.LEFT && chunk.a) {