diff --git a/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts b/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts index 3fb1a5365..9f2ab5080 100644 --- a/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts +++ b/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts @@ -15,7 +15,7 @@ const width = 800; const height = 600; describe('WebLinksAddon', () => { - before(async function(): Promise { + before(async function (): Promise { this.timeout(10000); browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, @@ -30,22 +30,22 @@ describe('WebLinksAddon', () => { await browser.close(); }); - beforeEach(async function(): Promise { + beforeEach(async function (): Promise { this.timeout(5000); await page.goto(APP); }); - it('.com', async function(): Promise { + it('.com', async function (): Promise { this.timeout(20000); await testHostName('foo.com'); }); - it('.com.au', async function(): Promise { + it('.com.au', async function (): Promise { this.timeout(20000); await testHostName('foo.com.au'); }); - it('.io', async function(): Promise { + it('.io', async function (): Promise { this.timeout(20000); await testHostName('foo.io'); }); diff --git a/src/Terminal.ts b/src/Terminal.ts index 3b849a8d0..82f9ef9ff 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -292,7 +292,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.register(this._inputHandler); this.linkifier = this.linkifier || new Linkifier(this._bufferService, this._logService); - this.linkifier2 = this.linkifier2 || new Linkifier2(this._bufferService); + this.linkifier2 = this.linkifier2 || new Linkifier2(this._bufferService, this._coreService); if (this.options.windowsMode) { this._windowsMode = applyWindowsMode(this); diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index 6273459ad..8d76dd06e 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -1,7 +1,7 @@ -import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent } from './Types'; +import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent, IBufferRange } from './Types'; import { IDisposable } from 'common/Types'; import { IMouseService } from './services/Services'; -import { IBufferService } from 'common/services/Services'; +import { IBufferService, ICoreService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; /** @@ -17,7 +17,9 @@ export class Linkifier2 implements ILinkifier2 { private _viewportElement: HTMLElement | undefined; private _linkProviders: ILinkProvider[] = []; private _mouseService: IMouseService | undefined; - private _linkCache: ILinkCache[] = []; + private _linkCache: ICachedLink[] = []; + private _lastMouseEvent: MouseEvent | undefined; + private _mouseOverLink: boolean = false; private _onShowTooltip = new EventEmitter(); public get onShowTooltip(): IEvent { return this._onShowTooltip.event; } @@ -25,7 +27,8 @@ export class Linkifier2 implements ILinkifier2 { public get onHideTooltip(): IEvent { return this._onHideTooltip.event; } constructor( - private readonly _bufferService: IBufferService + private readonly _bufferService: IBufferService, + private readonly _coreService: ICoreService ) { } @@ -35,7 +38,7 @@ export class Linkifier2 implements ILinkifier2 { return { dispose: () => { // Remove the link provider from the list - this._linkProviders = this._linkProviders.splice(this._linkProviders.indexOf(linkProvider), 1); + this._linkProviders.splice(this._linkProviders.indexOf(linkProvider), 1); } }; } @@ -48,9 +51,13 @@ export class Linkifier2 implements ILinkifier2 { this._element.addEventListener('mousemove', this._onMouseMove.bind(this)); this._element.addEventListener('click', this._onMouseDown.bind(this)); this._viewportElement.addEventListener('scroll', this._onScroll.bind(this)); + + this._coreService.onData(this._onData.bind(this)); } private _onMouseMove(event: MouseEvent): void { + this._lastMouseEvent = event; + if (!this._element || !this._mouseService) { return; } @@ -61,38 +68,40 @@ export class Linkifier2 implements ILinkifier2 { return; } - const scrollOffset = this._bufferService.buffer.ydisp; - // Check the cache for a link and determine if we need to show or hide tooltip let foundLink = false; + let mouseOver = false; for (let i = 0; i < this._linkCache.length; i++) { const cachedLink = this._linkCache[i].link; const isInPosition = this._linkAtPosition(cachedLink, position); - const range = cachedLink.range; // Check if the mouse position contains a link - if (isInPosition && !this._linkCache[i].mouseOver) { + // Also check if it isn't the current line + if (isInPosition && !this._linkCache[i].mouseOver && position.y < this._bufferService.buffer.y) { // Show the tooltip - this._onShowTooltip.fire(this._createLinkHoverEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x - 1, range.end.y - scrollOffset - 1, undefined)); - this._element.classList.add('xterm-cursor-pointer'); - - if (cachedLink.showTooltip) { - cachedLink.showTooltip(event, cachedLink.url); - } - + this._showTooltip(this._element, this._linkCache[i].link, event); this._linkCache[i].mouseOver = true; foundLink = true; + this._mouseOverLink = true; } else if (!isInPosition && this._linkCache[i].mouseOver) { // Hide the tooltip this._hideTooltip(this._element, this._linkCache[i].link, event); this._linkCache[i].mouseOver = false; } + + if (isInPosition) { + mouseOver = true; + } } if (foundLink) { return; } + if (!mouseOver) { + this._mouseOverLink = false; + } + // The is no link in the cache, so ask for one this._linkProviders.forEach(linkProvider => { linkProvider.provideLink(position, this._handleNewLink.bind(this)); @@ -118,30 +127,63 @@ export class Linkifier2 implements ILinkifier2 { } private _onScroll(event: Event): void { - this._invalidateCache(); + if (this._lastMouseEvent && this._mouseOverLink) { + this._hideAllTooltips(); + } + } + + private _onData(e: string): void { + if (this._lastMouseEvent && this._mouseOverLink) { + const index = this._hideAllTooltips(); + this._linkCache.splice(index, 1); + } } private _handleNewLink(link: ILink | undefined): void { - if (link && !this._linkCache.find(cachedLink => cachedLink.link = link)) { - this._linkCache.push({ link, mouseOver: false }); - this._bufferService.buffer.addMarker(link.range.start.y); - } - } - - private _invalidateCache(): void { - if (!this._element) { + if (!link || !this._element || !this._lastMouseEvent || !this._mouseService) { return; } - // We want to invalidate the cache - // but we need to check if we need to hide any tooltip - for (let i = 0; i < this._linkCache.length; i++) { - if (this._linkCache[i].mouseOver) { - this._hideTooltip(this._element, this._linkCache[i].link, new MouseEvent('invalid event')); - } + // Check if the link at this position is already cached + let linkIndex = this._linkCache.findIndex(cachedLink => { + return cachedLink.link.url === link.url && + cachedLink.link.range.start.x === link.range.start.x && + cachedLink.link.range.start.y === link.range.start.y && + cachedLink.link.range.end.x === link.range.end.x && + cachedLink.link.range.end.y === link.range.end.y; + }); + + const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService); + + if (!position) { + return; } - this._linkCache = []; + const linkAtPosition = this._linkAtPosition(link, position); + + if (linkIndex === -1) { + this._linkCache.push({ link, mouseOver: false }); + linkIndex = this._linkCache.length - 1; + } + + // Show the tooltip if the last mouse event was over it + if (linkAtPosition && !this._linkCache[linkIndex].mouseOver) { + this._showTooltip(this._element, link, this._lastMouseEvent); + this._linkCache[linkIndex].mouseOver = true; + this._mouseOverLink = true; + } + } + + private _showTooltip(element: HTMLElement, link: ILink, event: MouseEvent): void { + const range = link.range; + const scrollOffset = this._bufferService.buffer.ydisp; + + this._onShowTooltip.fire(this._createLinkHoverEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x - 1, range.end.y - scrollOffset - 1, undefined)); + element.classList.add('xterm-cursor-pointer'); + + if (link.showTooltip) { + link.showTooltip(event, link.url); + } } private _hideTooltip(element: HTMLElement, link: ILink, event: MouseEvent): void { @@ -156,6 +198,22 @@ export class Linkifier2 implements ILinkifier2 { } } + private _hideAllTooltips(): number { + if (!this._element) { + return -1; + } + + // Hide all the tooltips + for (let i = 0; i < this._linkCache.length; i++) { + if (this._linkCache[i].mouseOver) { + this._hideTooltip(this._element, this._linkCache[i].link, new MouseEvent('invalid event')); + return i; + } + } + + return -1; + } + /** * Check if the buffer position is within the link * @param link @@ -186,7 +244,7 @@ export class Linkifier2 implements ILinkifier2 { } } -interface ILinkCache { +interface ICachedLink { link: ILink; mouseOver: boolean; }