@@ -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
@@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
@@ -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 {
|
||||||
|
|||||||
Vendored
+4
-4
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user