From 8c19ce8ef61fee6316e05a21ff8eaffda3409d67 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Mon, 13 Apr 2026 09:08:40 -0700 Subject: [PATCH 1/4] Fix stale OSC 8 link hover tooltip when mouse cursor unhovers before trigger --- .../links/browser/terminalLinkManager.ts | 16 ++++++---- .../test/browser/terminalLinkManager.test.ts | 30 +++++++++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts index fb1496acdff50f..77a60c44c7bc4f 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts @@ -98,11 +98,16 @@ export class TerminalLinkManager extends DisposableStore { let activeHoverDisposable: IDisposable | undefined; let activeTooltipScheduler: RunOnceScheduler | undefined; + const clearActiveLinkHover = () => { + activeHoverDisposable?.dispose(); + activeHoverDisposable = undefined; + activeTooltipScheduler?.dispose(); + activeTooltipScheduler = undefined; + }; this.add(toDisposable(() => { this._clearLinkProviders(); dispose(this._externalLinkProviders); - activeHoverDisposable?.dispose(); - activeTooltipScheduler?.dispose(); + clearActiveLinkHover(); })); this._xterm.options.linkHandler = { allowNonHttpProtocols: true, @@ -146,9 +151,7 @@ export class TerminalLinkManager extends DisposableStore { }); }, hover: (e, text, range) => { - activeHoverDisposable?.dispose(); - activeHoverDisposable = undefined; - activeTooltipScheduler?.dispose(); + clearActiveLinkHover(); activeTooltipScheduler = new RunOnceScheduler(() => { interface XtermWithCore extends Terminal { _core: IXtermCore; @@ -172,6 +175,9 @@ export class TerminalLinkManager extends DisposableStore { activeTooltipScheduler = undefined; }, this._configurationService.getValue('workbench.hover.delay')); activeTooltipScheduler.schedule(); + }, + leave: () => { + clearActiveLinkHover(); } }; } diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkManager.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkManager.test.ts index e4aa727cb6819f..f55fa9eb6c4acd 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkManager.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkManager.test.ts @@ -26,6 +26,7 @@ import { TerminalLinkResolver } from '../../browser/terminalLinkResolver.js'; import { importAMDNodeModule } from '../../../../../../amdX.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { TestXtermLogger } from '../../../../../../platform/terminal/test/common/terminalTestHelpers.js'; +import { timeout } from '../../../../../../base/common/async.js'; const defaultTerminalConfig: Partial = { fontFamily: 'monospace', @@ -109,6 +110,35 @@ suite('TerminalLinkManager', () => { }); }); + suite('OSC 8 hover', () => { + test('should cancel delayed tooltip when leave happens before hover delay', async () => { + await configurationService.setUserConfiguration('workbench.hover.delay', 10); + const linkHandler = xterm.options.linkHandler; + if (!linkHandler?.hover || !linkHandler.leave) { + throw new Error('Expected linkHandler with hover/leave callbacks'); + } + let hoverShownCount = 0; + // eslint-disable-next-line @typescript-eslint/naming-convention + type TestableLinkManager = { _showHover: () => undefined }; + const testableLinkManager = linkManager as unknown as TestableLinkManager; + const originalShowHover = testableLinkManager._showHover; + testableLinkManager._showHover = () => { + hoverShownCount++; + return undefined; + }; + const range: Parameters[2] = { start: { x: 1, y: 1 }, end: { x: 10, y: 1 } }; + const event = new MouseEvent('mousemove'); + try { + linkHandler.hover(event, 'http://example.com', range); + linkHandler.leave(event, 'http://example.com', range); + await timeout(20); + strictEqual(hoverShownCount, 0); + } finally { + testableLinkManager._showHover = originalShowHover; + } + }); + }); + suite('getLinks and open recent link', () => { test('should return no links', async () => { const links = await linkManager.getLinks(); From f980d5b79be67c3197eda702643b31a93245604b Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Mon, 13 Apr 2026 09:19:36 -0700 Subject: [PATCH 2/4] Dismiss hover tooltip on leave/scroll --- .../links/browser/terminalLinkManager.ts | 15 ++++++- .../test/browser/terminalLinkManager.test.ts | 41 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts index 77a60c44c7bc4f..6a6f0eaa351baf 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts @@ -98,11 +98,14 @@ export class TerminalLinkManager extends DisposableStore { let activeHoverDisposable: IDisposable | undefined; let activeTooltipScheduler: RunOnceScheduler | undefined; + let activeHoverListeners: DisposableStore | undefined; const clearActiveLinkHover = () => { activeHoverDisposable?.dispose(); activeHoverDisposable = undefined; activeTooltipScheduler?.dispose(); activeTooltipScheduler = undefined; + activeHoverListeners?.dispose(); + activeHoverListeners = undefined; }; this.add(toDisposable(() => { this._clearLinkProviders(); @@ -165,11 +168,21 @@ export class TerminalLinkManager extends DisposableStore { width: this._xterm.cols, height: this._xterm.rows }; + const hoverViewportY = this._xterm.buffer.active.viewportY; activeHoverDisposable = this._showHover({ - viewportRange: convertBufferRangeToViewport(range, this._xterm.buffer.active.viewportY), + viewportRange: convertBufferRangeToViewport(range, hoverViewportY), cellDimensions, terminalDimensions }, this._getLinkHoverString(text, text), undefined, (text) => this._xterm.options.linkHandler?.activate(e, text, range)); + activeHoverListeners = new DisposableStore(); + activeHoverListeners.add(this._xterm.onScroll(() => clearActiveLinkHover())); + activeHoverListeners.add(this._xterm.onRender(renderedRange => { + // Convert buffer range to viewport range + const viewportRangeY = range.start.y - hoverViewportY - 1; + if (viewportRangeY >= renderedRange.start && viewportRangeY <= renderedRange.end) { + clearActiveLinkHover(); + } + })); // Clear out scheduler until next hover event activeTooltipScheduler?.dispose(); activeTooltipScheduler = undefined; diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkManager.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkManager.test.ts index f55fa9eb6c4acd..e493d1e1926e90 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkManager.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkManager.test.ts @@ -137,6 +137,47 @@ suite('TerminalLinkManager', () => { testableLinkManager._showHover = originalShowHover; } }); + + test('should dismiss shown tooltip on scroll', async () => { + await configurationService.setUserConfiguration('workbench.hover.delay', 0); + const linkHandler = xterm.options.linkHandler; + if (!linkHandler?.hover) { + throw new Error('Expected linkHandler with hover callback'); + } + let hoverDisposed = false; + // eslint-disable-next-line @typescript-eslint/naming-convention + type TestableLinkManager = { _showHover: () => { dispose: () => void } | undefined }; + const testableLinkManager = linkManager as unknown as TestableLinkManager; + const originalShowHover = testableLinkManager._showHover; + const testableXterm = xterm as unknown as Record; + const originalOnScroll = testableXterm['onScroll'] as ((listener: (e: number) => void) => { dispose: () => void }) | undefined; + let scrollListener: ((e: number) => void) | undefined; + testableLinkManager._showHover = () => ({ + dispose: () => { + hoverDisposed = true; + } + }); + testableXterm['onScroll'] = (listener: (e: number) => void) => { + scrollListener = listener; + return { + dispose: () => { + scrollListener = undefined; + } + }; + }; + const range: Parameters[2] = { start: { x: 1, y: 1 }, end: { x: 10, y: 1 } }; + const event = new MouseEvent('mousemove'); + try { + linkHandler.hover(event, 'http://example.com', range); + await timeout(10); + strictEqual(hoverDisposed, false); + scrollListener?.(1); + strictEqual(hoverDisposed, true); + } finally { + testableXterm['onScroll'] = originalOnScroll; + testableLinkManager._showHover = originalShowHover; + } + }); }); suite('getLinks and open recent link', () => { From 1a91f66854e77dbfe9520593f794fa87c1049a34 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Mon, 13 Apr 2026 10:21:48 -0700 Subject: [PATCH 3/4] Address feedback from Copilot --- .../links/browser/terminalLinkManager.ts | 7 ++- .../test/browser/terminalLinkManager.test.ts | 59 ++++++++++++++++--- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts index 6a6f0eaa351baf..3e7002c6980857 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkManager.ts @@ -177,9 +177,10 @@ export class TerminalLinkManager extends DisposableStore { activeHoverListeners = new DisposableStore(); activeHoverListeners.add(this._xterm.onScroll(() => clearActiveLinkHover())); activeHoverListeners.add(this._xterm.onRender(renderedRange => { - // Convert buffer range to viewport range - const viewportRangeY = range.start.y - hoverViewportY - 1; - if (viewportRangeY >= renderedRange.start && viewportRangeY <= renderedRange.end) { + // Convert buffer range to viewport range and check if the + // rendered range intersects any row of the link + const viewportRange = convertBufferRangeToViewport(range, hoverViewportY); + if (viewportRange.start.y <= renderedRange.end && viewportRange.end.y >= renderedRange.start) { clearActiveLinkHover(); } })); diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkManager.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkManager.test.ts index e493d1e1926e90..8dc3fe0bdca2ab 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkManager.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkManager.test.ts @@ -26,7 +26,7 @@ import { TerminalLinkResolver } from '../../browser/terminalLinkResolver.js'; import { importAMDNodeModule } from '../../../../../../amdX.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { TestXtermLogger } from '../../../../../../platform/terminal/test/common/terminalTestHelpers.js'; -import { timeout } from '../../../../../../base/common/async.js'; +import { runWithFakedTimers } from '../../../../../../base/test/common/timeTravelScheduler.js'; const defaultTerminalConfig: Partial = { fontFamily: 'monospace', @@ -111,7 +111,7 @@ suite('TerminalLinkManager', () => { }); suite('OSC 8 hover', () => { - test('should cancel delayed tooltip when leave happens before hover delay', async () => { + test('should cancel delayed tooltip when leave happens before hover delay', () => runWithFakedTimers({ useFakeTimers: true }, async () => { await configurationService.setUserConfiguration('workbench.hover.delay', 10); const linkHandler = xterm.options.linkHandler; if (!linkHandler?.hover || !linkHandler.leave) { @@ -131,14 +131,15 @@ suite('TerminalLinkManager', () => { try { linkHandler.hover(event, 'http://example.com', range); linkHandler.leave(event, 'http://example.com', range); - await timeout(20); + // Advance past hover delay to verify it was cancelled + await Promise.resolve(); strictEqual(hoverShownCount, 0); } finally { testableLinkManager._showHover = originalShowHover; } - }); + })); - test('should dismiss shown tooltip on scroll', async () => { + test('should dismiss shown tooltip on scroll', () => runWithFakedTimers({ useFakeTimers: true }, async () => { await configurationService.setUserConfiguration('workbench.hover.delay', 0); const linkHandler = xterm.options.linkHandler; if (!linkHandler?.hover) { @@ -169,7 +170,8 @@ suite('TerminalLinkManager', () => { const event = new MouseEvent('mousemove'); try { linkHandler.hover(event, 'http://example.com', range); - await timeout(10); + // Flush the 0ms RunOnceScheduler + await Promise.resolve(); strictEqual(hoverDisposed, false); scrollListener?.(1); strictEqual(hoverDisposed, true); @@ -177,7 +179,50 @@ suite('TerminalLinkManager', () => { testableXterm['onScroll'] = originalOnScroll; testableLinkManager._showHover = originalShowHover; } - }); + })); + + test('should dismiss shown tooltip on render', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await configurationService.setUserConfiguration('workbench.hover.delay', 0); + const linkHandler = xterm.options.linkHandler; + if (!linkHandler?.hover) { + throw new Error('Expected linkHandler with hover callback'); + } + let hoverDisposed = false; + // eslint-disable-next-line @typescript-eslint/naming-convention + type TestableLinkManager = { _showHover: () => { dispose: () => void } | undefined }; + const testableLinkManager = linkManager as unknown as TestableLinkManager; + const originalShowHover = testableLinkManager._showHover; + const testableXterm = xterm as unknown as Record; + const originalOnRender = testableXterm['onRender'] as ((listener: (e: { start: number; end: number }) => void) => { dispose: () => void }) | undefined; + let renderListener: ((e: { start: number; end: number }) => void) | undefined; + testableLinkManager._showHover = () => ({ + dispose: () => { + hoverDisposed = true; + } + }); + testableXterm['onRender'] = (listener: (e: { start: number; end: number }) => void) => { + renderListener = listener; + return { + dispose: () => { + renderListener = undefined; + } + }; + }; + const range: Parameters[2] = { start: { x: 1, y: 1 }, end: { x: 10, y: 1 } }; + const event = new MouseEvent('mousemove'); + try { + linkHandler.hover(event, 'http://example.com', range); + // Flush the 0ms RunOnceScheduler + await Promise.resolve(); + strictEqual(hoverDisposed, false); + // Simulate a render that includes the link's viewport row + renderListener?.({ start: 0, end: 5 }); + strictEqual(hoverDisposed, true); + } finally { + testableXterm['onRender'] = originalOnRender; + testableLinkManager._showHover = originalShowHover; + } + })); }); suite('getLinks and open recent link', () => { From dc33e9e9c42af69a073c7617333d0a5d33888532 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Mon, 13 Apr 2026 10:46:44 -0700 Subject: [PATCH 4/4] Refactor and fix tests --- .../test/browser/terminalLinkManager.test.ts | 128 +++++++++--------- 1 file changed, 62 insertions(+), 66 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkManager.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkManager.test.ts index 8dc3fe0bdca2ab..51a7e4fbb8126d 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkManager.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkManager.test.ts @@ -22,11 +22,14 @@ import { ITerminalConfiguration, ITerminalProcessManager } from '../../../../ter import { TestViewDescriptorService } from '../../../../terminal/test/browser/xterm/xtermTerminal.test.js'; import { TestStorageService } from '../../../../../test/common/workbenchTestServices.js'; import type { ILink, Terminal } from '@xterm/xterm'; +import { IXtermCore } from '../../../../terminal/browser/xterm-private.js'; import { TerminalLinkResolver } from '../../browser/terminalLinkResolver.js'; import { importAMDNodeModule } from '../../../../../../amdX.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { TestXtermLogger } from '../../../../../../platform/terminal/test/common/terminalTestHelpers.js'; import { runWithFakedTimers } from '../../../../../../base/test/common/timeTravelScheduler.js'; +import { timeout } from '../../../../../../base/common/async.js'; +import { IDisposable } from '../../../../../../base/common/lifecycle.js'; const defaultTerminalConfig: Partial = { fontFamily: 'monospace', @@ -110,6 +113,34 @@ suite('TerminalLinkManager', () => { }); }); + // eslint-disable-next-line @typescript-eslint/naming-convention + type TestableLinkManager = { _showHover: (...args: unknown[]) => IDisposable | undefined }; + + function overrideXtermEvent(terminal: Terminal, eventName: string, handler: (listener: (e: T) => void) => IDisposable): IDisposable { + const originalDescriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(terminal), eventName); + Object.defineProperty(terminal, eventName, { value: handler, configurable: true }); + return { + dispose: () => { + if (originalDescriptor) { + Object.defineProperty(terminal, eventName, originalDescriptor); + } else { + delete (terminal as unknown as Record)[eventName]; + } + } + }; + } + + function mockXtermCoreRenderService(): IDisposable { + interface XtermWithCore extends Terminal { _core: IXtermCore } + const xtermWithCore = xterm as unknown as XtermWithCore; + const origRenderService = xtermWithCore._core?._renderService; + if (!xtermWithCore._core) { (xtermWithCore as XtermWithCore)._core = {} as IXtermCore; } + xtermWithCore._core._renderService = { dimensions: { css: { cell: { width: 8, height: 16 } } }, _renderer: {} }; + return { + dispose: () => { xtermWithCore._core._renderService = origRenderService!; } + }; + } + suite('OSC 8 hover', () => { test('should cancel delayed tooltip when leave happens before hover delay', () => runWithFakedTimers({ useFakeTimers: true }, async () => { await configurationService.setUserConfiguration('workbench.hover.delay', 10); @@ -118,8 +149,6 @@ suite('TerminalLinkManager', () => { throw new Error('Expected linkHandler with hover/leave callbacks'); } let hoverShownCount = 0; - // eslint-disable-next-line @typescript-eslint/naming-convention - type TestableLinkManager = { _showHover: () => undefined }; const testableLinkManager = linkManager as unknown as TestableLinkManager; const originalShowHover = testableLinkManager._showHover; testableLinkManager._showHover = () => { @@ -131,97 +160,64 @@ suite('TerminalLinkManager', () => { try { linkHandler.hover(event, 'http://example.com', range); linkHandler.leave(event, 'http://example.com', range); - // Advance past hover delay to verify it was cancelled - await Promise.resolve(); + await timeout(0); strictEqual(hoverShownCount, 0); } finally { testableLinkManager._showHover = originalShowHover; } })); - test('should dismiss shown tooltip on scroll', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + /** + * Triggers the hover callback, flushes the 0ms scheduler, then + * fires the given xterm event and asserts the hover was disposed. + */ + async function assertHoverDismissedOnEvent( + overrideEvent: (setFireEvent: (fn: () => void) => void) => IDisposable, + ): Promise { await configurationService.setUserConfiguration('workbench.hover.delay', 0); const linkHandler = xterm.options.linkHandler; if (!linkHandler?.hover) { throw new Error('Expected linkHandler with hover callback'); } let hoverDisposed = false; - // eslint-disable-next-line @typescript-eslint/naming-convention - type TestableLinkManager = { _showHover: () => { dispose: () => void } | undefined }; const testableLinkManager = linkManager as unknown as TestableLinkManager; const originalShowHover = testableLinkManager._showHover; - const testableXterm = xterm as unknown as Record; - const originalOnScroll = testableXterm['onScroll'] as ((listener: (e: number) => void) => { dispose: () => void }) | undefined; - let scrollListener: ((e: number) => void) | undefined; testableLinkManager._showHover = () => ({ - dispose: () => { - hoverDisposed = true; - } + dispose: () => { hoverDisposed = true; } }); - testableXterm['onScroll'] = (listener: (e: number) => void) => { - scrollListener = listener; - return { - dispose: () => { - scrollListener = undefined; - } - }; - }; + const renderServiceRestore = mockXtermCoreRenderService(); const range: Parameters[2] = { start: { x: 1, y: 1 }, end: { x: 10, y: 1 } }; - const event = new MouseEvent('mousemove'); + let fireEvent: (() => void) | undefined; + const eventRestore = overrideEvent(fn => { fireEvent = fn; }); try { - linkHandler.hover(event, 'http://example.com', range); - // Flush the 0ms RunOnceScheduler - await Promise.resolve(); + linkHandler.hover(new MouseEvent('mousemove'), 'http://example.com', range); + await timeout(0); strictEqual(hoverDisposed, false); - scrollListener?.(1); + fireEvent?.(); strictEqual(hoverDisposed, true); } finally { - testableXterm['onScroll'] = originalOnScroll; + eventRestore.dispose(); + renderServiceRestore.dispose(); testableLinkManager._showHover = originalShowHover; } + } + + test('should dismiss shown tooltip on scroll', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + await assertHoverDismissedOnEvent(setFire => { + return overrideXtermEvent(xterm, 'onScroll', listener => { + setFire(() => listener(1)); + return { dispose: () => { } }; + }); + }); })); test('should dismiss shown tooltip on render', () => runWithFakedTimers({ useFakeTimers: true }, async () => { - await configurationService.setUserConfiguration('workbench.hover.delay', 0); - const linkHandler = xterm.options.linkHandler; - if (!linkHandler?.hover) { - throw new Error('Expected linkHandler with hover callback'); - } - let hoverDisposed = false; - // eslint-disable-next-line @typescript-eslint/naming-convention - type TestableLinkManager = { _showHover: () => { dispose: () => void } | undefined }; - const testableLinkManager = linkManager as unknown as TestableLinkManager; - const originalShowHover = testableLinkManager._showHover; - const testableXterm = xterm as unknown as Record; - const originalOnRender = testableXterm['onRender'] as ((listener: (e: { start: number; end: number }) => void) => { dispose: () => void }) | undefined; - let renderListener: ((e: { start: number; end: number }) => void) | undefined; - testableLinkManager._showHover = () => ({ - dispose: () => { - hoverDisposed = true; - } + await assertHoverDismissedOnEvent(setFire => { + return overrideXtermEvent<{ start: number; end: number }>(xterm, 'onRender', listener => { + setFire(() => listener({ start: 0, end: 5 })); + return { dispose: () => { } }; + }); }); - testableXterm['onRender'] = (listener: (e: { start: number; end: number }) => void) => { - renderListener = listener; - return { - dispose: () => { - renderListener = undefined; - } - }; - }; - const range: Parameters[2] = { start: { x: 1, y: 1 }, end: { x: 10, y: 1 } }; - const event = new MouseEvent('mousemove'); - try { - linkHandler.hover(event, 'http://example.com', range); - // Flush the 0ms RunOnceScheduler - await Promise.resolve(); - strictEqual(hoverDisposed, false); - // Simulate a render that includes the link's viewport row - renderListener?.({ start: 0, end: 5 }); - strictEqual(hoverDisposed, true); - } finally { - testableXterm['onRender'] = originalOnRender; - testableLinkManager._showHover = originalShowHover; - } })); });