mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
@@ -15,19 +15,20 @@ export class WebLinkProvider implements ILinkProvider {
|
||||
|
||||
}
|
||||
|
||||
public provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void): void {
|
||||
callback(LinkComputer.computeLink(position, this._regex, this._terminal, this._handler));
|
||||
public provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void {
|
||||
callback(LinkComputer.computeLink(y, this._regex, this._terminal, this._handler));
|
||||
}
|
||||
}
|
||||
|
||||
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 [line, startLineIndex] = LinkComputer._translateBufferLineToStringWithWrap(position.y - 1, false, terminal);
|
||||
const [line, startLineIndex] = LinkComputer._translateBufferLineToStringWithWrap(y - 1, false, terminal);
|
||||
|
||||
let match;
|
||||
let stringIndex = -1;
|
||||
const result: ILink[] = [];
|
||||
|
||||
while ((match = rex.exec(line)) !== null) {
|
||||
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 { IBufferService } from 'common/services/Services';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { Disposable, getDisposeArrayDisposable } from 'common/Lifecycle';
|
||||
import { Disposable, getDisposeArrayDisposable, disposeArray } from 'common/Lifecycle';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
|
||||
interface ILinkState {
|
||||
@@ -16,17 +16,23 @@ interface ILinkState {
|
||||
isHovered: boolean;
|
||||
}
|
||||
|
||||
interface ILinkWithState {
|
||||
link: ILink;
|
||||
state?: ILinkState;
|
||||
}
|
||||
|
||||
export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
private _element: HTMLElement | undefined;
|
||||
private _mouseService: IMouseService | undefined;
|
||||
private _renderService: IRenderService | undefined;
|
||||
private _linkProviders: ILinkProvider[] = [];
|
||||
private _currentLink: ILink | undefined;
|
||||
protected _currentLinkState: ILinkState | undefined;
|
||||
private _currentLink: ILinkWithState | undefined;
|
||||
private _lastMouseEvent: MouseEvent | undefined;
|
||||
private _linkCacheDisposables: IDisposable[] = [];
|
||||
private _lastBufferCell: IBufferCellPosition | undefined;
|
||||
private _isMouseOut: boolean = true;
|
||||
private _activeProviderReplies: Map<Number, ILinkWithState[] | undefined> | undefined;
|
||||
private _activeLine: number = -1;
|
||||
|
||||
private _onShowLinkUnderline = this.register(new EventEmitter<ILinkifierEvent>());
|
||||
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 {
|
||||
if (this._currentLink) {
|
||||
// Check the if the link is in the mouse position
|
||||
const isInPosition = this._linkAtPosition(this._currentLink, position);
|
||||
// TODO: This currently does not cache link provider results across wrapped lines, activeLine should be something like `activeRange: {startY, endY}`
|
||||
// Check if we need to clear the link
|
||||
if (this._activeLine !== position.y) {
|
||||
this._clearCurrentLink();
|
||||
this._askForLink(position, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we need to clear the link
|
||||
if (!isInPosition) {
|
||||
this._clearCurrentLink();
|
||||
this._askForLink(position);
|
||||
}
|
||||
} else {
|
||||
this._askForLink(position);
|
||||
// Check the if the link is in the mouse position
|
||||
const isCurrentLinkInPosition = this._currentLink && this._linkAtPosition(this._currentLink.link, position);
|
||||
if (!isCurrentLinkInPosition) {
|
||||
this._clearCurrentLink();
|
||||
this._askForLink(position, true);
|
||||
}
|
||||
}
|
||||
|
||||
private _askForLink(position: IBufferCellPosition): void {
|
||||
const providerReplies: Map<Number, ILink | undefined> = new Map();
|
||||
private _askForLink(position: IBufferCellPosition, useLineCache: boolean): void {
|
||||
if (!this._activeProviderReplies || !useLineCache) {
|
||||
this._activeProviderReplies = new Map();
|
||||
this._activeLine = position.y;
|
||||
}
|
||||
let linkProvided = false;
|
||||
|
||||
// There is no link cached, so ask for one
|
||||
this._linkProviders.forEach((linkProvider, i) => {
|
||||
linkProvider.provideLink(position, (link: ILink | undefined) => {
|
||||
if (this._isMouseOut) {
|
||||
return;
|
||||
}
|
||||
providerReplies.set(i, link);
|
||||
|
||||
// Check if every provider before this one has come back undefined
|
||||
let hasLinkBefore = false;
|
||||
for (let j = 0; j < i; j++) {
|
||||
if (!providerReplies.has(j) || providerReplies.get(j)) {
|
||||
hasLinkBefore = true;
|
||||
const existingReply = this._activeProviderReplies?.get(i);
|
||||
if (existingReply) {
|
||||
console.log('existing reply', existingReply);
|
||||
linkProvided = this._checkLinkProviderResult(i, position, linkProvided);
|
||||
} else {
|
||||
linkProvider.provideLinks(position.y, (links: ILink[] | undefined) => {
|
||||
console.log('provided links', links);
|
||||
if (this._isMouseOut) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If all providers with higher priority came back undefined, then this link should be used
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const linksWithState: ILinkWithState[] | undefined = links?.map(link => ({ link }));
|
||||
this._activeProviderReplies?.set(i, linksWithState);
|
||||
linkProvided = this._checkLinkProviderResult(i, position, linkProvided);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
if (!this._element || !this._mouseService || !this._currentLink) {
|
||||
return;
|
||||
@@ -167,8 +202,8 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._linkAtPosition(this._currentLink, position)) {
|
||||
this._currentLink.activate(event, this._currentLink.text);
|
||||
if (this._linkAtPosition(this._currentLink.link, position)) {
|
||||
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 (!startRow || !endRow || (this._currentLink.range.start.y >= startRow && this._currentLink.range.end.y <= endRow)) {
|
||||
this._linkLeave(this._element, this._currentLink, this._lastMouseEvent);
|
||||
if (!startRow || !endRow || (this._currentLink.link.range.start.y >= startRow && this._currentLink.link.range.end.y <= endRow)) {
|
||||
this._linkLeave(this._element, this._currentLink.link, this._lastMouseEvent);
|
||||
this._currentLink = undefined;
|
||||
this._currentLinkState = undefined;
|
||||
this._linkCacheDisposables.forEach(l => l.dispose());
|
||||
this._linkCacheDisposables = [];
|
||||
disposeArray(this._linkCacheDisposables);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleNewLink(link: ILink): void {
|
||||
private _handleNewLink(linkWithState: ILinkWithState): void {
|
||||
if (!this._element || !this._lastMouseEvent || !this._mouseService) {
|
||||
return;
|
||||
}
|
||||
@@ -199,38 +232,38 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
}
|
||||
|
||||
// Trigger hover if the we have a link at the position
|
||||
if (this._linkAtPosition(link, position)) {
|
||||
this._currentLink = link;
|
||||
this._currentLinkState = {
|
||||
if (this._linkAtPosition(linkWithState.link, position)) {
|
||||
this._currentLink = linkWithState;
|
||||
this._currentLink.state = {
|
||||
decorations: {
|
||||
underline: link.decorations === undefined ? true : link.decorations.underline,
|
||||
pointerCursor: link.decorations === undefined ? true : link.decorations.pointerCursor
|
||||
underline: linkWithState.link.decorations === undefined ? true : linkWithState.link.decorations.underline,
|
||||
pointerCursor: linkWithState.link.decorations === undefined ? true : linkWithState.link.decorations.pointerCursor
|
||||
},
|
||||
isHovered: true
|
||||
};
|
||||
this._linkHover(this._element, link, this._lastMouseEvent);
|
||||
this._linkHover(this._element, linkWithState.link, this._lastMouseEvent);
|
||||
|
||||
// Add listener for tracking decorations changes
|
||||
link.decorations = {} as ILinkDecorations;
|
||||
Object.defineProperties(link.decorations, {
|
||||
linkWithState.link.decorations = {} as ILinkDecorations;
|
||||
Object.defineProperties(linkWithState.link.decorations, {
|
||||
pointerCursor: {
|
||||
get: () => this._currentLinkState?.decorations.pointerCursor,
|
||||
get: () => this._currentLink?.state?.decorations.pointerCursor,
|
||||
set: v => {
|
||||
if (this._currentLinkState && this._currentLinkState?.decorations.pointerCursor !== v) {
|
||||
this._currentLinkState.decorations.pointerCursor = v;
|
||||
if (this._currentLinkState.isHovered) {
|
||||
if (this._currentLink?.state && this._currentLink.state.decorations.pointerCursor !== v) {
|
||||
this._currentLink.state.decorations.pointerCursor = v;
|
||||
if (this._currentLink.state.isHovered) {
|
||||
this._element?.classList.toggle('xterm-cursor-pointer', v);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
underline: {
|
||||
get: () => this._currentLinkState?.decorations.underline,
|
||||
get: () => this._currentLink?.state?.decorations.underline,
|
||||
set: v => {
|
||||
if (this._currentLinkState && this._currentLinkState?.decorations.underline !== v) {
|
||||
this._currentLinkState.decorations.underline = v;
|
||||
if (this._currentLinkState.isHovered) {
|
||||
this._fireUnderlineEvent(link, v);
|
||||
if (this._currentLink?.state && this._currentLink?.state?.decorations.underline !== v) {
|
||||
this._currentLink.state.decorations.underline = v;
|
||||
if (this._currentLink.state.isHovered) {
|
||||
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 {
|
||||
if (this._currentLinkState) {
|
||||
this._currentLinkState.isHovered = true;
|
||||
if (this._currentLinkState.decorations.underline) {
|
||||
if (this._currentLink?.state) {
|
||||
this._currentLink.state.isHovered = true;
|
||||
if (this._currentLink.state.decorations.underline) {
|
||||
console.log('add underline');
|
||||
this._fireUnderlineEvent(link, true);
|
||||
}
|
||||
if (this._currentLinkState.decorations.pointerCursor) {
|
||||
if (this._currentLink.state.decorations.pointerCursor) {
|
||||
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 {
|
||||
if (this._currentLinkState) {
|
||||
this._currentLinkState.isHovered = false;
|
||||
if (this._currentLinkState.decorations.underline) {
|
||||
if (this._currentLink?.state) {
|
||||
this._currentLink.state.isHovered = false;
|
||||
if (this._currentLink.state.decorations.underline) {
|
||||
console.log('lose underline');
|
||||
this._fireUnderlineEvent(link, false);
|
||||
}
|
||||
if (this._currentLinkState.decorations.pointerCursor) {
|
||||
if (this._currentLink.state.decorations.pointerCursor) {
|
||||
element.classList.remove('xterm-cursor-pointer');
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -264,7 +264,7 @@ export interface IMouseZone {
|
||||
}
|
||||
|
||||
interface ILinkProvider {
|
||||
provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void): void;
|
||||
provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void;
|
||||
}
|
||||
|
||||
interface ILink {
|
||||
|
||||
Vendored
+4
-4
@@ -1100,11 +1100,11 @@ declare module 'xterm' {
|
||||
interface ILinkProvider {
|
||||
/**
|
||||
* Provides a link a buffer position
|
||||
* @param position The position of the buffer that is currently active.
|
||||
* @param callback The callback to be fired with the resulting link or
|
||||
* `undefined` when ready.
|
||||
* @param y The y position of the buffer that is currently active.
|
||||
* @param callback The callback to be fired when ready with the resulting
|
||||
* 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