未验证 提交 8e6a4118 编写于 作者: D Daniel Imms 提交者: GitHub

Merge pull request #92613 from microsoft/tyriar/link_handlers

Implement terminal link handler API
......@@ -1086,6 +1086,22 @@ declare module 'vscode' {
//#endregion
//#region Terminal link handlers https://github.com/microsoft/vscode/issues/91606
export namespace window {
export function registerTerminalLinkHandler(handler: TerminalLinkHandler): Disposable;
}
export interface TerminalLinkHandler {
/**
* @return true when the link was handled (and should not be considered by
* other providers including the default), false when the link was not handled.
*/
handleLink(terminal: Terminal, link: string): ProviderResult<boolean>;
}
//#endregion
//#region Joh -> exclusive document filters
export interface DocumentFilter {
......
......@@ -3,13 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DisposableStore, Disposable } from 'vs/base/common/lifecycle';
import { DisposableStore, Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { IShellLaunchConfig, ITerminalProcessExtHostProxy, ISpawnExtHostProcessRequest, ITerminalDimensions, EXT_HOST_CREATION_DELAY, IAvailableShellsRequest, IDefaultShellAndArgsRequest, IStartExtensionTerminalRequest } from 'vs/workbench/contrib/terminal/common/terminal';
import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, IShellLaunchConfigDto, TerminalLaunchConfig, ITerminalDimensionsDto } from 'vs/workbench/api/common/extHost.protocol';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { URI } from 'vs/base/common/uri';
import { StopWatch } from 'vs/base/common/stopwatch';
import { ITerminalInstanceService, ITerminalService, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal';
import { ITerminalInstanceService, ITerminalService, ITerminalInstance, ITerminalBeforeHandleLinkEvent } from 'vs/workbench/contrib/terminal/browser/terminal';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering';
......@@ -23,6 +23,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
private readonly _terminalProcesses = new Map<number, Promise<ITerminalProcessExtHostProxy>>();
private readonly _terminalProcessesReady = new Map<number, (proxy: ITerminalProcessExtHostProxy) => void>();
private _dataEventTracker: TerminalDataEventTracker | undefined;
private _linkHandler: IDisposable | undefined;
constructor(
extHostContext: IExtHostContext,
......@@ -146,6 +147,22 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
}
}
public $startHandlingLinks(): void {
this._linkHandler?.dispose();
this._linkHandler = this._terminalService.addLinkHandler(this._remoteAuthority || '', e => this._handleLink(e));
}
public $stopHandlingLinks(): void {
this._linkHandler?.dispose();
}
private async _handleLink(e: ITerminalBeforeHandleLinkEvent): Promise<boolean> {
if (!e.terminal) {
return false;
}
return this._proxy.$handleLink(e.terminal.id, e.link);
}
private _onActiveTerminalChanged(terminalId: number | null): void {
this._proxy.$acceptActiveTerminalChanged(terminalId);
}
......
......@@ -564,6 +564,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
}
return extHostTerminalService.createTerminal(nameOrOptions, shellPath, shellArgs);
},
registerTerminalLinkHandler(handler: vscode.TerminalLinkHandler): vscode.Disposable {
checkProposedApiEnabled(extension);
return extHostTerminalService.registerLinkHandler(handler);
},
registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider<any>): vscode.Disposable {
return extHostTreeViews.registerTreeDataProvider(viewId, treeDataProvider, extension);
},
......
......@@ -432,6 +432,8 @@ export interface MainThreadTerminalServiceShape extends IDisposable {
$show(terminalId: number, preserveFocus: boolean): void;
$startSendingDataEvents(): void;
$stopSendingDataEvents(): void;
$startHandlingLinks(): void;
$stopHandlingLinks(): void;
// Process
$sendProcessTitle(terminalId: number, title: string): void;
......@@ -1311,6 +1313,7 @@ export interface ExtHostTerminalServiceShape {
$acceptWorkspacePermissionsChanged(isAllowed: boolean): void;
$getAvailableShells(): Promise<IShellDefinitionDto[]>;
$getDefaultShellAndArgs(useAutomationShell: boolean): Promise<IShellAndArgsDto>;
$handleLink(id: number, link: string): Promise<boolean>;
}
export interface ExtHostSCMShape {
......
......@@ -14,6 +14,7 @@ import { timeout } from 'vs/base/common/async';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering';
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { Disposable as VSCodeDisposable } from './extHostTypes';
export interface IExtHostTerminalService extends ExtHostTerminalServiceShape {
......@@ -34,6 +35,7 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape {
attachPtyToTerminal(id: number, pty: vscode.Pseudoterminal): void;
getDefaultShell(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string;
getDefaultShellArgs(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string[] | string;
registerLinkHandler(handler: vscode.TerminalLinkHandler): vscode.Disposable
}
export const IExtHostTerminalService = createDecorator<IExtHostTerminalService>('IExtHostTerminalService');
......@@ -295,6 +297,9 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ
protected _extensionTerminalAwaitingStart: { [id: number]: { initialDimensions: ITerminalDimensionsDto | undefined } | undefined } = {};
protected _getTerminalPromises: { [id: number]: Promise<ExtHostTerminal> } = {};
private readonly _bufferer: TerminalDataBufferer;
private readonly _linkHandlers: Set<vscode.TerminalLinkHandler> = new Set();
public get activeTerminal(): ExtHostTerminal | undefined { return this._activeTerminal; }
public get terminals(): ExtHostTerminal[] { return this._terminals; }
......@@ -309,8 +314,6 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ
protected readonly _onDidWriteTerminalData: Emitter<vscode.TerminalDataWriteEvent>;
public get onDidWriteTerminalData(): Event<vscode.TerminalDataWriteEvent> { return this._onDidWriteTerminalData && this._onDidWriteTerminalData.event; }
private readonly _bufferer: TerminalDataBufferer;
constructor(
@IExtHostRpcService extHostRpc: IExtHostRpcService
) {
......@@ -535,6 +538,38 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ
return id;
}
public registerLinkHandler(handler: vscode.TerminalLinkHandler): vscode.Disposable {
this._linkHandlers.add(handler);
if (this._linkHandlers.size === 1) {
this._proxy.$startHandlingLinks();
}
return new VSCodeDisposable(() => {
this._linkHandlers.delete(handler);
if (this._linkHandlers.size === 0) {
this._proxy.$stopHandlingLinks();
}
});
}
public async $handleLink(id: number, link: string): Promise<boolean> {
const terminal = this._getTerminalById(id);
if (!terminal) {
return false;
}
// Call each handler synchronously so multiple handlers aren't triggered at once
const it = this._linkHandlers.values();
let next = it.next();
while (!next.done) {
const handled = await next.value.handleLink(terminal, link);
if (handled) {
return true;
}
next = it.next();
}
return false;
}
private _onProcessExit(id: number, exitCode: number | undefined): void {
this._bufferer.stopBuffering(id);
......
......@@ -131,6 +131,14 @@ export interface ITerminalService {
findNext(): void;
findPrevious(): void;
/**
* Link handlers can be registered here to allow intercepting links clicked in the terminal.
* When a link is clicked, the link will be considered handled when the first interceptor
* resolves with true. It will be considered not handled when _all_ link handlers resolve with
* false, or 3 seconds have elapsed.
*/
addLinkHandler(key: string, callback: TerminalLinkHandlerCallback): IDisposable;
selectDefaultWindowsShell(): Promise<void>;
setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void;
......@@ -179,6 +187,18 @@ export enum WindowsShellType {
}
export type TerminalShellType = WindowsShellType | undefined;
export const LINK_INTERCEPT_THRESHOLD = 3000;
export interface ITerminalBeforeHandleLinkEvent {
terminal?: ITerminalInstance;
/** The text of the link */
link: string;
/** Call with whether the link was handled by the interceptor */
resolve(wasHandled: boolean): void;
}
export type TerminalLinkHandlerCallback = (e: ITerminalBeforeHandleLinkEvent) => Promise<boolean>;
export interface ITerminalInstance {
/**
* The ID of the terminal instance, this is an arbitrary number only used to identify the
......@@ -240,6 +260,11 @@ export interface ITerminalInstance {
*/
onExit: Event<number | undefined>;
/**
* Attach a listener to intercept and handle link clicks in the terminal.
*/
onBeforeHandleLink: Event<ITerminalBeforeHandleLinkEvent>;
readonly exitCode: number | undefined;
processReady: Promise<void>;
......
......@@ -30,7 +30,7 @@ import { ansiColorIdentifiers, TERMINAL_BACKGROUND_COLOR, TERMINAL_CURSOR_BACKGR
import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper';
import { TerminalLinkHandler } from 'vs/workbench/contrib/terminal/browser/terminalLinkHandler';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { ITerminalInstanceService, ITerminalInstance, TerminalShellType } from 'vs/workbench/contrib/terminal/browser/terminal';
import { ITerminalInstanceService, ITerminalInstance, TerminalShellType, ITerminalBeforeHandleLinkEvent } from 'vs/workbench/contrib/terminal/browser/terminal';
import { TerminalProcessManager } from 'vs/workbench/contrib/terminal/browser/terminalProcessManager';
import { Terminal as XTermTerminal, IBuffer, ITerminalAddon } from 'xterm';
import { SearchAddon, ISearchOptions } from 'xterm-addon-search';
......@@ -272,6 +272,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
public get onMaximumDimensionsChanged(): Event<void> { return this._onMaximumDimensionsChanged.event; }
private readonly _onFocus = new Emitter<ITerminalInstance>();
public get onFocus(): Event<ITerminalInstance> { return this._onFocus.event; }
private readonly _onBeforeHandleLink = new Emitter<ITerminalBeforeHandleLinkEvent>();
public get onBeforeHandleLink(): Event<ITerminalBeforeHandleLinkEvent> { return this._onBeforeHandleLink.event; }
public constructor(
private readonly _terminalFocusContextKey: IContextKey<boolean>,
......@@ -523,6 +525,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
});
}
this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, xterm, this._processManager, this._configHelper);
this._linkHandler.onBeforeHandleLink(e => {
e.terminal = this;
this._onBeforeHandleLink.fire(e);
});
});
this._commandTrackerAddon = new CommandTrackerAddon();
......
......@@ -16,9 +16,11 @@ import { IFileService } from 'vs/platform/files/common/files';
import { Terminal, ILinkMatcherOptions, IViewportRange } 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 { 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';
const pathPrefix = '(\\.\\.?|\\~)';
const pathSeparatorClause = '\\/';
......@@ -58,7 +60,7 @@ const CUSTOM_LINK_PRIORITY = -1;
/** Lowest */
const LOCAL_LINK_PRIORITY = -2;
export type XtermLinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void;
export type XtermLinkMatcherHandler = (event: MouseEvent, link: string) => Promise<void>;
export type XtermLinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
interface IPath {
......@@ -66,14 +68,28 @@ interface IPath {
normalize(path: string): string;
}
export class TerminalLinkHandler {
private readonly _hoverDisposables = new DisposableStore();
export class TerminalLinkHandler extends DisposableStore {
private _widgetManager: TerminalWidgetManager | undefined;
private _processCwd: string | undefined;
private _gitDiffPreImagePattern: RegExp;
private _gitDiffPostImagePattern: RegExp;
private readonly _tooltipCallback: (event: MouseEvent, uri: string, location: IViewportRange) => boolean | void;
private readonly _leaveCallback: () => void;
private _hasBeforeHandleLinkListeners = false;
protected static _LINK_INTERCEPT_THRESHOLD = LINK_INTERCEPT_THRESHOLD;
public static readonly LINK_INTERCEPT_THRESHOLD = TerminalLinkHandler._LINK_INTERCEPT_THRESHOLD;
private readonly _onBeforeHandleLink = this.add(new Emitter<ITerminalBeforeHandleLinkEvent>({
onFirstListenerAdd: () => this._hasBeforeHandleLinkListeners = true,
onLastListenerRemove: () => this._hasBeforeHandleLinkListeners = false
}));
/**
* Allows intercepting links and handling them outside of the default link handler. When fired
* the listener has a set amount of time to handle the link or the default handler will fire.
* This was designed to only be handled by a single listener.
*/
public get onBeforeHandleLink(): Event<ITerminalBeforeHandleLinkEvent> { return this._onBeforeHandleLink.event; }
constructor(
private _xterm: Terminal,
......@@ -83,8 +99,11 @@ export class TerminalLinkHandler {
@IEditorService private readonly _editorService: IEditorService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService,
@IFileService private readonly _fileService: IFileService
@IFileService private readonly _fileService: IFileService,
@ILogService private readonly _logService: ILogService
) {
super();
// Matches '--- a/src/file1', capturing 'src/file1' in group 1
this._gitDiffPreImagePattern = /^--- a\/(\S*)/;
// Matches '+++ b/src/file1', capturing 'src/file1' in group 1
......@@ -211,19 +230,40 @@ export class TerminalLinkHandler {
this._xterm.registerLinkMatcher(this._gitDiffPostImagePattern, wrappedHandler, options);
}
public dispose(): void {
this._hoverDisposables.dispose();
}
private _wrapLinkHandler(handler: (uri: string) => boolean | void): XtermLinkMatcherHandler {
return (event: MouseEvent, uri: string) => {
protected _wrapLinkHandler(handler: (link: string) => void): XtermLinkMatcherHandler {
return async (event: MouseEvent, link: string) => {
// Prevent default electron link handling so Alt+Click mode works normally
event.preventDefault();
// Require correct modifier on click
if (!this._isLinkActivationModifierDown(event)) {
return false;
return;
}
return handler(uri);
// Allow the link to be intercepted if there are listeners
if (this._hasBeforeHandleLinkListeners) {
const wasHandled = await new Promise<boolean>(r => {
const timeoutId = setTimeout(() => {
canceled = true;
this._logService.error('An extension intecepted a terminal link but did not return');
r(false);
}, TerminalLinkHandler.LINK_INTERCEPT_THRESHOLD);
let canceled = false;
const resolve = (handled: boolean) => {
if (!canceled) {
clearTimeout(timeoutId);
r(handled);
}
};
this._onBeforeHandleLink.fire({ link, resolve });
});
if (!wasHandled) {
handler(link);
}
return;
}
// Just call the handler if there is no before listener
handler(link);
};
}
......@@ -244,18 +284,17 @@ export class TerminalLinkHandler {
return this._gitDiffPostImagePattern;
}
private _handleLocalLink(link: string): PromiseLike<any> {
return this._resolvePath(link).then(resolvedLink => {
if (!resolvedLink) {
return Promise.resolve(null);
}
const lineColumnInfo: LineColumnInfo = this.extractLineColumnInfo(link);
const selection: ITextEditorSelection = {
startLineNumber: lineColumnInfo.lineNumber,
startColumn: lineColumnInfo.columnNumber
};
return this._editorService.openEditor({ resource: resolvedLink, options: { pinned: true, selection } });
});
private async _handleLocalLink(link: string): Promise<void> {
const resolvedLink = await this._resolvePath(link);
if (!resolvedLink) {
return;
}
const lineColumnInfo: LineColumnInfo = this.extractLineColumnInfo(link);
const selection: ITextEditorSelection = {
startLineNumber: lineColumnInfo.lineNumber,
startColumn: lineColumnInfo.columnNumber
};
await this._editorService.openEditor({ resource: resolvedLink, options: { pinned: true, selection } });
}
private _validateLocalLink(link: string, callback: (isValid: boolean) => void): void {
......@@ -270,7 +309,7 @@ export class TerminalLinkHandler {
this._openerService.open(url, { allowTunneling: !!(this._processManager && this._processManager.remoteAuthority) });
}
private _isLinkActivationModifierDown(event: MouseEvent): boolean {
protected _isLinkActivationModifierDown(event: MouseEvent): boolean {
const editorConf = this._configurationService.getValue<{ multiCursorModifier: 'ctrlCmd' | 'alt' }>('editor');
if (editorConf.multiCursorModifier === 'ctrlCmd') {
return !!event.altKey;
......@@ -346,19 +385,19 @@ export class TerminalLinkHandler {
return link;
}
private _resolvePath(link: string): PromiseLike<URI | null> {
private async _resolvePath(link: string): Promise<URI | undefined> {
if (!this._processManager) {
throw new Error('Process manager is required');
}
const preprocessedLink = this._preprocessPath(link);
if (!preprocessedLink) {
return Promise.resolve(null);
return undefined;
}
const linkUrl = this.extractLinkUrl(preprocessedLink);
if (!linkUrl) {
return Promise.resolve(null);
return undefined;
}
try {
......@@ -373,18 +412,20 @@ export class TerminalLinkHandler {
uri = URI.file(linkUrl);
}
return this._fileService.resolve(uri).then(stat => {
try {
const stat = await this._fileService.resolve(uri);
if (stat.isDirectory) {
return null;
return undefined;
}
return uri;
}).catch(() => {
}
catch (e) {
// Does not exist
return null;
});
return undefined;
}
} catch {
// Errors in parsing the path
return Promise.resolve(null);
return undefined;
}
}
......
......@@ -15,7 +15,7 @@ import { TerminalTab } from 'vs/workbench/contrib/terminal/browser/terminalTab';
import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminalInstance';
import { ITerminalService, ITerminalInstance, ITerminalTab, TerminalShellType, WindowsShellType } from 'vs/workbench/contrib/terminal/browser/terminal';
import { ITerminalService, ITerminalInstance, ITerminalTab, TerminalShellType, WindowsShellType, TerminalLinkHandlerCallback, LINK_INTERCEPT_THRESHOLD } from 'vs/workbench/contrib/terminal/browser/terminal';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper';
import { IQuickInputService, IQuickPickItem, IPickOptions } from 'vs/platform/quickinput/common/quickInput';
......@@ -30,6 +30,7 @@ import { IOpenFileRequest } from 'vs/platform/windows/common/windows';
import { find } from 'vs/base/common/arrays';
import { timeout } from 'vs/base/common/async';
import { IViewsService } from 'vs/workbench/common/views';
import { IDisposable } from 'vs/base/common/lifecycle';
interface IExtHostReadyEntry {
promise: Promise<void>;
......@@ -50,6 +51,7 @@ export class TerminalService implements ITerminalService {
private _findState: FindReplaceState;
private _extHostsReady: { [authority: string]: IExtHostReadyEntry | undefined } = {};
private _activeTabIndex: number;
private _linkHandlers: { [key: string]: TerminalLinkHandlerCallback } = {};
public get activeTabIndex(): number { return this._activeTabIndex; }
public get terminalInstances(): ITerminalInstance[] { return this._terminalInstances; }
......@@ -411,6 +413,50 @@ export class TerminalService implements ITerminalService {
instance.addDisposable(instance.onDimensionsChanged(() => this._onInstanceDimensionsChanged.fire(instance)));
instance.addDisposable(instance.onMaximumDimensionsChanged(() => this._onInstanceMaximumDimensionsChanged.fire(instance)));
instance.addDisposable(instance.onFocus(this._onActiveInstanceChanged.fire, this._onActiveInstanceChanged));
instance.addDisposable(instance.onBeforeHandleLink(async e => {
// No link handlers have been registered
const keys = Object.keys(this._linkHandlers);
if (keys.length === 0) {
e.resolve(false);
return;
}
// Fire each link interceptor and wait for either a true, all false or the cancel time
let resolved = false;
const promises: Promise<boolean>[] = [];
const timeout = setTimeout(() => {
resolved = true;
e.resolve(false);
}, LINK_INTERCEPT_THRESHOLD);
for (let i = 0; i < keys.length; i++) {
const p = this._linkHandlers[keys[i]](e);
p.then(handled => {
if (!resolved && handled) {
resolved = true;
clearTimeout(timeout);
e.resolve(true);
}
});
promises.push(p);
}
await Promise.all(promises);
if (!resolved) {
resolved = true;
clearTimeout(timeout);
e.resolve(false);
}
}));
}
public addLinkHandler(key: string, callback: TerminalLinkHandlerCallback): IDisposable {
this._linkHandlers[key] = callback;
return {
dispose: () => {
if (this._linkHandlers[key] === callback) {
delete this._linkHandlers[key];
}
}
};
}
private _getTabForInstance(instance: ITerminalInstance): ITerminalTab | undefined {
......
......@@ -5,11 +5,12 @@
import * as assert from 'assert';
import { OperatingSystem } from 'vs/base/common/platform';
import { TerminalLinkHandler, LineColumnInfo } from 'vs/workbench/contrib/terminal/browser/terminalLinkHandler';
import { TerminalLinkHandler, LineColumnInfo, XtermLinkMatcherHandler } from 'vs/workbench/contrib/terminal/browser/terminalLinkHandler';
import * as strings from 'vs/base/common/strings';
import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { Event } from 'vs/base/common/event';
import { ITerminalConfigHelper } from 'vs/workbench/contrib/terminal/common/terminal';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
class TestTerminalLinkHandler extends TerminalLinkHandler {
public get localLinkRegex(): RegExp {
......@@ -24,6 +25,13 @@ class TestTerminalLinkHandler extends TerminalLinkHandler {
public preprocessPath(link: string): string | null {
return this._preprocessPath(link);
}
protected _isLinkActivationModifierDown(event: MouseEvent): boolean {
return true;
}
public wrapLinkHandler(handler: (link: string) => void): XtermLinkMatcherHandler {
TerminalLinkHandler._LINK_INTERCEPT_THRESHOLD = 0;
return this._wrapLinkHandler(handler);
}
}
class TestXterm {
......@@ -81,7 +89,7 @@ suite('Workbench - TerminalLinkHandler', () => {
const terminalLinkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
os: OperatingSystem.Windows,
userHome: ''
} as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!);
} as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!, null!);
function testLink(link: string, linkUrl: string, lineNo?: string, columnNo?: string) {
assert.equal(terminalLinkHandler.extractLinkUrl(link), linkUrl);
assert.equal(terminalLinkHandler.extractLinkUrl(`:${link}:`), linkUrl);
......@@ -157,7 +165,7 @@ suite('Workbench - TerminalLinkHandler', () => {
const terminalLinkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
os: OperatingSystem.Linux,
userHome: ''
} as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!);
} as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!, null!);
function testLink(link: string, linkUrl: string, lineNo?: string, columnNo?: string) {
assert.equal(terminalLinkHandler.extractLinkUrl(link), linkUrl);
assert.equal(terminalLinkHandler.extractLinkUrl(`:${link}:`), linkUrl);
......@@ -225,7 +233,7 @@ suite('Workbench - TerminalLinkHandler', () => {
const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
os: OperatingSystem.Windows,
userHome: 'C:\\Users\\Me'
} as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!);
} as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!, null!);
linkHandler.processCwd = 'C:\\base';
assert.equal(linkHandler.preprocessPath('./src/file1'), 'C:\\base\\src\\file1');
......@@ -238,7 +246,7 @@ suite('Workbench - TerminalLinkHandler', () => {
const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
os: OperatingSystem.Windows,
userHome: 'C:\\Users\\M e'
} as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!);
} as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!, null!);
linkHandler.processCwd = 'C:\\base dir';
assert.equal(linkHandler.preprocessPath('./src/file1'), 'C:\\base dir\\src\\file1');
......@@ -252,7 +260,7 @@ suite('Workbench - TerminalLinkHandler', () => {
const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
os: OperatingSystem.Linux,
userHome: '/home/me'
} as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!);
} as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!, null!);
linkHandler.processCwd = '/base';
assert.equal(linkHandler.preprocessPath('./src/file1'), '/base/src/file1');
......@@ -265,7 +273,7 @@ suite('Workbench - TerminalLinkHandler', () => {
const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
os: OperatingSystem.Linux,
userHome: '/home/me'
} as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!);
} as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!, null!);
assert.equal(linkHandler.preprocessPath('./src/file1'), null);
assert.equal(linkHandler.preprocessPath('src/file2'), null);
......@@ -279,7 +287,7 @@ suite('Workbench - TerminalLinkHandler', () => {
const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
os: OperatingSystem.Linux,
userHome: ''
} as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!);
} as any, testConfigHelper, null!, null!, null!, new MockTerminalInstanceService(), null!, null!);
function assertAreGoodMatches(matches: RegExpMatchArray | null) {
if (matches) {
......@@ -302,4 +310,35 @@ suite('Workbench - TerminalLinkHandler', () => {
assert.equal(linkHandler.gitDiffLinkPostImageRegex.test('+++ /dev/null'), false);
assert.equal(linkHandler.gitDiffLinkPostImageRegex.test('+++ /dev/null '), false);
});
suite.only('wrapLinkHandler', () => {
const nullMouseEvent: any = Object.freeze({ preventDefault: () => { } });
test('should allow intercepting of links with onBeforeHandleLink', async () => {
const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
os: OperatingSystem.Linux,
userHome: ''
} as any, testConfigHelper, null!, null!, new TestConfigurationService(), new MockTerminalInstanceService(), null!, null!);
linkHandler.onBeforeHandleLink(e => {
if (e.link === 'https://www.microsoft.com') {
intercepted = true;
e.resolve(true);
}
e.resolve(false);
});
const wrappedHandler = linkHandler.wrapLinkHandler(() => defaultHandled = true);
let defaultHandled = false;
let intercepted = false;
await wrappedHandler(nullMouseEvent, 'https://www.visualstudio.com');
assert.equal(intercepted, false);
assert.equal(defaultHandled, true);
defaultHandled = false;
intercepted = false;
await wrappedHandler(nullMouseEvent, 'https://www.microsoft.com');
assert.equal(intercepted, true);
assert.equal(defaultHandled, false);
});
});
});
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册