Merge "Listen to token highlights"
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index 26701e8..1453fd0 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -209,6 +209,18 @@
   line_wrapping?: boolean;
 }
 
+/**
+ * Listens to changes in token highlighting - when a new token starts or stopped being highlighted.
+ * Examples:
+ * - Token highlighted: ('myFunctionName', 12, [Element]).
+ * - Token unhighlighted: (undefined, 0, undefined).
+ */
+export type TokenHighlightedListener = (
+  newHighlight: string | undefined,
+  newLineNumber: number,
+  hoveredElement?: Element
+) => void;
+
 export declare interface ImageDiffPreferences {
   automatic_blink?: boolean;
 }
diff --git a/polygerrit-ui/app/api/embed.ts b/polygerrit-ui/app/api/embed.ts
index ba378e2..fed724e 100644
--- a/polygerrit-ui/app/api/embed.ts
+++ b/polygerrit-ui/app/api/embed.ts
@@ -20,14 +20,24 @@
  * limitations under the License.
  */
 
-import {DiffLayer, GrAnnotation, GrDiffCursor} from './diff';
+import {
+  DiffLayer,
+  GrAnnotation,
+  GrDiffCursor,
+  TokenHighlightedListener,
+} from './diff';
 
 declare global {
   interface Window {
     grdiff: {
       GrAnnotation: GrAnnotation;
       GrDiffCursor: {new (): GrDiffCursor};
-      TokenHighlightLayer: {new (container?: HTMLElement): DiffLayer};
+      TokenHighlightLayer: {
+        new (
+          container?: HTMLElement,
+          listener?: TokenHighlightedListener
+        ): DiffLayer;
+      };
     };
   }
 }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
index 56bb073..480e26c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 import {DiffLayer, DiffLayerListener} from '../../../types/types';
-import {GrDiffLine, Side} from '../../../api/diff';
+import {GrDiffLine, Side, TokenHighlightedListener} from '../../../api/diff';
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
 import {debounce, DelayedTask} from '../../../utils/async-util';
 import {
@@ -65,6 +65,9 @@
   /** The currently highlighted token. */
   private currentHighlight?: string;
 
+  /** Trigger when a new token starts or stoped being highlighted.*/
+  private readonly tokenHighlightedListener?: TokenHighlightedListener;
+
   /**
    * The line of the currently highlighted token. We store this in order to
    * re-render only relevant lines of the diff. Only lines visible on the screen
@@ -95,7 +98,11 @@
 
   private updateTokenTask?: DelayedTask;
 
-  constructor(container: HTMLElement = document.documentElement) {
+  constructor(
+    container: HTMLElement = document.documentElement,
+    tokenHighlightedListener?: TokenHighlightedListener
+  ) {
+    this.tokenHighlightedListener = tokenHighlightedListener;
     container.addEventListener('click', e => {
       this.handleContainerClick(e);
     });
@@ -188,7 +195,7 @@
     this.updateTokenTask = debounce(
       this.updateTokenTask,
       () => {
-        this.updateTokenHighlight(newHighlight, line);
+        this.updateTokenHighlight(newHighlight, line, element);
       },
       HOVER_DELAY_MS
     );
@@ -203,7 +210,7 @@
     if (element) return;
     this.hoveredElement = undefined;
     this.updateTokenTask?.cancel();
-    this.updateTokenHighlight(undefined, 0);
+    this.updateTokenHighlight(undefined, 0, undefined);
   }
 
   private interferesWithSelection() {
@@ -241,7 +248,8 @@
 
   private updateTokenHighlight(
     newHighlight: string | undefined,
-    newLineNumber: number
+    newLineNumber: number,
+    newHoveredElement: Element | undefined
   ) {
     if (
       this.currentHighlight === newHighlight &&
@@ -253,6 +261,13 @@
     this.currentHighlight = newHighlight;
     this.currentHighlightLineNumber = newLineNumber;
 
+    if (this.tokenHighlightedListener) {
+      this.tokenHighlightedListener(
+        newHighlight,
+        newLineNumber,
+        newHoveredElement
+      );
+    }
     this.notifyForToken(oldHighlight, oldLineNumber);
     this.notifyForToken(newHighlight, newLineNumber);
   }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
index 9fc69b5..2cb08d6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
@@ -66,12 +66,22 @@
   let container: HTMLElement;
   let listener: MockListener;
   let highlighter: TokenHighlightLayer;
+  let tokenHighlightingCalls: any[] = [];
+
+  function tokenHighlightedListener(
+    newHighlight: string | undefined,
+    newLineNumber: number,
+    hoveredElement?: Element
+  ) {
+    tokenHighlightingCalls.push({newHighlight, newLineNumber, hoveredElement});
+  }
 
   setup(async () => {
     listener = new MockListener();
+    tokenHighlightingCalls = [];
     container = document.createElement('div');
     document.body.appendChild(container);
-    highlighter = new TokenHighlightLayer(container);
+    highlighter = new TokenHighlightLayer(container, tokenHighlightedListener);
     highlighter.addListener((...args) => listener.notify(...args));
   });
 
@@ -251,6 +261,37 @@
       assert.equal(_testOnly_allTasks.size, 0);
     });
 
+    test('triggers listener for applying and clearing highlighting', async () => {
+      const clock = sinon.useFakeTimers();
+      const line1 = createLine('two words');
+      annotate(line1);
+      const line2 = createLine('three words', 2);
+      annotate(line2, Side.RIGHT, 2);
+      const words1 = queryAndAssert(line1, '.tk-words');
+      assert.isTrue(words1.classList.contains('token'));
+      dispatchMouseEvent(
+        'mouseover',
+        MockInteractions.middleOfNode(words1),
+        words1
+      );
+      assert.equal(tokenHighlightingCalls.length, 0);
+      clock.tick(HOVER_DELAY_MS);
+      assert.equal(tokenHighlightingCalls.length, 1);
+      assert.deepEqual(tokenHighlightingCalls[0], {
+        newHighlight: 'words',
+        newLineNumber: 1,
+        hoveredElement: words1,
+      });
+
+      MockInteractions.click(container);
+      assert.equal(tokenHighlightingCalls.length, 2);
+      assert.deepEqual(tokenHighlightingCalls[1], {
+        newHighlight: undefined,
+        newLineNumber: 0,
+        hoveredElement: undefined,
+      });
+    });
+
     test('clicking clears highlight', async () => {
       const clock = sinon.useFakeTimers();
       const line1 = createLine('two words');