Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adopt terminal link provider API #90336

Merged
merged 20 commits into from
Apr 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
623c8af
Adopt terminal link provider api
jmbockhorst Feb 12, 2020
bfdfcc6
Merge branch 'master' into linkProvider
Tyriar Feb 13, 2020
d82305e
Fix failing tests
jmbockhorst Feb 13, 2020
0cb2785
Merge branch 'linkProvider' of https://github.com/jmbockhorst/vscode …
jmbockhorst Feb 13, 2020
d124b68
Fix link end position
jmbockhorst Feb 13, 2020
12fed21
Merge branch 'master' into linkProvider
Tyriar Feb 25, 2020
dedad58
Merge branch 'master' of https://github.com/Microsoft/vscode into lin…
jmbockhorst Mar 13, 2020
bcb7290
Merge branch 'master' of https://github.com/Microsoft/vscode into lin…
jmbockhorst Mar 13, 2020
e28a234
Only close correct widgets
jmbockhorst Mar 13, 2020
99443cb
Merge branch 'linkProvider' of https://github.com/jmbockhorst/vscode …
jmbockhorst Mar 13, 2020
870a07d
Add delay to link provider hovers for widgets
jmbockhorst Mar 13, 2020
fae5347
Merge remote-tracking branch 'origin/master' into pr/jmbockhorst/90336
Tyriar Apr 11, 2020
2bc275e
Merge branch 'master' into linkProvider
Tyriar Apr 11, 2020
3aef38c
Elaborate in setting description
Tyriar Apr 11, 2020
6487bba
Remove activate callback prop
Tyriar Apr 11, 2020
bf2dd20
Remove logs
Tyriar Apr 12, 2020
1db2966
Consider wide characters in link positions
Tyriar Apr 12, 2020
c12076a
Cover case where a wide character lands on last cell in row
Tyriar Apr 12, 2020
e5cffca
Move web link provider into its own file and add provider tests
Tyriar Apr 12, 2020
098d2b2
Merge branch 'master' into linkProvider
Tyriar Apr 12, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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