Change link provider API to return all links for the line

Fixes #2848
This commit is contained in:
Daniel Imms
2020-05-05 18:19:57 -07:00
parent 1168c6c52f
commit d488b49fc4
4 changed files with 128 additions and 89 deletions
@@ -15,19 +15,20 @@ export class WebLinkProvider implements ILinkProvider {
} }
public provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void): void { public provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void {
callback(LinkComputer.computeLink(position, this._regex, this._terminal, this._handler)); callback(LinkComputer.computeLink(y, this._regex, this._terminal, this._handler));
} }
} }
export class LinkComputer { export class LinkComputer {
public static computeLink(position: IBufferCellPosition, regex: RegExp, terminal: Terminal, handler: (event: MouseEvent, uri: string) => void): ILink | undefined { public static computeLink(y: number, regex: RegExp, terminal: Terminal, handler: (event: MouseEvent, uri: string) => void): ILink[] {
const rex = new RegExp(regex.source, (regex.flags || '') + 'g'); const rex = new RegExp(regex.source, (regex.flags || '') + 'g');
const [line, startLineIndex] = LinkComputer._translateBufferLineToStringWithWrap(position.y - 1, false, terminal); const [line, startLineIndex] = LinkComputer._translateBufferLineToStringWithWrap(y - 1, false, terminal);
let match; let match;
let stringIndex = -1; let stringIndex = -1;
const result: ILink[] = [];
while ((match = rex.exec(line)) !== null) { while ((match = rex.exec(line)) !== null) {
const text = match[1]; const text = match[1];
@@ -68,8 +69,11 @@ export class LinkComputer {
} }
}; };
return { range, text, activate: handler }; result.push({ range, text, activate: handler });
} }
console.log('computed', result);
return result;
} }
/** /**
+114 -79
View File
@@ -8,7 +8,7 @@ import { IDisposable } from 'common/Types';
import { IMouseService, IRenderService } from './services/Services'; import { IMouseService, IRenderService } from './services/Services';
import { IBufferService } from 'common/services/Services'; import { IBufferService } from 'common/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter'; import { EventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable, getDisposeArrayDisposable } from 'common/Lifecycle'; import { Disposable, getDisposeArrayDisposable, disposeArray } from 'common/Lifecycle';
import { addDisposableDomListener } from 'browser/Lifecycle'; import { addDisposableDomListener } from 'browser/Lifecycle';
interface ILinkState { interface ILinkState {
@@ -16,17 +16,23 @@ interface ILinkState {
isHovered: boolean; isHovered: boolean;
} }
interface ILinkWithState {
link: ILink;
state?: ILinkState;
}
export class Linkifier2 extends Disposable implements ILinkifier2 { export class Linkifier2 extends Disposable implements ILinkifier2 {
private _element: HTMLElement | undefined; private _element: HTMLElement | undefined;
private _mouseService: IMouseService | undefined; private _mouseService: IMouseService | undefined;
private _renderService: IRenderService | undefined; private _renderService: IRenderService | undefined;
private _linkProviders: ILinkProvider[] = []; private _linkProviders: ILinkProvider[] = [];
private _currentLink: ILink | undefined; private _currentLink: ILinkWithState | undefined;
protected _currentLinkState: ILinkState | undefined;
private _lastMouseEvent: MouseEvent | undefined; private _lastMouseEvent: MouseEvent | undefined;
private _linkCacheDisposables: IDisposable[] = []; private _linkCacheDisposables: IDisposable[] = [];
private _lastBufferCell: IBufferCellPosition | undefined; private _lastBufferCell: IBufferCellPosition | undefined;
private _isMouseOut: boolean = true; private _isMouseOut: boolean = true;
private _activeProviderReplies: Map<Number, ILinkWithState[] | undefined> | undefined;
private _activeLine: number = -1;
private _onShowLinkUnderline = this.register(new EventEmitter<ILinkifierEvent>()); private _onShowLinkUnderline = this.register(new EventEmitter<ILinkifierEvent>());
public get onShowLinkUnderline(): IEvent<ILinkifierEvent> { return this._onShowLinkUnderline.event; } public get onShowLinkUnderline(): IEvent<ILinkifierEvent> { return this._onShowLinkUnderline.event; }
@@ -101,61 +107,90 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
} }
private _onHover(position: IBufferCellPosition): void { private _onHover(position: IBufferCellPosition): void {
if (this._currentLink) { // TODO: This currently does not cache link provider results across wrapped lines, activeLine should be something like `activeRange: {startY, endY}`
// Check the if the link is in the mouse position // Check if we need to clear the link
const isInPosition = this._linkAtPosition(this._currentLink, position); if (this._activeLine !== position.y) {
this._clearCurrentLink();
this._askForLink(position, false);
return;
}
// Check if we need to clear the link // Check the if the link is in the mouse position
if (!isInPosition) { const isCurrentLinkInPosition = this._currentLink && this._linkAtPosition(this._currentLink.link, position);
this._clearCurrentLink(); if (!isCurrentLinkInPosition) {
this._askForLink(position); this._clearCurrentLink();
} this._askForLink(position, true);
} else {
this._askForLink(position);
} }
} }
private _askForLink(position: IBufferCellPosition): void { private _askForLink(position: IBufferCellPosition, useLineCache: boolean): void {
const providerReplies: Map<Number, ILink | undefined> = new Map(); if (!this._activeProviderReplies || !useLineCache) {
this._activeProviderReplies = new Map();
this._activeLine = position.y;
}
let linkProvided = false; let linkProvided = false;
// There is no link cached, so ask for one // There is no link cached, so ask for one
this._linkProviders.forEach((linkProvider, i) => { this._linkProviders.forEach((linkProvider, i) => {
linkProvider.provideLink(position, (link: ILink | undefined) => { const existingReply = this._activeProviderReplies?.get(i);
if (this._isMouseOut) { if (existingReply) {
return; console.log('existing reply', existingReply);
} linkProvided = this._checkLinkProviderResult(i, position, linkProvided);
providerReplies.set(i, link); } else {
linkProvider.provideLinks(position.y, (links: ILink[] | undefined) => {
// Check if every provider before this one has come back undefined console.log('provided links', links);
let hasLinkBefore = false; if (this._isMouseOut) {
for (let j = 0; j < i; j++) { return;
if (!providerReplies.has(j) || providerReplies.get(j)) {
hasLinkBefore = true;
} }
} const linksWithState: ILinkWithState[] | undefined = links?.map(link => ({ link }));
this._activeProviderReplies?.set(i, linksWithState);
// If all providers with higher priority came back undefined, then this link should be used linkProvided = this._checkLinkProviderResult(i, position, linkProvided);
if (!hasLinkBefore && link) { });
linkProvided = true; }
this._handleNewLink(link);
}
// Check if all the providers have responded
if (providerReplies.size === this._linkProviders.length && !linkProvided) {
// Respect the order of the link providers
for (let j = 0; j < providerReplies.size; j++) {
const currentLink = providerReplies.get(j);
if (currentLink) {
this._handleNewLink(currentLink);
break;
}
}
}
});
}); });
} }
private _checkLinkProviderResult(index: number, position: IBufferCellPosition, linkProvided: boolean): boolean {
if (!this._activeProviderReplies) {
return linkProvided;
}
const links = this._activeProviderReplies.get(index);
// Check if every provider before this one has come back undefined
let hasLinkBefore = false;
for (let j = 0; j < index; j++) {
if (!this._activeProviderReplies.has(j) || this._activeProviderReplies.get(j)) {
hasLinkBefore = true;
}
}
// If all providers with higher priority came back undefined, then this provider's link for
// the position should be used
if (!hasLinkBefore && links) {
const linkAtPosition = links.find(link => this._linkAtPosition(link.link, position));
if (linkAtPosition) {
linkProvided = true;
this._handleNewLink(linkAtPosition);
}
}
// Check if all the providers have responded
if (this._activeProviderReplies.size === this._linkProviders.length && !linkProvided) {
// Respect the order of the link providers
for (let j = 0; j < this._activeProviderReplies.size; j++) {
const currentLink = this._activeProviderReplies.get(j)?.find(link => this._linkAtPosition(link.link, position));
if (currentLink) {
linkProvided = true;
this._handleNewLink(currentLink);
break;
}
}
}
return linkProvided;
}
private _onClick(event: MouseEvent): void { private _onClick(event: MouseEvent): void {
if (!this._element || !this._mouseService || !this._currentLink) { if (!this._element || !this._mouseService || !this._currentLink) {
return; return;
@@ -167,8 +202,8 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
return; return;
} }
if (this._linkAtPosition(this._currentLink, position)) { if (this._linkAtPosition(this._currentLink.link, position)) {
this._currentLink.activate(event, this._currentLink.text); this._currentLink.link.activate(event, this._currentLink.link.text);
} }
} }
@@ -178,16 +213,14 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
} }
// If we have a start and end row, check that the link is within it // If we have a start and end row, check that the link is within it
if (!startRow || !endRow || (this._currentLink.range.start.y >= startRow && this._currentLink.range.end.y <= endRow)) { if (!startRow || !endRow || (this._currentLink.link.range.start.y >= startRow && this._currentLink.link.range.end.y <= endRow)) {
this._linkLeave(this._element, this._currentLink, this._lastMouseEvent); this._linkLeave(this._element, this._currentLink.link, this._lastMouseEvent);
this._currentLink = undefined; this._currentLink = undefined;
this._currentLinkState = undefined; disposeArray(this._linkCacheDisposables);
this._linkCacheDisposables.forEach(l => l.dispose());
this._linkCacheDisposables = [];
} }
} }
private _handleNewLink(link: ILink): void { private _handleNewLink(linkWithState: ILinkWithState): void {
if (!this._element || !this._lastMouseEvent || !this._mouseService) { if (!this._element || !this._lastMouseEvent || !this._mouseService) {
return; return;
} }
@@ -199,38 +232,38 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
} }
// Trigger hover if the we have a link at the position // Trigger hover if the we have a link at the position
if (this._linkAtPosition(link, position)) { if (this._linkAtPosition(linkWithState.link, position)) {
this._currentLink = link; this._currentLink = linkWithState;
this._currentLinkState = { this._currentLink.state = {
decorations: { decorations: {
underline: link.decorations === undefined ? true : link.decorations.underline, underline: linkWithState.link.decorations === undefined ? true : linkWithState.link.decorations.underline,
pointerCursor: link.decorations === undefined ? true : link.decorations.pointerCursor pointerCursor: linkWithState.link.decorations === undefined ? true : linkWithState.link.decorations.pointerCursor
}, },
isHovered: true isHovered: true
}; };
this._linkHover(this._element, link, this._lastMouseEvent); this._linkHover(this._element, linkWithState.link, this._lastMouseEvent);
// Add listener for tracking decorations changes // Add listener for tracking decorations changes
link.decorations = {} as ILinkDecorations; linkWithState.link.decorations = {} as ILinkDecorations;
Object.defineProperties(link.decorations, { Object.defineProperties(linkWithState.link.decorations, {
pointerCursor: { pointerCursor: {
get: () => this._currentLinkState?.decorations.pointerCursor, get: () => this._currentLink?.state?.decorations.pointerCursor,
set: v => { set: v => {
if (this._currentLinkState && this._currentLinkState?.decorations.pointerCursor !== v) { if (this._currentLink?.state && this._currentLink.state.decorations.pointerCursor !== v) {
this._currentLinkState.decorations.pointerCursor = v; this._currentLink.state.decorations.pointerCursor = v;
if (this._currentLinkState.isHovered) { if (this._currentLink.state.isHovered) {
this._element?.classList.toggle('xterm-cursor-pointer', v); this._element?.classList.toggle('xterm-cursor-pointer', v);
} }
} }
} }
}, },
underline: { underline: {
get: () => this._currentLinkState?.decorations.underline, get: () => this._currentLink?.state?.decorations.underline,
set: v => { set: v => {
if (this._currentLinkState && this._currentLinkState?.decorations.underline !== v) { if (this._currentLink?.state && this._currentLink?.state?.decorations.underline !== v) {
this._currentLinkState.decorations.underline = v; this._currentLink.state.decorations.underline = v;
if (this._currentLinkState.isHovered) { if (this._currentLink.state.isHovered) {
this._fireUnderlineEvent(link, v); this._fireUnderlineEvent(linkWithState.link, v);
} }
} }
} }
@@ -250,12 +283,13 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
} }
protected _linkHover(element: HTMLElement, link: ILink, event: MouseEvent): void { protected _linkHover(element: HTMLElement, link: ILink, event: MouseEvent): void {
if (this._currentLinkState) { if (this._currentLink?.state) {
this._currentLinkState.isHovered = true; this._currentLink.state.isHovered = true;
if (this._currentLinkState.decorations.underline) { if (this._currentLink.state.decorations.underline) {
console.log('add underline');
this._fireUnderlineEvent(link, true); this._fireUnderlineEvent(link, true);
} }
if (this._currentLinkState.decorations.pointerCursor) { if (this._currentLink.state.decorations.pointerCursor) {
element.classList.add('xterm-cursor-pointer'); element.classList.add('xterm-cursor-pointer');
} }
} }
@@ -274,12 +308,13 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
} }
protected _linkLeave(element: HTMLElement, link: ILink, event: MouseEvent): void { protected _linkLeave(element: HTMLElement, link: ILink, event: MouseEvent): void {
if (this._currentLinkState) { if (this._currentLink?.state) {
this._currentLinkState.isHovered = false; this._currentLink.state.isHovered = false;
if (this._currentLinkState.decorations.underline) { if (this._currentLink.state.decorations.underline) {
console.log('lose underline');
this._fireUnderlineEvent(link, false); this._fireUnderlineEvent(link, false);
} }
if (this._currentLinkState.decorations.pointerCursor) { if (this._currentLink.state.decorations.pointerCursor) {
element.classList.remove('xterm-cursor-pointer'); element.classList.remove('xterm-cursor-pointer');
} }
} }
+1 -1
View File
@@ -264,7 +264,7 @@ export interface IMouseZone {
} }
interface ILinkProvider { interface ILinkProvider {
provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void): void; provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void;
} }
interface ILink { interface ILink {
+4 -4
View File
@@ -1100,11 +1100,11 @@ declare module 'xterm' {
interface ILinkProvider { interface ILinkProvider {
/** /**
* Provides a link a buffer position * Provides a link a buffer position
* @param position The position of the buffer that is currently active. * @param y The y position of the buffer that is currently active.
* @param callback The callback to be fired with the resulting link or * @param callback The callback to be fired when ready with the resulting
* `undefined` when ready. * link(s) for the line or `undefined`.
*/ */
provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void): void; provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void;
} }
/** /**