Skip to content

Commit

Permalink
Adopt terminal link provider api
Browse files Browse the repository at this point in the history
  • Loading branch information
jmbockhorst committed Feb 12, 2020
1 parent 718331d commit 623c8af
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,12 @@ configurationRegistry.registerConfiguration({
],
default: '11',
description: nls.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: nls.localize('terminal.integrated.experimentalLinkProvider', "An experimental setting that will enable the use of VS Code's shared link detection system in the terminal."),
type: 'boolean',
default: false
},
}
});

Expand Down
192 changes: 184 additions & 8 deletions src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,22 @@

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, ILinkProvider, IBufferCellPosition, ILink, IBufferRange, ITerminalAddon } from 'xterm';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { posix, win32 } from 'vs/base/common/path';
import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { OperatingSystem, isMacintosh } from 'vs/base/common/platform';
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
import { ILinkComputerTarget, LinkComputer } from 'vs/editor/common/modes/linkComputer';
import { IRange } from 'vs/editor/common/core/range';

const pathPrefix = '(\\.\\.?|\\~)';
const pathSeparatorClause = '\\/';
Expand Down Expand Up @@ -66,14 +68,39 @@ interface IPath {
normalize(path: string): string;
}

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

getLineCount(): number {
return 1;
}

getLineContent(lineNumber: number): string {
let line = '';

for (let i = this._lineStart; i <= this._lineEnd; i++) {
line += this._xterm.buffer.getLine(i)?.translateToString();
}
return line;
}
}

export class TerminalLinkHandler {
private readonly _hoverDisposables = new DisposableStore();
private _widgetManager: TerminalWidgetManager | undefined;
private _processCwd: string | undefined;
private _gitDiffPreImagePattern: RegExp;
private _gitDiffPostImagePattern: RegExp;
private readonly _activateCallback: (event: MouseEvent, uri: string) => void;
private readonly _tooltipCallback: (event: MouseEvent, uri: string, location: IViewportRange) => boolean | void;
private readonly _leaveCallback: () => void;
private _linkMatchers: number[] = [];
private _webLinksAddon: ITerminalAddon | undefined;
private _linkProvider: IDisposable | undefined;

constructor(
private _xterm: Terminal,
Expand All @@ -90,6 +117,12 @@ export class TerminalLinkHandler {
// Matches '+++ b/src/file1', capturing 'src/file1' in group 1
this._gitDiffPostImagePattern = /^\+\+\+ b\/(\S*)/;

this._activateCallback = (e: MouseEvent, uri: string) => {
if (this._isLinkActivationModifierDown(e)) {
this._handleHypertextLink(uri);
}
};

this._tooltipCallback = (e: MouseEvent, uri: string, location: IViewportRange) => {
if (!this._widgetManager) {
return;
Expand Down Expand Up @@ -134,6 +167,26 @@ export class TerminalLinkHandler {
}
};

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 @@ -143,6 +196,14 @@ export class TerminalLinkHandler {
}
}

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 @@ -173,26 +234,27 @@ export class TerminalLinkHandler {
const wrappedHandler = this._wrapLinkHandler(uri => {
this._handleHypertextLink(uri);
});
this._xterm.loadAddon(new WebLinksAddon(wrappedHandler, {
this._webLinksAddon = new WebLinksAddon(wrappedHandler, {
validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateWebLink(uri, callback),
tooltipCallback: this._tooltipCallback,
leaveCallback: this._leaveCallback,
willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e)
}));
});
this._xterm.loadAddon(this._webLinksAddon);
});
}

public registerLocalLinkHandler(): void {
const wrappedHandler = this._wrapLinkHandler(url => {
this._handleLocalLink(url);
});
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: this._tooltipCallback,
leaveCallback: this._leaveCallback,
willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e),
priority: LOCAL_LINK_PRIORITY
});
}));
}

public registerGitDiffLinkHandlers(): void {
Expand All @@ -207,8 +269,12 @@ export class TerminalLinkHandler {
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 {
this._linkProvider = this._xterm.registerLinkProvider(new TerminalLinkProvider(this._xterm, this._activateCallback, this._tooltipCallback, this._leaveCallback));
}

public dispose(): void {
Expand Down Expand Up @@ -441,3 +507,113 @@ export interface LineColumnInfo {
lineNumber: number;
columnNumber: number;
}

class TerminalLinkProvider 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;

while (this._xterm.buffer.getLine(startLine)?.isWrapped) {
startLine--;
}

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

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

let found = false;
links.forEach(link => {
const range = this._convertLinkRangeToBuffer(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) => {
this._tooltipCallback(event, text, this._convertBufferRangeToViewport(range));
},
leave: () => {
this._leaveCallback();
}
});
}
});

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

private _convertLinkRangeToBuffer(range: IRange, startLine: number) {
const bufferRange: IBufferRange = {
start: {
x: range.startColumn,
y: range.startLineNumber + startLine
},
end: {
x: range.endColumn,
y: range.endLineNumber + startLine
}
};

const bufferWidth = this._xterm.cols;

// 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;
}

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;
}

private _convertBufferRangeToViewport(bufferRange: IBufferRange): IViewportRange {
return {
start: {
x: bufferRange.start.x - 1,
y: bufferRange.start.y - this._xterm.buffer.viewportY - 1
},
end: {
x: bufferRange.end.x - 1,
y: bufferRange.end.y - this._xterm.buffer.viewportY - 1
}
};
}
}
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 @@ -123,6 +123,7 @@ export interface ITerminalConfiguration {
experimentalUseTitleEvent: boolean;
enableFileLinks: boolean;
unicodeVersion: '6' | '11';
experimentalLinkProvider: boolean;
}

export interface ITerminalConfigHelper {
Expand Down

0 comments on commit 623c8af

Please sign in to comment.