Fixed link caching errors

This commit is contained in:
Jon Bockhorst
2019-11-04 23:37:05 -06:00
parent e57cb8bf7e
commit fdfadfefbc
3 changed files with 97 additions and 39 deletions
@@ -15,7 +15,7 @@ const width = 800;
const height = 600;
describe('WebLinksAddon', () => {
before(async function(): Promise<any> {
before(async function (): Promise<any> {
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<any> {
beforeEach(async function (): Promise<any> {
this.timeout(5000);
await page.goto(APP);
});
it('.com', async function(): Promise<any> {
it('.com', async function (): Promise<any> {
this.timeout(20000);
await testHostName('foo.com');
});
it('.com.au', async function(): Promise<any> {
it('.com.au', async function (): Promise<any> {
this.timeout(20000);
await testHostName('foo.com.au');
});
it('.io', async function(): Promise<any> {
it('.io', async function (): Promise<any> {
this.timeout(20000);
await testHostName('foo.io');
});
+1 -1
View File
@@ -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);
+91 -33
View File
@@ -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<ILinkifierEvent>();
public get onShowTooltip(): IEvent<ILinkifierEvent> { return this._onShowTooltip.event; }
@@ -25,7 +27,8 @@ export class Linkifier2 implements ILinkifier2 {
public get onHideTooltip(): IEvent<ILinkifierEvent> { 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;
}