未验证 提交 165686e3 编写于 作者: D Daniel Imms 提交者: GitHub

Merge pull request #47987 from Microsoft/tyriar/terminal_process_refactor

Separate terminal instance from terminal process
......@@ -55,7 +55,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
ignoreConfigurationCwd: true,
env
};
return TPromise.as(this.terminalService.createInstance(shellLaunchConfig).id);
return TPromise.as(this.terminalService.createTerminal(shellLaunchConfig).id);
}
public $show(terminalId: number, preserveFocus: boolean): void {
......
......@@ -42,7 +42,7 @@ export class TerminalLauncher implements ITerminalLauncher {
let t = this.integratedTerminalInstance;
if ((t && this.isBusy(t)) || !t) {
t = this.terminalService.createInstance({ name: args.title || nls.localize('debug.terminal.title', "debuggee") });
t = this.terminalService.createTerminal({ name: args.title || nls.localize('debug.terminal.title', "debuggee") });
this.integratedTerminalInstance = t;
}
this.terminalService.setActiveInstance(t);
......
......@@ -91,7 +91,7 @@ CommandsRegistry.registerCommand({
const directoriesToOpen = distinct(stats.map(({ stat }) => stat.isDirectory ? stat.resource.fsPath : paths.dirname(stat.resource.fsPath)));
return directoriesToOpen.map(dir => {
if (configurationService.getValue<ITerminalConfiguration>().terminal.explorerKind === 'integrated') {
const instance = integratedTerminalService.createInstance({ cwd: dir }, true);
const instance = integratedTerminalService.createTerminal({ cwd: dir }, true);
if (instance && (resources.length === 1 || !resource || dir === resource.fsPath || dir === paths.dirname(resource.fsPath))) {
integratedTerminalService.setActiveInstance(instance);
integratedTerminalService.showPanel(true);
......
......@@ -571,7 +571,7 @@ export class TerminalTaskSystem implements ITaskSystem {
return [terminalToReuse.terminal, commandExecutable];
}
const result = this.terminalService.createInstance(shellLaunchConfig);
const result = this.terminalService.createTerminal(shellLaunchConfig);
const terminalKey = result.id.toString();
result.onDisposed((terminal) => {
let terminalData = this.terminals[terminalKey];
......
......@@ -2,7 +2,6 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
......
......@@ -3,11 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ITerminalInstance, IShellLaunchConfig, ITerminalTab, Direction, ITerminalService } from 'vs/workbench/parts/terminal/common/terminal';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper';
import { ITerminalInstance, IShellLaunchConfig, ITerminalTab, Direction, ITerminalService, ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal';
import { IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { TerminalInstance } from 'vs/workbench/parts/terminal/electron-browser/terminalInstance';
import { Event, Emitter, anyEvent } from 'vs/base/common/event';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { SplitView, Orientation, IView } from 'vs/base/browser/ui/splitview/splitview';
......@@ -255,10 +252,9 @@ export class TerminalTab extends Disposable implements ITerminalTab {
constructor(
terminalFocusContextKey: IContextKey<boolean>,
configHelper: TerminalConfigHelper,
configHelper: ITerminalConfigHelper,
private _container: HTMLElement,
shellLaunchConfig: IShellLaunchConfig,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@ITerminalService private readonly _terminalService: ITerminalService,
@IPartService private readonly _partService: IPartService
) {
......@@ -266,11 +262,12 @@ export class TerminalTab extends Disposable implements ITerminalTab {
this._onDisposed = new Emitter<ITerminalTab>();
this._onInstancesChanged = new Emitter<void>();
const instance = this._instantiationService.createInstance(TerminalInstance,
const instance = this._terminalService.createInstance(
terminalFocusContextKey,
configHelper,
undefined,
shellLaunchConfig);
shellLaunchConfig,
true);
this._terminalInstances.push(instance);
this._initInstanceListeners(instance);
this._activeInstanceIndex = 0;
......@@ -392,14 +389,15 @@ export class TerminalTab extends Disposable implements ITerminalTab {
public split(
terminalFocusContextKey: IContextKey<boolean>,
configHelper: TerminalConfigHelper,
configHelper: ITerminalConfigHelper,
shellLaunchConfig: IShellLaunchConfig
): ITerminalInstance {
const instance = this._instantiationService.createInstance(TerminalInstance,
const instance = this._terminalService.createInstance(
terminalFocusContextKey,
configHelper,
undefined,
shellLaunchConfig);
shellLaunchConfig,
true);
this._terminalInstances.splice(this._activeInstanceIndex + 1, 0, instance);
this._initInstanceListeners(instance);
this._setActiveInstance(instance);
......
......@@ -2,7 +2,6 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
......@@ -157,7 +156,17 @@ export interface ITerminalService {
terminalInstances: ITerminalInstance[];
terminalTabs: ITerminalTab[];
createInstance(shell?: IShellLaunchConfig, wasNewTerminalAction?: boolean): ITerminalInstance;
/**
* Creates a terminal.
* @param shell The shell launch configuration to use.
* @param wasNewTerminalAction Whether this was triggered by a new terminal action, if so a
* default shell selection dialog may display.
*/
createTerminal(shell?: IShellLaunchConfig, wasNewTerminalAction?: boolean): ITerminalInstance;
/**
* Creates a raw terminal instance, this should not be used outside of the terminal part.
*/
createInstance(terminalFocusContextKey: IContextKey<boolean>, configHelper: ITerminalConfigHelper, container: HTMLElement, shellLaunchConfig: IShellLaunchConfig, doCreateProcess: boolean): ITerminalInstance;
getInstanceFromId(terminalId: number): ITerminalInstance;
getInstanceFromIndex(terminalIndex: number): ITerminalInstance;
getTabLabels(): string[];
......@@ -456,3 +465,44 @@ export interface ITerminalCommandTracker {
selectToPreviousCommand(): void;
selectToNextCommand(): void;
}
export interface ITerminalProcessMessage {
type: 'pid' | 'data' | 'title';
content: number | string;
}
export interface ITerminalProcessManager extends IDisposable {
readonly processState: ProcessState;
readonly ptyProcessReady: TPromise<void>;
readonly shellProcessId: number;
readonly initialCwd: string;
readonly onProcessReady: Event<void>;
readonly onProcessData: Event<string>;
readonly onProcessTitle: Event<string>;
readonly onProcessExit: Event<number>;
addDisposable(disposable: IDisposable);
createProcess(shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number);
write(data: string): void;
setDimensions(cols: number, rows: number): void;
}
export enum ProcessState {
// The process has not been initialized yet.
UNINITIALIZED,
// The process is currently launching, the process is marked as launching
// for a short duration after being created and is helpful to indicate
// whether the process died as a result of bad shell and args.
LAUNCHING,
// The process is running normally.
RUNNING,
// The process was killed during launch, likely as a result of bad shell and
// args.
KILLED_DURING_LAUNCH,
// The process was killed by the user (the event originated from VS Code).
KILLED_BY_USER,
// The process was killed by itself, for example the shell crashed or `exit`
// was run.
KILLED_BY_PROCESS
}
......@@ -75,7 +75,8 @@ export abstract class TerminalService implements ITerminalService {
}
protected abstract _showTerminalCloseConfirmation(): TPromise<boolean>;
public abstract createInstance(shell?: IShellLaunchConfig, wasNewTerminalAction?: boolean): ITerminalInstance;
public abstract createTerminal(shell?: IShellLaunchConfig, wasNewTerminalAction?: boolean): ITerminalInstance;
public abstract createInstance(terminalFocusContextKey: IContextKey<boolean>, configHelper: ITerminalConfigHelper, container: HTMLElement, shellLaunchConfig: IShellLaunchConfig, doCreateProcess: boolean): ITerminalInstance;
public abstract getActiveOrCreateInstance(wasNewTerminalAction?: boolean): ITerminalInstance;
public abstract selectDefaultWindowsShell(): TPromise<string>;
public abstract setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void;
......@@ -96,7 +97,7 @@ export abstract class TerminalService implements ITerminalService {
}
tabConfigs.forEach(tabConfig => {
const instance = this.createInstance(tabConfig.instances[0]);
const instance = this.createTerminal(tabConfig.instances[0]);
for (let i = 1; i < tabConfig.instances.length; i++) {
this.splitInstance(instance, tabConfig.instances[i]);
}
......
......@@ -11,7 +11,7 @@ import * as debugActions from 'vs/workbench/parts/debug/browser/debugActions';
import * as nls from 'vs/nls';
import * as panel from 'vs/workbench/browser/panel';
import * as platform from 'vs/base/common/platform';
import * as terminalCommands from 'vs/workbench/parts/terminal/electron-browser/terminalCommands';
import * as terminalCommands from 'vs/workbench/parts/terminal/common/terminalCommands';
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
import { ITerminalService, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_PANEL_ID, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, TerminalCursorStyle, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_NOT_VISIBLE } from 'vs/workbench/parts/terminal/common/terminal';
import { getTerminalDefaultShellUnixLike, getTerminalDefaultShellWindows } from 'vs/workbench/parts/terminal/electron-browser/terminal';
......@@ -28,7 +28,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { OpenNextRecentlyUsedEditorInGroupAction, OpenPreviousRecentlyUsedEditorInGroupAction, FocusActiveGroupAction, FocusFirstGroupAction, FocusSecondGroupAction, FocusThirdGroupAction } from 'vs/workbench/browser/parts/editor/editorActions';
import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions';
import { registerColors } from './terminalColorRegistry';
import { registerColors } from 'vs/workbench/parts/terminal/common/terminalColorRegistry';
import { NavigateUpAction, NavigateDownAction, NavigateLeftAction, NavigateRightAction } from 'vs/workbench/electron-browser/actions';
import { QUICKOPEN_ACTION_ID, getQuickNavigateHandler, QUICKOPEN_FOCUS_SECONDARY_ACTION_ID } from 'vs/workbench/browser/parts/quickopen/quickopen';
import { IQuickOpenRegistry, Extensions as QuickOpenExtensions, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen';
......
......@@ -2,7 +2,6 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as os from 'os';
import * as platform from 'vs/base/common/platform';
......
......@@ -46,7 +46,7 @@ export class ToggleTerminalAction extends TogglePanelAction {
if (this.terminalService.terminalInstances.length === 0) {
// If there is not yet an instance attempt to create it here so that we can suggest a
// new shell on Windows (and not do so when the panel is restored on reload).
const newTerminalInstance = this.terminalService.createInstance(undefined, true);
const newTerminalInstance = this.terminalService.createTerminal(undefined, true);
const toDispose = newTerminalInstance.onProcessIdReady(() => {
newTerminalInstance.focus();
toDispose.dispose();
......@@ -258,7 +258,7 @@ export class CreateNewTerminalAction extends Action {
if (folders.length <= 1) {
// Allow terminal service to handle the path when there is only a
// single root
instancePromise = TPromise.as(this.terminalService.createInstance(undefined, true));
instancePromise = TPromise.as(this.terminalService.createTerminal(undefined, true));
} else {
const options: IPickOptions = {
placeHolder: nls.localize('workbench.action.terminal.newWorkspacePlaceholder', "Select current working directory for new terminal")
......@@ -268,7 +268,7 @@ export class CreateNewTerminalAction extends Action {
// Don't create the instance if the workspace picker was canceled
return null;
}
return this.terminalService.createInstance({ cwd: workspace.uri.fsPath }, true);
return this.terminalService.createTerminal({ cwd: workspace.uri.fsPath }, true);
});
}
......@@ -295,7 +295,7 @@ export class CreateNewInActiveWorkspaceTerminalAction extends Action {
}
public run(event?: any): TPromise<any> {
const instance = this.terminalService.createInstance(undefined, true);
const instance = this.terminalService.createTerminal(undefined, true);
if (!instance) {
return TPromise.as(void 0);
}
......
......@@ -3,11 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as dom from 'vs/base/browser/dom';
import * as nls from 'vs/nls';
import * as platform from 'vs/base/common/platform';
import * as terminalEnvironment from 'vs/workbench/parts/terminal/node/terminalEnvironment';
import { Action, IAction } from 'vs/base/common/actions';
import { IActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
......@@ -16,7 +15,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ITerminalService, TERMINAL_PANEL_ID } from 'vs/workbench/parts/terminal/common/terminal';
import { IThemeService, ITheme, registerThemingParticipant, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
import { TerminalFindWidget } from './terminalFindWidget';
import { TerminalFindWidget } from 'vs/workbench/parts/terminal/browser/terminalFindWidget';
import { editorHoverBackground, editorHoverBorder, editorForeground } from 'vs/platform/theme/common/colorRegistry';
import { KillTerminalAction, SwitchTerminalAction, SwitchTerminalActionItem, CopyTerminalSelectionAction, TerminalPasteAction, ClearTerminalAction, SelectAllTerminalAction, CreateNewTerminalAction, SplitTerminalAction } from 'vs/workbench/parts/terminal/electron-browser/terminalActions';
import { Panel } from 'vs/workbench/browser/panel';
......@@ -24,7 +23,7 @@ import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { TPromise } from 'vs/base/common/winjs.base';
import URI from 'vs/base/common/uri';
import { PANEL_BACKGROUND, PANEL_BORDER } from 'vs/workbench/common/theme';
import { TERMINAL_BACKGROUND_COLOR, TERMINAL_BORDER_COLOR } from 'vs/workbench/parts/terminal/electron-browser/terminalColorRegistry';
import { TERMINAL_BACKGROUND_COLOR, TERMINAL_BORDER_COLOR } from 'vs/workbench/parts/terminal/common/terminalColorRegistry';
import { DataTransfers } from 'vs/base/browser/dnd';
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
......@@ -112,7 +111,7 @@ export class TerminalPanel extends Panel {
return;
}
const instance = this._terminalService.createInstance();
const instance = this._terminalService.createTerminal();
if (instance) {
this._updateFont();
this._updateTheme();
......@@ -290,7 +289,7 @@ export class TerminalPanel extends Panel {
}
const terminal = this._terminalService.getActiveInstance();
terminal.sendText(TerminalPanel.preparePathForTerminal(path), false);
terminal.sendText(terminalEnvironment.preparePathForTerminal(path), false);
}
}));
}
......@@ -311,30 +310,6 @@ export class TerminalPanel extends Panel {
// dom.toggleClass(this._parentDomElement, 'enable-ligatures', this._terminalService.configHelper.config.fontLigatures);
this.layout(new dom.Dimension(this._parentDomElement.offsetWidth, this._parentDomElement.offsetHeight));
}
/**
* Adds quotes to a path if it contains whitespaces
*/
public static preparePathForTerminal(path: string): string {
if (platform.isWindows) {
if (/\s+/.test(path)) {
return `"${path}"`;
}
return path;
}
path = path.replace(/(%5C|\\)/g, '\\\\');
const charsToEscape = [
' ', '\'', '"', '?', ':', ';', '!', '*', '(', ')', '{', '}', '[', ']'
];
for (let i = 0; i < path.length; i++) {
const indexOfChar = charsToEscape.indexOf(path.charAt(i));
if (indexOfChar >= 0) {
path = `${path.substring(0, i)}\\${path.charAt(i)}${path.substring(i + 1)}`;
i++; // Skip char due to escape char being added
}
}
return path;
}
}
registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as cp from 'child_process';
import * as path from 'path';
import * as platform from 'vs/base/common/platform';
import * as terminalEnvironment from 'vs/workbench/parts/terminal/node/terminalEnvironment';
import Uri from 'vs/base/common/uri';
import { IDisposable } from 'vs/base/common/lifecycle';
import { ProcessState, ITerminalProcessManager, ITerminalProcessMessage, IShellLaunchConfig, ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal';
import { TPromise } from 'vs/base/common/winjs.base';
import { ILogService } from 'vs/platform/log/common/log';
import { Emitter, Event } from 'vs/base/common/event';
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
/** The amount of time to consider terminal errors to be related to the launch */
const LAUNCHING_DURATION = 500;
/**
* Holds all state related to the creation and management of terminal processes.
*
* Internal definitions:
* - Process: The process launched with the terminalProcess.ts file, or the pty as a whole
* - Pty Process: The pseudoterminal master process (or the winpty agent process)
* - Shell Process: The pseudoterminal slave process (ie. the shell)
*/
export class TerminalProcessManager implements ITerminalProcessManager {
public processState: ProcessState = ProcessState.UNINITIALIZED;
public ptyProcessReady: TPromise<void>;
public shellProcessId: number;
public initialCwd: string;
private _process: cp.ChildProcess;
private _preLaunchInputQueue: string[] = [];
private _disposables: IDisposable[] = [];
private readonly _onProcessReady: Emitter<void> = new Emitter<void>();
public get onProcessReady(): Event<void> { return this._onProcessReady.event; }
private readonly _onProcessData: Emitter<string> = new Emitter<string>();
public get onProcessData(): Event<string> { return this._onProcessData.event; }
private readonly _onProcessTitle: Emitter<string> = new Emitter<string>();
public get onProcessTitle(): Event<string> { return this._onProcessTitle.event; }
private readonly _onProcessExit: Emitter<number> = new Emitter<number>();
public get onProcessExit(): Event<number> { return this._onProcessExit.event; }
constructor(
private _configHelper: ITerminalConfigHelper,
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
@IHistoryService private readonly _historyService: IHistoryService,
@IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService,
@ILogService private _logService: ILogService
) {
}
public dispose(): void {
if (this._process) {
if (this._process.connected) {
// If the process was still connected this dispose came from
// within VS Code, not the process, so mark the process as
// killed by the user.
this.processState = ProcessState.KILLED_BY_USER;
this._process.send({ event: 'shutdown' });
}
this._process = null;
}
this._disposables.forEach(d => d.dispose());
this._disposables.length = 0;
}
public addDisposable(disposable: IDisposable) {
this._disposables.push(disposable);
}
public createProcess(
shellLaunchConfig: IShellLaunchConfig,
cols: number,
rows: number
): void {
this.ptyProcessReady = new TPromise<void>(c => {
this.onProcessReady(() => {
this._logService.debug(`Terminal process ready (shellProcessId: ${this.shellProcessId})`);
c(void 0);
});
});
const locale = this._configHelper.config.setLocaleVariables ? platform.locale : undefined;
if (!shellLaunchConfig.executable) {
this._configHelper.mergeDefaultShellPathAndArgs(shellLaunchConfig);
}
const lastActiveWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot('file');
this.initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, lastActiveWorkspaceRootUri, this._configHelper);
// Resolve env vars from config and shell
const lastActiveWorkspaceRoot = this._workspaceContextService.getWorkspaceFolder(lastActiveWorkspaceRootUri);
const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux');
const envFromConfig = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...this._configHelper.config.env[platformKey] }, lastActiveWorkspaceRoot);
const envFromShell = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...shellLaunchConfig.env }, lastActiveWorkspaceRoot);
shellLaunchConfig.env = envFromShell;
// Merge process env with the env from config
const parentEnv = { ...process.env };
terminalEnvironment.mergeEnvironments(parentEnv, envFromConfig);
// Continue env initialization, merging in the env from the launch
// config and adding keys that are needed to create the process
const env = terminalEnvironment.createTerminalEnv(parentEnv, shellLaunchConfig, this.initialCwd, locale, cols, rows);
const cwd = Uri.parse(path.dirname(require.toUrl('../node/terminalProcess'))).fsPath;
const options = { env, cwd };
this._logService.debug(`Terminal process launching`, options);
this._process = cp.fork(Uri.parse(require.toUrl('bootstrap')).fsPath, ['--type=terminal'], options);
this.processState = ProcessState.LAUNCHING;
this._process.on('message', message => this._onMessage(message));
this._process.on('exit', exitCode => this._onExit(exitCode));
setTimeout(() => {
if (this.processState === ProcessState.LAUNCHING) {
this.processState = ProcessState.RUNNING;
}
}, LAUNCHING_DURATION);
}
public setDimensions(cols: number, rows: number): void {
if (this._process && this._process.connected) {
// The child process could aready be terminated
try {
this._process.send({ event: 'resize', cols, rows });
} catch (error) {
// We tried to write to a closed pipe / channel.
if (error.code !== 'EPIPE' && error.code !== 'ERR_IPC_CHANNEL_CLOSED') {
throw (error);
}
}
}
}
public write(data: string): void {
if (this.shellProcessId) {
// Send data if the pty is ready
this._process.send({
event: 'input',
data
});
} else {
// If the pty is not ready, queue the data received to send later
this._preLaunchInputQueue.push(data);
}
}
private _onMessage(message: ITerminalProcessMessage): void {
this._logService.trace(`terminalProcessManager#_onMessage (shellProcessId: ${this.shellProcessId}`, message);
switch (message.type) {
case 'data':
this._onProcessData.fire(<string>message.content);
break;
case 'pid':
this.shellProcessId = <number>message.content;
this._onProcessReady.fire();
// Send any queued data that's waiting
if (this._preLaunchInputQueue.length > 0) {
this._process.send({
event: 'input',
data: this._preLaunchInputQueue.join('')
});
this._preLaunchInputQueue.length = 0;
}
break;
case 'title':
this._onProcessTitle.fire(<string>message.content);
break;
}
}
private _onExit(exitCode: number): void {
this._process = null;
// If the process is marked as launching then mark the process as killed
// during launch. This typically means that there is a problem with the
// shell and args.
if (this.processState === ProcessState.LAUNCHING) {
this.processState = ProcessState.KILLED_DURING_LAUNCH;
}
// If TerminalInstance did not know about the process exit then it was
// triggered by the process, not on VS Code's side.
if (this.processState === ProcessState.RUNNING) {
this.processState = ProcessState.KILLED_BY_PROCESS;
}
this._onProcessExit.fire(exitCode);
}
}
\ No newline at end of file
......@@ -6,7 +6,7 @@
import * as nls from 'vs/nls';
import * as pfs from 'vs/base/node/pfs';
import * as platform from 'vs/base/common/platform';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
......@@ -21,11 +21,12 @@ import Severity from 'vs/base/common/severity';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { getTerminalDefaultShellWindows } from 'vs/workbench/parts/terminal/electron-browser/terminal';
import { TerminalPanel } from 'vs/workbench/parts/terminal/electron-browser/terminalPanel';
import { TerminalTab } from 'vs/workbench/parts/terminal/electron-browser/terminalTab';
import { TerminalTab } from 'vs/workbench/parts/terminal/browser/terminalTab';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ipcRenderer as ipc } from 'electron';
import { IOpenFileRequest } from 'vs/platform/windows/common/windows';
import { TerminalInstance } from 'vs/workbench/parts/terminal/electron-browser/terminalInstance';
export class TerminalService extends AbstractTerminalService implements ITerminalService {
private _configHelper: TerminalConfigHelper;
......@@ -67,7 +68,7 @@ export class TerminalService extends AbstractTerminalService implements ITermina
});
}
public createInstance(shell: IShellLaunchConfig = {}, wasNewTerminalAction?: boolean): ITerminalInstance {
public createTerminal(shell: IShellLaunchConfig = {}, wasNewTerminalAction?: boolean): ITerminalInstance {
const terminalTab = this._instantiationService.createInstance(TerminalTab,
this._terminalFocusContextKey,
this._configHelper,
......@@ -88,6 +89,10 @@ export class TerminalService extends AbstractTerminalService implements ITermina
return instance;
}
public createInstance(terminalFocusContextKey: IContextKey<boolean>, configHelper: ITerminalConfigHelper, container: HTMLElement, shellLaunchConfig: IShellLaunchConfig, doCreateProcess: boolean): ITerminalInstance {
return this._instantiationService.createInstance(TerminalInstance, terminalFocusContextKey, configHelper, undefined, shellLaunchConfig, true);
}
public focusFindWidget(): TPromise<void> {
return this.showPanel(false).then(() => {
let panel = this._panelService.getActivePanel() as TerminalPanel;
......@@ -154,7 +159,7 @@ export class TerminalService extends AbstractTerminalService implements ITermina
return TPromise.as(null);
}
// Launch a new instance with the newly selected shell
const instance = this.createInstance({
const instance = this.createTerminal({
executable: shell,
args: this._configHelper.config.shellArgs.windows
});
......@@ -234,7 +239,7 @@ export class TerminalService extends AbstractTerminalService implements ITermina
public getActiveOrCreateInstance(wasNewTerminalAction?: boolean): ITerminalInstance {
const activeInstance = this.getActiveInstance();
return activeInstance ? activeInstance : this.createInstance(undefined, wasNewTerminalAction);
return activeInstance ? activeInstance : this.createTerminal(undefined, wasNewTerminalAction);
}
protected _showTerminalCloseConfirmation(): TPromise<boolean> {
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import * as paths from 'vs/base/common/paths';
import * as platform from 'vs/base/common/platform';
import pkg from 'vs/platform/node/package';
import Uri from 'vs/base/common/uri';
import { IStringDictionary } from 'vs/base/common/collections';
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { IShellLaunchConfig, ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal';
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
/**
* This module contains utility functions related to the environment, cwd and paths.
*/
export function mergeEnvironments(parent: IStringDictionary<string>, other: IStringDictionary<string>) {
if (!other) {
return;
}
// On Windows apply the new values ignoring case, while still retaining
// the case of the original key.
if (platform.isWindows) {
for (let configKey in other) {
let actualKey = configKey;
for (let envKey in parent) {
if (configKey.toLowerCase() === envKey.toLowerCase()) {
actualKey = envKey;
break;
}
}
const value = other[configKey];
_mergeEnvironmentValue(parent, actualKey, value);
}
} else {
Object.keys(other).forEach((key) => {
const value = other[key];
_mergeEnvironmentValue(parent, key, value);
});
}
}
function _mergeEnvironmentValue(env: IStringDictionary<string>, key: string, value: string | null) {
if (typeof value === 'string') {
env[key] = value;
} else {
delete env[key];
}
}
export function createTerminalEnv(parentEnv: IStringDictionary<string>, shell: IShellLaunchConfig, cwd: string, locale: string, cols?: number, rows?: number): IStringDictionary<string> {
const env = { ...parentEnv };
if (shell.env) {
mergeEnvironments(env, shell.env);
}
env['PTYPID'] = process.pid.toString();
env['PTYSHELL'] = shell.executable;
env['TERM_PROGRAM'] = 'vscode';
env['TERM_PROGRAM_VERSION'] = pkg.version;
if (shell.args) {
if (typeof shell.args === 'string') {
env[`PTYSHELLCMDLINE`] = shell.args;
} else {
shell.args.forEach((arg, i) => env[`PTYSHELLARG${i}`] = arg);
}
}
env['PTYCWD'] = cwd;
env['LANG'] = _getLangEnvVariable(locale);
if (cols && rows) {
env['PTYCOLS'] = cols.toString();
env['PTYROWS'] = rows.toString();
}
env['AMD_ENTRYPOINT'] = 'vs/workbench/parts/terminal/node/terminalProcess';
return env;
}
// TODO:should be protected/non-static
export function resolveConfigurationVariables(configurationResolverService: IConfigurationResolverService, env: IStringDictionary<string>, lastActiveWorkspaceRoot: IWorkspaceFolder): IStringDictionary<string> {
Object.keys(env).forEach((key) => {
if (typeof env[key] === 'string') {
env[key] = configurationResolverService.resolve(lastActiveWorkspaceRoot, env[key]);
}
});
return env;
}
function _getLangEnvVariable(locale?: string) {
const parts = locale ? locale.split('-') : [];
const n = parts.length;
if (n === 0) {
// Fallback to en_US to prevent possible encoding issues.
return 'en_US.UTF-8';
}
if (n === 1) {
// app.getLocale can return just a language without a variant, fill in the variant for
// supported languages as many shells expect a 2-part locale.
const languageVariants = {
de: 'DE',
en: 'US',
es: 'ES',
fi: 'FI',
fr: 'FR',
it: 'IT',
ja: 'JP',
ko: 'KR',
pl: 'PL',
ru: 'RU',
zh: 'CN'
};
if (parts[0] in languageVariants) {
parts.push(languageVariants[parts[0]]);
}
} else {
// Ensure the variant is uppercase
parts[1] = parts[1].toUpperCase();
}
return parts.join('_') + '.UTF-8';
}
export function getCwd(shell: IShellLaunchConfig, root: Uri, configHelper: ITerminalConfigHelper): string {
if (shell.cwd) {
return shell.cwd;
}
let cwd: string;
// TODO: Handle non-existent customCwd
if (!shell.ignoreConfigurationCwd) {
// Evaluate custom cwd first
const customCwd = configHelper.config.cwd;
if (customCwd) {
if (paths.isAbsolute(customCwd)) {
cwd = customCwd;
} else if (root) {
cwd = paths.normalize(paths.join(root.fsPath, customCwd));
}
}
}
// If there was no custom cwd or it was relative with no workspace
if (!cwd) {
cwd = root ? root.fsPath : os.homedir();
}
return _sanitizeCwd(cwd);
}
function _sanitizeCwd(cwd: string): string {
// Make the drive letter uppercase on Windows (see #9448)
if (platform.platform === platform.Platform.Windows && cwd && cwd[1] === ':') {
return cwd[0].toUpperCase() + cwd.substr(1);
}
return cwd;
}
/**
* Adds quotes to a path if it contains whitespaces
*/
export function preparePathForTerminal(path: string): string {
if (platform.isWindows) {
if (/\s+/.test(path)) {
return `"${path}"`;
}
return path;
}
path = path.replace(/(%5C|\\)/g, '\\\\');
const charsToEscape = [
' ', '\'', '"', '?', ':', ';', '!', '*', '(', ')', '{', '}', '[', ']'
];
for (let i = 0; i < path.length; i++) {
const indexOfChar = charsToEscape.indexOf(path.charAt(i));
if (indexOfChar >= 0) {
path = `${path.substring(0, i)}\\${path.charAt(i)}${path.substring(i + 1)}`;
i++; // Skip char due to escape char being added
}
}
return path;
}
......@@ -3,12 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import { Extensions as ThemeingExtensions, IColorRegistry, ColorIdentifier } from 'vs/platform/theme/common/colorRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { ansiColorIdentifiers, registerColors } from 'vs/workbench/parts/terminal/electron-browser/terminalColorRegistry';
import { ansiColorIdentifiers, registerColors } from 'vs/workbench/parts/terminal/common/terminalColorRegistry';
import { ITheme, ThemeType } from 'vs/platform/theme/common/themeService';
import { Color } from 'vs/base/common/color';
......
......@@ -3,8 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper';
import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions';
......
......@@ -3,8 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import { Platform } from 'vs/base/common/platform';
import { TerminalLinkHandler, LineColumnInfo } from 'vs/workbench/parts/terminal/electron-browser/terminalLinkHandler';
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import { TerminalPanel } from 'vs/workbench/parts/terminal/electron-browser/terminalPanel';
import * as platform from 'vs/base/common/platform';
suite('Workbench - TerminalPanel', () => {
test('preparePathForTerminal', function () {
if (platform.isWindows) {
assert.equal(TerminalPanel.preparePathForTerminal('C:\\foo'), 'C:\\foo');
assert.equal(TerminalPanel.preparePathForTerminal('C:\\foo bar'), '"C:\\foo bar"');
return;
}
assert.equal(TerminalPanel.preparePathForTerminal('/a/\\foo bar"\'? ;\'?? :'), '/a/\\\\foo\\ bar\\"\\\'\\?\\ \\;\\\'\\?\\?\\ \\ \\:');
assert.equal(TerminalPanel.preparePathForTerminal('/\\\'"?:;!*(){}[]'), '/\\\\\\\'\\"\\?\\:\\;\\!\\*\\(\\)\\{\\}\\[\\]');
});
});
\ No newline at end of file
......@@ -3,8 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import { Terminal } from 'vscode-xterm';
import { TerminalCommandTracker } from 'vs/workbench/parts/terminal/node/terminalCommandTracker';
......
......@@ -3,48 +3,15 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import * as os from 'os';
import * as platform from 'vs/base/common/platform';
import * as terminalEnvironment from 'vs/workbench/parts/terminal/node/terminalEnvironment';
import Uri from 'vs/base/common/uri';
import { IStringDictionary } from 'vs/base/common/collections';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { TerminalInstance } from 'vs/workbench/parts/terminal/electron-browser/terminalInstance';
import { IShellLaunchConfig } from 'vs/workbench/parts/terminal/common/terminal';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { TestNotificationService, TestContextService, TestHistoryService } from 'vs/workbench/test/workbenchTestServices';
import { MockContextKeyService, MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { TPromise } from 'vs/base/common/winjs.base';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ILogService, NullLogService } from 'vs/platform/log/common/log';
class TestTerminalInstance extends TerminalInstance {
public _getCwd(shell: IShellLaunchConfig, root: Uri): string {
return super._getCwd(shell, root);
}
protected _createProcess(): void { }
protected _createXterm(): TPromise<void> { return TPromise.as(void 0); }
}
suite('Workbench - TerminalInstance', () => {
let instantiationService: TestInstantiationService;
setup(() => {
instantiationService = new TestInstantiationService();
instantiationService.stub(INotificationService, new TestNotificationService());
instantiationService.stub(IHistoryService, new TestHistoryService());
});
import { IShellLaunchConfig, ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal';
suite('Workbench - TerminalEnvironment', () => {
test('createTerminalEnv', function () {
const shell1 = {
executable: '/bin/foosh',
......@@ -53,7 +20,7 @@ suite('Workbench - TerminalInstance', () => {
const parentEnv1: IStringDictionary<string> = {
ok: true
} as any;
const env1 = TerminalInstance.createTerminalEnv(parentEnv1, shell1, '/foo', 'en-au');
const env1 = terminalEnvironment.createTerminalEnv(parentEnv1, shell1, '/foo', 'en-au');
assert.ok(env1['ok'], 'Parent environment is copied');
assert.deepStrictEqual(parentEnv1, { ok: true }, 'Parent environment is unchanged');
assert.equal(env1['PTYPID'], process.pid.toString(), 'PTYPID is equal to the current PID');
......@@ -71,15 +38,15 @@ suite('Workbench - TerminalInstance', () => {
const parentEnv2: IStringDictionary<string> = {
LANG: 'en_US.UTF-8'
};
const env2 = TerminalInstance.createTerminalEnv(parentEnv2, shell2, '/foo', 'en-au');
const env2 = terminalEnvironment.createTerminalEnv(parentEnv2, shell2, '/foo', 'en-au');
assert.ok(!('PTYSHELLARG0' in env2), 'PTYSHELLARG0 is unset');
assert.equal(env2['PTYCWD'], '/foo', 'PTYCWD is equal to /foo');
assert.equal(env2['LANG'], 'en_AU.UTF-8', 'LANG is equal to the requested locale with UTF-8');
const env3 = TerminalInstance.createTerminalEnv(parentEnv1, shell1, '/', null);
const env3 = terminalEnvironment.createTerminalEnv(parentEnv1, shell1, '/', null);
assert.equal(env3['LANG'], 'en_US.UTF-8', 'LANG is equal to en_US.UTF-8 as fallback.'); // More info on issue #14586
const env4 = TerminalInstance.createTerminalEnv(parentEnv2, shell1, '/', null);
const env4 = terminalEnvironment.createTerminalEnv(parentEnv2, shell1, '/', null);
assert.equal(env4['LANG'], 'en_US.UTF-8', 'LANG is equal to the parent environment\'s LANG');
});
......@@ -91,7 +58,7 @@ suite('Workbench - TerminalInstance', () => {
const other = {
c: 'd'
};
TerminalInstance.mergeEnvironments(parent, other);
terminalEnvironment.mergeEnvironments(parent, other);
assert.deepEqual(parent, {
a: 'b',
c: 'd'
......@@ -108,7 +75,7 @@ suite('Workbench - TerminalInstance', () => {
const other = {
A: 'c'
};
TerminalInstance.mergeEnvironments(parent, other);
terminalEnvironment.mergeEnvironments(parent, other);
assert.deepEqual(parent, {
a: 'c'
});
......@@ -122,7 +89,7 @@ suite('Workbench - TerminalInstance', () => {
const other: IStringDictionary<string> = {
a: null
};
TerminalInstance.mergeEnvironments(parent, other);
terminalEnvironment.mergeEnvironments(parent, other);
assert.deepEqual(parent, {
c: 'd'
});
......@@ -139,36 +106,22 @@ suite('Workbench - TerminalInstance', () => {
const other: IStringDictionary<string> = {
A: null
};
TerminalInstance.mergeEnvironments(parent, other);
terminalEnvironment.mergeEnvironments(parent, other);
assert.deepEqual(parent, {
c: 'd'
});
});
});
suite('_getCwd', () => {
let instance: TestTerminalInstance;
let instantiationService: TestInstantiationService;
let configHelper: { config: { cwd: string } };
suite('getCwd', () => {
let configHelper: ITerminalConfigHelper;
setup(() => {
let contextKeyService = new MockContextKeyService();
let keybindingService = new MockKeybindingService();
let terminalFocusContextKey = contextKeyService.createKey('test', false);
instantiationService = new TestInstantiationService();
instantiationService.stub(IConfigurationService, new TestConfigurationService());
instantiationService.stub(INotificationService, new TestNotificationService());
instantiationService.stub(IWorkspaceContextService, new TestContextService());
instantiationService.stub(IKeybindingService, keybindingService);
instantiationService.stub(IContextKeyService, contextKeyService);
instantiationService.stub(IHistoryService, new TestHistoryService());
instantiationService.stub(ILogService, new NullLogService());
configHelper = {
configHelper = <ITerminalConfigHelper>{
config: {
cwd: null
}
};
instance = instantiationService.createInstance(TestTerminalInstance, terminalFocusContextKey, configHelper, null, null);
});
// This helper checks the paths in a cross-platform friendly manner
......@@ -177,39 +130,49 @@ suite('Workbench - TerminalInstance', () => {
}
test('should default to os.homedir() for an empty workspace', () => {
assertPathsMatch(instance._getCwd({ executable: null, args: [] }, null), os.homedir());
assertPathsMatch(terminalEnvironment.getCwd({ executable: null, args: [] }, null, configHelper), os.homedir());
});
test('should use to the workspace if it exists', () => {
assertPathsMatch(instance._getCwd({ executable: null, args: [] }, Uri.file('/foo')), '/foo');
assertPathsMatch(terminalEnvironment.getCwd({ executable: null, args: [] }, Uri.file('/foo'), configHelper), '/foo');
});
test('should use an absolute custom cwd as is', () => {
configHelper.config.cwd = '/foo';
assertPathsMatch(instance._getCwd({ executable: null, args: [] }, null), '/foo');
assertPathsMatch(terminalEnvironment.getCwd({ executable: null, args: [] }, null, configHelper), '/foo');
});
test('should normalize a relative custom cwd against the workspace path', () => {
configHelper.config.cwd = 'foo';
assertPathsMatch(instance._getCwd({ executable: null, args: [] }, Uri.file('/bar')), '/bar/foo');
assertPathsMatch(terminalEnvironment.getCwd({ executable: null, args: [] }, Uri.file('/bar'), configHelper), '/bar/foo');
configHelper.config.cwd = './foo';
assertPathsMatch(instance._getCwd({ executable: null, args: [] }, Uri.file('/bar')), '/bar/foo');
assertPathsMatch(terminalEnvironment.getCwd({ executable: null, args: [] }, Uri.file('/bar'), configHelper), '/bar/foo');
configHelper.config.cwd = '../foo';
assertPathsMatch(instance._getCwd({ executable: null, args: [] }, Uri.file('/bar'), ), '/foo');
assertPathsMatch(terminalEnvironment.getCwd({ executable: null, args: [] }, Uri.file('/bar'), configHelper), '/foo');
});
test('should fall back for relative a custom cwd that doesn\'t have a workspace', () => {
configHelper.config.cwd = 'foo';
assertPathsMatch(instance._getCwd({ executable: null, args: [] }, null), os.homedir());
assertPathsMatch(terminalEnvironment.getCwd({ executable: null, args: [] }, null, configHelper), os.homedir());
configHelper.config.cwd = './foo';
assertPathsMatch(instance._getCwd({ executable: null, args: [] }, null), os.homedir());
assertPathsMatch(terminalEnvironment.getCwd({ executable: null, args: [] }, null, configHelper), os.homedir());
configHelper.config.cwd = '../foo';
assertPathsMatch(instance._getCwd({ executable: null, args: [] }, null), os.homedir());
assertPathsMatch(terminalEnvironment.getCwd({ executable: null, args: [] }, null, configHelper), os.homedir());
});
test('should ignore custom cwd when told to ignore', () => {
configHelper.config.cwd = '/foo';
assertPathsMatch(instance._getCwd({ executable: null, args: [], ignoreConfigurationCwd: true }, Uri.file('/bar')), '/bar');
assertPathsMatch(terminalEnvironment.getCwd({ executable: null, args: [], ignoreConfigurationCwd: true }, Uri.file('/bar'), configHelper), '/bar');
});
});
test('preparePathForTerminal', function () {
if (platform.isWindows) {
assert.equal(terminalEnvironment.preparePathForTerminal('C:\\foo'), 'C:\\foo');
assert.equal(terminalEnvironment.preparePathForTerminal('C:\\foo bar'), '"C:\\foo bar"');
return;
}
assert.equal(terminalEnvironment.preparePathForTerminal('/a/\\foo bar"\'? ;\'?? :'), '/a/\\\\foo\\ bar\\"\\\'\\?\\ \\;\\\'\\?\\?\\ \\ \\:');
assert.equal(terminalEnvironment.preparePathForTerminal('/\\\'"?:;!*(){}[]'), '/\\\\\\\'\\"\\?\\:\\;\\!\\*\\(\\)\\{\\}\\[\\]');
});
});
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册