mirror of
https://github.com/Microsoft/vscode
synced 2024-10-30 04:17:37 +00:00
Merge pull request #141755 from microsoft/tyriar/141743
Terminal link refactors
This commit is contained in:
commit
1b0574e02c
19 changed files with 1744 additions and 786 deletions
98
src/vs/workbench/contrib/terminal/browser/links/links.ts
Normal file
98
src/vs/workbench/contrib/terminal/browser/links/links.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IBufferLine, IBufferRange, Terminal } from 'xterm';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
/**
|
||||
* A link detector can search for and return links within the xterm.js buffer. A single link
|
||||
* detector can return multiple links of differing types.
|
||||
*/
|
||||
export interface ITerminalLinkDetector {
|
||||
/**
|
||||
* The xterm.js instance this detector belongs to.
|
||||
*/
|
||||
readonly xterm: Terminal;
|
||||
|
||||
/**
|
||||
* Detects links within the _wrapped_ line range provided and returns them as an array.
|
||||
*
|
||||
* @param lines The individual buffer lines that make up the wrapped line.
|
||||
* @param startLine The start of the wrapped line. This _will not_ be validated that it is
|
||||
* indeed the start of a wrapped line.
|
||||
* @param endLine The end of the wrapped line. This _will not_ be validated that it is indeed
|
||||
* the end of a wrapped line.
|
||||
*/
|
||||
detect(lines: IBufferLine[], startLine: number, endLine: number): ITerminalSimpleLink[] | Promise<ITerminalSimpleLink[]>;
|
||||
}
|
||||
|
||||
export interface ITerminalSimpleLink {
|
||||
/**
|
||||
* The text of the link.
|
||||
*/
|
||||
text: string;
|
||||
|
||||
/**
|
||||
* The URI of the link if it has been resolved.
|
||||
*/
|
||||
uri?: URI;
|
||||
|
||||
/**
|
||||
* A hover label to override the default for the type.
|
||||
*/
|
||||
label?: string;
|
||||
|
||||
/**
|
||||
* The buffer range of the link.
|
||||
*/
|
||||
readonly bufferRange: IBufferRange;
|
||||
|
||||
/**
|
||||
* The type of link, which determines how it is handled when activated.
|
||||
*/
|
||||
readonly type: TerminalLinkType;
|
||||
}
|
||||
|
||||
export type TerminalLinkType = TerminalBuiltinLinkType | ITerminalExternalLinkType;
|
||||
|
||||
export const enum TerminalBuiltinLinkType {
|
||||
/**
|
||||
* The link is validated to be a file on the file system and will open an editor.
|
||||
*/
|
||||
LocalFile,
|
||||
|
||||
/**
|
||||
* The link is validated to be a folder on the file system and is outside the workspace. It will
|
||||
* reveal the folder within the explorer.
|
||||
*/
|
||||
LocalFolderOutsideWorkspace,
|
||||
|
||||
/**
|
||||
* The link is validated to be a folder on the file system and is within the workspace and will
|
||||
* reveal the folder within the explorer.
|
||||
*/
|
||||
LocalFolderInWorkspace,
|
||||
|
||||
/**
|
||||
* A low confidence link which will search for the file in the workspace. If there is a single
|
||||
* match, it will open the file; otherwise, it will present the matches in a quick pick.
|
||||
*/
|
||||
Search,
|
||||
|
||||
/**
|
||||
* A link whose text is a valid URI.
|
||||
*/
|
||||
Url
|
||||
}
|
||||
|
||||
export interface ITerminalExternalLinkType {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ITerminalLinkOpener {
|
||||
open(link: ITerminalSimpleLink): Promise<void>;
|
||||
}
|
||||
|
||||
export type OmitFirstArg<F> = F extends (x: any, ...args: infer P) => infer R ? (...args: P) => R : never;
|
|
@ -0,0 +1,58 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ITerminalLinkDetector, ITerminalSimpleLink, OmitFirstArg } from 'vs/workbench/contrib/terminal/browser/links/links';
|
||||
import { convertLinkRangeToBuffer, getXtermLineContent } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers';
|
||||
import { ITerminalExternalLinkProvider } from 'vs/workbench/contrib/terminal/browser/terminal';
|
||||
import { IBufferLine, Terminal } from 'xterm';
|
||||
|
||||
const enum Constants {
|
||||
/**
|
||||
* The max line length to try extract word links from.
|
||||
*/
|
||||
MaxLineLength = 2000
|
||||
}
|
||||
|
||||
export class TerminalExternalLinkDetector implements ITerminalLinkDetector {
|
||||
constructor(
|
||||
readonly id: string,
|
||||
readonly xterm: Terminal,
|
||||
private readonly _provideLinks: OmitFirstArg<ITerminalExternalLinkProvider['provideLinks']>
|
||||
) {
|
||||
}
|
||||
|
||||
async detect(lines: IBufferLine[], startLine: number, endLine: number): Promise<ITerminalSimpleLink[]> {
|
||||
// Get the text representation of the wrapped line
|
||||
const text = getXtermLineContent(this.xterm.buffer.active, startLine, endLine, this.xterm.cols);
|
||||
if (text === '' || text.length > Constants.MaxLineLength) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const externalLinks = await this._provideLinks(text);
|
||||
if (!externalLinks) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = externalLinks.map(link => {
|
||||
const bufferRange = convertLinkRangeToBuffer(lines, this.xterm.cols, {
|
||||
startColumn: link.startIndex + 1,
|
||||
startLineNumber: 1,
|
||||
endColumn: link.startIndex + link.length + 1,
|
||||
endLineNumber: 1
|
||||
}, startLine);
|
||||
const matchingText = text.substring(link.startIndex, link.startIndex + link.length) || '';
|
||||
|
||||
const l = {
|
||||
text: matchingText,
|
||||
label: link.label,
|
||||
bufferRange,
|
||||
type: { id: this.id }
|
||||
};
|
||||
return l;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -27,8 +27,6 @@ export class TerminalLink extends DisposableStore implements ILink {
|
|||
private readonly _onInvalidated = new Emitter<void>();
|
||||
get onInvalidated(): Event<void> { return this._onInvalidated.event; }
|
||||
|
||||
|
||||
|
||||
constructor(
|
||||
private readonly _xterm: Terminal,
|
||||
readonly range: IBufferRange,
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ITerminalLinkDetector, ITerminalSimpleLink, TerminalBuiltinLinkType, TerminalLinkType } from 'vs/workbench/contrib/terminal/browser/links/links';
|
||||
import { TerminalLink } from 'vs/workbench/contrib/terminal/browser/links/terminalLink';
|
||||
import { XtermLinkMatcherHandler } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager';
|
||||
import { IBufferLine, ILink, ILinkProvider, IViewportRange } from 'xterm';
|
||||
|
||||
export interface IActivateLinkEvent {
|
||||
link: ITerminalSimpleLink;
|
||||
event?: MouseEvent;
|
||||
}
|
||||
|
||||
export interface IShowHoverEvent {
|
||||
link: TerminalLink;
|
||||
viewportRange: IViewportRange;
|
||||
modifierDownCallback?: () => void;
|
||||
modifierUpCallback?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a link detector object so it can be used in xterm.js
|
||||
*/
|
||||
export class TerminalLinkDetectorAdapter extends Disposable implements ILinkProvider {
|
||||
private _activeLinks: TerminalLink[] | undefined;
|
||||
|
||||
private readonly _onDidActivateLink = this._register(new Emitter<IActivateLinkEvent>());
|
||||
readonly onDidActivateLink = this._onDidActivateLink.event;
|
||||
private readonly _onDidShowHover = this._register(new Emitter<IShowHoverEvent>());
|
||||
readonly onDidShowHover = this._onDidShowHover.event;
|
||||
|
||||
constructor(
|
||||
private readonly _detector: ITerminalLinkDetector,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async provideLinks(bufferLineNumber: number, callback: (links: ILink[] | undefined) => void) {
|
||||
this._activeLinks?.forEach(l => l.dispose());
|
||||
this._activeLinks = await this._provideLinks(bufferLineNumber);
|
||||
callback(this._activeLinks);
|
||||
}
|
||||
|
||||
private async _provideLinks(bufferLineNumber: number): Promise<TerminalLink[]> {
|
||||
// Dispose of all old links if new links are provided, links are only cached for the current line
|
||||
const links: TerminalLink[] = [];
|
||||
|
||||
let startLine = bufferLineNumber - 1;
|
||||
let endLine = startLine;
|
||||
|
||||
const lines: IBufferLine[] = [
|
||||
this._detector.xterm.buffer.active.getLine(startLine)!
|
||||
];
|
||||
|
||||
while (startLine >= 0 && this._detector.xterm.buffer.active.getLine(startLine)?.isWrapped) {
|
||||
lines.unshift(this._detector.xterm.buffer.active.getLine(startLine - 1)!);
|
||||
startLine--;
|
||||
}
|
||||
|
||||
while (endLine < this._detector.xterm.buffer.active.length && this._detector.xterm.buffer.active.getLine(endLine + 1)?.isWrapped) {
|
||||
lines.push(this._detector.xterm.buffer.active.getLine(endLine + 1)!);
|
||||
endLine++;
|
||||
}
|
||||
|
||||
const detectedLinks = await this._detector.detect(lines, startLine, endLine);
|
||||
for (const link of detectedLinks) {
|
||||
links.push(this._createTerminalLink(link, async (event) => {
|
||||
this._onDidActivateLink.fire({ link, event });
|
||||
}));
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
private _createTerminalLink(l: ITerminalSimpleLink, activateCallback: XtermLinkMatcherHandler): TerminalLink {
|
||||
// Remove trailing colon if there is one so the link is more useful
|
||||
if (l.text.length > 0 && l.text.charAt(l.text.length - 1) === ':') {
|
||||
l.text = l.text.slice(0, -1);
|
||||
l.bufferRange.end.x--;
|
||||
}
|
||||
return this._instantiationService.createInstance(TerminalLink,
|
||||
this._detector.xterm,
|
||||
l.bufferRange,
|
||||
l.text,
|
||||
this._detector.xterm.buffer.active.viewportY,
|
||||
activateCallback,
|
||||
(link, viewportRange, modifierDownCallback, modifierUpCallback) => this._onDidShowHover.fire({
|
||||
link,
|
||||
viewportRange,
|
||||
modifierDownCallback,
|
||||
modifierUpCallback
|
||||
}),
|
||||
l.type !== TerminalBuiltinLinkType.Search, // Only search is low confidence
|
||||
l.label || this._getLabel(l.type)
|
||||
);
|
||||
}
|
||||
|
||||
private _getLabel(type: TerminalLinkType): string {
|
||||
switch (type) {
|
||||
case TerminalBuiltinLinkType.Search: return localize('searchWorkspace', 'Search workspace');
|
||||
case TerminalBuiltinLinkType.LocalFile: return localize('openFile', 'Open file in editor');
|
||||
case TerminalBuiltinLinkType.LocalFolderInWorkspace: return localize('focusFolder', 'Focus folder in explorer');
|
||||
case TerminalBuiltinLinkType.LocalFolderOutsideWorkspace: return localize('openFolder', 'Open folder in new window');
|
||||
case TerminalBuiltinLinkType.Url:
|
||||
default:
|
||||
return localize('followLink', 'Follow link');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,34 +3,35 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { DisposableStore, IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ITerminalProcessManager, ITerminalConfiguration, TERMINAL_CONFIG_SECTION } 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 type { Terminal, IViewportRange, ILinkProvider, ILink } from 'xterm';
|
||||
import { EventType } from 'vs/base/browser/dom';
|
||||
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { posix, win32 } from 'vs/base/common/path';
|
||||
import { ITerminalExternalLinkProvider, ITerminalInstance, TerminalLinkQuickPickEvent } from 'vs/workbench/contrib/terminal/browser/terminal';
|
||||
import { OperatingSystem, isMacintosh, OS } from 'vs/base/common/platform';
|
||||
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { TerminalProtocolLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalProtocolLinkProvider';
|
||||
import { TerminalValidatedLocalLinkProvider, lineAndColumnClause, unixLocalLinkClause, winLocalLinkClause, winDrivePrefix, winLineAndColumnMatchIndex, unixLineAndColumnMatchIndex, lineAndColumnClauseGroupCount } from 'vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider';
|
||||
import { TerminalWordLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider';
|
||||
import { isMacintosh, OperatingSystem, OS } from 'vs/base/common/platform';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as nls from 'vs/nls';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private';
|
||||
import { TerminalHover, ILinkHoverTargetOptions } from 'vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget';
|
||||
import { TerminalLink } from 'vs/workbench/contrib/terminal/browser/links/terminalLink';
|
||||
import { TerminalExternalLinkProviderAdapter } from 'vs/workbench/contrib/terminal/browser/links/terminalExternalLinkProviderAdapter';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { ITunnelService } from 'vs/platform/tunnel/common/tunnel';
|
||||
import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal';
|
||||
import { ITerminalLinkDetector, ITerminalLinkOpener, ITerminalSimpleLink, OmitFirstArg, TerminalBuiltinLinkType, TerminalLinkType } from 'vs/workbench/contrib/terminal/browser/links/links';
|
||||
import { TerminalExternalLinkDetector } from 'vs/workbench/contrib/terminal/browser/links/terminalExternalLinkDetector';
|
||||
import { TerminalLink } from 'vs/workbench/contrib/terminal/browser/links/terminalLink';
|
||||
import { TerminalLinkDetectorAdapter } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkDetectorAdapter';
|
||||
import { TerminalLocalFileLinkOpener, TerminalLocalFolderInWorkspaceLinkOpener, TerminalLocalFolderOutsideWorkspaceLinkOpener, TerminalSearchLinkOpener, TerminalUrlLinkOpener } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners';
|
||||
import { TerminalLocalLinkDetector } from 'vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector';
|
||||
import { TerminalUriLinkDetector } from 'vs/workbench/contrib/terminal/browser/links/terminalUriLinkDetector';
|
||||
import { lineAndColumnClause, unixLocalLinkClause, winDrivePrefix, winLocalLinkClause } from 'vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider';
|
||||
import { TerminalWordLinkDetector } from 'vs/workbench/contrib/terminal/browser/links/terminalWordLinkDetector';
|
||||
import { ITerminalExternalLinkProvider, TerminalLinkQuickPickEvent } from 'vs/workbench/contrib/terminal/browser/terminal';
|
||||
import { ILinkHoverTargetOptions, TerminalHover } from 'vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget';
|
||||
import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager';
|
||||
import { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private';
|
||||
import { ITerminalCapabilityStore, TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities';
|
||||
import { EventType } from 'vs/base/browser/dom';
|
||||
import { ITerminalConfiguration, ITerminalProcessManager, TERMINAL_CONFIG_SECTION } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import type { ILink, ILinkProvider, IViewportRange, Terminal } from 'xterm';
|
||||
|
||||
export type XtermLinkMatcherHandler = (event: MouseEvent | undefined, link: string) => Promise<void>;
|
||||
export type XtermLinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
|
||||
|
@ -47,70 +48,78 @@ interface IPath {
|
|||
export class TerminalLinkManager extends DisposableStore {
|
||||
private _widgetManager: TerminalWidgetManager | undefined;
|
||||
private _processCwd: string | undefined;
|
||||
private _standardLinkProviders: Map<string, ILinkProvider> = new Map();
|
||||
private _linkProvidersDisposables: IDisposable[] = [];
|
||||
private readonly _xterm: Terminal;
|
||||
private readonly _standardLinkProviders: Map<string, ILinkProvider> = new Map();
|
||||
private readonly _linkProvidersDisposables: IDisposable[] = [];
|
||||
private readonly _externalLinkProviders: IDisposable[] = [];
|
||||
private readonly _openers: Map<TerminalLinkType, ITerminalLinkOpener> = new Map();
|
||||
|
||||
constructor(
|
||||
private _xtermTerminal: XtermTerminal,
|
||||
private readonly _xterm: Terminal,
|
||||
private readonly _processManager: ITerminalProcessManager,
|
||||
private readonly _capabilities: ITerminalCapabilityStore,
|
||||
@IOpenerService private readonly _openerService: IOpenerService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
capabilities: ITerminalCapabilityStore,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@ITunnelService private readonly _tunnelService: ITunnelService
|
||||
) {
|
||||
super();
|
||||
this._xterm = _xtermTerminal.raw;
|
||||
// Protocol links
|
||||
const wrappedActivateCallback = this._wrapLinkHandler((_, link) => this._handleProtocolLink(link));
|
||||
const protocolProvider = this._instantiationService.createInstance(TerminalProtocolLinkProvider,
|
||||
this._xterm,
|
||||
wrappedActivateCallback,
|
||||
this._wrapLinkHandler.bind(this),
|
||||
this._tooltipCallback.bind(this),
|
||||
async (link, cb) => cb(await this._resolvePath(link)));
|
||||
this._standardLinkProviders.set(TerminalProtocolLinkProvider.id, protocolProvider);
|
||||
|
||||
// Validated local links
|
||||
// Setup link detectors in their order of priority
|
||||
this._setupLinkDetector(TerminalUriLinkDetector.id, this._instantiationService.createInstance(TerminalUriLinkDetector, this._xterm, this._resolvePath.bind(this)));
|
||||
if (this._configurationService.getValue<ITerminalConfiguration>(TERMINAL_CONFIG_SECTION).enableFileLinks) {
|
||||
const wrappedTextLinkActivateCallback = this._wrapLinkHandler((_, link) => this._handleLocalLink(link));
|
||||
const validatedProvider = this._instantiationService.createInstance(TerminalValidatedLocalLinkProvider,
|
||||
this._xterm,
|
||||
this._processManager.os || OS,
|
||||
wrappedTextLinkActivateCallback,
|
||||
this._wrapLinkHandler.bind(this),
|
||||
this._tooltipCallback.bind(this),
|
||||
async (linkCandidates, cb) => {
|
||||
for (const link of linkCandidates) {
|
||||
const result = await this._resolvePath(link);
|
||||
if (result) {
|
||||
return cb(result);
|
||||
}
|
||||
}
|
||||
return cb(undefined);
|
||||
});
|
||||
this._standardLinkProviders.set(TerminalValidatedLocalLinkProvider.id, validatedProvider);
|
||||
this._setupLinkDetector(TerminalLocalLinkDetector.id, this._instantiationService.createInstance(TerminalLocalLinkDetector, this._xterm, this._processManager.os || OS, this._resolvePath.bind(this)));
|
||||
}
|
||||
this._setupLinkDetector(TerminalWordLinkDetector.id, this._instantiationService.createInstance(TerminalWordLinkDetector, this._xterm));
|
||||
|
||||
this._capabilities.get(TerminalCapability.CwdDetection)?.onDidChangeCwd(cwd => {
|
||||
capabilities.get(TerminalCapability.CwdDetection)?.onDidChangeCwd(cwd => {
|
||||
this.processCwd = cwd;
|
||||
});
|
||||
|
||||
// Word links
|
||||
const wordProvider = this._instantiationService.createInstance(TerminalWordLinkProvider, this._xtermTerminal, this._capabilities, this._wrapLinkHandler.bind(this), this._tooltipCallback.bind(this));
|
||||
this._standardLinkProviders.set(TerminalWordLinkProvider.id, wordProvider);
|
||||
// Setup link openers
|
||||
const localFileOpener = this._instantiationService.createInstance(TerminalLocalFileLinkOpener, this._processManager.os || OS);
|
||||
this._openers.set(TerminalBuiltinLinkType.LocalFile, localFileOpener);
|
||||
this._openers.set(TerminalBuiltinLinkType.LocalFolderInWorkspace, this._instantiationService.createInstance(TerminalLocalFolderInWorkspaceLinkOpener));
|
||||
this._openers.set(TerminalBuiltinLinkType.LocalFolderOutsideWorkspace, this._instantiationService.createInstance(TerminalLocalFolderOutsideWorkspaceLinkOpener));
|
||||
this._openers.set(TerminalBuiltinLinkType.Search, this._instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, localFileOpener, this._processManager.os || OS));
|
||||
this._openers.set(TerminalBuiltinLinkType.Url, this._instantiationService.createInstance(TerminalUrlLinkOpener, !!this._processManager.remoteAuthority));
|
||||
|
||||
this._registerStandardLinkProviders();
|
||||
}
|
||||
|
||||
private _setupLinkDetector(id: string, detector: ITerminalLinkDetector, isExternal: boolean = false): ILinkProvider {
|
||||
const detectorAdapter = this._instantiationService.createInstance(TerminalLinkDetectorAdapter, detector);
|
||||
detectorAdapter.onDidActivateLink(e => {
|
||||
// Prevent default electron link handling so Alt+Click mode works normally
|
||||
e.event?.preventDefault();
|
||||
// Require correct modifier on click unless event is coming from linkQuickPick selection
|
||||
if (e.event && !(e.event instanceof TerminalLinkQuickPickEvent) && !this._isLinkActivationModifierDown(e.event)) {
|
||||
return;
|
||||
}
|
||||
// Just call the handler if there is no before listener
|
||||
this._openLink(e.link);
|
||||
});
|
||||
detectorAdapter.onDidShowHover(e => this._tooltipCallback(e.link, e.viewportRange, e.modifierDownCallback, e.modifierUpCallback));
|
||||
if (!isExternal) {
|
||||
this._standardLinkProviders.set(id, detectorAdapter);
|
||||
}
|
||||
return detectorAdapter;
|
||||
}
|
||||
|
||||
private async _openLink(link: ITerminalSimpleLink): Promise<void> {
|
||||
this._logService.debug('Opening link', link);
|
||||
const opener = this._openers.get(link.type);
|
||||
if (!opener) {
|
||||
throw new Error(`No matching opener for link type "${link.type}"`);
|
||||
}
|
||||
await opener.open(link);
|
||||
}
|
||||
|
||||
async openRecentLink(type: 'file' | 'web'): Promise<ILink | undefined> {
|
||||
let links;
|
||||
let i = this._xtermTerminal.raw.buffer.active.length;
|
||||
while ((!links || links.length === 0) && i >= this._xtermTerminal.raw.buffer.active.viewportY) {
|
||||
links = await this.getLinksForType(i, type);
|
||||
let i = this._xterm.buffer.active.length;
|
||||
while ((!links || links.length === 0) && i >= this._xterm.buffer.active.viewportY) {
|
||||
links = await this._getLinksForType(i, type);
|
||||
i--;
|
||||
}
|
||||
|
||||
|
@ -127,7 +136,7 @@ export class TerminalLinkManager extends DisposableStore {
|
|||
const webResults: ILink[] = [];
|
||||
const fileResults: ILink[] = [];
|
||||
|
||||
for (let i = this._xtermTerminal.raw.buffer.active.length - 1; i >= this._xtermTerminal.raw.buffer.active.viewportY; i--) {
|
||||
for (let i = this._xterm.buffer.active.length - 1; i >= this._xterm.buffer.active.viewportY; i--) {
|
||||
const links = await this._getLinksForLine(i);
|
||||
if (links) {
|
||||
const { wordLinks, webLinks, fileLinks } = links;
|
||||
|
@ -146,9 +155,9 @@ export class TerminalLinkManager extends DisposableStore {
|
|||
}
|
||||
|
||||
private async _getLinksForLine(y: number): Promise<IDetectedLinks | undefined> {
|
||||
let unfilteredWordLinks = await this.getLinksForType(y, 'word');
|
||||
const webLinks = await this.getLinksForType(y, 'web');
|
||||
const fileLinks = await this.getLinksForType(y, 'file');
|
||||
let unfilteredWordLinks = await this._getLinksForType(y, 'word');
|
||||
const webLinks = await this._getLinksForType(y, 'web');
|
||||
const fileLinks = await this._getLinksForType(y, 'file');
|
||||
const words = new Set();
|
||||
let wordLinks;
|
||||
if (unfilteredWordLinks) {
|
||||
|
@ -163,14 +172,14 @@ export class TerminalLinkManager extends DisposableStore {
|
|||
return { wordLinks, webLinks, fileLinks };
|
||||
}
|
||||
|
||||
async getLinksForType(y: number, type: 'word' | 'web' | 'file'): Promise<ILink[] | undefined> {
|
||||
protected async _getLinksForType(y: number, type: 'word' | 'web' | 'file'): Promise<ILink[] | undefined> {
|
||||
switch (type) {
|
||||
case 'word':
|
||||
return (await new Promise<ILink[] | undefined>(r => this._standardLinkProviders.get(TerminalWordLinkProvider.id)?.provideLinks(y, r)));
|
||||
return (await new Promise<ILink[] | undefined>(r => this._standardLinkProviders.get(TerminalWordLinkDetector.id)?.provideLinks(y, r)));
|
||||
case 'web':
|
||||
return (await new Promise<ILink[] | undefined>(r => this._standardLinkProviders.get(TerminalProtocolLinkProvider.id)?.provideLinks(y, r)));
|
||||
return (await new Promise<ILink[] | undefined>(r => this._standardLinkProviders.get(TerminalUriLinkDetector.id)?.provideLinks(y, r)));
|
||||
case 'file':
|
||||
return (await new Promise<ILink[] | undefined>(r => this._standardLinkProviders.get(TerminalValidatedLocalLinkProvider.id)?.provideLinks(y, r)));
|
||||
return (await new Promise<ILink[] | undefined>(r => this._standardLinkProviders.get(TerminalLocalLinkDetector.id)?.provideLinks(y, r)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -224,7 +233,7 @@ export class TerminalLinkManager extends DisposableStore {
|
|||
|
||||
private _clearLinkProviders(): void {
|
||||
dispose(this._linkProvidersDisposables);
|
||||
this._linkProvidersDisposables = [];
|
||||
this._linkProvidersDisposables.length = 0;
|
||||
}
|
||||
|
||||
private _registerStandardLinkProviders(): void {
|
||||
|
@ -233,31 +242,17 @@ export class TerminalLinkManager extends DisposableStore {
|
|||
}
|
||||
}
|
||||
|
||||
registerExternalLinkProvider(instance: ITerminalInstance, linkProvider: ITerminalExternalLinkProvider): IDisposable {
|
||||
registerExternalLinkProvider(provideLinks: OmitFirstArg<ITerminalExternalLinkProvider['provideLinks']>): IDisposable {
|
||||
// Clear and re-register the standard link providers so they are a lower priority than the new one
|
||||
this._clearLinkProviders();
|
||||
const wrappedLinkProvider = this._instantiationService.createInstance(TerminalExternalLinkProviderAdapter, this._xterm, instance, linkProvider, this._wrapLinkHandler.bind(this), this._tooltipCallback.bind(this));
|
||||
const detectorId = `extension-${this._externalLinkProviders.length}`;
|
||||
const wrappedLinkProvider = this._setupLinkDetector(detectorId, new TerminalExternalLinkDetector(detectorId, this._xterm, provideLinks), true);
|
||||
const newLinkProvider = this._xterm.registerLinkProvider(wrappedLinkProvider);
|
||||
this._linkProvidersDisposables.push(newLinkProvider);
|
||||
this._externalLinkProviders.push(newLinkProvider);
|
||||
this._registerStandardLinkProviders();
|
||||
return newLinkProvider;
|
||||
}
|
||||
|
||||
protected _wrapLinkHandler(handler: (event: MouseEvent | undefined, link: string) => void): XtermLinkMatcherHandler {
|
||||
return async (event: MouseEvent | undefined, link: string) => {
|
||||
// Prevent default electron link handling so Alt+Click mode works normally
|
||||
event?.preventDefault();
|
||||
|
||||
// Require correct modifier on click unless event is coming from linkQuickPick selection
|
||||
if (event && !(event instanceof TerminalLinkQuickPickEvent) && !this._isLinkActivationModifierDown(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Just call the handler if there is no before listener
|
||||
handler(event, link);
|
||||
};
|
||||
}
|
||||
|
||||
protected get _localLinkRegex(): RegExp {
|
||||
if (!this._processManager) {
|
||||
throw new Error('Process manager is required');
|
||||
|
@ -267,45 +262,6 @@ export class TerminalLinkManager extends DisposableStore {
|
|||
return new RegExp(`${baseLocalLinkClause}(${lineAndColumnClause})`);
|
||||
}
|
||||
|
||||
private async _handleLocalLink(link: string): Promise<void> {
|
||||
// TODO: This gets resolved again but doesn't need to as it's already validated
|
||||
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.uri,
|
||||
options: { pinned: true, selection, revealIfOpened: true }
|
||||
});
|
||||
}
|
||||
|
||||
private _handleHypertextLink(url: string): void {
|
||||
this._openerService.open(url, {
|
||||
allowTunneling: !!(this._processManager && this._processManager.remoteAuthority),
|
||||
allowContributedOpeners: true,
|
||||
});
|
||||
}
|
||||
|
||||
private async _handleProtocolLink(link: string): Promise<void> {
|
||||
// Check if it's a file:/// link, hand off to local link handler so to open an editor and
|
||||
// respect line/col attachment
|
||||
const uri = URI.parse(link);
|
||||
if (uri.scheme === Schemas.file) {
|
||||
// Just using fsPath here is unsafe: https://github.com/microsoft/vscode/issues/109076
|
||||
const fsPath = uri.fsPath;
|
||||
this._handleLocalLink(((this._osPath.sep === posix.sep) && this._processManager.os === OperatingSystem.Windows) ? fsPath.replace(/\\/g, posix.sep) : fsPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// Open as a web link if it's not a file
|
||||
this._handleHypertextLink(link);
|
||||
}
|
||||
|
||||
protected _isLinkActivationModifierDown(event: MouseEvent): boolean {
|
||||
const editorConf = this._configurationService.getValue<{ multiCursorModifier: 'ctrlCmd' | 'alt' }>('editor');
|
||||
if (editorConf.multiCursorModifier === 'ctrlCmd') {
|
||||
|
@ -408,11 +364,22 @@ export class TerminalLinkManager extends DisposableStore {
|
|||
return link;
|
||||
}
|
||||
|
||||
private async _resolvePath(link: string): Promise<{ uri: URI; link: string; isDirectory: boolean } | undefined> {
|
||||
private async _resolvePath(link: string, uri?: URI): Promise<{ uri: URI; link: string; isDirectory: boolean } | undefined> {
|
||||
if (!this._processManager) {
|
||||
throw new Error('Process manager is required');
|
||||
}
|
||||
|
||||
if (uri) {
|
||||
try {
|
||||
const stat = await this._fileService.resolve(uri);
|
||||
return { uri, link, isDirectory: stat.isDirectory };
|
||||
}
|
||||
catch (e) {
|
||||
// Does not exist
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const preprocessedLink = this._preprocessPath(link);
|
||||
if (!preprocessedLink) {
|
||||
return undefined;
|
||||
|
@ -449,40 +416,6 @@ export class TerminalLinkManager extends DisposableStore {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns line and column number of URl if that is present.
|
||||
*
|
||||
* @param link Url link which may contain line and column number.
|
||||
*/
|
||||
extractLineColumnInfo(link: string): LineColumnInfo {
|
||||
const matches: string[] | null = this._localLinkRegex.exec(link);
|
||||
const lineColumnInfo: LineColumnInfo = {
|
||||
lineNumber: 1,
|
||||
columnNumber: 1
|
||||
};
|
||||
|
||||
if (!matches || !this._processManager) {
|
||||
return lineColumnInfo;
|
||||
}
|
||||
|
||||
const lineAndColumnMatchIndex = this._processManager.os === OperatingSystem.Windows ? winLineAndColumnMatchIndex : unixLineAndColumnMatchIndex;
|
||||
for (let i = 0; i < lineAndColumnClause.length; i++) {
|
||||
const lineMatchIndex = lineAndColumnMatchIndex + (lineAndColumnClauseGroupCount * i);
|
||||
const rowNumber = matches[lineMatchIndex];
|
||||
if (rowNumber) {
|
||||
lineColumnInfo['lineNumber'] = parseInt(rowNumber, 10);
|
||||
// Check if column number exists
|
||||
const columnNumber = matches[lineMatchIndex + 2];
|
||||
if (columnNumber) {
|
||||
lineColumnInfo['columnNumber'] = parseInt(columnNumber, 10);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return lineColumnInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns url from link as link may contain line and column information.
|
||||
*
|
||||
|
@ -497,7 +430,7 @@ export class TerminalLinkManager extends DisposableStore {
|
|||
}
|
||||
}
|
||||
|
||||
export interface LineColumnInfo {
|
||||
export interface ILineColumnInfo {
|
||||
lineNumber: number;
|
||||
columnNumber: number;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,240 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IPath, posix, win32 } from 'vs/base/common/path';
|
||||
import { OperatingSystem } from 'vs/base/common/platform';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { ITextEditorSelection } from 'vs/platform/editor/common/editor';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { ITerminalLinkOpener, ITerminalSimpleLink } from 'vs/workbench/contrib/terminal/browser/links/links';
|
||||
import { ILineColumnInfo } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager';
|
||||
import { getLocalLinkRegex, lineAndColumnClause, lineAndColumnClauseGroupCount, unixLineAndColumnMatchIndex, winLineAndColumnMatchIndex } from 'vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector';
|
||||
import { ITerminalCapabilityStore, TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IHostService } from 'vs/workbench/services/host/browser/host';
|
||||
import { QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder';
|
||||
import { ISearchService } from 'vs/workbench/services/search/common/search';
|
||||
|
||||
export class TerminalLocalFileLinkOpener implements ITerminalLinkOpener {
|
||||
constructor(
|
||||
private readonly _os: OperatingSystem,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
) {
|
||||
}
|
||||
|
||||
async open(link: ITerminalSimpleLink): Promise<void> {
|
||||
if (!link.uri) {
|
||||
throw new Error('Tried to open file link without a resolved URI');
|
||||
}
|
||||
const lineColumnInfo: ILineColumnInfo = this.extractLineColumnInfo(link.text);
|
||||
const selection: ITextEditorSelection = {
|
||||
startLineNumber: lineColumnInfo.lineNumber,
|
||||
startColumn: lineColumnInfo.columnNumber
|
||||
};
|
||||
await this._editorService.openEditor({
|
||||
resource: link.uri,
|
||||
options: { pinned: true, selection, revealIfOpened: true }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns line and column number of URl if that is present, otherwise line 1 column 1.
|
||||
*
|
||||
* @param link Url link which may contain line and column number.
|
||||
*/
|
||||
extractLineColumnInfo(link: string): ILineColumnInfo {
|
||||
const matches: string[] | null = getLocalLinkRegex(this._os).exec(link);
|
||||
const lineColumnInfo: ILineColumnInfo = {
|
||||
lineNumber: 1,
|
||||
columnNumber: 1
|
||||
};
|
||||
|
||||
if (!matches) {
|
||||
return lineColumnInfo;
|
||||
}
|
||||
|
||||
const lineAndColumnMatchIndex = this._os === OperatingSystem.Windows ? winLineAndColumnMatchIndex : unixLineAndColumnMatchIndex;
|
||||
for (let i = 0; i < lineAndColumnClause.length; i++) {
|
||||
const lineMatchIndex = lineAndColumnMatchIndex + (lineAndColumnClauseGroupCount * i);
|
||||
const rowNumber = matches[lineMatchIndex];
|
||||
if (rowNumber) {
|
||||
lineColumnInfo['lineNumber'] = parseInt(rowNumber, 10);
|
||||
// Check if column number exists
|
||||
const columnNumber = matches[lineMatchIndex + 2];
|
||||
if (columnNumber) {
|
||||
lineColumnInfo['columnNumber'] = parseInt(columnNumber, 10);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return lineColumnInfo;
|
||||
}
|
||||
}
|
||||
|
||||
export class TerminalLocalFolderInWorkspaceLinkOpener implements ITerminalLinkOpener {
|
||||
constructor(@ICommandService private readonly _commandService: ICommandService) {
|
||||
}
|
||||
|
||||
async open(link: ITerminalSimpleLink): Promise<void> {
|
||||
if (!link.uri) {
|
||||
throw new Error('Tried to open folder in workspace link without a resolved URI');
|
||||
}
|
||||
await this._commandService.executeCommand('revealInExplorer', link.uri);
|
||||
}
|
||||
}
|
||||
|
||||
export class TerminalLocalFolderOutsideWorkspaceLinkOpener implements ITerminalLinkOpener {
|
||||
constructor(@IHostService private readonly _hostService: IHostService) {
|
||||
}
|
||||
|
||||
async open(link: ITerminalSimpleLink): Promise<void> {
|
||||
if (!link.uri) {
|
||||
throw new Error('Tried to open folder in workspace link without a resolved URI');
|
||||
}
|
||||
this._hostService.openWindow([{ folderUri: link.uri }], { forceNewWindow: true });
|
||||
}
|
||||
}
|
||||
|
||||
export class TerminalSearchLinkOpener implements ITerminalLinkOpener {
|
||||
private readonly _fileQueryBuilder = this._instantiationService.createInstance(QueryBuilder);
|
||||
|
||||
constructor(
|
||||
private readonly _capabilities: ITerminalCapabilityStore,
|
||||
private readonly _localFileOpener: TerminalLocalFileLinkOpener,
|
||||
private readonly _os: OperatingSystem,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IQuickInputService private readonly _quickInputService: IQuickInputService,
|
||||
@ISearchService private readonly _searchService: ISearchService,
|
||||
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
|
||||
@IWorkbenchEnvironmentService private readonly _workbenchEnvironmentService: IWorkbenchEnvironmentService,
|
||||
) {
|
||||
}
|
||||
|
||||
async open(link: ITerminalSimpleLink): Promise<void> {
|
||||
const pathSeparator = osPathModule(this._os).sep;
|
||||
// Remove file:/// and any leading ./ or ../ since quick access doesn't understand that format
|
||||
let text = link.text.replace(/^file:\/\/\/?/, '');
|
||||
text = osPathModule(this._os).normalize(text).replace(/^(\.+[\\/])+/, '');
|
||||
|
||||
// Remove `:in` from the end which is how Ruby outputs stack traces
|
||||
text = text.replace(/:in$/, '');
|
||||
// If any of the names of the folders in the workspace matches
|
||||
// a prefix of the link, remove that prefix and continue
|
||||
this._workspaceContextService.getWorkspace().folders.forEach((folder) => {
|
||||
if (text.substring(0, folder.name.length + 1) === folder.name + pathSeparator) {
|
||||
text = text.substring(folder.name.length + 1);
|
||||
return;
|
||||
}
|
||||
});
|
||||
let matchLink = text;
|
||||
if (this._capabilities.has(TerminalCapability.CommandDetection)) {
|
||||
matchLink = this._updateLinkWithRelativeCwd(link.bufferRange.start.y, text, pathSeparator) || text;
|
||||
}
|
||||
const sanitizedLink = matchLink.replace(/:\d+(:\d+)?$/, '');
|
||||
try {
|
||||
const uri = await this._getExactMatch(sanitizedLink);
|
||||
if (uri) {
|
||||
return this._localFileOpener.open({
|
||||
text: matchLink,
|
||||
uri,
|
||||
bufferRange: link.bufferRange,
|
||||
type: link.type
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Fallback to searching quick access
|
||||
return this._quickInputService.quickAccess.show(text);
|
||||
}
|
||||
// Fallback to searching quick access
|
||||
return this._quickInputService.quickAccess.show(text);
|
||||
}
|
||||
|
||||
/*
|
||||
* For shells with the CwdDetection capability, the cwd relative to the line
|
||||
* of the particular link is used to narrow down the result for an exact file match, if possible.
|
||||
*/
|
||||
private _updateLinkWithRelativeCwd(y: number, text: string, pathSeparator: string): string | undefined {
|
||||
const cwd = this._capabilities.get(TerminalCapability.CommandDetection)?.getCwdForLine(y);
|
||||
if (!cwd) {
|
||||
return undefined;
|
||||
}
|
||||
if (!text.includes(pathSeparator)) {
|
||||
text = cwd + pathSeparator + text;
|
||||
} else {
|
||||
let commonDirs = 0;
|
||||
let i = 0;
|
||||
const cwdPath = cwd.split(pathSeparator).reverse();
|
||||
const linkPath = text.split(pathSeparator);
|
||||
while (i < cwdPath.length) {
|
||||
if (cwdPath[i] === linkPath[i]) {
|
||||
commonDirs++;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
text = cwd + pathSeparator + linkPath.slice(commonDirs).join(pathSeparator);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private async _getExactMatch(sanitizedLink: string): Promise<URI | undefined> {
|
||||
let exactResource: URI | undefined;
|
||||
if (osPathModule(this._os).isAbsolute(sanitizedLink)) {
|
||||
const scheme = this._workbenchEnvironmentService.remoteAuthority ? Schemas.vscodeRemote : Schemas.file;
|
||||
const resource = URI.from({ scheme, path: sanitizedLink });
|
||||
try {
|
||||
const fileStat = await this._fileService.resolve(resource);
|
||||
if (fileStat.isFile) {
|
||||
exactResource = resource;
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist, continue on
|
||||
}
|
||||
}
|
||||
if (!exactResource) {
|
||||
const results = await this._searchService.fileSearch(
|
||||
this._fileQueryBuilder.file(this._workspaceContextService.getWorkspace().folders, {
|
||||
// Remove optional :row:col from the link as openEditor supports it
|
||||
filePattern: sanitizedLink,
|
||||
maxResults: 2
|
||||
})
|
||||
);
|
||||
if (results.results.length === 1) {
|
||||
exactResource = results.results[0].resource;
|
||||
}
|
||||
}
|
||||
return exactResource;
|
||||
}
|
||||
}
|
||||
|
||||
export class TerminalUrlLinkOpener implements ITerminalLinkOpener {
|
||||
constructor(
|
||||
private readonly _isRemote: boolean,
|
||||
@IOpenerService private readonly _openerService: IOpenerService,
|
||||
) {
|
||||
}
|
||||
|
||||
async open(link: ITerminalSimpleLink): Promise<void> {
|
||||
if (!link.uri) {
|
||||
throw new Error('Tried to open a url without a resolved URI');
|
||||
}
|
||||
this._openerService.open(link.uri || URI.parse(link.text), {
|
||||
allowTunneling: this._isRemote,
|
||||
allowContributedOpeners: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function osPathModule(os: OperatingSystem): IPath {
|
||||
return os === OperatingSystem.Windows ? win32 : posix;
|
||||
}
|
|
@ -0,0 +1,226 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OperatingSystem } from 'vs/base/common/platform';
|
||||
import { withUndefinedAsNull } from 'vs/base/common/types';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { ITerminalLinkDetector, ITerminalSimpleLink, TerminalBuiltinLinkType } from 'vs/workbench/contrib/terminal/browser/links/links';
|
||||
import { convertLinkRangeToBuffer, getXtermLineContent } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers';
|
||||
import { IBufferLine, Terminal } from 'xterm';
|
||||
|
||||
const enum Constants {
|
||||
/**
|
||||
* The max line length to try extract word links from.
|
||||
*/
|
||||
MaxLineLength = 2000,
|
||||
|
||||
/**
|
||||
* The maximum number of links in a line to resolve against the file system. This limit is put
|
||||
* in place to avoid sending excessive data when remote connections are in place.
|
||||
*/
|
||||
MaxResolvedLinksInLine = 10,
|
||||
|
||||
/**
|
||||
* The maximum length of a link to resolve against the file system. This limit is put in place
|
||||
* to avoid sending excessive data when remote connections are in place.
|
||||
*/
|
||||
MaxResolvedLinkLength = 1024,
|
||||
}
|
||||
|
||||
const pathPrefix = '(\\.\\.?|\\~)';
|
||||
const pathSeparatorClause = '\\/';
|
||||
// '":; are allowed in paths but they are often separators so ignore them
|
||||
// Also disallow \\ to prevent a catastropic backtracking case #24795
|
||||
const excludedPathCharactersClause = '[^\\0\\s!`&*()\\[\\]\'":;\\\\]';
|
||||
/** A regex that matches paths in the form /foo, ~/foo, ./foo, ../foo, foo/bar */
|
||||
export const unixLocalLinkClause = '((' + pathPrefix + '|(' + excludedPathCharactersClause + ')+)?(' + pathSeparatorClause + '(' + excludedPathCharactersClause + ')+)+)';
|
||||
|
||||
export const winDrivePrefix = '(?:\\\\\\\\\\?\\\\)?[a-zA-Z]:';
|
||||
const winPathPrefix = '(' + winDrivePrefix + '|\\.\\.?|\\~)';
|
||||
const winPathSeparatorClause = '(\\\\|\\/)';
|
||||
const winExcludedPathCharactersClause = '[^\\0<>\\?\\|\\/\\s!`&*()\\[\\]\'":;]';
|
||||
/** A regex that matches paths in the form \\?\c:\foo c:\foo, ~\foo, .\foo, ..\foo, foo\bar */
|
||||
export const winLocalLinkClause = '((' + winPathPrefix + '|(' + winExcludedPathCharactersClause + ')+)?(' + winPathSeparatorClause + '(' + winExcludedPathCharactersClause + ')+)+)';
|
||||
|
||||
/** As xterm reads from DOM, space in that case is nonbreaking char ASCII code - 160,
|
||||
replacing space with nonBreakningSpace or space ASCII code - 32. */
|
||||
export const lineAndColumnClause = [
|
||||
'((\\S*)[\'"], line ((\\d+)( column (\\d+))?))', // "(file path)", line 45 [see #40468]
|
||||
'((\\S*)[\'"],((\\d+)(:(\\d+))?))', // "(file path)",45 [see #78205]
|
||||
'((\\S*) on line ((\\d+)(, column (\\d+))?))', // (file path) on line 8, column 13
|
||||
'((\\S*):line ((\\d+)(, column (\\d+))?))', // (file path):line 8, column 13
|
||||
'(([^\\s\\(\\)]*)(\\s?[\\(\\[](\\d+)(,\\s?(\\d+))?)[\\)\\]])', // (file path)(45), (file path) (45), (file path)(45,18), (file path) (45,18), (file path)(45, 18), (file path) (45, 18), also with []
|
||||
'(([^:\\s\\(\\)<>\'\"\\[\\]]*)(:(\\d+))?(:(\\d+))?)' // (file path):336, (file path):336:9
|
||||
].join('|').replace(/ /g, `[${'\u00A0'} ]`);
|
||||
|
||||
// Changing any regex may effect this value, hence changes this as well if required.
|
||||
export const winLineAndColumnMatchIndex = 12;
|
||||
export const unixLineAndColumnMatchIndex = 11;
|
||||
|
||||
// Each line and column clause have 6 groups (ie no. of expressions in round brackets)
|
||||
export const lineAndColumnClauseGroupCount = 6;
|
||||
|
||||
const cachedValidatedLinks = new Map<string, { uri: URI; link: string; isDirectory: boolean } | null>();
|
||||
|
||||
export class TerminalLocalLinkDetector implements ITerminalLinkDetector {
|
||||
static id = 'local';
|
||||
|
||||
private _cacheTilTimeout = 0;
|
||||
protected _enableCaching = true;
|
||||
|
||||
constructor(
|
||||
readonly xterm: Terminal,
|
||||
private readonly _os: OperatingSystem,
|
||||
private readonly _resolvePath: (link: string) => Promise<{ uri: URI; link: string; isDirectory: boolean } | undefined>,
|
||||
@IUriIdentityService private readonly _uriIdentityService: IUriIdentityService,
|
||||
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService
|
||||
) {
|
||||
}
|
||||
|
||||
async detect(lines: IBufferLine[], startLine: number, endLine: number): Promise<ITerminalSimpleLink[]> {
|
||||
const links: ITerminalSimpleLink[] = [];
|
||||
|
||||
// Reset cached link TTL
|
||||
if (this._enableCaching) {
|
||||
if (this._cacheTilTimeout) {
|
||||
window.clearTimeout(this._cacheTilTimeout);
|
||||
}
|
||||
this._cacheTilTimeout = window.setTimeout(() => cachedValidatedLinks.clear(), 10000);
|
||||
}
|
||||
|
||||
// Get the text representation of the wrapped line
|
||||
const text = getXtermLineContent(this.xterm.buffer.active, startLine, endLine, this.xterm.cols);
|
||||
if (text === '' || text.length > Constants.MaxLineLength) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// clone regex to do a global search on text
|
||||
const rex = new RegExp(getLocalLinkRegex(this._os), 'g');
|
||||
let match;
|
||||
let stringIndex = -1;
|
||||
let resolvedLinkCount = 0;
|
||||
while ((match = rex.exec(text)) !== null) {
|
||||
// const link = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
|
||||
let link = match[0];
|
||||
if (!link) {
|
||||
// something matched but does not comply with the given matchIndex
|
||||
// since this is most likely a bug the regex itself we simply do nothing here
|
||||
// this._logService.debug('match found without corresponding matchIndex', match, matcher);
|
||||
break;
|
||||
}
|
||||
|
||||
// Get index, match.index is for the outer match which includes negated chars
|
||||
// therefore we cannot use match.index directly, instead we search the position
|
||||
// of the match group in text again
|
||||
// also correct regex and string search offsets for the next loop run
|
||||
stringIndex = text.indexOf(link, stringIndex + 1);
|
||||
rex.lastIndex = stringIndex + link.length;
|
||||
if (stringIndex < 0) {
|
||||
// invalid stringIndex (should not have happened)
|
||||
break;
|
||||
}
|
||||
|
||||
// Adjust the link range to exclude a/ and b/ if it looks like a git diff
|
||||
if (
|
||||
// --- a/foo/bar
|
||||
// +++ b/foo/bar
|
||||
((text.startsWith('--- a/') || text.startsWith('+++ b/')) && stringIndex === 4) ||
|
||||
// diff --git a/foo/bar b/foo/bar
|
||||
(text.startsWith('diff --git') && (link.startsWith('a/') || link.startsWith('b/')))
|
||||
) {
|
||||
link = link.substring(2);
|
||||
stringIndex += 2;
|
||||
}
|
||||
|
||||
// Convert the link text's string index into a wrapped buffer range
|
||||
const bufferRange = convertLinkRangeToBuffer(lines, this.xterm.cols, {
|
||||
startColumn: stringIndex + 1,
|
||||
startLineNumber: 1,
|
||||
endColumn: stringIndex + link.length + 1,
|
||||
endLineNumber: 1
|
||||
}, startLine);
|
||||
|
||||
|
||||
// Don't try resolve any links of excessive length
|
||||
if (link.length > Constants.MaxResolvedLinkLength) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let linkStat = cachedValidatedLinks.get(link);
|
||||
|
||||
// The link is cached as doesn't exist
|
||||
if (linkStat === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The link isn't cached
|
||||
if (linkStat === undefined) {
|
||||
const linkCandidates = [link];
|
||||
if (link.match(/^(\.\.[\/\\])+/)) {
|
||||
linkCandidates.push(link.replace(/^(\.\.[\/\\])+/, ''));
|
||||
}
|
||||
linkStat = await this._validateLinkCandidates(linkCandidates);
|
||||
if (this._enableCaching) {
|
||||
cachedValidatedLinks.set(link, withUndefinedAsNull(linkStat));
|
||||
}
|
||||
}
|
||||
|
||||
// Create the link if validated
|
||||
if (linkStat) {
|
||||
let type: TerminalBuiltinLinkType;
|
||||
if (linkStat.isDirectory) {
|
||||
if (this._isDirectoryInsideWorkspace(linkStat.uri)) {
|
||||
type = TerminalBuiltinLinkType.LocalFolderInWorkspace;
|
||||
} else {
|
||||
type = TerminalBuiltinLinkType.LocalFolderOutsideWorkspace;
|
||||
}
|
||||
} else {
|
||||
type = TerminalBuiltinLinkType.LocalFile;
|
||||
}
|
||||
links.push({
|
||||
text: linkStat.link,
|
||||
uri: linkStat.uri,
|
||||
bufferRange,
|
||||
type
|
||||
});
|
||||
|
||||
// Stop early if too many links exist in the line
|
||||
if (++resolvedLinkCount >= Constants.MaxResolvedLinksInLine) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
private _isDirectoryInsideWorkspace(uri: URI) {
|
||||
const folders = this._workspaceContextService.getWorkspace().folders;
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
if (this._uriIdentityService.extUri.isEqualOrParent(uri, folders[i].uri)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async _validateLinkCandidates(linkCandidates: string[]): Promise<{ uri: URI; link: string; isDirectory: boolean } | undefined> {
|
||||
for (const link of linkCandidates) {
|
||||
const result = await this._resolvePath(link);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getLocalLinkRegex(os: OperatingSystem): RegExp {
|
||||
const baseLocalLinkClause = os === OperatingSystem.Windows ? winLocalLinkClause : unixLocalLinkClause;
|
||||
// Append line and column number regex
|
||||
return new RegExp(`${baseLocalLinkClause}(${lineAndColumnClause})`);
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { withUndefinedAsNull } from 'vs/base/common/types';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ILinkComputerTarget, LinkComputer } from 'vs/editor/common/languages/linkComputer';
|
||||
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { ITerminalLinkDetector, ITerminalSimpleLink, TerminalBuiltinLinkType } from 'vs/workbench/contrib/terminal/browser/links/links';
|
||||
import { convertLinkRangeToBuffer, getXtermLineContent } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers';
|
||||
import { IBufferLine, Terminal } from 'xterm';
|
||||
|
||||
const enum Constants {
|
||||
/**
|
||||
* The maximum number of links in a line to resolve against the file system. This limit is put
|
||||
* in place to avoid sending excessive data when remote connections are in place.
|
||||
*/
|
||||
MaxResolvedLinksInLine = 10,
|
||||
|
||||
/**
|
||||
* The maximum length of a link to resolve against the file system. This limit is put in place
|
||||
* to avoid sending excessive data when remote connections are in place.
|
||||
*/
|
||||
MaxResolvedLinkLength = 1024,
|
||||
}
|
||||
|
||||
const cachedValidatedLinks = new Map<string, { uri: URI; link: string; isDirectory: boolean } | null>();
|
||||
|
||||
export class TerminalUriLinkDetector implements ITerminalLinkDetector {
|
||||
static id = 'uri';
|
||||
|
||||
private _cacheTilTimeout = 0;
|
||||
protected _enableCaching = true;
|
||||
|
||||
constructor(
|
||||
readonly xterm: Terminal,
|
||||
private readonly _resolvePath: (link: string, uri?: URI) => Promise<{ uri: URI; link: string; isDirectory: boolean } | undefined>,
|
||||
@IUriIdentityService private readonly _uriIdentityService: IUriIdentityService,
|
||||
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService
|
||||
) {
|
||||
}
|
||||
|
||||
async detect(lines: IBufferLine[], startLine: number, endLine: number): Promise<ITerminalSimpleLink[]> {
|
||||
const links: ITerminalSimpleLink[] = [];
|
||||
|
||||
// Reset cached link TTL
|
||||
if (this._enableCaching) {
|
||||
if (this._cacheTilTimeout) {
|
||||
window.clearTimeout(this._cacheTilTimeout);
|
||||
}
|
||||
this._cacheTilTimeout = window.setTimeout(() => cachedValidatedLinks.clear(), 10000);
|
||||
}
|
||||
|
||||
const linkComputerTarget = new TerminalLinkAdapter(this.xterm, startLine, endLine);
|
||||
const computedLinks = LinkComputer.computeLinks(linkComputerTarget);
|
||||
|
||||
let resolvedLinkCount = 0;
|
||||
for (const computedLink of computedLinks) {
|
||||
const bufferRange = convertLinkRangeToBuffer(lines, this.xterm.cols, computedLink.range, startLine);
|
||||
|
||||
// Check if the link is within the mouse position
|
||||
const uri = computedLink.url
|
||||
? (typeof computedLink.url === 'string' ? URI.parse(computedLink.url) : computedLink.url)
|
||||
: undefined;
|
||||
|
||||
if (!uri) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const text = computedLink.url?.toString() || '';
|
||||
|
||||
// Handle non-file scheme links
|
||||
if (uri.scheme !== Schemas.file) {
|
||||
links.push({
|
||||
text,
|
||||
uri,
|
||||
bufferRange,
|
||||
type: TerminalBuiltinLinkType.Url
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Don't try resolve any links of excessive length
|
||||
if (text.length > Constants.MaxResolvedLinkLength) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter out URI with unrecognized authorities
|
||||
if (uri.authority.length !== 2 && uri.authority.endsWith(':')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let linkStat = cachedValidatedLinks.get(text);
|
||||
|
||||
// The link is cached as doesn't exist
|
||||
if (linkStat === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The link isn't cached
|
||||
if (linkStat === undefined) {
|
||||
linkStat = await this._resolvePath(text, uri);
|
||||
if (this._enableCaching) {
|
||||
cachedValidatedLinks.set(text, withUndefinedAsNull(linkStat));
|
||||
}
|
||||
}
|
||||
|
||||
// Create the link if validated
|
||||
if (linkStat) {
|
||||
let type: TerminalBuiltinLinkType;
|
||||
if (linkStat.isDirectory) {
|
||||
if (this._isDirectoryInsideWorkspace(linkStat.uri)) {
|
||||
type = TerminalBuiltinLinkType.LocalFolderInWorkspace;
|
||||
} else {
|
||||
type = TerminalBuiltinLinkType.LocalFolderOutsideWorkspace;
|
||||
}
|
||||
} else {
|
||||
type = TerminalBuiltinLinkType.LocalFile;
|
||||
}
|
||||
links.push({
|
||||
text: linkStat.link,
|
||||
uri: linkStat.uri,
|
||||
bufferRange,
|
||||
type
|
||||
});
|
||||
|
||||
// Stop early if too many links exist in the line
|
||||
if (++resolvedLinkCount >= Constants.MaxResolvedLinksInLine) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
private _isDirectoryInsideWorkspace(uri: URI) {
|
||||
const folders = this._workspaceContextService.getWorkspace().folders;
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
if (this._uriIdentityService.extUri.isEqualOrParent(uri, folders[i].uri)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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, this._xterm.cols);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ITerminalSimpleLink, ITerminalLinkDetector, TerminalBuiltinLinkType } from 'vs/workbench/contrib/terminal/browser/links/links';
|
||||
import { convertLinkRangeToBuffer, getXtermLineContent } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers';
|
||||
import { ITerminalConfiguration, TERMINAL_CONFIG_SECTION } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { IBufferLine, Terminal } from 'xterm';
|
||||
|
||||
const enum Constants {
|
||||
/**
|
||||
* The max line length to try extract word links from.
|
||||
*/
|
||||
MaxLineLength = 2000
|
||||
}
|
||||
|
||||
interface Word {
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export class TerminalWordLinkDetector implements ITerminalLinkDetector {
|
||||
static id = 'word';
|
||||
|
||||
constructor(
|
||||
readonly xterm: Terminal,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
) {
|
||||
}
|
||||
|
||||
detect(lines: IBufferLine[], startLine: number, endLine: number): ITerminalSimpleLink[] {
|
||||
const links: ITerminalSimpleLink[] = [];
|
||||
const wordSeparators = this._configurationService.getValue<ITerminalConfiguration>(TERMINAL_CONFIG_SECTION).wordSeparators;
|
||||
|
||||
// Get the text representation of the wrapped line
|
||||
const text = getXtermLineContent(this.xterm.buffer.active, startLine, endLine, this.xterm.cols);
|
||||
if (text === '' || text.length > Constants.MaxLineLength) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Parse out all words from the wrapped line
|
||||
const words: Word[] = this._parseWords(text, wordSeparators);
|
||||
|
||||
// Map the words to ITerminalLink objects
|
||||
for (const word of words) {
|
||||
if (word.text === '') {
|
||||
continue;
|
||||
}
|
||||
if (word.text.length > 0 && word.text.charAt(word.text.length - 1) === ':') {
|
||||
word.text = word.text.slice(0, -1);
|
||||
word.endIndex--;
|
||||
}
|
||||
const bufferRange = convertLinkRangeToBuffer(
|
||||
lines,
|
||||
this.xterm.cols,
|
||||
{
|
||||
startColumn: word.startIndex + 1,
|
||||
startLineNumber: 1,
|
||||
endColumn: word.endIndex + 1,
|
||||
endLineNumber: 1
|
||||
},
|
||||
startLine
|
||||
);
|
||||
links.push({
|
||||
text: word.text,
|
||||
bufferRange,
|
||||
type: TerminalBuiltinLinkType.Search
|
||||
});
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
private _parseWords(text: string, separators: string): Word[] {
|
||||
const words: Word[] = [];
|
||||
|
||||
const wordSeparators: string[] = separators.split('');
|
||||
const characters = text.split('');
|
||||
|
||||
let startIndex = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (wordSeparators.includes(characters[i])) {
|
||||
words.push({ startIndex, endIndex: i, text: text.substring(startIndex, i) });
|
||||
startIndex = i + 1;
|
||||
}
|
||||
}
|
||||
if (startIndex < text.length) {
|
||||
words.push({ startIndex, endIndex: text.length, text: text.substring(startIndex) });
|
||||
}
|
||||
|
||||
return words;
|
||||
}
|
||||
}
|
|
@ -670,7 +670,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
|||
if (this._processManager.os === OperatingSystem.Windows) {
|
||||
xterm.raw.options.windowsMode = processTraits.requiresWindowsMode || false;
|
||||
}
|
||||
this._linkManager = this._instantiationService.createInstance(TerminalLinkManager, xterm, this._processManager!, this.capabilities);
|
||||
this._linkManager = this._instantiationService.createInstance(TerminalLinkManager, xterm.raw, this._processManager!, this.capabilities);
|
||||
this._areLinksReady = true;
|
||||
this._onLinksReady.fire(this);
|
||||
});
|
||||
|
@ -2042,7 +2042,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
|||
if (!this._linkManager) {
|
||||
throw new Error('TerminalInstance.registerLinkProvider before link manager was ready');
|
||||
}
|
||||
return this._linkManager.registerExternalLinkProvider(this, provider);
|
||||
// Avoid a circular dependency by binding the terminal instances to the external link provider
|
||||
return this._linkManager.registerExternalLinkProvider(provider.provideLinks.bind(provider, this));
|
||||
}
|
||||
|
||||
async rename(title?: string | 'triggerQuickpick') {
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { deepStrictEqual } from 'assert';
|
||||
import { ITerminalLinkDetector, ITerminalSimpleLink, TerminalLinkType } from 'vs/workbench/contrib/terminal/browser/links/links';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IBufferLine } from 'xterm';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
|
||||
export async function assertLinkHelper(
|
||||
text: string,
|
||||
expected: (Pick<ITerminalSimpleLink, 'text'> & { range: [number, number][] })[],
|
||||
detector: ITerminalLinkDetector,
|
||||
expectedType: TerminalLinkType
|
||||
) {
|
||||
detector.xterm.reset();
|
||||
|
||||
// Write the text and wait for the parser to finish
|
||||
await new Promise<void>(r => detector.xterm.write(text, r));
|
||||
|
||||
// Ensure all links are provided
|
||||
const lines: IBufferLine[] = [];
|
||||
for (let i = 0; i < detector.xterm.buffer.active.cursorY + 1; i++) {
|
||||
lines.push(detector.xterm.buffer.active.getLine(i)!);
|
||||
}
|
||||
|
||||
const actualLinks = (await detector.detect(lines, 0, detector.xterm.buffer.active.cursorY)).map(e => {
|
||||
return {
|
||||
text: e.text,
|
||||
type: expectedType,
|
||||
bufferRange: e.bufferRange
|
||||
};
|
||||
});
|
||||
const expectedLinks = expected.map(e => {
|
||||
return {
|
||||
type: expectedType,
|
||||
text: e.text,
|
||||
bufferRange: {
|
||||
start: { x: e.range[0][0], y: e.range[0][1] },
|
||||
end: { x: e.range[1][0], y: e.range[1][1] },
|
||||
}
|
||||
};
|
||||
});
|
||||
deepStrictEqual(actualLinks, expectedLinks);
|
||||
}
|
||||
|
||||
export async function resolveLinkForTest(link: string, uri?: URI): Promise<{ uri: URI; link: string; isDirectory: boolean } | undefined> {
|
||||
return {
|
||||
link,
|
||||
uri: URI.from({ scheme: Schemas.file, path: link }),
|
||||
isDirectory: false,
|
||||
};
|
||||
}
|
|
@ -3,27 +3,23 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ILink, Terminal } from 'xterm';
|
||||
import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal';
|
||||
import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { ITerminalConfigHelper, ITerminalConfiguration, ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { deepStrictEqual, strictEqual } from 'assert';
|
||||
import { equals } from 'vs/base/common/arrays';
|
||||
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
|
||||
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IViewDescriptorService } from 'vs/workbench/common/views';
|
||||
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { ILogService, NullLogService } from 'vs/platform/log/common/log';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
|
||||
import { TerminalLocation } from 'vs/platform/terminal/common/terminal';
|
||||
import { TestViewDescriptorService } from 'vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
|
||||
import { IViewDescriptorService } from 'vs/workbench/common/views';
|
||||
import { IDetectedLinks, TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager';
|
||||
import { equals } from 'vs/base/common/arrays';
|
||||
import { ITerminalCapabilityImplMap, ITerminalCapabilityStore, TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities';
|
||||
import { TerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/terminalCapabilityStore';
|
||||
import { ITerminalConfiguration, ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { TestViewDescriptorService } from 'vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test';
|
||||
import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
|
||||
import { ILink, Terminal } from 'xterm';
|
||||
|
||||
const defaultTerminalConfig: Partial<ITerminalConfiguration> = {
|
||||
fontFamily: 'monospace',
|
||||
|
@ -38,7 +34,7 @@ const defaultTerminalConfig: Partial<ITerminalConfiguration> = {
|
|||
|
||||
class TestLinkManager extends TerminalLinkManager {
|
||||
private _links: IDetectedLinks | undefined;
|
||||
override async getLinksForType(y: number, type: 'word' | 'web' | 'file'): Promise<ILink[] | undefined> {
|
||||
protected override async _getLinksForType(y: number, type: 'word' | 'web' | 'file'): Promise<ILink[] | undefined> {
|
||||
switch (type) {
|
||||
case 'word':
|
||||
return this._links?.wordLinks?.[y] ? [this._links?.wordLinks?.[y]] : undefined;
|
||||
|
@ -58,8 +54,7 @@ suite('TerminalLinkManager', () => {
|
|||
let configurationService: TestConfigurationService;
|
||||
let themeService: TestThemeService;
|
||||
let viewDescriptorService: TestViewDescriptorService;
|
||||
let xterm: XtermTerminal;
|
||||
let configHelper: ITerminalConfigHelper;
|
||||
let xterm: Terminal;
|
||||
let linkManager: TestLinkManager;
|
||||
|
||||
setup(() => {
|
||||
|
@ -82,8 +77,7 @@ suite('TerminalLinkManager', () => {
|
|||
instantiationService.stub(IThemeService, themeService);
|
||||
instantiationService.stub(IViewDescriptorService, viewDescriptorService);
|
||||
|
||||
configHelper = instantiationService.createInstance(TerminalConfigHelper);
|
||||
xterm = instantiationService.createInstance(XtermTerminal, Terminal, configHelper, 80, 30, TerminalLocation.Panel, new TerminalCapabilityStore());
|
||||
xterm = new Terminal({ cols: 80, rows: 30 });
|
||||
linkManager = instantiationService.createInstance(TestLinkManager, xterm, upcastPartial<ITerminalProcessManager>({}), {
|
||||
get<T extends TerminalCapability>(capability: T): ITerminalCapabilityImplMap[T] | undefined {
|
||||
return undefined;
|
||||
|
|
|
@ -0,0 +1,197 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { deepStrictEqual } from 'assert';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { OperatingSystem } from 'vs/base/common/platform';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ITextResourceEditorInput } from 'vs/platform/editor/common/editor';
|
||||
import { IFileService, IFileStat, IFileStatWithMetadata, IResolveFileOptions, IResolveMetadataFileOptions } from 'vs/platform/files/common/files';
|
||||
import { FileService } from 'vs/platform/files/common/fileService';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { ILogService, NullLogService } from 'vs/platform/log/common/log';
|
||||
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { CommandDetectionCapability } from 'vs/workbench/contrib/terminal/browser/capabilities/commandDetectionCapability';
|
||||
import { TerminalBuiltinLinkType } from 'vs/workbench/contrib/terminal/browser/links/links';
|
||||
import { TerminalLocalFileLinkOpener, TerminalSearchLinkOpener } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners';
|
||||
import { TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities';
|
||||
import { TerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/terminalCapabilityStore';
|
||||
import { ITerminalCommand, IXtermMarker } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices';
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
export interface ITerminalLinkActivationResult {
|
||||
source: 'editor' | 'search';
|
||||
link: string;
|
||||
}
|
||||
|
||||
class TestCommandDetectionCapability extends CommandDetectionCapability {
|
||||
setCommands(commands: ITerminalCommand[]) {
|
||||
this._commands = commands;
|
||||
}
|
||||
}
|
||||
|
||||
class TestFileService extends FileService {
|
||||
private _files: URI[] | '*' = '*';
|
||||
override async resolve(resource: URI, options: IResolveMetadataFileOptions): Promise<IFileStatWithMetadata>;
|
||||
override async resolve(resource: URI, options?: IResolveFileOptions): Promise<IFileStat>;
|
||||
override async resolve(resource: URI, options?: IResolveFileOptions): Promise<IFileStat> {
|
||||
if (this._files === '*' || this._files.some(e => e.toString() === resource.toString())) {
|
||||
return { isFile: true, isDirectory: false, isSymbolicLink: false } as IFileStat;
|
||||
} else {
|
||||
return { isFile: false, isDirectory: false, isSymbolicLink: false } as IFileStat;
|
||||
}
|
||||
}
|
||||
setFiles(files: URI[] | '*'): void {
|
||||
this._files = files;
|
||||
}
|
||||
}
|
||||
|
||||
suite('Workbench - TerminalLinkOpeners', () => {
|
||||
let instantiationService: TestInstantiationService;
|
||||
let fileService: TestFileService;
|
||||
let activationResult: ITerminalLinkActivationResult | undefined;
|
||||
let xterm: Terminal;
|
||||
|
||||
setup(() => {
|
||||
instantiationService = new TestInstantiationService();
|
||||
fileService = new TestFileService(new NullLogService());
|
||||
instantiationService.set(IFileService, fileService);
|
||||
instantiationService.set(ILogService, new NullLogService());
|
||||
instantiationService.set(IWorkspaceContextService, new TestContextService());
|
||||
instantiationService.stub(IWorkbenchEnvironmentService, {
|
||||
remoteAuthority: undefined
|
||||
} as Partial<IWorkbenchEnvironmentService>);
|
||||
// Allow intercepting link activations
|
||||
activationResult = undefined;
|
||||
instantiationService.stub(IQuickInputService, {
|
||||
quickAccess: {
|
||||
show(link: string) {
|
||||
activationResult = { link, source: 'search' };
|
||||
}
|
||||
}
|
||||
} as Partial<IQuickInputService>);
|
||||
instantiationService.stub(IEditorService, {
|
||||
async openEditor(editor: ITextResourceEditorInput): Promise<any> {
|
||||
activationResult = {
|
||||
source: 'editor',
|
||||
link: editor.resource?.toString()
|
||||
};
|
||||
}
|
||||
} as Partial<IEditorService>);
|
||||
// /*editorServiceSpy = */instantiationService.spy(IEditorService, 'openEditor');
|
||||
xterm = new Terminal();
|
||||
});
|
||||
|
||||
suite('TerminalSearchLinkOpener', () => {
|
||||
let opener: TerminalSearchLinkOpener;
|
||||
let capabilities: TerminalCapabilityStore;
|
||||
let commandDetection: TestCommandDetectionCapability;
|
||||
let localFileOpener: TerminalLocalFileLinkOpener;
|
||||
|
||||
setup(() => {
|
||||
capabilities = new TerminalCapabilityStore();
|
||||
commandDetection = instantiationService.createInstance(TestCommandDetectionCapability, xterm);
|
||||
capabilities.add(TerminalCapability.CommandDetection, commandDetection);
|
||||
});
|
||||
|
||||
suite('macOS/Linux', () => {
|
||||
setup(() => {
|
||||
localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener, OperatingSystem.Linux);
|
||||
opener = instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, localFileOpener, OperatingSystem.Linux);
|
||||
});
|
||||
|
||||
test('should apply the cwd to the link only when the file exists and cwdDetection is enabled', async () => {
|
||||
const cwd = '/Users/home/folder';
|
||||
const absoluteFile = '/Users/home/folder/file.txt';
|
||||
fileService.setFiles([
|
||||
URI.from({ scheme: Schemas.file, path: absoluteFile })
|
||||
]);
|
||||
|
||||
// Set a fake detected command starting as line 0 to establish the cwd
|
||||
commandDetection.setCommands([{
|
||||
command: '',
|
||||
cwd,
|
||||
timestamp: 0,
|
||||
getOutput() { return undefined; },
|
||||
marker: {
|
||||
line: 0
|
||||
} as Partial<IXtermMarker> as any
|
||||
}]);
|
||||
await opener.open({
|
||||
text: 'file.txt',
|
||||
bufferRange: { start: { x: 1, y: 1 }, end: { x: 8, y: 1 } },
|
||||
type: TerminalBuiltinLinkType.Search
|
||||
});
|
||||
deepStrictEqual(activationResult, {
|
||||
link: 'file:///Users/home/folder/file.txt',
|
||||
source: 'editor'
|
||||
});
|
||||
|
||||
// Clear deteceted commands and ensure the same request results in a search
|
||||
commandDetection.setCommands([]);
|
||||
await opener.open({
|
||||
text: 'file.txt',
|
||||
bufferRange: { start: { x: 1, y: 1 }, end: { x: 8, y: 1 } },
|
||||
type: TerminalBuiltinLinkType.Search
|
||||
});
|
||||
deepStrictEqual(activationResult, {
|
||||
link: 'file.txt',
|
||||
source: 'search'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('Windows', () => {
|
||||
setup(() => {
|
||||
localFileOpener = instantiationService.createInstance(TerminalLocalFileLinkOpener, OperatingSystem.Windows);
|
||||
opener = instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, localFileOpener, OperatingSystem.Windows);
|
||||
});
|
||||
|
||||
test('should apply the cwd to the link only when the file exists and cwdDetection is enabled', async () => {
|
||||
const cwd = 'c:\\Users\\home\\folder';
|
||||
const absoluteFile = 'c:\\Users\\home\\folder\\file.txt';
|
||||
fileService.setFiles([
|
||||
URI.from({ scheme: Schemas.file, path: absoluteFile })
|
||||
]);
|
||||
|
||||
// Set a fake detected command starting as line 0 to establish the cwd
|
||||
commandDetection.setCommands([{
|
||||
command: '',
|
||||
cwd,
|
||||
timestamp: 0,
|
||||
getOutput() { return undefined; },
|
||||
marker: {
|
||||
line: 0
|
||||
} as Partial<IXtermMarker> as any
|
||||
}]);
|
||||
await opener.open({
|
||||
text: 'file.txt',
|
||||
bufferRange: { start: { x: 1, y: 1 }, end: { x: 8, y: 1 } },
|
||||
type: TerminalBuiltinLinkType.Search
|
||||
});
|
||||
deepStrictEqual(activationResult, {
|
||||
link: 'file:///c%3A%5CUsers%5Chome%5Cfolder%5Cfile.txt',
|
||||
source: 'editor'
|
||||
});
|
||||
|
||||
// Clear deteceted commands and ensure the same request results in a search
|
||||
commandDetection.setCommands([]);
|
||||
await opener.open({
|
||||
text: 'file.txt',
|
||||
bufferRange: { start: { x: 1, y: 1 }, end: { x: 8, y: 1 } },
|
||||
type: TerminalBuiltinLinkType.Search
|
||||
});
|
||||
deepStrictEqual(activationResult, {
|
||||
link: 'file.txt',
|
||||
source: 'search'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,172 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OperatingSystem } from 'vs/base/common/platform';
|
||||
import { format } from 'vs/base/common/strings';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { ITerminalSimpleLink, TerminalBuiltinLinkType } from 'vs/workbench/contrib/terminal/browser/links/links';
|
||||
import { TerminalLocalLinkDetector } from 'vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector';
|
||||
import { assertLinkHelper, resolveLinkForTest } from 'vs/workbench/contrib/terminal/test/browser/links/linkTestUtils';
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
const unixLinks = [
|
||||
'/foo',
|
||||
'~/foo',
|
||||
'./foo',
|
||||
'./$foo',
|
||||
'../foo',
|
||||
'/foo/bar',
|
||||
'/foo/bar+more',
|
||||
'foo/bar',
|
||||
'foo/bar+more',
|
||||
];
|
||||
|
||||
const windowsLinks = [
|
||||
'c:\\foo',
|
||||
'\\\\?\\c:\\foo',
|
||||
'c:/foo',
|
||||
'.\\foo',
|
||||
'./foo',
|
||||
'./$foo',
|
||||
'..\\foo',
|
||||
'~\\foo',
|
||||
'~/foo',
|
||||
'c:/foo/bar',
|
||||
'c:\\foo\\bar',
|
||||
'c:\\foo\\bar+more',
|
||||
'c:\\foo/bar\\baz',
|
||||
'foo/bar',
|
||||
'foo/bar',
|
||||
'foo\\bar',
|
||||
'foo\\bar+more',
|
||||
];
|
||||
|
||||
interface LinkFormatInfo {
|
||||
urlFormat: string;
|
||||
line?: string;
|
||||
column?: string;
|
||||
}
|
||||
|
||||
const supportedLinkFormats: LinkFormatInfo[] = [
|
||||
{ urlFormat: '{0}' },
|
||||
{ urlFormat: '{0} on line {1}', line: '5' },
|
||||
{ urlFormat: '{0} on line {1}, column {2}', line: '5', column: '3' },
|
||||
{ urlFormat: '{0}:line {1}', line: '5' },
|
||||
{ urlFormat: '{0}:line {1}, column {2}', line: '5', column: '3' },
|
||||
{ urlFormat: '{0}({1})', line: '5' },
|
||||
{ urlFormat: '{0} ({1})', line: '5' },
|
||||
{ urlFormat: '{0}({1},{2})', line: '5', column: '3' },
|
||||
{ urlFormat: '{0} ({1},{2})', line: '5', column: '3' },
|
||||
{ urlFormat: '{0}({1}, {2})', line: '5', column: '3' },
|
||||
{ urlFormat: '{0} ({1}, {2})', line: '5', column: '3' },
|
||||
{ urlFormat: '{0}:{1}', line: '5' },
|
||||
{ urlFormat: '{0}:{1}:{2}', line: '5', column: '3' },
|
||||
{ urlFormat: '{0}[{1}]', line: '5' },
|
||||
{ urlFormat: '{0} [{1}]', line: '5' },
|
||||
{ urlFormat: '{0}[{1},{2}]', line: '5', column: '3' },
|
||||
{ urlFormat: '{0} [{1},{2}]', line: '5', column: '3' },
|
||||
{ urlFormat: '{0}[{1}, {2}]', line: '5', column: '3' },
|
||||
{ urlFormat: '{0} [{1}, {2}]', line: '5', column: '3' },
|
||||
{ urlFormat: '{0}",{1}', line: '5' },
|
||||
{ urlFormat: '{0}\',{1}', line: '5' }
|
||||
];
|
||||
|
||||
suite('Workbench - TerminalLocalLinkDetector', () => {
|
||||
let instantiationService: TestInstantiationService;
|
||||
let configurationService: TestConfigurationService;
|
||||
let detector: TerminalLocalLinkDetector;
|
||||
let xterm: Terminal;
|
||||
|
||||
async function assertLink(
|
||||
type: TerminalBuiltinLinkType,
|
||||
text: string,
|
||||
expected: (Pick<ITerminalSimpleLink, 'text'> & { range: [number, number][] })[]
|
||||
) {
|
||||
await assertLinkHelper(text, expected, detector, type);
|
||||
}
|
||||
|
||||
setup(() => {
|
||||
instantiationService = new TestInstantiationService();
|
||||
configurationService = new TestConfigurationService();
|
||||
instantiationService.stub(IConfigurationService, configurationService);
|
||||
|
||||
xterm = new Terminal({ cols: 80, rows: 30 });
|
||||
});
|
||||
|
||||
suite('platform independent', () => {
|
||||
setup(() => {
|
||||
detector = instantiationService.createInstance(TerminalLocalLinkDetector, xterm, OperatingSystem.Linux, resolveLinkForTest);
|
||||
});
|
||||
|
||||
test('should support multiple link results', async () => {
|
||||
await assertLink(TerminalBuiltinLinkType.LocalFile, './foo ./bar', [
|
||||
{ range: [[1, 1], [5, 1]], text: './foo' },
|
||||
{ range: [[7, 1], [11, 1]], text: './bar' }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
suite('macOS/Linux', () => {
|
||||
setup(() => {
|
||||
detector = instantiationService.createInstance(TerminalLocalLinkDetector, xterm, OperatingSystem.Linux, resolveLinkForTest);
|
||||
});
|
||||
|
||||
for (const baseLink of unixLinks) {
|
||||
suite(`Link: ${baseLink}`, () => {
|
||||
for (let i = 0; i < supportedLinkFormats.length; i++) {
|
||||
const linkFormat = supportedLinkFormats[i];
|
||||
test(`Format: ${linkFormat.urlFormat}`, async () => {
|
||||
const formattedLink = format(linkFormat.urlFormat, baseLink, linkFormat.line, linkFormat.column);
|
||||
await assertLink(TerminalBuiltinLinkType.LocalFile, formattedLink, [{ text: formattedLink, range: [[1, 1], [formattedLink.length, 1]] }]);
|
||||
await assertLink(TerminalBuiltinLinkType.LocalFile, ` ${formattedLink} `, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]);
|
||||
await assertLink(TerminalBuiltinLinkType.LocalFile, `(${formattedLink})`, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]);
|
||||
await assertLink(TerminalBuiltinLinkType.LocalFile, `[${formattedLink}]`, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test('Git diff links', async () => {
|
||||
await assertLink(TerminalBuiltinLinkType.LocalFile, `diff --git a/foo/bar b/foo/bar`, [
|
||||
{ text: 'foo/bar', range: [[14, 1], [20, 1]] },
|
||||
{ text: 'foo/bar', range: [[24, 1], [30, 1]] }
|
||||
]);
|
||||
await assertLink(TerminalBuiltinLinkType.LocalFile, `--- a/foo/bar`, [{ text: 'foo/bar', range: [[7, 1], [13, 1]] }]);
|
||||
await assertLink(TerminalBuiltinLinkType.LocalFile, `+++ b/foo/bar`, [{ text: 'foo/bar', range: [[7, 1], [13, 1]] }]);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Windows', () => {
|
||||
setup(() => {
|
||||
detector = instantiationService.createInstance(TerminalLocalLinkDetector, xterm, OperatingSystem.Windows, resolveLinkForTest);
|
||||
});
|
||||
|
||||
for (const baseLink of windowsLinks) {
|
||||
suite(`Link "${baseLink}"`, () => {
|
||||
for (let i = 0; i < supportedLinkFormats.length; i++) {
|
||||
const linkFormat = supportedLinkFormats[i];
|
||||
test(`Format: ${linkFormat.urlFormat}`, async () => {
|
||||
const formattedLink = format(linkFormat.urlFormat, baseLink, linkFormat.line, linkFormat.column);
|
||||
await assertLink(TerminalBuiltinLinkType.LocalFile, formattedLink, [{ text: formattedLink, range: [[1, 1], [formattedLink.length, 1]] }]);
|
||||
await assertLink(TerminalBuiltinLinkType.LocalFile, ` ${formattedLink} `, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]);
|
||||
await assertLink(TerminalBuiltinLinkType.LocalFile, `(${formattedLink})`, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]);
|
||||
await assertLink(TerminalBuiltinLinkType.LocalFile, `[${formattedLink}]`, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test('Git diff links', async () => {
|
||||
await assertLink(TerminalBuiltinLinkType.LocalFile, `diff --git a/foo/bar b/foo/bar`, [
|
||||
{ text: 'foo/bar', range: [[14, 1], [20, 1]] },
|
||||
{ text: 'foo/bar', range: [[24, 1], [30, 1]] }
|
||||
]);
|
||||
await assertLink(TerminalBuiltinLinkType.LocalFile, `--- a/foo/bar`, [{ text: 'foo/bar', range: [[7, 1], [13, 1]] }]);
|
||||
await assertLink(TerminalBuiltinLinkType.LocalFile, `+++ b/foo/bar`, [{ text: 'foo/bar', range: [[7, 1], [13, 1]] }]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,105 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { TerminalProtocolLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalProtocolLinkProvider';
|
||||
import { Terminal, ILink } from 'xterm';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
suite('Workbench - TerminalProtocolLinkProvider', () => {
|
||||
let instantiationService: TestInstantiationService;
|
||||
|
||||
setup(() => {
|
||||
instantiationService = new TestInstantiationService();
|
||||
instantiationService.stub(IConfigurationService, TestConfigurationService);
|
||||
});
|
||||
|
||||
async function assertLink(text: string, expected: { text: string; range: [number, number][] }[]) {
|
||||
const xterm = new Terminal();
|
||||
const provider = instantiationService.createInstance(TerminalProtocolLinkProvider, xterm, () => { }, () => { }, () => { }, (text: string, cb: (result: { uri: URI; isDirectory: boolean } | undefined) => void) => {
|
||||
cb({ uri: URI.parse(text), isDirectory: false });
|
||||
});
|
||||
|
||||
// Write the text and wait for the parser to finish
|
||||
await new Promise<void>(r => xterm.write(text, r));
|
||||
|
||||
// Ensure all links are provided
|
||||
const links = (await new Promise<ILink[] | undefined>(r => provider.provideLinks(1, r)))!;
|
||||
assert.strictEqual(links.length, expected.length);
|
||||
const actual = links.map(e => ({
|
||||
text: e.text,
|
||||
range: e.range
|
||||
}));
|
||||
const expectedVerbose = expected.map(e => ({
|
||||
text: e.text,
|
||||
range: {
|
||||
start: { x: e.range[0][0], y: e.range[0][1] },
|
||||
end: { x: e.range[1][0], y: e.range[1][1] },
|
||||
}
|
||||
}));
|
||||
assert.deepStrictEqual(actual, expectedVerbose);
|
||||
}
|
||||
|
||||
// These tests are based on LinkComputer.test.ts
|
||||
test('LinkComputer cases', async () => {
|
||||
await assertLink('x = "http://foo.bar";', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
|
||||
await assertLink('x = (http://foo.bar);', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
|
||||
await assertLink('x = \'http://foo.bar\';', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
|
||||
await assertLink('x = http://foo.bar ;', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
|
||||
await assertLink('x = <http://foo.bar>;', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
|
||||
await assertLink('x = {http://foo.bar};', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
|
||||
await assertLink('(see http://foo.bar)', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
|
||||
await assertLink('[see http://foo.bar]', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
|
||||
await assertLink('{see http://foo.bar}', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
|
||||
await assertLink('<see http://foo.bar>', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
|
||||
await assertLink('<url>http://foo.bar</url>', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
|
||||
await assertLink('// Click here to learn more. https://go.microsoft.com/fwlink/?LinkID=513275&clcid=0x409', [{ range: [[30, 1], [7, 2]], text: 'https://go.microsoft.com/fwlink/?LinkID=513275&clcid=0x409' }]);
|
||||
await assertLink('// Click here to learn more. https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx', [{ range: [[30, 1], [28, 2]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx' }]);
|
||||
await assertLink('// https://github.com/projectkudu/kudu/blob/master/Kudu.Core/Scripts/selectNodeVersion.js', [{ range: [[4, 1], [9, 2]], text: 'https://github.com/projectkudu/kudu/blob/master/Kudu.Core/Scripts/selectNodeVersion.js' }]);
|
||||
await assertLink('<!-- !!! Do not remove !!! WebContentRef(link:https://go.microsoft.com/fwlink/?LinkId=166007, area:Admin, updated:2015, nextUpdate:2016, tags:SqlServer) !!! Do not remove !!! -->', [{ range: [[49, 1], [14, 2]], text: 'https://go.microsoft.com/fwlink/?LinkId=166007' }]);
|
||||
await assertLink('For instructions, see https://go.microsoft.com/fwlink/?LinkId=166007.</value>', [{ range: [[23, 1], [68, 1]], text: 'https://go.microsoft.com/fwlink/?LinkId=166007' }]);
|
||||
await assertLink('For instructions, see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx.</value>', [{ range: [[23, 1], [21, 2]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx' }]);
|
||||
await assertLink('x = "https://en.wikipedia.org/wiki/Zürich";', [{ range: [[6, 1], [41, 1]], text: 'https://en.wikipedia.org/wiki/Zürich' }]);
|
||||
await assertLink('請參閱 http://go.microsoft.com/fwlink/?LinkId=761051。', [{ range: [[8, 1], [53, 1]], text: 'http://go.microsoft.com/fwlink/?LinkId=761051' }]);
|
||||
await assertLink('(請參閱 http://go.microsoft.com/fwlink/?LinkId=761051)', [{ range: [[10, 1], [55, 1]], text: 'http://go.microsoft.com/fwlink/?LinkId=761051' }]);
|
||||
await assertLink('x = "file:///foo.bar";', [{ range: [[6, 1], [20, 1]], text: 'file:///foo.bar' }]);
|
||||
await assertLink('x = "file://c:/foo.bar";', [{ range: [[6, 1], [22, 1]], text: 'file://c:/foo.bar' }]);
|
||||
await assertLink('x = "file://shares/foo.bar";', [{ range: [[6, 1], [26, 1]], text: 'file://shares/foo.bar' }]);
|
||||
await assertLink('x = "file://shäres/foo.bar";', [{ range: [[6, 1], [26, 1]], text: 'file://shäres/foo.bar' }]);
|
||||
await assertLink('Some text, then http://www.bing.com.', [{ range: [[17, 1], [35, 1]], text: 'http://www.bing.com' }]);
|
||||
await assertLink('let url = `http://***/_api/web/lists/GetByTitle(\'Teambuildingaanvragen\')/items`;', [{ range: [[12, 1], [78, 1]], text: 'http://***/_api/web/lists/GetByTitle(\'Teambuildingaanvragen\')/items' }]);
|
||||
await assertLink('7. At this point, ServiceMain has been called. There is no functionality presently in ServiceMain, but you can consult the [MSDN documentation](https://msdn.microsoft.com/en-us/library/windows/desktop/ms687414(v=vs.85).aspx) to add functionality as desired!', [{ range: [[66, 2], [64, 3]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/ms687414(v=vs.85).aspx' }]);
|
||||
await assertLink('let x = "http://[::1]:5000/connect/token"', [{ range: [[10, 1], [40, 1]], text: 'http://[::1]:5000/connect/token' }]);
|
||||
await assertLink('2. Navigate to **https://portal.azure.com**', [{ range: [[18, 1], [41, 1]], text: 'https://portal.azure.com' }]);
|
||||
await assertLink('POST|https://portal.azure.com|2019-12-05|', [{ range: [[6, 1], [29, 1]], text: 'https://portal.azure.com' }]);
|
||||
await assertLink('aa https://foo.bar/[this is foo site] aa', [{ range: [[5, 1], [38, 1]], text: 'https://foo.bar/[this is foo site]' }]);
|
||||
});
|
||||
|
||||
test('should support multiple link results', async () => {
|
||||
await assertLink('http://foo.bar http://bar.foo', [
|
||||
{ range: [[1, 1], [14, 1]], text: 'http://foo.bar' },
|
||||
{ range: [[16, 1], [29, 1]], text: 'http://bar.foo' }
|
||||
]);
|
||||
});
|
||||
test('should cap link size', async () => {
|
||||
await assertLink([
|
||||
'https://foo.bar/foo.bar/foo.bar/foo.bar/foo.bar/foo.bar/foo.bar/foo.bar',
|
||||
'/foo.bar/foo.bar/foo.bar/foo.bar/foo.bar/foo.bar/foo.bar/foo.bar'.repeat(69)]
|
||||
.join(''),
|
||||
[{
|
||||
range: [[1, 1], [80, 26]],
|
||||
text:
|
||||
[
|
||||
'https://foo.bar/foo.bar/foo.bar/foo.bar/foo.bar/foo.bar/foo.bar/foo.bar/foo.bar/foo.bar/foo.bar',
|
||||
'/foo.bar/foo.bar/foo.bar/foo.bar/foo.bar/foo.bar/foo.bar/foo.bar'.repeat(31),
|
||||
'/'
|
||||
].join('')
|
||||
}]
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,98 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { ITerminalSimpleLink, TerminalBuiltinLinkType } from 'vs/workbench/contrib/terminal/browser/links/links';
|
||||
import { TerminalUriLinkDetector } from 'vs/workbench/contrib/terminal/browser/links/terminalUriLinkDetector';
|
||||
import { assertLinkHelper, resolveLinkForTest } from 'vs/workbench/contrib/terminal/test/browser/links/linkTestUtils';
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
suite('Workbench - TerminalUriLinkDetector', () => {
|
||||
let configurationService: TestConfigurationService;
|
||||
let detector: TerminalUriLinkDetector;
|
||||
let xterm: Terminal;
|
||||
|
||||
setup(() => {
|
||||
const instantiationService = new TestInstantiationService();
|
||||
configurationService = new TestConfigurationService();
|
||||
|
||||
instantiationService.stub(IConfigurationService, configurationService);
|
||||
|
||||
xterm = new Terminal({ cols: 80, rows: 30 });
|
||||
detector = instantiationService.createInstance(TerminalUriLinkDetector, xterm, resolveLinkForTest);
|
||||
});
|
||||
|
||||
async function assertLink(
|
||||
type: TerminalBuiltinLinkType,
|
||||
text: string,
|
||||
expected: (Pick<ITerminalSimpleLink, 'text'> & { range: [number, number][] })[]
|
||||
) {
|
||||
await assertLinkHelper(text, expected, detector, type);
|
||||
}
|
||||
|
||||
test('LinkComputer cases', async () => {
|
||||
await assertLink(TerminalBuiltinLinkType.Url, 'x = "http://foo.bar";', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, 'x = (http://foo.bar);', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, 'x = \'http://foo.bar\';', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, 'x = http://foo.bar ;', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, 'x = <http://foo.bar>;', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, 'x = {http://foo.bar};', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, '(see http://foo.bar)', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, '[see http://foo.bar]', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, '{see http://foo.bar}', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, '<see http://foo.bar>', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, '<url>http://foo.bar</url>', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, '// Click here to learn more. https://go.microsoft.com/fwlink/?LinkID=513275&clcid=0x409', [{ range: [[30, 1], [7, 2]], text: 'https://go.microsoft.com/fwlink/?LinkID=513275&clcid=0x409' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, '// Click here to learn more. https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx', [{ range: [[30, 1], [28, 2]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, '// https://github.com/projectkudu/kudu/blob/master/Kudu.Core/Scripts/selectNodeVersion.js', [{ range: [[4, 1], [9, 2]], text: 'https://github.com/projectkudu/kudu/blob/master/Kudu.Core/Scripts/selectNodeVersion.js' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, '<!-- !!! Do not remove !!! WebContentRef(link:https://go.microsoft.com/fwlink/?LinkId=166007, area:Admin, updated:2015, nextUpdate:2016, tags:SqlServer) !!! Do not remove !!! -->', [{ range: [[49, 1], [14, 2]], text: 'https://go.microsoft.com/fwlink/?LinkId=166007' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, 'For instructions, see https://go.microsoft.com/fwlink/?LinkId=166007.</value>', [{ range: [[23, 1], [68, 1]], text: 'https://go.microsoft.com/fwlink/?LinkId=166007' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, 'For instructions, see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx.</value>', [{ range: [[23, 1], [21, 2]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, 'x = "https://en.wikipedia.org/wiki/Zürich";', [{ range: [[6, 1], [41, 1]], text: 'https://en.wikipedia.org/wiki/Zürich' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, '請參閱 http://go.microsoft.com/fwlink/?LinkId=761051。', [{ range: [[8, 1], [53, 1]], text: 'http://go.microsoft.com/fwlink/?LinkId=761051' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, '(請參閱 http://go.microsoft.com/fwlink/?LinkId=761051)', [{ range: [[10, 1], [55, 1]], text: 'http://go.microsoft.com/fwlink/?LinkId=761051' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.LocalFile, 'x = "file:///foo.bar";', [{ range: [[6, 1], [20, 1]], text: 'file:///foo.bar' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.LocalFile, 'x = "file://c:/foo.bar";', [{ range: [[6, 1], [22, 1]], text: 'file://c:/foo.bar' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.LocalFile, 'x = "file://shares/foo.bar";', [{ range: [[6, 1], [26, 1]], text: 'file://shares/foo.bar' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.LocalFile, 'x = "file://shäres/foo.bar";', [{ range: [[6, 1], [26, 1]], text: 'file://shäres/foo.bar' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, 'Some text, then http://www.bing.com.', [{ range: [[17, 1], [35, 1]], text: 'http://www.bing.com' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, 'let url = `http://***/_api/web/lists/GetByTitle(\'Teambuildingaanvragen\')/items`;', [{ range: [[12, 1], [78, 1]], text: 'http://***/_api/web/lists/GetByTitle(\'Teambuildingaanvragen\')/items' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, '7. At this point, ServiceMain has been called. There is no functionality presently in ServiceMain, but you can consult the [MSDN documentation](https://msdn.microsoft.com/en-us/library/windows/desktop/ms687414(v=vs.85).aspx) to add functionality as desired!', [{ range: [[66, 2], [64, 3]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/ms687414(v=vs.85).aspx' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, 'let x = "http://[::1]:5000/connect/token"', [{ range: [[10, 1], [40, 1]], text: 'http://[::1]:5000/connect/token' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, '2. Navigate to **https://portal.azure.com**', [{ range: [[18, 1], [41, 1]], text: 'https://portal.azure.com' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, 'POST|https://portal.azure.com|2019-12-05|', [{ range: [[6, 1], [29, 1]], text: 'https://portal.azure.com' }]);
|
||||
await assertLink(TerminalBuiltinLinkType.Url, 'aa https://foo.bar/[this is foo site] aa', [{ range: [[5, 1], [38, 1]], text: 'https://foo.bar/[this is foo site]' }]);
|
||||
});
|
||||
|
||||
test('should support multiple link results', async () => {
|
||||
await assertLink(TerminalBuiltinLinkType.Url, 'http://foo.bar http://bar.foo', [
|
||||
{ range: [[1, 1], [14, 1]], text: 'http://foo.bar' },
|
||||
{ range: [[16, 1], [29, 1]], text: 'http://bar.foo' }
|
||||
]);
|
||||
});
|
||||
test('should not filtrer out https:// link that exceed 1024 characters', async () => {
|
||||
// 8 + 101 * 10 = 1018 characters
|
||||
await assertLink(TerminalBuiltinLinkType.Url, `https://${'foobarbaz/'.repeat(101)}`, [{
|
||||
range: [[1, 1], [58, 13]],
|
||||
text: `https://${'foobarbaz/'.repeat(101)}`
|
||||
}]);
|
||||
// 8 + 102 * 10 = 1028 characters
|
||||
await assertLink(TerminalBuiltinLinkType.Url, `https://${'foobarbaz/'.repeat(102)}`, [{
|
||||
range: [[1, 1], [68, 13]],
|
||||
text: `https://${'foobarbaz/'.repeat(102)}`
|
||||
}]);
|
||||
});
|
||||
test('should filter out file:// links that exceed 1024 characters', async () => {
|
||||
// 8 + 101 * 10 = 1018 characters
|
||||
await assertLink(TerminalBuiltinLinkType.LocalFile, `file:///${'foobarbaz/'.repeat(101)}`, [{
|
||||
text: `file:///${'foobarbaz/'.repeat(101)}`,
|
||||
range: [[1, 1], [58, 13]]
|
||||
}]);
|
||||
// 8 + 102 * 10 = 1028 characters
|
||||
await assertLink(TerminalBuiltinLinkType.LocalFile, `file:///${'foobarbaz/'.repeat(102)}`, []);
|
||||
});
|
||||
});
|
|
@ -1,179 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { TerminalValidatedLocalLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider';
|
||||
import { Terminal, ILink } from 'xterm';
|
||||
import { OperatingSystem } from 'vs/base/common/platform';
|
||||
import { format } from 'vs/base/common/strings';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
|
||||
const unixLinks = [
|
||||
'/foo',
|
||||
'~/foo',
|
||||
'./foo',
|
||||
'./$foo',
|
||||
'../foo',
|
||||
'/foo/bar',
|
||||
'/foo/bar+more',
|
||||
'foo/bar',
|
||||
'foo/bar+more',
|
||||
];
|
||||
|
||||
const windowsLinks = [
|
||||
'c:\\foo',
|
||||
'\\\\?\\c:\\foo',
|
||||
'c:/foo',
|
||||
'.\\foo',
|
||||
'./foo',
|
||||
'./$foo',
|
||||
'..\\foo',
|
||||
'~\\foo',
|
||||
'~/foo',
|
||||
'c:/foo/bar',
|
||||
'c:\\foo\\bar',
|
||||
'c:\\foo\\bar+more',
|
||||
'c:\\foo/bar\\baz',
|
||||
'foo/bar',
|
||||
'foo/bar',
|
||||
'foo\\bar',
|
||||
'foo\\bar+more',
|
||||
];
|
||||
|
||||
class TestTerminalValidatedLocalLinkProvider extends TerminalValidatedLocalLinkProvider {
|
||||
override _enableCaching: boolean = false;
|
||||
}
|
||||
|
||||
interface LinkFormatInfo {
|
||||
urlFormat: string;
|
||||
line?: string;
|
||||
column?: string;
|
||||
}
|
||||
|
||||
const supportedLinkFormats: LinkFormatInfo[] = [
|
||||
{ urlFormat: '{0}' },
|
||||
{ urlFormat: '{0} on line {1}', line: '5' },
|
||||
{ urlFormat: '{0} on line {1}, column {2}', line: '5', column: '3' },
|
||||
{ urlFormat: '{0}:line {1}', line: '5' },
|
||||
{ urlFormat: '{0}:line {1}, column {2}', line: '5', column: '3' },
|
||||
{ urlFormat: '{0}({1})', line: '5' },
|
||||
{ urlFormat: '{0} ({1})', line: '5' },
|
||||
{ urlFormat: '{0}({1},{2})', line: '5', column: '3' },
|
||||
{ urlFormat: '{0} ({1},{2})', line: '5', column: '3' },
|
||||
{ urlFormat: '{0}({1}, {2})', line: '5', column: '3' },
|
||||
{ urlFormat: '{0} ({1}, {2})', line: '5', column: '3' },
|
||||
{ urlFormat: '{0}:{1}', line: '5' },
|
||||
{ urlFormat: '{0}:{1}:{2}', line: '5', column: '3' },
|
||||
{ urlFormat: '{0}[{1}]', line: '5' },
|
||||
{ urlFormat: '{0} [{1}]', line: '5' },
|
||||
{ urlFormat: '{0}[{1},{2}]', line: '5', column: '3' },
|
||||
{ urlFormat: '{0} [{1},{2}]', line: '5', column: '3' },
|
||||
{ urlFormat: '{0}[{1}, {2}]', line: '5', column: '3' },
|
||||
{ urlFormat: '{0} [{1}, {2}]', line: '5', column: '3' },
|
||||
{ urlFormat: '{0}",{1}', line: '5' },
|
||||
{ urlFormat: '{0}\',{1}', line: '5' }
|
||||
];
|
||||
|
||||
suite('Workbench - TerminalValidatedLocalLinkProvider', () => {
|
||||
let instantiationService: TestInstantiationService;
|
||||
|
||||
setup(() => {
|
||||
instantiationService = new TestInstantiationService();
|
||||
instantiationService.stub(IConfigurationService, TestConfigurationService);
|
||||
});
|
||||
|
||||
async function assertLink(text: string, os: OperatingSystem, expected: { text: string; range: [number, number][] }[]) {
|
||||
const xterm = new Terminal();
|
||||
const provider = instantiationService.createInstance(
|
||||
TestTerminalValidatedLocalLinkProvider,
|
||||
xterm,
|
||||
os,
|
||||
() => { },
|
||||
() => { },
|
||||
() => { },
|
||||
(linkCandidates: string, cb: (result: { uri: URI; link: string; isDirectory: boolean } | undefined) => void) => {
|
||||
cb({ uri: URI.file('/'), link: linkCandidates[0], isDirectory: false });
|
||||
}
|
||||
);
|
||||
// Write the text and wait for the parser to finish
|
||||
await new Promise<void>(r => xterm.write(text, r));
|
||||
|
||||
// Ensure all links are provided
|
||||
const links = (await new Promise<ILink[] | undefined>(r => provider.provideLinks(1, r)))!;
|
||||
assert.strictEqual(links.length, expected.length);
|
||||
const actual = links.map(e => ({
|
||||
text: e.text,
|
||||
range: e.range
|
||||
}));
|
||||
const expectedVerbose = expected.map(e => ({
|
||||
text: e.text,
|
||||
range: {
|
||||
start: { x: e.range[0][0], y: e.range[0][1] },
|
||||
end: { x: e.range[1][0], y: e.range[1][1] },
|
||||
}
|
||||
}));
|
||||
assert.deepStrictEqual(actual, expectedVerbose);
|
||||
}
|
||||
|
||||
suite('Linux/macOS', () => {
|
||||
unixLinks.forEach(baseLink => {
|
||||
suite(`Link: ${baseLink}`, () => {
|
||||
for (let i = 0; i < supportedLinkFormats.length; i++) {
|
||||
const linkFormat = supportedLinkFormats[i];
|
||||
test(`Format: ${linkFormat.urlFormat}`, async () => {
|
||||
const formattedLink = format(linkFormat.urlFormat, baseLink, linkFormat.line, linkFormat.column);
|
||||
await assertLink(formattedLink, OperatingSystem.Linux, [{ text: formattedLink, range: [[1, 1], [formattedLink.length, 1]] }]);
|
||||
await assertLink(` ${formattedLink} `, OperatingSystem.Linux, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]);
|
||||
await assertLink(`(${formattedLink})`, OperatingSystem.Linux, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]);
|
||||
await assertLink(`[${formattedLink}]`, OperatingSystem.Linux, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
test('Git diff links', async () => {
|
||||
await assertLink(`diff --git a/foo/bar b/foo/bar`, OperatingSystem.Linux, [
|
||||
{ text: 'foo/bar', range: [[14, 1], [20, 1]] },
|
||||
{ text: 'foo/bar', range: [[24, 1], [30, 1]] }
|
||||
]);
|
||||
await assertLink(`--- a/foo/bar`, OperatingSystem.Linux, [{ text: 'foo/bar', range: [[7, 1], [13, 1]] }]);
|
||||
await assertLink(`+++ b/foo/bar`, OperatingSystem.Linux, [{ text: 'foo/bar', range: [[7, 1], [13, 1]] }]);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Windows', () => {
|
||||
windowsLinks.forEach(baseLink => {
|
||||
suite(`Link "${baseLink}"`, () => {
|
||||
for (let i = 0; i < supportedLinkFormats.length; i++) {
|
||||
const linkFormat = supportedLinkFormats[i];
|
||||
test(`Format: ${linkFormat.urlFormat}`, async () => {
|
||||
const formattedLink = format(linkFormat.urlFormat, baseLink, linkFormat.line, linkFormat.column);
|
||||
await assertLink(formattedLink, OperatingSystem.Windows, [{ text: formattedLink, range: [[1, 1], [formattedLink.length, 1]] }]);
|
||||
await assertLink(` ${formattedLink} `, OperatingSystem.Windows, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]);
|
||||
await assertLink(`(${formattedLink})`, OperatingSystem.Windows, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]);
|
||||
await assertLink(`[${formattedLink}]`, OperatingSystem.Windows, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
test('Git diff links', async () => {
|
||||
await assertLink(`diff --git a/foo/bar b/foo/bar`, OperatingSystem.Linux, [
|
||||
{ text: 'foo/bar', range: [[14, 1], [20, 1]] },
|
||||
{ text: 'foo/bar', range: [[24, 1], [30, 1]] }
|
||||
]);
|
||||
await assertLink(`--- a/foo/bar`, OperatingSystem.Linux, [{ text: 'foo/bar', range: [[7, 1], [13, 1]] }]);
|
||||
await assertLink(`+++ b/foo/bar`, OperatingSystem.Linux, [{ text: 'foo/bar', range: [[7, 1], [13, 1]] }]);
|
||||
});
|
||||
});
|
||||
|
||||
test('should support multiple link results', async () => {
|
||||
await assertLink('./foo ./bar', OperatingSystem.Linux, [
|
||||
{ range: [[1, 1], [5, 1]], text: './foo' },
|
||||
{ range: [[7, 1], [11, 1]], text: './bar' }
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,107 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { ITerminalSimpleLink, TerminalBuiltinLinkType } from 'vs/workbench/contrib/terminal/browser/links/links';
|
||||
import { TerminalWordLinkDetector } from 'vs/workbench/contrib/terminal/browser/links/terminalWordLinkDetector';
|
||||
import { assertLinkHelper } from 'vs/workbench/contrib/terminal/test/browser/links/linkTestUtils';
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
suite('Workbench - TerminalWordLinkDetector', () => {
|
||||
let configurationService: TestConfigurationService;
|
||||
let detector: TerminalWordLinkDetector;
|
||||
let xterm: Terminal;
|
||||
|
||||
setup(() => {
|
||||
const instantiationService = new TestInstantiationService();
|
||||
configurationService = new TestConfigurationService();
|
||||
|
||||
instantiationService.stub(IConfigurationService, configurationService);
|
||||
|
||||
xterm = new Terminal({ cols: 80, rows: 30 });
|
||||
detector = instantiationService.createInstance(TerminalWordLinkDetector, xterm);
|
||||
});
|
||||
|
||||
async function assertLink(
|
||||
text: string,
|
||||
expected: (Pick<ITerminalSimpleLink, 'text'> & { range: [number, number][] })[]
|
||||
) {
|
||||
await assertLinkHelper(text, expected, detector, TerminalBuiltinLinkType.Search);
|
||||
}
|
||||
|
||||
test('should link words as defined by wordSeparators', async () => {
|
||||
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ()[]' } });
|
||||
await assertLink('foo', [{ range: [[1, 1], [3, 1]], text: 'foo' }]);
|
||||
await assertLink('foo', [{ range: [[1, 1], [3, 1]], text: 'foo' }]);
|
||||
await assertLink('foo', [{ range: [[1, 1], [3, 1]], text: 'foo' }]);
|
||||
await assertLink(' foo ', [{ range: [[2, 1], [4, 1]], text: 'foo' }]);
|
||||
await assertLink('(foo)', [{ range: [[2, 1], [4, 1]], text: 'foo' }]);
|
||||
await assertLink('[foo]', [{ range: [[2, 1], [4, 1]], text: 'foo' }]);
|
||||
await assertLink('{foo}', [{ range: [[1, 1], [5, 1]], text: '{foo}' }]);
|
||||
|
||||
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ' } });
|
||||
await assertLink('foo', [{ range: [[1, 1], [3, 1]], text: 'foo' }]);
|
||||
await assertLink(' foo ', [{ range: [[2, 1], [4, 1]], text: 'foo' }]);
|
||||
await assertLink('(foo)', [{ range: [[1, 1], [5, 1]], text: '(foo)' }]);
|
||||
await assertLink('[foo]', [{ range: [[1, 1], [5, 1]], text: '[foo]' }]);
|
||||
await assertLink('{foo}', [{ range: [[1, 1], [5, 1]], text: '{foo}' }]);
|
||||
|
||||
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' []' } });
|
||||
await assertLink('aabbccdd.txt ', [{ range: [[1, 1], [12, 1]], text: 'aabbccdd.txt' }]);
|
||||
await assertLink(' aabbccdd.txt ', [{ range: [[2, 1], [13, 1]], text: 'aabbccdd.txt' }]);
|
||||
await assertLink(' [aabbccdd.txt] ', [{ range: [[3, 1], [14, 1]], text: 'aabbccdd.txt' }]);
|
||||
});
|
||||
|
||||
// These are failing - the link's start x is 1 px too far to the right bc it starts
|
||||
// with a wide character, which the terminalLinkHelper currently doesn't account for
|
||||
test.skip('should support wide characters', async () => {
|
||||
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' []' } });
|
||||
await assertLink('我是学生.txt ', [{ range: [[1, 1], [12, 1]], text: '我是学生.txt' }]);
|
||||
await assertLink(' 我是学生.txt ', [{ range: [[2, 1], [13, 1]], text: '我是学生.txt' }]);
|
||||
await assertLink(' [我是学生.txt] ', [{ range: [[3, 1], [14, 1]], text: '我是学生.txt' }]);
|
||||
});
|
||||
|
||||
test('should support multiple link results', async () => {
|
||||
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ' } });
|
||||
await assertLink('foo bar', [
|
||||
{ range: [[1, 1], [3, 1]], text: 'foo' },
|
||||
{ range: [[5, 1], [7, 1]], text: 'bar' }
|
||||
]);
|
||||
});
|
||||
|
||||
test('should remove trailing colon in the link results', async () => {
|
||||
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ' } });
|
||||
await assertLink('foo:5:6: bar:0:32:', [
|
||||
{ range: [[1, 1], [7, 1]], text: 'foo:5:6' },
|
||||
{ range: [[10, 1], [17, 1]], text: 'bar:0:32' }
|
||||
]);
|
||||
});
|
||||
|
||||
test('should support wrapping', async () => {
|
||||
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ' } });
|
||||
await assertLink('fsdjfsdkfjslkdfjskdfjsldkfjsdlkfjslkdjfskldjflskdfjskldjflskdfjsdklfjsdklfjsldkfjsdlkfjsdlkfjsdlkfjsldkfjslkdfjsdlkfjsldkfjsdlkfjskdfjsldkfjsdlkfjslkdfjsdlkfjsldkfjsldkfjsldkfjslkdfjsdlkfjslkdfjsdklfsd', [
|
||||
{ range: [[1, 1], [41, 3]], text: 'fsdjfsdkfjslkdfjskdfjsldkfjsdlkfjslkdjfskldjflskdfjskldjflskdfjsdklfjsdklfjsldkfjsdlkfjsdlkfjsdlkfjsldkfjslkdfjsdlkfjsldkfjsdlkfjskdfjsldkfjsdlkfjslkdfjsdlkfjsldkfjsldkfjsldkfjslkdfjsdlkfjslkdfjsdklfsd' },
|
||||
]);
|
||||
});
|
||||
test('should support wrapping with multiple links', async () => {
|
||||
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ' } });
|
||||
await assertLink('fsdjfsdkfjslkdfjskdfjsldkfj sdlkfjslkdjfskldjflskdfjskldjflskdfj sdklfjsdklfjsldkfjsdlkfjsdlkfjsdlkfjsldkfjslkdfjsdlkfjsldkfjsdlkfjskdfjsldkfjsdlkfjslkdfjsdlkfjsldkfjsldkfjsldkfjslkdfjsdlkfjslkdfjsdklfsd', [
|
||||
{ range: [[1, 1], [27, 1]], text: 'fsdjfsdkfjslkdfjskdfjsldkfj' },
|
||||
{ range: [[29, 1], [64, 1]], text: 'sdlkfjslkdjfskldjflskdfjskldjflskdfj' },
|
||||
{ range: [[66, 1], [43, 3]], text: 'sdklfjsdklfjsldkfjsdlkfjsdlkfjsdlkfjsldkfjslkdfjsdlkfjsldkfjsdlkfjskdfjsldkfjsdlkfjslkdfjsdlkfjsldkfjsldkfjsldkfjslkdfjsdlkfjslkdfjsdklfsd' }
|
||||
]);
|
||||
});
|
||||
test('does not return any links for empty text', async () => {
|
||||
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ' } });
|
||||
await assertLink('', []);
|
||||
});
|
||||
test('should support file scheme links', async () => {
|
||||
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ' } });
|
||||
await assertLink('file:///C:/users/test/file.txt ', [{ range: [[1, 1], [30, 1]], text: 'file:///C:/users/test/file.txt' }]);
|
||||
await assertLink('file:///C:/users/test/file.txt:1:10 ', [{ range: [[1, 1], [35, 1]], text: 'file:///C:/users/test/file.txt:1:10' }]);
|
||||
});
|
||||
});
|
|
@ -1,312 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { Terminal } from 'xterm';
|
||||
import { TerminalWordLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IEditorOptions, ITextResourceEditorInput } from 'vs/platform/editor/common/editor';
|
||||
import { ILogService, NullLogService } from 'vs/platform/log/common/log';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
|
||||
import { IViewDescriptorService } from 'vs/workbench/common/views';
|
||||
import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper';
|
||||
import { ITerminalCommand, ITerminalConfigHelper, ITerminalConfiguration, IXtermMarker } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { TestContextService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
|
||||
import { TerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/terminalCapabilityStore';
|
||||
import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal';
|
||||
import { TerminalLocation } from 'vs/platform/terminal/common/terminal';
|
||||
import { TestViewDescriptorService } from 'vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { XtermLinkMatcherHandler } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager';
|
||||
import { FileService } from 'vs/platform/files/common/fileService';
|
||||
import { IResolveMetadataFileOptions, IFileStatWithMetadata, IResolveFileOptions, IFileStat, IFileService } from 'vs/platform/files/common/files';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { TerminalLinkQuickPickEvent } from 'vs/workbench/contrib/terminal/browser/terminal';
|
||||
import { EventType } from 'vs/base/browser/dom';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { TerminalLink } from 'vs/workbench/contrib/terminal/browser/links/terminalLink';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities';
|
||||
import { CommandDetectionCapability } from 'vs/workbench/contrib/terminal/browser/capabilities/commandDetectionCapability';
|
||||
|
||||
const defaultTerminalConfig: Partial<ITerminalConfiguration> = {
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: 'normal',
|
||||
fontWeightBold: 'normal',
|
||||
gpuAcceleration: 'off',
|
||||
scrollback: 1000,
|
||||
fastScrollSensitivity: 2,
|
||||
mouseWheelScrollSensitivity: 1,
|
||||
unicodeVersion: '11',
|
||||
wordSeparators: ' ()[]{}\',"`─‘’'
|
||||
};
|
||||
|
||||
export interface ITerminalLinkActivationResult {
|
||||
source: 'editor' | 'search';
|
||||
link: string;
|
||||
}
|
||||
|
||||
class TestFileService extends FileService {
|
||||
private _files: string[] | '*' = '*';
|
||||
override async resolve(resource: URI, options: IResolveMetadataFileOptions): Promise<IFileStatWithMetadata>;
|
||||
override async resolve(resource: URI, options?: IResolveFileOptions): Promise<IFileStat>;
|
||||
override async resolve(resource: URI, options?: IResolveFileOptions): Promise<IFileStat> {
|
||||
if (this._files === '*' || this._files.includes(resource.fsPath)) {
|
||||
return { isFile: true, isDirectory: false, isSymbolicLink: false } as IFileStat;
|
||||
} else {
|
||||
return { isFile: false, isDirectory: false, isSymbolicLink: false } as IFileStat;
|
||||
}
|
||||
}
|
||||
setFiles(files: string[]): void {
|
||||
this._files = files;
|
||||
}
|
||||
}
|
||||
|
||||
class TestCommandDetectionCapability extends CommandDetectionCapability {
|
||||
setCommands(commands: ITerminalCommand[]) {
|
||||
this._commands = commands;
|
||||
}
|
||||
}
|
||||
|
||||
suite('Workbench - TerminalWordLinkProvider', () => {
|
||||
let instantiationService: TestInstantiationService;
|
||||
let configurationService: TestConfigurationService;
|
||||
let themeService: TestThemeService;
|
||||
let fileService: TestFileService;
|
||||
let viewDescriptorService: TestViewDescriptorService;
|
||||
let xterm: XtermTerminal;
|
||||
let configHelper: ITerminalConfigHelper;
|
||||
let capabilities: TerminalCapabilityStore;
|
||||
let activationResult: ITerminalLinkActivationResult | undefined;
|
||||
let commandDetection: TestCommandDetectionCapability;
|
||||
|
||||
setup(() => {
|
||||
instantiationService = new TestInstantiationService();
|
||||
fileService = new TestFileService(new NullLogService());
|
||||
configurationService = new TestConfigurationService();
|
||||
instantiationService.stub(IConfigurationService, configurationService);
|
||||
configurationService = new TestConfigurationService({
|
||||
editor: {
|
||||
fastScrollSensitivity: 2,
|
||||
mouseWheelScrollSensitivity: 1
|
||||
} as Partial<IEditorOptions>,
|
||||
terminal: {
|
||||
integrated: defaultTerminalConfig
|
||||
}
|
||||
});
|
||||
|
||||
themeService = new TestThemeService();
|
||||
viewDescriptorService = new TestViewDescriptorService();
|
||||
instantiationService = new TestInstantiationService();
|
||||
instantiationService.stub(IWorkbenchEnvironmentService, {
|
||||
remoteAuthority: undefined
|
||||
} as Partial<IWorkbenchEnvironmentService>);
|
||||
instantiationService.stub(IConfigurationService, configurationService);
|
||||
instantiationService.stub(ILogService, new NullLogService());
|
||||
instantiationService.stub(IStorageService, new TestStorageService());
|
||||
instantiationService.stub(IThemeService, themeService);
|
||||
instantiationService.stub(IFileService, fileService);
|
||||
instantiationService.stub(IViewDescriptorService, viewDescriptorService);
|
||||
instantiationService.stub(IWorkspaceContextService, new TestContextService());
|
||||
|
||||
// Allow intercepting link activations
|
||||
activationResult = undefined;
|
||||
instantiationService.stub(IQuickInputService, {
|
||||
quickAccess: {
|
||||
show(link: string) {
|
||||
activationResult = { link, source: 'search' };
|
||||
}
|
||||
}
|
||||
} as Partial<IQuickInputService>);
|
||||
instantiationService.stub(IEditorService, {
|
||||
async openEditor(editor: ITextResourceEditorInput): Promise<any> {
|
||||
activationResult = {
|
||||
source: 'editor',
|
||||
link: editor.resource?.toString()
|
||||
};
|
||||
}
|
||||
} as Partial<IEditorService>);
|
||||
|
||||
configHelper = instantiationService.createInstance(TerminalConfigHelper);
|
||||
capabilities = new TerminalCapabilityStore();
|
||||
xterm = instantiationService.createInstance(XtermTerminal, Terminal, configHelper, 80, 30, TerminalLocation.Panel, capabilities);
|
||||
commandDetection = new TestCommandDetectionCapability(xterm.raw, new NullLogService());
|
||||
capabilities.add(TerminalCapability.CommandDetection, commandDetection);
|
||||
});
|
||||
|
||||
async function assertLink(text: string, expected: { text: string; range: [number, number][]; linkActivationResult?: ITerminalLinkActivationResult }[], assertActicationResult?: boolean) {
|
||||
xterm.dispose();
|
||||
xterm = instantiationService.createInstance(XtermTerminal, Terminal, configHelper, 80, 30, TerminalLocation.Panel, capabilities);
|
||||
// We don't want to cancel the event or anything from the tests so just pass in a wrapped
|
||||
// link handler that does nothing.
|
||||
|
||||
const testWrappedLinkHandler = (handler: (event: MouseEvent | undefined, link: string) => void): XtermLinkMatcherHandler => {
|
||||
return async (event: MouseEvent | undefined, link: string) => {
|
||||
await handler(event, link);
|
||||
};
|
||||
};
|
||||
const provider: TerminalWordLinkProvider = instantiationService.createInstance(TerminalWordLinkProvider,
|
||||
xterm,
|
||||
capabilities,
|
||||
testWrappedLinkHandler,
|
||||
() => { }
|
||||
);
|
||||
|
||||
// Write the text and wait for the parser to finish
|
||||
await new Promise<void>(r => xterm.raw.write(text, r));
|
||||
|
||||
// Ensure all links are provided
|
||||
const links = (await new Promise<TerminalLink[] | undefined>(r => provider.provideLinks(1, r)))!;
|
||||
const actualLinks = await Promise.all(links.map(async e => {
|
||||
if (capabilities.has(TerminalCapability.CommandDetection)) {
|
||||
e.activate(new TerminalLinkQuickPickEvent(EventType.CLICK), e.text);
|
||||
await e.asyncActivate;
|
||||
}
|
||||
return {
|
||||
text: e.text,
|
||||
range: e.range,
|
||||
activationResult
|
||||
};
|
||||
}));
|
||||
|
||||
const expectedVerbose = expected.map(e => ({
|
||||
text: e.text,
|
||||
range: {
|
||||
start: { x: e.range[0][0], y: e.range[0][1] },
|
||||
end: { x: e.range[1][0], y: e.range[1][1] },
|
||||
},
|
||||
activationResult: e.linkActivationResult
|
||||
}));
|
||||
assert.deepStrictEqual(
|
||||
actualLinks.map(e => ({ text: e.text, range: e.range })),
|
||||
expectedVerbose.map(e => ({ text: e.text, range: e.range }))
|
||||
);
|
||||
if (assertActicationResult) {
|
||||
assert.deepStrictEqual(
|
||||
actualLinks.map(e => e.activationResult),
|
||||
expected.map(e => e.linkActivationResult)
|
||||
);
|
||||
}
|
||||
assert.strictEqual(links.length, expected.length);
|
||||
}
|
||||
|
||||
test('should link words as defined by wordSeparators', async () => {
|
||||
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ()[]' } });
|
||||
await assertLink('foo', [{ range: [[1, 1], [3, 1]], text: 'foo' }]);
|
||||
await assertLink('foo', [{ range: [[1, 1], [3, 1]], text: 'foo' }]);
|
||||
await assertLink(' foo ', [{ range: [[2, 1], [4, 1]], text: 'foo' }]);
|
||||
await assertLink('(foo)', [{ range: [[2, 1], [4, 1]], text: 'foo' }]);
|
||||
await assertLink('[foo]', [{ range: [[2, 1], [4, 1]], text: 'foo' }]);
|
||||
await assertLink('{foo}', [{ range: [[1, 1], [5, 1]], text: '{foo}' }]);
|
||||
|
||||
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ' } });
|
||||
await assertLink('foo', [{ range: [[1, 1], [3, 1]], text: 'foo' }]);
|
||||
await assertLink(' foo ', [{ range: [[2, 1], [4, 1]], text: 'foo' }]);
|
||||
await assertLink('(foo)', [{ range: [[1, 1], [5, 1]], text: '(foo)' }]);
|
||||
await assertLink('[foo]', [{ range: [[1, 1], [5, 1]], text: '[foo]' }]);
|
||||
await assertLink('{foo}', [{ range: [[1, 1], [5, 1]], text: '{foo}' }]);
|
||||
|
||||
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' []' } });
|
||||
await assertLink('aabbccdd.txt ', [{ range: [[1, 1], [12, 1]], text: 'aabbccdd.txt' }]);
|
||||
await assertLink(' aabbccdd.txt ', [{ range: [[2, 1], [13, 1]], text: 'aabbccdd.txt' }]);
|
||||
await assertLink(' [aabbccdd.txt] ', [{ range: [[3, 1], [14, 1]], text: 'aabbccdd.txt' }]);
|
||||
});
|
||||
|
||||
// These are failing - the link's start x is 1 px too far to the right bc it starts
|
||||
// with a wide character, which the terminalLinkHelper currently doesn't account for
|
||||
test.skip('should support wide characters', async () => {
|
||||
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' []' } });
|
||||
await assertLink('我是学生.txt ', [{ range: [[1, 1], [12, 1]], text: '我是学生.txt' }]);
|
||||
await assertLink(' 我是学生.txt ', [{ range: [[2, 1], [13, 1]], text: '我是学生.txt' }]);
|
||||
await assertLink(' [我是学生.txt] ', [{ range: [[3, 1], [14, 1]], text: '我是学生.txt' }]);
|
||||
});
|
||||
|
||||
test('should support multiple link results', async () => {
|
||||
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ' } });
|
||||
await assertLink('foo bar', [
|
||||
{ range: [[1, 1], [3, 1]], text: 'foo' },
|
||||
{ range: [[5, 1], [7, 1]], text: 'bar' }
|
||||
]);
|
||||
});
|
||||
|
||||
test('should remove trailing colon in the link results', async () => {
|
||||
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ' } });
|
||||
await assertLink('foo:5:6: bar:0:32:', [
|
||||
{ range: [[1, 1], [7, 1]], text: 'foo:5:6' },
|
||||
{ range: [[10, 1], [17, 1]], text: 'bar:0:32' }
|
||||
]);
|
||||
});
|
||||
|
||||
test('should support wrapping', async () => {
|
||||
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ' } });
|
||||
await assertLink('fsdjfsdkfjslkdfjskdfjsldkfjsdlkfjslkdjfskldjflskdfjskldjflskdfjsdklfjsdklfjsldkfjsdlkfjsdlkfjsdlkfjsldkfjslkdfjsdlkfjsldkfjsdlkfjskdfjsldkfjsdlkfjslkdfjsdlkfjsldkfjsldkfjsldkfjslkdfjsdlkfjslkdfjsdklfsd', [
|
||||
{ range: [[1, 1], [41, 3]], text: 'fsdjfsdkfjslkdfjskdfjsldkfjsdlkfjslkdjfskldjflskdfjskldjflskdfjsdklfjsdklfjsldkfjsdlkfjsdlkfjsdlkfjsldkfjslkdfjsdlkfjsldkfjsdlkfjskdfjsldkfjsdlkfjslkdfjsdlkfjsldkfjsldkfjsldkfjslkdfjsdlkfjslkdfjsdklfsd' },
|
||||
]);
|
||||
});
|
||||
test('should support wrapping with multiple links', async () => {
|
||||
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ' } });
|
||||
await assertLink('fsdjfsdkfjslkdfjskdfjsldkfj sdlkfjslkdjfskldjflskdfjskldjflskdfj sdklfjsdklfjsldkfjsdlkfjsdlkfjsdlkfjsldkfjslkdfjsdlkfjsldkfjsdlkfjskdfjsldkfjsdlkfjslkdfjsdlkfjsldkfjsldkfjsldkfjslkdfjsdlkfjslkdfjsdklfsd', [
|
||||
{ range: [[1, 1], [27, 1]], text: 'fsdjfsdkfjslkdfjskdfjsldkfj' },
|
||||
{ range: [[29, 1], [64, 1]], text: 'sdlkfjslkdjfskldjflskdfjskldjflskdfj' },
|
||||
{ range: [[66, 1], [43, 3]], text: 'sdklfjsdklfjsldkfjsdlkfjsdlkfjsdlkfjsldkfjslkdfjsdlkfjsldkfjsdlkfjskdfjsldkfjsdlkfjslkdfjsdlkfjsldkfjsldkfjsldkfjslkdfjsdlkfjslkdfjsdklfsd' }
|
||||
]);
|
||||
});
|
||||
test('does not return any links for empty text', async () => {
|
||||
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ' } });
|
||||
await assertLink('', []);
|
||||
});
|
||||
test('should support file scheme links', async () => {
|
||||
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ' } });
|
||||
await assertLink('file:///C:/users/test/file.txt ', [{ range: [[1, 1], [30, 1]], text: 'file:///C:/users/test/file.txt' }]);
|
||||
await assertLink('file:///C:/users/test/file.txt:1:10 ', [{ range: [[1, 1], [35, 1]], text: 'file:///C:/users/test/file.txt:1:10' }]);
|
||||
});
|
||||
test('should apply the cwd to the link only when the file exists and cwdDetection is enabled', async () => {
|
||||
const cwd = (isWindows
|
||||
? 'c:\\Users\\home\\folder'
|
||||
: '/Users/home/folder'
|
||||
);
|
||||
const absoluteFile = (isWindows
|
||||
? 'c:\\Users\\home\\folder\\file.txt'
|
||||
: '/Users/home/folder/file.txt'
|
||||
);
|
||||
fileService.setFiles([absoluteFile]);
|
||||
|
||||
// Set a fake detected command starting as line 0 to establish the cwd
|
||||
commandDetection.setCommands([{
|
||||
command: '',
|
||||
cwd,
|
||||
timestamp: 0,
|
||||
getOutput() { return undefined; },
|
||||
marker: {
|
||||
line: 0
|
||||
} as Partial<IXtermMarker> as any
|
||||
}]);
|
||||
await assertLink('file.txt', [{
|
||||
range: [[1, 1], [8, 1]],
|
||||
text: 'file.txt',
|
||||
linkActivationResult: {
|
||||
link: (isWindows
|
||||
? 'file:///c%3A%5CUsers%5Chome%5Cfolder%5Cfile.txt'
|
||||
: 'file:///Users/home/folder/file.txt'
|
||||
),
|
||||
source: 'editor'
|
||||
}
|
||||
}]);
|
||||
|
||||
// Clear deteceted commands and ensure the same request results in a search
|
||||
commandDetection.setCommands([]);
|
||||
await assertLink('file.txt', [{
|
||||
range: [[1, 1], [8, 1]],
|
||||
text: 'file.txt',
|
||||
linkActivationResult: { link: 'file.txt', source: 'search' }
|
||||
}]);
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue