From e57cb8bf7e8df25e79454cff1ae037864adce1d5 Mon Sep 17 00:00:00 2001 From: Jon Bockhorst Date: Fri, 1 Nov 2019 17:38:08 -0500 Subject: [PATCH] Use regex parser for web links --- .../src/WebLinkProvider.ts | 37 +++++++++++-- .../src/WebLinksAddon.ts | 12 ++--- src/Terminal.ts | 2 +- src/browser/Linkifier2.ts | 54 +++++++++++++++---- src/browser/Types.d.ts | 2 +- 5 files changed, 86 insertions(+), 21 deletions(-) diff --git a/addons/xterm-addon-web-links/src/WebLinkProvider.ts b/addons/xterm-addon-web-links/src/WebLinkProvider.ts index 9c9a4e6f9..a5875e9f5 100644 --- a/addons/xterm-addon-web-links/src/WebLinkProvider.ts +++ b/addons/xterm-addon-web-links/src/WebLinkProvider.ts @@ -16,7 +16,7 @@ export default class WebLinkProvider implements ILinkProvider { } export class LinkComputer { - public static computeLink(position: IBufferCellPosition, regex: RegExp, buffer: IBuffer, handler: (event: MouseEvent, uri: string) => void): ILink | undefined { + public static computeLink(position: IBufferCellPosition, regex: RegExp, buffer: IBuffer, handle: (event: MouseEvent, uri: string) => void): ILink | undefined { const rex = new RegExp(regex.source, (regex.flags || '') + 'g'); const bufferLine = buffer.getLine(position.y - 1); @@ -29,9 +29,38 @@ export class LinkComputer { let match; let stringIndex = -1; - // while ((match = rex.exec(line)) !== null) { - // const uri = match[1]; - // } + while ((match = rex.exec(line)) !== null) { + const url = match[1]; + if (!url) { + // something matched but does not comply with the given matchIndex + // since this is most likely a bug the regex itself we simply do nothing here + console.log('match found without corresponding matchIndex'); + break; + } + // Get index, match.index is for the outer match which includes negated chars + // therefore we cannot use match.index directly, instead we search the position + // of the match group in text again + // also correct regex and string search offsets for the next loop run + stringIndex = line.indexOf(url, stringIndex + 1); + rex.lastIndex = stringIndex + url.length; + if (stringIndex < 0) { + // invalid stringIndex (should not have happened) + break; + } + + const range = { + start: { + x: stringIndex + 1, + y: position.y + }, + end: { + x: stringIndex + url.length + 1, + y: position.y + } + }; + + return { range, url, handle }; + } } } diff --git a/addons/xterm-addon-web-links/src/WebLinksAddon.ts b/addons/xterm-addon-web-links/src/WebLinksAddon.ts index e09e6225c..4dbcc3366 100644 --- a/addons/xterm-addon-web-links/src/WebLinksAddon.ts +++ b/addons/xterm-addon-web-links/src/WebLinksAddon.ts @@ -45,12 +45,12 @@ export class WebLinksAddon implements ITerminalAddon { public activate(terminal: Terminal): void { this._terminal = terminal; - // if ('registerLinkProvider' in this._terminal) { - this._linkProvider = new WebLinkProvider(this._terminal, strictUrlRegex, this._handler); - this._terminal.registerLinkProvider(this._linkProvider); - // } else { - // this._linkMatcherId = this._terminal.registerLinkMatcher(strictUrlRegex, this._handler, this._options); - // } + if ('registerLinkProvider' in this._terminal as any) { + this._linkProvider = new WebLinkProvider(this._terminal, strictUrlRegex, this._handler); + this._terminal.registerLinkProvider(this._linkProvider); + } else { + this._linkMatcherId = this._terminal.registerLinkMatcher(strictUrlRegex, this._handler, this._options); + } } public dispose(): void { diff --git a/src/Terminal.ts b/src/Terminal.ts index fb49b10b8..3b849a8d0 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -622,7 +622,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.register(this._mouseZoneManager); this.register(this.onScroll(() => this._mouseZoneManager.clearAll())); this.linkifier.attachToDom(this.element, this._mouseZoneManager); - this.linkifier2.attachToDom(this.element, this._mouseService); + this.linkifier2.attachToDom(this.element, this._viewportElement, this._mouseService); // This event listener must be registered aftre MouseZoneManager is created this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService.onMouseDown(e))); diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index aeb3cf6d9..6273459ad 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -14,6 +14,7 @@ import { EventEmitter, IEvent } from 'common/EventEmitter'; */ export class Linkifier2 implements ILinkifier2 { private _element: HTMLElement | undefined; + private _viewportElement: HTMLElement | undefined; private _linkProviders: ILinkProvider[] = []; private _mouseService: IMouseService | undefined; private _linkCache: ILinkCache[] = []; @@ -31,15 +32,22 @@ export class Linkifier2 implements ILinkifier2 { public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { this._linkProviders.push(linkProvider); - return { dispose: () => console.log('disposing link providers') }; + return { + dispose: () => { + // Remove the link provider from the list + this._linkProviders = this._linkProviders.splice(this._linkProviders.indexOf(linkProvider), 1); + } + }; } - public attachToDom(element: HTMLElement, mouseService: IMouseService): void { + public attachToDom(element: HTMLElement, viewportElement: HTMLElement, mouseService: IMouseService): void { this._element = element; + this._viewportElement = viewportElement; this._mouseService = mouseService; this._element.addEventListener('mousemove', this._onMouseMove.bind(this)); this._element.addEventListener('click', this._onMouseDown.bind(this)); + this._viewportElement.addEventListener('scroll', this._onScroll.bind(this)); } private _onMouseMove(event: MouseEvent): void { @@ -62,6 +70,7 @@ export class Linkifier2 implements ILinkifier2 { const isInPosition = this._linkAtPosition(cachedLink, position); const range = cachedLink.range; + // Check if the mouse position contains a link if (isInPosition && !this._linkCache[i].mouseOver) { // 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)); @@ -75,13 +84,7 @@ export class Linkifier2 implements ILinkifier2 { foundLink = true; } else if (!isInPosition && this._linkCache[i].mouseOver) { // Hide the tooltip - this._onHideTooltip.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.remove('xterm-cursor-pointer'); - - if (cachedLink.hideTooltip) { - cachedLink.hideTooltip(event, cachedLink.url); - } - + this._hideTooltip(this._element, this._linkCache[i].link, event); this._linkCache[i].mouseOver = false; } } @@ -114,9 +117,42 @@ export class Linkifier2 implements ILinkifier2 { }); } + private _onScroll(event: Event): void { + this._invalidateCache(); + } + 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) { + 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')); + } + } + + this._linkCache = []; + } + + private _hideTooltip(element: HTMLElement, link: ILink, event: MouseEvent): void { + const range = link.range; + const scrollOffset = this._bufferService.buffer.ydisp; + + this._onHideTooltip.fire(this._createLinkHoverEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x - 1, range.end.y - scrollOffset - 1, undefined)); + element.classList.remove('xterm-cursor-pointer'); + + if (link.hideTooltip) { + link.hideTooltip(event, link.url); } } diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index c7b40f348..b7072c5c3 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -98,7 +98,7 @@ export interface ILinkifier2 { onShowTooltip: IEvent; onHideTooltip: IEvent; - attachToDom(element: HTMLElement, mouseService: IMouseService): void; + attachToDom(element: HTMLElement, viewportElement: HTMLElement, mouseService: IMouseService): void; registerLinkProvider(linkProvider: ILinkProvider): IDisposable; }