Skip to content

Commit

Permalink
Merge pull request #90336 from jmbockhorst/linkProvider
Browse files Browse the repository at this point in the history
Adopt terminal link provider API
  • Loading branch information
Tyriar committed Apr 12, 2020
2 parents a656a72 + 098d2b2 commit 304fc63
Show file tree
Hide file tree
Showing 6 changed files with 518 additions and 14 deletions.
66 changes: 54 additions & 12 deletions src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,23 @@

import * as nls from 'vs/nls';
import { URI } from 'vs/base/common/uri';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { TerminalWidgetManager, WidgetVerticalAlignment } from 'vs/workbench/contrib/terminal/browser/terminalWidgetManager';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ITerminalProcessManager, ITerminalConfigHelper } from 'vs/workbench/contrib/terminal/common/terminal';
import { ITextEditorSelection } from 'vs/platform/editor/common/editor';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IFileService } from 'vs/platform/files/common/files';
import { Terminal, ILinkMatcherOptions, IViewportRange } from 'xterm';
import { Terminal, ILinkMatcherOptions, IViewportRange, ITerminalAddon } from 'xterm';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { posix, win32 } from 'vs/base/common/path';
import { ITerminalInstanceService, ITerminalBeforeHandleLinkEvent, LINK_INTERCEPT_THRESHOLD } from 'vs/workbench/contrib/terminal/browser/terminal';
import { OperatingSystem, isMacintosh } from 'vs/base/common/platform';
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
import { Emitter, Event } from 'vs/base/common/event';
import { ILogService } from 'vs/platform/log/common/log';
import { TerminalWebLinkProvider } from 'vs/workbench/contrib/terminal/browser/terminalWebLinkProvider';

const pathPrefix = '(\\.\\.?|\\~)';
const pathSeparatorClause = '\\/';
Expand Down Expand Up @@ -75,6 +76,9 @@ export class TerminalLinkHandler extends DisposableStore {
private _gitDiffPostImagePattern: RegExp;
private readonly _tooltipCallback: (event: MouseEvent, uri: string, location: IViewportRange, linkHandler: (url: string) => void) => boolean | void;
private readonly _leaveCallback: () => void;
private _linkMatchers: number[] = [];
private _webLinksAddon: ITerminalAddon | undefined;
private _linkProvider: IDisposable | undefined;
private _hasBeforeHandleLinkListeners = false;

protected static _LINK_INTERCEPT_THRESHOLD = LINK_INTERCEPT_THRESHOLD;
Expand Down Expand Up @@ -153,6 +157,26 @@ export class TerminalLinkHandler extends DisposableStore {
}
};

if (this._configHelper.config.experimentalLinkProvider) {
this.registerLinkProvider();
} else {
this._registerLinkMatchers();
}

this._configurationService?.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('terminal.integrated.experimentalLinkProvider')) {
if (this._configHelper.config.experimentalLinkProvider) {
this._deregisterLinkMatchers();
this.registerLinkProvider();
} else {
this._linkProvider?.dispose();
this._registerLinkMatchers();
}
}
});
}

private _registerLinkMatchers() {
this.registerWebLinkHandler();
if (this._processManager) {
if (this._configHelper.config.enableFileLinks) {
Expand All @@ -162,6 +186,14 @@ export class TerminalLinkHandler extends DisposableStore {
}
}

private _deregisterLinkMatchers() {
this._webLinksAddon?.dispose();

this._linkMatchers.forEach(matcherId => {
this._xterm.deregisterLinkMatcher(matcherId);
});
}

public setWidgetManager(widgetManager: TerminalWidgetManager): void {
this._widgetManager = widgetManager;
}
Expand Down Expand Up @@ -192,18 +224,19 @@ export class TerminalLinkHandler extends DisposableStore {
if (!this._xterm) {
return;
}
const wrappedHandler = this._wrapLinkHandler(uri => {
this._handleHypertextLink(uri);
const wrappedHandler = this._wrapLinkHandler(link => {
this._handleHypertextLink(link);
});
const tooltipCallback = (event: MouseEvent, uri: string, location: IViewportRange) => {
this._tooltipCallback(event, uri, location, this._handleHypertextLink.bind(this));
};
this._xterm.loadAddon(new WebLinksAddon(wrappedHandler, {
validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateWebLink(uri, callback),
this._webLinksAddon = new WebLinksAddon(wrappedHandler, {
validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateWebLink(callback),
tooltipCallback,
leaveCallback: this._leaveCallback,
willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e)
}));
});
this._xterm.loadAddon(this._webLinksAddon);
});
}

Expand All @@ -214,13 +247,13 @@ export class TerminalLinkHandler extends DisposableStore {
const tooltipCallback = (event: MouseEvent, uri: string, location: IViewportRange) => {
this._tooltipCallback(event, uri, location, this._handleLocalLink.bind(this));
};
this._xterm.registerLinkMatcher(this._localLinkRegex, wrappedHandler, {
this._linkMatchers.push(this._xterm.registerLinkMatcher(this._localLinkRegex, wrappedHandler, {
validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateLocalLink(uri, callback),
tooltipCallback,
leaveCallback: this._leaveCallback,
willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e),
priority: LOCAL_LINK_PRIORITY
});
}));
}

public registerGitDiffLinkHandlers(): void {
Expand All @@ -238,8 +271,17 @@ export class TerminalLinkHandler extends DisposableStore {
willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e),
priority: LOCAL_LINK_PRIORITY
};
this._xterm.registerLinkMatcher(this._gitDiffPreImagePattern, wrappedHandler, options);
this._xterm.registerLinkMatcher(this._gitDiffPostImagePattern, wrappedHandler, options);
this._linkMatchers.push(this._xterm.registerLinkMatcher(this._gitDiffPreImagePattern, wrappedHandler, options));
this._linkMatchers.push(this._xterm.registerLinkMatcher(this._gitDiffPostImagePattern, wrappedHandler, options));
}

public registerLinkProvider(): void {
// Web links
const tooltipCallback = (event: MouseEvent, link: string, location: IViewportRange) => {
this._tooltipCallback(event, link, location, this._handleHypertextLink.bind(this, link));
};
const wrappedActivateCallback = this._wrapLinkHandler(this._handleHypertextLink.bind(this));
this._linkProvider = this._xterm.registerLinkProvider(new TerminalWebLinkProvider(this._xterm, wrappedActivateCallback, tooltipCallback, this._leaveCallback));
}

protected _wrapLinkHandler(handler: (link: string) => void): XtermLinkMatcherHandler {
Expand Down Expand Up @@ -313,7 +355,7 @@ export class TerminalLinkHandler extends DisposableStore {
this._resolvePath(link).then(resolvedLink => callback(!!resolvedLink));
}

private _validateWebLink(link: string, callback: (isValid: boolean) => void): void {
private _validateWebLink(callback: (isValid: boolean) => void): void {
callback(true);
}

Expand Down
192 changes: 192 additions & 0 deletions src/vs/workbench/contrib/terminal/browser/terminalWebLinkProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Terminal, IViewportRange, ILinkProvider, IBufferCellPosition, ILink, IBufferRange, IBuffer, IBufferLine } from 'xterm';
import { ILinkComputerTarget, LinkComputer } from 'vs/editor/common/modes/linkComputer';
import { IRange } from 'vs/editor/common/core/range';


export class TerminalWebLinkProvider implements ILinkProvider {
private _linkComputerTarget: ILinkComputerTarget | undefined;

constructor(
private readonly _xterm: Terminal,
private readonly _activateCallback: (event: MouseEvent, uri: string) => void,
private readonly _tooltipCallback: (event: MouseEvent, uri: string, location: IViewportRange) => boolean | void,
private readonly _leaveCallback: () => void
) {
}

public provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void): void {
let startLine = position.y - 1;
let endLine = startLine;

const lines: IBufferLine[] = [
this._xterm.buffer.active.getLine(startLine)!
];

while (this._xterm.buffer.active.getLine(startLine)?.isWrapped) {
lines.unshift(this._xterm.buffer.active.getLine(startLine - 1)!);
startLine--;
}

while (this._xterm.buffer.active.getLine(endLine + 1)?.isWrapped) {
lines.push(this._xterm.buffer.active.getLine(endLine + 1)!);
endLine++;
}

this._linkComputerTarget = new TerminalLinkAdapter(this._xterm, startLine, endLine);
const links = LinkComputer.computeLinks(this._linkComputerTarget);

let found = false;
links.forEach(link => {
const range = convertLinkRangeToBuffer(lines, this._xterm.cols, link.range, startLine);

// Check if the link if within the mouse position
if (this._positionIsInRange(position, range)) {
found = true;

callback({
text: link.url?.toString() || '',
range,
activate: (event: MouseEvent, text: string) => {
this._activateCallback(event, text);
},
hover: (event: MouseEvent, text: string) => {
setTimeout(() => {
this._tooltipCallback(event, text, convertBufferRangeToViewport(range, this._xterm.buffer.active.viewportY));
}, 200);
},
leave: () => {
this._leaveCallback();
}
});
}
});

if (!found) {
callback(undefined);
}
}

private _positionIsInRange(position: IBufferCellPosition, range: IBufferRange): boolean {
if (position.y < range.start.y || position.y > range.end.y) {
return false;
}
if (position.y === range.start.y && position.x < range.start.x) {
return false;
}
if (position.y === range.end.y && position.x > range.end.x) {
return false;
}
return true;
}
}

