提交 c3378834 编写于 作者: D Daniel Imms

Refactor for terminal links in live share

上级 daccb87f
......@@ -6,7 +6,8 @@
import { Terminal as XTermTerminal } from 'vscode-xterm';
import { ITerminalInstance, IWindowsShellHelper, ITerminalProcessManager, ITerminalConfigHelper, ITerminalChildProcess, IShellLaunchConfig } from 'vs/workbench/contrib/terminal/common/terminal';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IProcessEnvironment } from 'vs/base/common/platform';
import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
export const ITerminalInstanceService = createDecorator<ITerminalInstanceService>('terminalInstanceService');
......@@ -17,6 +18,8 @@ export interface ITerminalInstanceService {
createWindowsShellHelper(shellProcessId: number, instance: ITerminalInstance, xterm: XTermTerminal): IWindowsShellHelper;
createTerminalProcessManager(id: number, configHelper: ITerminalConfigHelper): ITerminalProcessManager;
createTerminalProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean): ITerminalChildProcess;
getRemoteOperatingSystem(): Promise<OperatingSystem | undefined>;
getRemoteUserHome(): Promise<URI | undefined>;
}
export interface IBrowserTerminalConfigHelper extends ITerminalConfigHelper {
......
......@@ -430,7 +430,7 @@ export class TerminalInstance implements ITerminalInstance {
this._processManager.onProcessData(data => this._onProcessData(data));
this._xterm.on('data', data => this._processManager!.write(data));
// TODO: How does the cwd work on detached processes?
this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, this._xterm, platform.platform);
this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, this._xterm, platform.platform, this._processManager);
this.processReady.then(async () => {
this._linkHandler.processCwd = await this._processManager!.getInitialCwd();
});
......
......@@ -4,18 +4,19 @@
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import * as path from 'vs/base/common/path';
import * as platform from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/terminalWidgetManager';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ITerminalService } from 'vs/workbench/contrib/terminal/common/terminal';
import { ITerminalService, ITerminalProcessManager } 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 { ILinkMatcherOptions } from 'vscode-xterm';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { posix, win32 } from 'vs/base/common/path';
const pathPrefix = '(\\.\\.?|\\~)';
const pathSeparatorClause = '\\/';
......@@ -57,12 +58,16 @@ const LOCAL_LINK_PRIORITY = -2;
export type XtermLinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void;
export type XtermLinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
interface IPath {
join(...paths: string[]): string;
normalize(path: string): string;
}
export class TerminalLinkHandler {
private _hoverDisposables: IDisposable[] = [];
private _mouseMoveDisposable: IDisposable;
private _widgetManager: TerminalWidgetManager;
private _processCwd: string;
private _localLinkPattern: RegExp;
private _gitDiffPreImagePattern: RegExp;
private _gitDiffPostImagePattern: RegExp;
private readonly _tooltipCallback: (event: MouseEvent, uri: string) => boolean | void;
......@@ -70,15 +75,13 @@ export class TerminalLinkHandler {
constructor(
private _xterm: any,
private _platform: platform.Platform,
private readonly _processManager: ITerminalProcessManager,
@IOpenerService private readonly _openerService: IOpenerService,
@IEditorService private readonly _editorService: IEditorService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@ITerminalService private readonly _terminalService: ITerminalService,
@IFileService private readonly _fileService: IFileService
) {
const baseLocalLinkClause = _platform === platform.Platform.Windows ? winLocalLinkClause : unixLocalLinkClause;
// Append line and column number regex
this._localLinkPattern = new RegExp(`${baseLocalLinkClause}(${lineAndColumnClause})`);
// Matches '--- a/src/file1', capturing 'src/file1' in group 1
this._gitDiffPreImagePattern = /^--- a\/(\S*)/;
// Matches '+++ b/src/file1', capturing 'src/file1' in group 1
......@@ -183,7 +186,9 @@ export class TerminalLinkHandler {
}
protected get _localLinkRegex(): RegExp {
return this._localLinkPattern;
const baseLocalLinkClause = this._processManager.os === platform.OperatingSystem.Windows ? winLocalLinkClause : unixLocalLinkClause;
// Append line and column number regex
return new RegExp(`${baseLocalLinkClause}(${lineAndColumnClause})`);
}
protected get _gitDiffPreImageRegex(): RegExp {
......@@ -199,19 +204,12 @@ export class TerminalLinkHandler {
if (!resolvedLink) {
return Promise.resolve(null);
}
const normalizedPath = path.normalize(path.resolve(resolvedLink));
const normalizedUrl = this.extractLinkUrl(normalizedPath);
if (!normalizedUrl) {
return Promise.resolve(null);
}
const resource = URI.file(normalizedUrl);
const lineColumnInfo: LineColumnInfo = this.extractLineColumnInfo(link);
const selection: ITextEditorSelection = {
startLineNumber: lineColumnInfo.lineNumber,
startColumn: lineColumnInfo.columnNumber
};
return this._editorService.openEditor({ resource, options: { pinned: true, selection } });
return this._editorService.openEditor({ resource: resolvedLink, options: { pinned: true, selection } });
});
}
......@@ -247,37 +245,41 @@ export class TerminalLinkHandler {
return nls.localize('terminalLinkHandler.followLinkCtrl', 'Ctrl + click to follow link');
}
private get osPath(): IPath {
if (this._processManager.os === platform.OperatingSystem.Windows) {
return win32;
}
return posix;
}
protected _preprocessPath(link: string): string | null {
if (this._platform === platform.Platform.Windows) {
// Resolve ~ -> %HOMEDRIVE%\%HOMEPATH%
if (link.charAt(0) === '~') {
if (!process.env.HOMEDRIVE || !process.env.HOMEPATH) {
return null;
if (link.charAt(0) === '~') {
// Resolve ~ -> userHome
link = this.osPath.join(this._processManager.userHome, link.substring(1));
} else if (link.charAt(0) !== '/' && link.charAt(0) !== '~') {
// Resolve workspace path . | .. | <relative_path> -> <path>/. | <path>/.. | <path>/<relative_path>
if (this._processManager.os === platform.OperatingSystem.Windows) {
if (!link.match('^' + winDrivePrefix)) {
if (!this._processCwd) {
// Abort if no workspace is open
return null;
}
link = this.osPath.join(this._processCwd, link);
}
link = `${process.env.HOMEDRIVE}\\${process.env.HOMEPATH + link.substring(1)}`;
}
// Resolve relative paths (.\a, ..\a, ~\a, a\b)
if (!link.match('^' + winDrivePrefix)) {
} else {
if (!this._processCwd) {
// Abort if no workspace is open
return null;
}
link = path.join(this._processCwd, link);
link = this.osPath.join(this._processCwd, link);
}
}
// Resolve workspace path . | .. | <relative_path> -> <path>/. | <path>/.. | <path>/<relative_path>
else if (link.charAt(0) !== '/' && link.charAt(0) !== '~') {
if (!this._processCwd) {
// Abort if no workspace is open
return null;
}
link = path.join(this._processCwd, link);
}
link = this.osPath.normalize(link);
return link;
}
private _resolvePath(link: string): PromiseLike<string | null> {
private _resolvePath(link: string): PromiseLike<URI | null> {
const preprocessedLink = this._preprocessPath(link);
if (!preprocessedLink) {
return Promise.resolve(null);
......@@ -288,13 +290,31 @@ export class TerminalLinkHandler {
return Promise.resolve(null);
}
// Ensure the file exists on disk, so an editor can be opened after clicking it
return this._fileService.resolveFile(URI.file(linkUrl)).then(stat => {
if (stat.isDirectory) {
return null;
try {
let uri: URI;
if (this._processManager.remoteAuthority) {
uri = URI.from({
scheme: REMOTE_HOST_SCHEME,
authority: this._processManager.remoteAuthority,
path: linkUrl
});
} else {
uri = URI.file(linkUrl);
}
return preprocessedLink;
});
return this._fileService.resolveFile(uri).then(stat => {
if (stat.isDirectory) {
return null;
}
return uri;
}).catch(() => {
// Does not exist
return null;
});
} catch {
// Errors in parsing the path
return Promise.resolve(null);
}
}
/**
......
......@@ -38,6 +38,9 @@ export class TerminalProcessManager implements ITerminalProcessManager {
public processState: ProcessState = ProcessState.UNINITIALIZED;
public ptyProcessReady: Promise<void>;
public shellProcessId: number;
public remoteAuthority: string | undefined;
public os: platform.OperatingSystem;
public userHome: string;
private _process: ITerminalChildProcess | null = null;
private _preLaunchInputQueue: string[] = [];
......@@ -96,17 +99,24 @@ export class TerminalProcessManager implements ITerminalProcessManager {
cols: number,
rows: number
): void {
let launchRemotely = false;
const forceExtHostProcess = (this._configHelper.config as any).extHostProcess;
if (shellLaunchConfig.cwd && typeof shellLaunchConfig.cwd === 'object') {
launchRemotely = !!getRemoteAuthority(shellLaunchConfig.cwd);
this.remoteAuthority = getRemoteAuthority(shellLaunchConfig.cwd);
} else {
launchRemotely = !!this._windowService.getConfiguration().remoteAuthority;
this.remoteAuthority = this._windowService.getConfiguration().remoteAuthority;
}
const hasRemoteAuthority = !!this.remoteAuthority;
let launchRemotely = hasRemoteAuthority || forceExtHostProcess;
this.userHome = this._environmentService.userHome;
this.os = platform.OS;
if (launchRemotely) {
if (hasRemoteAuthority) {
this._terminalInstanceService.getRemoteUserHome().then(userHome => this.userHome = userHome.path);
this._terminalInstanceService.getRemoteOperatingSystem().then(os => this.os = os);
}
if (launchRemotely || forceExtHostProcess) {
const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(forceExtHostProcess ? undefined : REMOTE_HOST_SCHEME);
const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(hasRemoteAuthority ? REMOTE_HOST_SCHEME : undefined);
this._process = this._instantiationService.createInstance(TerminalProcessExtHostProxy, this._terminalId, shellLaunchConfig, activeWorkspaceRootUri, cols, rows);
} else {
if (!shellLaunchConfig.executable) {
......
......@@ -637,6 +637,9 @@ export interface ITerminalProcessManager extends IDisposable {
readonly processState: ProcessState;
readonly ptyProcessReady: Promise<void>;
readonly shellProcessId: number;
readonly remoteAuthority: string | undefined;
readonly os: platform.OperatingSystem;
readonly userHome: string;
readonly onProcessReady: Event<void>;
readonly onProcessData: Event<string>;
......
......@@ -10,8 +10,10 @@ import { ITerminalInstance, IWindowsShellHelper, ITerminalConfigHelper, ITermina
import { WindowsShellHelper } from 'vs/workbench/contrib/terminal/node/windowsShellHelper';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TerminalProcessManager } from 'vs/workbench/contrib/terminal/browser/terminalProcessManager';
import { IProcessEnvironment } from 'vs/base/common/platform';
import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform';
import { TerminalProcess } from 'vs/workbench/contrib/terminal/node/terminalProcess';
import { IRemoteAgentService, IRemoteAgentEnvironment } from 'vs/workbench/services/remote/node/remoteAgentService';
import { URI } from 'vs/base/common/uri';
let Terminal: typeof XTermTerminal;
......@@ -23,8 +25,11 @@ let Terminal: typeof XTermTerminal;
export class TerminalInstanceService implements ITerminalInstanceService {
public _serviceBrand: any;
private _remoteAgentEnvironment: IRemoteAgentEnvironment | undefined | null;
constructor(
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService
) {
}
......@@ -54,4 +59,32 @@ export class TerminalInstanceService implements ITerminalInstanceService {
public createTerminalProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean): ITerminalChildProcess {
return new TerminalProcess(shellLaunchConfig, cwd, cols, rows, env, windowsEnableConpty);
}
private async _fetchRemoteAgentEnvironment(): Promise<IRemoteAgentEnvironment | null> {
if (this._remoteAgentEnvironment === undefined) {
const connection = await this._remoteAgentService.getConnection();
if (!connection) {
this._remoteAgentEnvironment = null;
return this._remoteAgentEnvironment;
}
this._remoteAgentEnvironment = await connection.getEnvironment();
}
return this._remoteAgentEnvironment;
}
public async getRemoteUserHome(): Promise<URI | undefined> {
const env = await this._fetchRemoteAgentEnvironment();
if (env === null) {
return undefined;
}
return env.userHome;
}
public async getRemoteOperatingSystem(): Promise<OperatingSystem | undefined> {
const env = await this._fetchRemoteAgentEnvironment();
if (env === null) {
return undefined;
}
return env.os;
}
}
\ No newline at end of file
......@@ -4,11 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Platform } from 'vs/base/common/platform';
import { Platform, OperatingSystem } from 'vs/base/common/platform';
import { TerminalLinkHandler, LineColumnInfo } from 'vs/workbench/contrib/terminal/browser/terminalLinkHandler';
import * as strings from 'vs/base/common/strings';
import * as path from 'vs/base/common/path';
import * as sinon from 'sinon';
class TestTerminalLinkHandler extends TerminalLinkHandler {
public get localLinkRegex(): RegExp {
......@@ -39,7 +37,10 @@ interface LinkFormatInfo {
suite('Workbench - TerminalLinkHandler', () => {
suite('localLinkRegex', () => {
test('Windows', () => {
const terminalLinkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Windows, null!, null!, null!, null!, null!);
const terminalLinkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Windows, {
os: OperatingSystem.Windows,
userHome: ''
} as any, null!, null!, null!, null!, null!);
function testLink(link: string, linkUrl: string, lineNo?: string, columnNo?: string) {
assert.equal(terminalLinkHandler.extractLinkUrl(link), linkUrl);
assert.equal(terminalLinkHandler.extractLinkUrl(`:${link}:`), linkUrl);
......@@ -111,7 +112,10 @@ suite('Workbench - TerminalLinkHandler', () => {
});
test('Linux', () => {
const terminalLinkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Linux, null!, null!, null!, null!, null!);
const terminalLinkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Linux, {
os: OperatingSystem.Linux,
userHome: ''
} as any, null!, null!, null!, null!, null!);
function testLink(link: string, linkUrl: string, lineNo?: string, columnNo?: string) {
assert.equal(terminalLinkHandler.extractLinkUrl(link), linkUrl);
assert.equal(terminalLinkHandler.extractLinkUrl(`:${link}:`), linkUrl);
......@@ -175,58 +179,64 @@ suite('Workbench - TerminalLinkHandler', () => {
suite('preprocessPath', () => {
test('Windows', () => {
const linkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Windows, null!, null!, null!, null!, null!);
const linkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Windows, {
os: OperatingSystem.Windows,
userHome: 'C:\\Users\\Me'
} as any, null!, null!, null!, null!, null!);
linkHandler.processCwd = 'C:\\base';
let stub = sinon.stub(path, 'join', function (arg1: string, arg2: string) {
return arg1 + '\\' + arg2;
});
assert.equal(linkHandler.preprocessPath('./src/file1'), 'C:\\base\\./src/file1');
assert.equal(linkHandler.preprocessPath('./src/file1'), 'C:\\base\\src\\file1');
assert.equal(linkHandler.preprocessPath('src\\file2'), 'C:\\base\\src\\file2');
assert.equal(linkHandler.preprocessPath('C:\\absolute\\path\\file3'), 'C:\\absolute\\path\\file3');
stub.restore();
assert.equal(linkHandler.preprocessPath('~/src/file3'), 'C:\\Users\\Me\\src\\file3');
assert.equal(linkHandler.preprocessPath('~\\src\\file4'), 'C:\\Users\\Me\\src\\file4');
assert.equal(linkHandler.preprocessPath('C:\\absolute\\path\\file5'), 'C:\\absolute\\path\\file5');
});
test('Windows - spaces', () => {
const linkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Windows, null!, null!, null!, null!, null!);
const linkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Windows, {
os: OperatingSystem.Windows,
userHome: 'C:\\Users\\M e'
} as any, null!, null!, null!, null!, null!);
linkHandler.processCwd = 'C:\\base dir';
let stub = sinon.stub(path, 'join', function (arg1: string, arg2: string) {
return arg1 + '\\' + arg2;
});
assert.equal(linkHandler.preprocessPath('./src/file1'), 'C:\\base dir\\./src/file1');
assert.equal(linkHandler.preprocessPath('./src/file1'), 'C:\\base dir\\src\\file1');
assert.equal(linkHandler.preprocessPath('src\\file2'), 'C:\\base dir\\src\\file2');
assert.equal(linkHandler.preprocessPath('C:\\absolute\\path\\file3'), 'C:\\absolute\\path\\file3');
stub.restore();
assert.equal(linkHandler.preprocessPath('~/src/file3'), 'C:\\Users\\M e\\src\\file3');
assert.equal(linkHandler.preprocessPath('~\\src\\file4'), 'C:\\Users\\M e\\src\\file4');
assert.equal(linkHandler.preprocessPath('C:\\abso lute\\path\\file5'), 'C:\\abso lute\\path\\file5');
});
test('Linux', () => {
const linkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Linux, null!, null!, null!, null!, null!);
const linkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Linux, {
os: OperatingSystem.Linux,
userHome: '/home/me'
} as any, null!, null!, null!, null!, null!);
linkHandler.processCwd = '/base';
let stub = sinon.stub(path, 'join', function (arg1: string, arg2: string) {
return arg1 + '/' + arg2;
});
assert.equal(linkHandler.preprocessPath('./src/file1'), '/base/./src/file1');
assert.equal(linkHandler.preprocessPath('./src/file1'), '/base/src/file1');
assert.equal(linkHandler.preprocessPath('src/file2'), '/base/src/file2');
assert.equal(linkHandler.preprocessPath('/absolute/path/file3'), '/absolute/path/file3');
stub.restore();
assert.equal(linkHandler.preprocessPath('~/src/file3'), '/home/me/src/file3');
assert.equal(linkHandler.preprocessPath('/absolute/path/file4'), '/absolute/path/file4');
});
test('No Workspace', () => {
const linkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Linux, null!, null!, null!, null!, null!);
const linkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Linux, {
os: OperatingSystem.Linux,
userHome: '/home/me'
} as any, null!, null!, null!, null!, null!);
assert.equal(linkHandler.preprocessPath('./src/file1'), null);
assert.equal(linkHandler.preprocessPath('src/file2'), null);
assert.equal(linkHandler.preprocessPath('/absolute/path/file3'), '/absolute/path/file3');
assert.equal(linkHandler.preprocessPath('~/src/file3'), '/home/me/src/file3');
assert.equal(linkHandler.preprocessPath('/absolute/path/file4'), '/absolute/path/file4');
});
});
test('gitDiffLinkRegex', () => {
// The platform is irrelevant because the links generated by Git are the same format regardless of platform
const linkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Linux, null!, null!, null!, null!, null!);
const linkHandler = new TestTerminalLinkHandler(new TestXterm(), Platform.Linux, {
os: OperatingSystem.Linux,
userHome: ''
} as any, null!, null!, null!, null!, null!);
function assertAreGoodMatches(matches: RegExpMatchArray | null) {
if (matches) {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册