export function convertLinkRangeToBuffer(lines: IBufferLine[], bufferWidth: number, range: IRange, startLine: number) {
const bufferRange: IBufferRange = {
start: {
x: range.startColumn,
y: range.startLineNumber + startLine
},
end: {
x: range.endColumn - 1,
y: range.endLineNumber + startLine
}
};

// Shift start range right for each wide character before the link
let startOffset = 0;
const startWrappedLineCount = Math.ceil(range.startColumn / bufferWidth);
for (let y = 0; y < startWrappedLineCount; y++) {
const lineLength = Math.min(bufferWidth, range.startColumn - y * bufferWidth);
let lineOffset = 0;
const line = lines[y];
for (let x = 0; x < Math.min(bufferWidth, lineLength + lineOffset); x++) {
const width = line.getCell(x)?.getWidth();
if (width === 2) {
lineOffset++;
}
}
startOffset += lineOffset;
}

// Shift end range right for each wide character inside the link
let endOffset = 0;
const endWrappedLineCount = Math.ceil(range.endColumn / bufferWidth);
for (let y = startWrappedLineCount - 1; y < endWrappedLineCount; y++) {
const start = (y === startWrappedLineCount - 1 ? (range.startColumn + startOffset) % bufferWidth : 0);
const lineLength = Math.min(bufferWidth, range.endColumn + startOffset - y * bufferWidth);
const startLineOffset = (y === startWrappedLineCount - 1 ? startOffset : 0);
let lineOffset = 0;
const line = lines[y];
for (let x = start; x < Math.min(bufferWidth, lineLength + lineOffset + startLineOffset); x++) {
const cell = line.getCell(x)!;
const width = cell.getWidth();
// Offset for 0 cells following wide characters
if (width === 2) {
lineOffset++;
}
// Offset for early wrapping when the last cell in row is a wide character
if (x === bufferWidth - 1 && cell.getChars() === '') {
lineOffset++;
}
}
endOffset += lineOffset;
}

// Apply the width character offsets to the result
bufferRange.start.x += startOffset;
bufferRange.end.x += startOffset + endOffset;

// Convert back to wrapped lines
while (bufferRange.start.x > bufferWidth) {
bufferRange.start.x -= bufferWidth;
bufferRange.start.y++;
}
while (bufferRange.end.x > bufferWidth) {
bufferRange.end.x -= bufferWidth;
bufferRange.end.y++;
}

return bufferRange;
}

function convertBufferRangeToViewport(bufferRange: IBufferRange, viewportY: number): IViewportRange {
return {
start: {
x: bufferRange.start.x - 1,
y: bufferRange.start.y - viewportY - 1
},
end: {
x: bufferRange.end.x - 1,
y: bufferRange.end.y - viewportY - 1
}
};
}

class TerminalLinkAdapter implements ILinkComputerTarget {
constructor(
private _xterm: Terminal,
private _lineStart: number,
private _lineEnd: number
) { }

getLineCount(): number {
return 1;
}

getLineContent(): string {
return getXtermLineContent(this._xterm.buffer.active, this._lineStart, this._lineEnd);
}
}

function getXtermLineContent(buffer: IBuffer, lineStart: number, lineEnd: number): string {
let line = '';
for (let i = lineStart; i <= lineEnd; i++) {
line += buffer.getLine(i)?.translateToString(true);
}
return line;
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export class TerminalWidgetManager implements IDisposable {
}

public showMessage(left: number, y: number, text: IMarkdownString, verticalAlignment: WidgetVerticalAlignment = WidgetVerticalAlignment.Bottom, linkHandler: (url: string) => void): void {
if (!this._container) {
if (!this._container || this._messageWidget?.mouseOver) {
return;
}
dispose(this._messageWidget);
Expand All @@ -61,8 +61,9 @@ export class TerminalWidgetManager implements IDisposable {

public closeMessage(): void {
this._messageListeners.clear();
const currentWidget = this._messageWidget;
setTimeout(() => {
if (this._messageWidget && !this._messageWidget.mouseOver) {
if (this._messageWidget && !this._messageWidget.mouseOver && this._messageWidget === currentWidget) {
this._messageListeners.add(MessageWidget.fadeOut(this._messageWidget));
}
}, 50);
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/contrib/terminal/common/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export interface ITerminalConfiguration {
experimentalUseTitleEvent: boolean;
enableFileLinks: boolean;
unicodeVersion: '6' | '11';
experimentalLinkProvider: boolean;
}

export interface ITerminalConfigHelper {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,11 @@ export const terminalConfiguration: IConfigurationNode = {
],
default: '11',
description: localize('terminal.integrated.unicodeVersion', "Controls what version of unicode to use when evaluating the width of characters in the terminal. If you experience emoji or other wide characters not taking up the right amount of space or backspace either deleting too much or too little then you may want to try tweaking this setting.")
},
'terminal.integrated.experimentalLinkProvider': {
description: localize('terminal.integrated.experimentalLinkProvider', "An experimental setting that aims to improve link detection in the terminal by improving when links are detected and by enabling shared link detection with the editor. Currently this only supports web links."),
type: 'boolean',
default: false
}
}
};
Expand Down
Loading

0 comments on commit 304fc63

Please sign in to comment.