提交 6ab796c6 编写于 作者: D Daniel Imms 提交者: GitHub

Merge pull request #11839 from Microsoft/tyriar/11275_terminal_refactor

Refactor terminal to enable processes to exist without the panel
......@@ -153,7 +153,7 @@ export abstract class MainThreadOutputServiceShape {
}
export abstract class MainThreadTerminalServiceShape {
$createTerminal(name?: string, shellPath?: string): TPromise<number> { throw ni(); }
$createTerminal(name?: string, shellPath?: string): number { throw ni(); }
$dispose(terminalId: number): void { throw ni(); }
$hide(terminalId: number): void { throw ni(); }
$sendText(terminalId: number, text: string, addNewLine: boolean): void { throw ni(); }
......
......@@ -16,18 +16,12 @@ export class ExtHostTerminal implements vscode.Terminal {
private _id: number;
private _proxy: MainThreadTerminalServiceShape;
private _disposed: boolean;
private _queuedRequests: ApiRequest[] = [];
constructor(proxy: MainThreadTerminalServiceShape, id: number, name?: string, shellPath?: string) {
this._name = name;
this._shellPath = shellPath;
this._proxy = proxy;
this._proxy.$createTerminal(name, shellPath).then((terminalId) => {
this._id = terminalId;
this._queuedRequests.forEach((r) => {
r.run(this._proxy, this._id);
});
});
this._id = this._proxy.$createTerminal(name, shellPath);
}
public get name(): string {
......@@ -37,32 +31,17 @@ export class ExtHostTerminal implements vscode.Terminal {
public sendText(text: string, addNewLine: boolean = true): void {
this._checkDisposed();
let request: ApiRequest = new ApiRequest(this._proxy.$sendText, [text, addNewLine]);
if (!this._id) {
this._queuedRequests.push(request);
return;
}
request.run(this._proxy, this._id);
this._proxy.$sendText(this._id, text, addNewLine);
}
public show(preserveFocus: boolean): void {
this._checkDisposed();
let request: ApiRequest = new ApiRequest(this._proxy.$show, [preserveFocus]);
if (!this._id) {
this._queuedRequests.push(request);
return;
}
request.run(this._proxy, this._id);
this._proxy.$show(this._id, preserveFocus);
}
public hide(): void {
this._checkDisposed();
let request: ApiRequest = new ApiRequest(this._proxy.$hide, []);
if (!this._id) {
this._queuedRequests.push(request);
return;
}
request.run(this._proxy, this._id);
this._proxy.$hide(this._id);
}
public dispose(): void {
......@@ -91,17 +70,3 @@ export class ExtHostTerminalService {
return new ExtHostTerminal(this._proxy, -1, name, shellPath);
}
}
class ApiRequest {
private _callback: (...args: any[]) => void;
private _args: any[];
constructor(callback: (...args: any[]) => void, args: any[]) {
this._callback = callback;
this._args = args;
}
public run(proxy: MainThreadTerminalServiceShape, id: number) {
this._callback.apply(proxy, [id].concat(this._args));
}
}
......@@ -4,50 +4,50 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import {TPromise} from 'vs/base/common/winjs.base';
import {ITerminalService} from 'vs/workbench/parts/terminal/electron-browser/terminal';
import {IPanelService} from 'vs/workbench/services/panel/common/panelService';
import {IPartService} from 'vs/workbench/services/part/common/partService';
import {MainThreadTerminalServiceShape} from './extHost.protocol';
export class MainThreadTerminalService extends MainThreadTerminalServiceShape {
private _terminalService: ITerminalService;
constructor(
@ITerminalService terminalService: ITerminalService
@IPanelService private panelService: IPanelService,
@IPartService private partService: IPartService,
@ITerminalService private terminalService: ITerminalService
) {
super();
this._terminalService = terminalService;
}
public $createTerminal(name?: string, shellPath?: string): TPromise<number> {
return this._terminalService.createNew(name, shellPath);
public $createTerminal(name?: string, shellPath?: string): number {
return this.terminalService.createInstance(name, shellPath).id;
}
public $show(terminalId: number, preserveFocus: boolean): void {
this._terminalService.show(!preserveFocus).then((terminalPanel) => {
this._terminalService.setActiveTerminalById(terminalId);
if (!preserveFocus) {
// If the panel was already showing an explicit focus call is necessary here.
terminalPanel.focus();
}
});
let terminalInstance = this.terminalService.getInstanceFromId(terminalId);
if (terminalInstance) {
this.terminalService.setActiveInstance(terminalInstance);
this.terminalService.showPanel(!preserveFocus);
}
}
public $hide(terminalId: number): void {
this._terminalService.hideTerminalInstance(terminalId);
if (this.terminalService.getActiveInstance().id === terminalId) {
this.terminalService.hidePanel();
}
}
public $dispose(terminalId: number): void {
// TODO: This could be improved by not first showing the terminal to be disposed
this._terminalService.show(false).then((terminalPanel) => {
terminalPanel.closeTerminalById(terminalId);
});;
let terminalInstance = this.terminalService.getInstanceFromId(terminalId);
if (terminalInstance) {
terminalInstance.dispose();
}
}
public $sendText(terminalId: number, text: string, addNewLine: boolean): void {
this._terminalService.show(false).then((terminalPanel) => {
this._terminalService.setActiveTerminalById(terminalId);
terminalPanel.sendTextToActiveTerminal(text, addNewLine);
});
let terminalInstance = this.terminalService.getInstanceFromId(terminalId);
if (terminalInstance) {
terminalInstance.sendText(text, addNewLine);
}
}
}
......@@ -6,13 +6,13 @@
import nls = require('vs/nls');
import platform = require('vs/base/common/platform');
import {TPromise} from 'vs/base/common/winjs.base';
import {ITerminalService} from 'vs/workbench/parts/terminal/electron-browser/terminal';
import {ITerminalService, ITerminalInstance} from 'vs/workbench/parts/terminal/electron-browser/terminal';
import {ITerminalService as IExternalTerminalService} from 'vs/workbench/parts/execution/common/execution';
export class TerminalSupport {
private static terminalId: number;
private static integratedTerminalInstance: ITerminalInstance;
public static runInTerminal(terminalService: ITerminalService, nativeTerminalService: IExternalTerminalService, args: DebugProtocol.RunInTerminalRequestArguments, response: DebugProtocol.RunInTerminalResponse): TPromise<void> {
......@@ -24,14 +24,14 @@ export class TerminalSupport {
private static runInIntegratedTerminal(terminalService: ITerminalService, args: DebugProtocol.RunInTerminalRequestArguments): TPromise<void> {
return (!TerminalSupport.terminalId ? terminalService.createNew(args.title || nls.localize('debuggee', "debuggee")) : TPromise.as(TerminalSupport.terminalId)).then(id => {
TerminalSupport.terminalId = id;
return terminalService.show(false).then(terminalPanel => {
terminalService.setActiveTerminalById(id);
const command = this.prepareCommand(args);
terminalPanel.sendTextToActiveTerminal(command, true);
});
});
if (!TerminalSupport.integratedTerminalInstance) {
TerminalSupport.integratedTerminalInstance = terminalService.createInstance(args.title || nls.localize('debuggee', "debuggee"));
}
terminalService.setActiveInstance(TerminalSupport.integratedTerminalInstance);
terminalService.showPanel(true);
const command = this.prepareCommand(args);
TerminalSupport.integratedTerminalInstance.sendText(command, true);
return TPromise.as(void 0);
}
private static prepareCommand(args: DebugProtocol.RunInTerminalRequestArguments): string {
......
......@@ -9,18 +9,18 @@ import 'vs/css!./media/xterm';
import * as panel from 'vs/workbench/browser/panel';
import * as platform from 'vs/base/common/platform';
import nls = require('vs/nls');
import {Extensions, IConfigurationRegistry} from 'vs/platform/configuration/common/configurationRegistry';
import {ITerminalService, KEYBINDING_CONTEXT_TERMINAL_FOCUS, TERMINAL_PANEL_ID, TERMINAL_DEFAULT_SHELL_LINUX, TERMINAL_DEFAULT_SHELL_OSX, TERMINAL_DEFAULT_SHELL_WINDOWS} from 'vs/workbench/parts/terminal/electron-browser/terminal';
import {IWorkbenchActionRegistry, Extensions as ActionExtensions} from 'vs/workbench/common/actionRegistry';
import {KeyCode, KeyMod} from 'vs/base/common/keyCodes';
import {KillTerminalAction, CopyTerminalSelectionAction, CreateNewTerminalAction, FocusTerminalAction, FocusNextTerminalAction, FocusPreviousTerminalAction, RunSelectedTextInTerminalAction, ScrollDownTerminalAction, ScrollUpTerminalAction, TerminalPasteAction, ToggleTerminalAction} from 'vs/workbench/parts/terminal/electron-browser/terminalActions';
import {Registry} from 'vs/platform/platform';
import {SyncActionDescriptor} from 'vs/platform/actions/common/actions';
import {TerminalService} from 'vs/workbench/parts/terminal/electron-browser/terminalService';
import {registerSingleton} from 'vs/platform/instantiation/common/extensions';
import {GlobalQuickOpenAction} from 'vs/workbench/browser/parts/quickopen/quickopen.contribution';
import {ShowAllCommandsAction} from 'vs/workbench/parts/quickopen/browser/commandsHandler';
import {ToggleTabFocusModeAction} from 'vs/editor/contrib/toggleTabFocusMode/common/toggleTabFocusMode';
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
import { GlobalQuickOpenAction } from 'vs/workbench/browser/parts/quickopen/quickopen.contribution';
import { ITerminalService, KEYBINDING_CONTEXT_TERMINAL_FOCUS, TERMINAL_PANEL_ID, TERMINAL_DEFAULT_SHELL_LINUX, TERMINAL_DEFAULT_SHELL_OSX, TERMINAL_DEFAULT_SHELL_WINDOWS } from 'vs/workbench/parts/terminal/electron-browser/terminal';
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actionRegistry';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { KillTerminalAction, CopyTerminalSelectionAction, CreateNewTerminalAction, FocusTerminalAction, FocusNextTerminalAction, FocusPreviousTerminalAction, RunSelectedTextInTerminalAction, ScrollDownTerminalAction, ScrollUpTerminalAction, TerminalPasteAction, ToggleTerminalAction } from 'vs/workbench/parts/terminal/electron-browser/terminalActions';
import { Registry } from 'vs/platform/platform';
import { ShowAllCommandsAction } from 'vs/workbench/parts/quickopen/browser/commandsHandler';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { TerminalService } from 'vs/workbench/parts/terminal/electron-browser/terminalService';
import { ToggleTabFocusModeAction } from 'vs/editor/contrib/toggleTabFocusMode/common/toggleTabFocusMode';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
let configurationRegistry = <IConfigurationRegistry>Registry.as(Extensions.Configuration);
configurationRegistry.registerConfiguration({
......
......@@ -5,13 +5,13 @@
'use strict';
import Event from 'vs/base/common/event';
import cp = require('child_process');
import platform = require('vs/base/common/platform');
import processes = require('vs/base/node/processes');
import {Builder} from 'vs/base/browser/builder';
import {TPromise} from 'vs/base/common/winjs.base';
import {createDecorator} from 'vs/platform/instantiation/common/instantiation';
import {RawContextKey, ContextKeyExpr} from 'vs/platform/contextkey/common/contextkey';
import { Builder, Dimension } from 'vs/base/browser/builder';
import { RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { TPromise } from 'vs/base/common/winjs.base';
import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const TERMINAL_PANEL_ID = 'workbench.panel.terminal';
......@@ -52,41 +52,126 @@ export interface ITerminalConfiguration {
};
}
export interface ITerminalProcess {
title: string;
process: cp.ChildProcess;
}
export interface ITerminalService {
_serviceBrand: any;
activeTerminalInstanceIndex: number;
configHelper: TerminalConfigHelper;
onActiveInstanceChanged: Event<string>;
onInstancesChanged: Event<string>;
onInstanceTitleChanged: Event<string>;
terminalInstances: ITerminalInstance[];
createInstance(name?: string, shellPath?: string): ITerminalInstance;
getInstanceFromId(terminalId: number): ITerminalInstance;
getInstanceLabels(): string[];
getActiveInstance(): ITerminalInstance;
setActiveInstance(terminalInstance: ITerminalInstance): void;
setActiveInstanceByIndex(terminalIndex: number): void;
setActiveInstanceToNext(): void;
setActiveInstanceToPrevious(): void;
close(): TPromise<any>;
copySelection(): TPromise<any>;
createNew(name?: string, shellPath?: string): TPromise<number>;
focusNext(): TPromise<any>;
focusPrevious(): TPromise<any>;
hide(): TPromise<any>;
hideTerminalInstance(terminalId: number): TPromise<any>;
paste(): TPromise<any>;
runSelectedText(): TPromise<any>;
scrollDown(): TPromise<any>;
scrollUp(): TPromise<any>;
show(focus: boolean): TPromise<ITerminalPanel>;
setActiveTerminal(index: number): TPromise<any>;
setActiveTerminalById(terminalId: number): void;
toggle(): TPromise<any>;
getActiveTerminalIndex(): number;
getTerminalInstanceTitles(): string[];
initConfigHelper(panelContainer: Builder): void;
killTerminalProcess(terminalProcess: ITerminalProcess): void;
showPanel(focus?: boolean): TPromise<void>;
hidePanel(): void;
togglePanel(): TPromise<void>;
setContainers(panelContainer: Builder, terminalContainer: HTMLElement): void;
}
export interface ITerminalPanel {
closeTerminalById(terminalId: number): TPromise<void>;
focus(): void;
sendTextToActiveTerminal(text: string, addNewLine: boolean): void;
export interface ITerminalInstance {
/**
* The ID of the terminal instance, this is an arbitrary number only used to identify the
* terminal instance.
*/
id: number;
/**
* An event that fires when the terminal instance's title changes.
*/
onTitleChanged: Event<string>;
/**
* The title of the terminal. This is either title or the process currently running or an
* explicit name given to the terminal instance through the extension API.
*
* @readonly
*/
title: string;
/**
* Dispose the terminal instance, removing it from the panel/service and freeing up resources.
*/
dispose(): void;
/**
* Copies the terminal selection to the clipboard.
*/
copySelection(): void;
/**
* Focuses the terminal instance.
*
* @param focus Force focus even if there is a selection.
*/
focus(force?: boolean): void;
/**
* Focuses and pastes the contents of the clipboard into the terminal instance.
*/
paste(): void;
/**
* Send text to the terminal instance. The text is written to the stdin of the underlying pty
* process (shell) of the terminal instance.
*
* @param text The text to send.
* @param addNewLine Whether to add a new line to the text being sent, this is normally
* required to run a command in the terminal. The character(s) added are \n or \r\n
* depending on the platform. This defaults to `true`.
*/
sendText(text: string, addNewLine: boolean): void;
/**
* Scroll the terminal buffer down 1 line.
*/
scrollDown(): void;
/**
* Scroll the terminal buffer up 1 line.
*/
scrollUp(): void;
/**
* Attaches the terminal instance to an element on the DOM, before this is called the terminal
* instance process may run in the background but cannot be displayed on the UI.
*
* @param container The element to attach the terminal instance to.
*/
attachToElement(container: HTMLElement): void;
/**
* Sets whether the terminal instance's cursor will blink or be solid.
*
* @param blink Whether the cursor will blink.
*/
setCursorBlink(blink: boolean): void;
/**
* Sets the array of commands that skip the shell process so they can be handled by VS Code's
* keybinding system.
*/
setCommandsToSkipShell(commands: string[]): void;
/**
* Configure the dimensions of the terminal instance.
*
* @param dimension The dimensions of the container.
*/
layout(dimension: Dimension): void;
/**
* Sets whether the terminal instance's element is visible in the DOM.
*
* @param visible Whether the element is visible.
*/
setVisible(visible: boolean): void;
}
......@@ -4,10 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import nls = require('vs/nls');
import {Action, IAction} from 'vs/base/common/actions';
import {ITerminalService} from 'vs/workbench/parts/terminal/electron-browser/terminal';
import {SelectActionItem} from 'vs/base/browser/ui/actionbar/actionbar';
import {TPromise} from 'vs/base/common/winjs.base';
import os = require('os');
import { Action, IAction } from 'vs/base/common/actions';
import { EndOfLinePreference } from 'vs/editor/common/editorCommon';
import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService';
import { ITerminalService } from 'vs/workbench/parts/terminal/electron-browser/terminal';
import { SelectActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { TPromise } from 'vs/base/common/winjs.base';
export class ToggleTerminalAction extends Action {
......@@ -22,7 +25,7 @@ export class ToggleTerminalAction extends Action {
}
public run(event?: any): TPromise<any> {
return this.terminalService.toggle();
return this.terminalService.togglePanel();
}
}
......@@ -41,7 +44,11 @@ export class KillTerminalAction extends Action {
}
public run(event?: any): TPromise<any> {
return this.terminalService.close();
let terminalInstance = this.terminalService.getActiveInstance();
if (terminalInstance) {
this.terminalService.getActiveInstance().dispose();
}
return TPromise.as(void 0);
}
}
......@@ -62,7 +69,11 @@ export class CopyTerminalSelectionAction extends Action {
}
public run(event?: any): TPromise<any> {
return this.terminalService.copySelection();
let terminalInstance = this.terminalService.getActiveInstance();
if (terminalInstance) {
terminalInstance.copySelection();
}
return TPromise.as(void 0);
}
}
......@@ -81,7 +92,8 @@ export class CreateNewTerminalAction extends Action {
}
public run(event?: any): TPromise<any> {
return this.terminalService.createNew();
this.terminalService.setActiveInstance(this.terminalService.createInstance());
return this.terminalService.showPanel(true);
}
}
......@@ -98,7 +110,12 @@ export class FocusTerminalAction extends Action {
}
public run(event?: any): TPromise<any> {
return this.terminalService.show(true);
let terminalInstance = this.terminalService.getActiveInstance();
if (!terminalInstance) {
terminalInstance = this.terminalService.createInstance();
}
this.terminalService.setActiveInstance(terminalInstance);
return this.terminalService.showPanel(true);
}
}
......@@ -115,7 +132,8 @@ export class FocusNextTerminalAction extends Action {
}
public run(event?: any): TPromise<any> {
return this.terminalService.focusNext();
this.terminalService.setActiveInstanceToNext();
return this.terminalService.showPanel(true);
}
}
......@@ -132,7 +150,8 @@ export class FocusPreviousTerminalAction extends Action {
}
public run(event?: any): TPromise<any> {
return this.terminalService.focusPrevious();
this.terminalService.setActiveInstanceToPrevious();
return this.terminalService.showPanel(true);
}
}
export class TerminalPasteAction extends Action {
......@@ -148,7 +167,12 @@ export class TerminalPasteAction extends Action {
}
public run(event?: any): TPromise<any> {
return this.terminalService.paste();
let terminalInstance = this.terminalService.getActiveInstance();
if (!terminalInstance) {
terminalInstance = this.terminalService.createInstance();
}
terminalInstance.paste();
return TPromise.as(void 0);
}
}
......@@ -159,13 +183,28 @@ export class RunSelectedTextInTerminalAction extends Action {
constructor(
id: string, label: string,
@ICodeEditorService private codeEditorService: ICodeEditorService,
@ITerminalService private terminalService: ITerminalService
) {
super(id, label);
}
public run(event?: any): TPromise<any> {
return this.terminalService.runSelectedText();
let terminalInstance = this.terminalService.getActiveInstance();
if (!terminalInstance) {
terminalInstance = this.terminalService.createInstance();
}
let editor = this.codeEditorService.getFocusedCodeEditor();
let selection = editor.getSelection();
let text: string;
if (selection.isEmpty()) {
text = editor.getValue();
} else {
let endOfLinePreference = os.EOL === '\n' ? EndOfLinePreference.LF : EndOfLinePreference.CRLF;
text = editor.getModel().getValueInRange(selection, endOfLinePreference);
}
terminalInstance.sendText(text, true);
return TPromise.as(void 0);
}
}
......@@ -184,9 +223,8 @@ export class SwitchTerminalInstanceAction extends Action {
public run(item?: string): TPromise<any> {
let selectedTerminalIndex = parseInt(item.split(':')[0], 10) - 1;
return this.terminalService.show(true).then(() => {
this.terminalService.setActiveTerminal(selectedTerminalIndex);
});
this.terminalService.setActiveInstanceByIndex(selectedTerminalIndex);
return this.terminalService.showPanel(true);
}
}
......@@ -196,14 +234,14 @@ export class SwitchTerminalInstanceActionItem extends SelectActionItem {
action: IAction,
@ITerminalService private terminalService: ITerminalService
) {
super(null, action, terminalService.getTerminalInstanceTitles(), terminalService.getActiveTerminalIndex());
this.toDispose.push(this.terminalService.onInstancesChanged(this.updateItems, this));
this.toDispose.push(this.terminalService.onActiveInstanceChanged(this.updateItems, this));
this.toDispose.push(this.terminalService.onInstanceTitleChanged(this.updateItems, this));
super(null, action, terminalService.getInstanceLabels(), terminalService.activeTerminalInstanceIndex);
this.toDispose.push(terminalService.onInstancesChanged(this.updateItems, this));
this.toDispose.push(terminalService.onActiveInstanceChanged(this.updateItems, this));
this.toDispose.push(terminalService.onInstanceTitleChanged(this.updateItems, this));
}
private updateItems(): void {
this.setOptions(this.terminalService.getTerminalInstanceTitles(), this.terminalService.getActiveTerminalIndex());
this.setOptions(this.terminalService.getInstanceLabels(), this.terminalService.activeTerminalInstanceIndex);
}
}
......@@ -220,7 +258,11 @@ export class ScrollDownTerminalAction extends Action {
}
public run(event?: any): TPromise<any> {
return this.terminalService.scrollDown();
let terminalInstance = this.terminalService.getActiveInstance();
if (terminalInstance) {
terminalInstance.scrollDown();
}
return TPromise.as(void 0);
}
}
......@@ -237,6 +279,10 @@ export class ScrollUpTerminalAction extends Action {
}
public run(event?: any): TPromise<any> {
return this.terminalService.scrollUp();
let terminalInstance = this.terminalService.getActiveInstance();
if (terminalInstance) {
terminalInstance.scrollUp();
}
return TPromise.as(void 0);
}
}
\ No newline at end of file
......@@ -3,12 +3,12 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {Platform} from 'vs/base/common/platform';
import {IConfiguration} from 'vs/editor/common/config/defaultConfig';
import {IConfigurationService} from 'vs/platform/configuration/common/configuration';
import {ITerminalConfiguration} from 'vs/workbench/parts/terminal/electron-browser/terminal';
import {Builder} from 'vs/base/browser/builder';
import {DefaultConfig} from 'vs/editor/common/config/defaultConfig';
import { Builder } from 'vs/base/browser/builder';
import { DefaultConfig } from 'vs/editor/common/config/defaultConfig';
import { IConfiguration } from 'vs/editor/common/config/defaultConfig';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ITerminalConfiguration } from 'vs/workbench/parts/terminal/electron-browser/terminal';
import { Platform } from 'vs/base/common/platform';
const DEFAULT_LINE_HEIGHT = 1.2;
......@@ -87,12 +87,13 @@ export interface IShell {
* specific test cases can be written.
*/
export class TerminalConfigHelper {
public panelContainer: Builder;
private charMeasureElement: HTMLElement;
public constructor(
private platform: Platform,
private configurationService: IConfigurationService,
private panelContainer: Builder) {
@IConfigurationService private configurationService: IConfigurationService) {
}
public getTheme(baseThemeId: string): string[] {
......
......@@ -4,64 +4,90 @@
*--------------------------------------------------------------------------------------------*/
import DOM = require('vs/base/browser/dom');
import Event, {Emitter} from 'vs/base/common/event';
import URI from 'vs/base/common/uri';
import cp = require('child_process');
import lifecycle = require('vs/base/common/lifecycle');
import nls = require('vs/nls');
import os = require('os');
import path = require('path');
import platform = require('vs/base/common/platform');
import xterm = require('xterm');
import {Dimension} from 'vs/base/browser/builder';
import {IContextMenuService} from 'vs/platform/contextview/browser/contextView';
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
import {IKeybindingService} from 'vs/platform/keybinding/common/keybinding';
import {IContextKey} from 'vs/platform/contextkey/common/contextkey';
import {IMessageService, Severity} from 'vs/platform/message/common/message';
import {ITerminalFont} from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper';
import {ITerminalProcess, ITerminalService} from 'vs/workbench/parts/terminal/electron-browser/terminal';
import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace';
import {Keybinding} from 'vs/base/common/keyCodes';
import {StandardKeyboardEvent} from 'vs/base/browser/keyboardEvent';
import {TabFocus} from 'vs/editor/common/config/commonEditorConfig';
import { Dimension } from 'vs/base/browser/builder';
import { IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IMessageService, Severity } from 'vs/platform/message/common/message';
import { IStringDictionary } from 'vs/base/common/collections';
import { ITerminalInstance } from 'vs/workbench/parts/terminal/electron-browser/terminal';
import { IWorkspace } from 'vs/platform/workspace/common/workspace';
import { Keybinding } from 'vs/base/common/keyCodes';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { TabFocus } from 'vs/editor/common/config/commonEditorConfig';
import { TerminalConfigHelper, IShell } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper';
export class TerminalInstance {
export class TerminalInstance implements ITerminalInstance {
private static ID_COUNTER = 1;
private static EOL_REGEX = /\r?\n/g;
public id: number;
private static eolRegex = /\r?\n/g;
private _id: number;
private _title: string;
private _onTitleChanged: Emitter<string>;
public get id(): number { return this._id; }
public get title(): string { return this._title; }
public get onTitleChanged(): Event<string> { return this._onTitleChanged.event; }
private isExiting: boolean = false;
private toDispose: lifecycle.IDisposable[] = [];
private skipTerminalKeybindings: Keybinding[] = [];
private toDispose: lifecycle.IDisposable[];
private xterm;
private terminalDomElement: HTMLDivElement;
private process: cp.ChildProcess;
private xterm: any;
private wrapperElement: HTMLDivElement;
private font: ITerminalFont;
private xtermElement: HTMLDivElement;
public constructor(
private terminalProcess: ITerminalProcess,
private parentDomElement: HTMLElement,
private contextMenuService: IContextMenuService,
private contextService: IWorkspaceContextService,
private instantiationService: IInstantiationService,
private keybindingService: IKeybindingService,
private terminalService: ITerminalService,
private messageService: IMessageService,
private terminalFocusContextKey: IContextKey<boolean>,
private onExitCallback: (TerminalInstance) => void
private onExitCallback: (TerminalInstance) => void,
private configHelper: TerminalConfigHelper,
private container: HTMLElement,
private workspace: IWorkspace,
name: string,
shellPath: string,
@IKeybindingService private keybindingService: IKeybindingService,
@IMessageService private messageService: IMessageService
) {
this.toDispose = [];
this._id = TerminalInstance.ID_COUNTER++;
this._onTitleChanged = new Emitter<string>();
this.createProcess(workspace, name, shellPath);
if (container) {
this.attachToElement(container);
}
}
public addDisposable(disposable: lifecycle.IDisposable): void {
this.toDispose.push(disposable);
}
public attachToElement(container: HTMLElement): void {
if (this.wrapperElement) {
throw new Error('The terminal instance has already been attached to a container');
}
this.wrapperElement = document.createElement('div');
DOM.addClass(this.wrapperElement, 'terminal-wrapper');
this.terminalDomElement = document.createElement('div');
this.xtermElement = document.createElement('div');
this.xterm = xterm();
this.xterm.open(this.xtermElement);
this.id = this.terminalProcess.process.pid;
this.terminalProcess.process.on('message', (message) => {
this.process.on('message', (message) => {
if (message.type === 'data') {
this.xterm.write(message.content);
}
});
this.xterm.on('data', (data) => {
this.terminalProcess.process.send({
this.process.send({
event: 'input',
data: this.sanitizeInput(data)
});
......@@ -81,20 +107,6 @@ export class TerminalInstance {
return false;
}
});
this.terminalProcess.process.on('exit', (exitCode) => {
// Prevent dispose functions being triggered multiple times
if (!this.isExiting) {
this.isExiting = true;
this.dispose();
if (exitCode) {
this.messageService.show(Severity.Error, nls.localize('terminal.integrated.exitedWithCode', 'The terminal process terminated with exit code: {0}', exitCode));
}
this.onExitCallback(this);
}
});
this.xterm.open(this.terminalDomElement);
let xtermHelper: HTMLElement = this.xterm.element.querySelector('.xterm-helpers');
let focusTrap: HTMLElement = document.createElement('div');
......@@ -123,79 +135,67 @@ export class TerminalInstance {
this.terminalFocusContextKey.reset();
}));
this.wrapperElement.appendChild(this.terminalDomElement);
this.parentDomElement.appendChild(this.wrapperElement);
}
this.wrapperElement.appendChild(this.xtermElement);
this.container.appendChild(this.wrapperElement);
private sanitizeInput(data: any) {
return typeof data === 'string' ? data.replace(TerminalInstance.eolRegex, os.EOL) : data;
this.layout(new Dimension(this.container.offsetWidth, this.container.offsetHeight));
}
public layout(dimension: Dimension): void {
if (!this.font || !this.font.charWidth || !this.font.charHeight) {
return;
public copySelection(): void {
if (document.activeElement.classList.contains('xterm')) {
document.execCommand('copy');
} else {
this.messageService.show(Severity.Warning, nls.localize('terminal.integrated.copySelection.noSelection', 'Cannot copy terminal selection when terminal does not have focus'));
}
if (!dimension.height) { // Minimized
return;
}
public dispose(): void {
if (this.wrapperElement) {
this.container.removeChild(this.wrapperElement);
this.wrapperElement = null;
}
let leftPadding = parseInt(getComputedStyle(document.querySelector('.terminal-outer-container')).paddingLeft.split('px')[0], 10);
let innerWidth = dimension.width - leftPadding;
let cols = Math.floor(innerWidth / this.font.charWidth);
let rows = Math.floor(dimension.height / this.font.charHeight);
if (this.xterm) {
this.xterm.resize(cols, rows);
this.xterm.element.style.width = innerWidth + 'px';
this.xterm.destroy();
this.xterm = null;
}
if (this.terminalProcess.process.connected) {
this.terminalProcess.process.send({
event: 'resize',
cols: cols,
rows: rows
});
if (this.process) {
if (this.process.connected) {
this.process.disconnect();
this.process.kill();
}
this.process = null;
}
this.toDispose = lifecycle.dispose(this.toDispose);
this.onExitCallback(this);
}
public toggleVisibility(visible: boolean) {
DOM.toggleClass(this.wrapperElement, 'active', visible);
}
public setFont(font: ITerminalFont): void {
this.font = font;
}
public setCursorBlink(blink: boolean): void {
if (this.xterm && this.xterm.cursorBlink !== blink) {
this.xterm.cursorBlink = blink;
this.xterm.refresh(0, this.xterm.rows - 1);
public focus(force?: boolean): void {
if (!this.xterm) {
return;
}
let text = window.getSelection().toString();
if (!text || force) {
this.xterm.focus();
}
}
public setCommandsToSkipShell(commands: string[]): void {
this.skipTerminalKeybindings = commands.map((c) => {
return this.keybindingService.lookupKeybindings(c);
}).reduce((prev, curr) => {
return prev.concat(curr);
});
public paste(): void {
this.focus();
document.execCommand('paste');
}
public sendText(text: string, addNewLine: boolean): void {;
public sendText(text: string, addNewLine: boolean): void {
if (addNewLine && text.substr(text.length - os.EOL.length) !== os.EOL) {
text += os.EOL;
}
this.terminalProcess.process.send({
this.process.send({
event: 'input',
data: text
});
}
public focus(force?: boolean): void {
if (!this.xterm) {
return;
}
let text = window.getSelection().toString();
if (!text || force) {
this.xterm.focus();
}
public setVisible(visible: boolean): void {
DOM.toggleClass(this.wrapperElement, 'active', visible);
}
public scrollDown(): void {
......@@ -206,19 +206,116 @@ export class TerminalInstance {
this.xterm.scrollDisp(-1);
}
public dispose(): void {
if (this.wrapperElement) {
this.parentDomElement.removeChild(this.wrapperElement);
this.wrapperElement = null;
private sanitizeInput(data: any) {
return typeof data === 'string' ? data.replace(TerminalInstance.EOL_REGEX, os.EOL) : data;
}
private createProcess(workspace: IWorkspace, name?: string, shellPath?: string) {
let locale = this.configHelper.isSetLocaleVariables() ? platform.locale : undefined;
let shell = shellPath ? { executable: shellPath, args: [] } : this.configHelper.getShell();
let env = TerminalInstance.createTerminalEnv(process.env, shell, workspace, locale);
this._title = name ? name : '';
this.process = cp.fork('./terminalProcess', [], {
env: env,
cwd: URI.parse(path.dirname(require.toUrl('./terminalProcess'))).fsPath
});
if (!name) {
// Only listen for process title changes when a name is not provided
this.process.on('message', (message) => {
if (message.type === 'title') {
this._title = message.content ? message.content : '';
this._onTitleChanged.fire(this._title);
}
});
}
this.process.on('exit', (exitCode) => {
// Prevent dispose functions being triggered multiple times
if (!this.isExiting) {
this.isExiting = true;
this.dispose();
if (exitCode) {
this.messageService.show(Severity.Error, nls.localize('terminal.integrated.exitedWithCode', 'The terminal process terminated with exit code: {0}', exitCode));
}
}
});
}
public static createTerminalEnv(parentEnv: IStringDictionary<string>, shell: IShell, workspace: IWorkspace, locale?: string): IStringDictionary<string> {
let env = TerminalInstance.cloneEnv(parentEnv);
env['PTYPID'] = process.pid.toString();
env['PTYSHELL'] = shell.executable;
shell.args.forEach((arg, i) => {
env[`PTYSHELLARG${i}`] = arg;
});
env['PTYCWD'] = TerminalInstance.sanitizeCwd(workspace ? workspace.resource.fsPath : os.homedir());
if (locale) {
env['LANG'] = TerminalInstance.getLangEnvVariable(locale);
}
return env;
}
private static sanitizeCwd(cwd: 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;
}
private static cloneEnv(env: IStringDictionary<string>): IStringDictionary<string> {
let newEnv: IStringDictionary<string> = Object.create(null);
Object.keys(env).forEach((key) => {
newEnv[key] = env[key];
});
return newEnv;
}
private static getLangEnvVariable(locale: string) {
const parts = locale.split('-');
const n = parts.length;
if (n > 1) {
parts[n - 1] = parts[n - 1].toUpperCase();
}
return parts.join('_') + '.UTF-8';
}
public setCursorBlink(blink: boolean): void {
if (this.xterm && this.xterm.cursorBlink !== blink) {
this.xterm.cursorBlink = blink;
this.xterm.refresh(0, this.xterm.rows - 1);
}
}
public setCommandsToSkipShell(commands: string[]): void {
this.skipTerminalKeybindings = commands.map((c) => {
return this.keybindingService.lookupKeybindings(c);
}).reduce((prev, curr) => {
return prev.concat(curr);
});
}
public layout(dimension: Dimension): void {
let font = this.configHelper.getFont();
if (!font || !font.charWidth || !font.charHeight) {
return;
}
if (!dimension.height) { // Minimized
return;
}
let leftPadding = parseInt(getComputedStyle(document.querySelector('.terminal-outer-container')).paddingLeft.split('px')[0], 10);
let innerWidth = dimension.width - leftPadding;
let cols = Math.floor(innerWidth / font.charWidth);
let rows = Math.floor(dimension.height / font.charHeight);
if (this.xterm) {
this.xterm.destroy();
this.xterm = null;
this.xterm.resize(cols, rows);
this.xterm.element.style.width = innerWidth + 'px';
}
if (this.terminalProcess) {
this.terminalService.killTerminalProcess(this.terminalProcess);
this.terminalProcess = null;
if (this.process.connected) {
this.process.send({
event: 'resize',
cols: cols,
rows: rows
});
}
this.toDispose = lifecycle.dispose(this.toDispose);
}
}
\ No newline at end of file
}
......@@ -7,32 +7,27 @@ import DOM = require('vs/base/browser/dom');
import lifecycle = require('vs/base/common/lifecycle');
import nls = require('vs/nls');
import platform = require('vs/base/common/platform');
import {Action, IAction} from 'vs/base/common/actions';
import {Builder, Dimension} from 'vs/base/browser/builder';
import {getBaseThemeId} from 'vs/platform/theme/common/themes';
import {IActionItem} from 'vs/base/browser/ui/actionbar/actionbar';
import {IConfigurationService} from 'vs/platform/configuration/common/configuration';
import {IContextMenuService} from 'vs/platform/contextview/browser/contextView';
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
import {IKeybindingService} from 'vs/platform/keybinding/common/keybinding';
import {IContextKey} from 'vs/platform/contextkey/common/contextkey';
import {IMessageService} from 'vs/platform/message/common/message';
import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry';
import {ITerminalFont, TerminalConfigHelper} from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper';
import {ITerminalPanel, ITerminalProcess, ITerminalService, TERMINAL_PANEL_ID} from 'vs/workbench/parts/terminal/electron-browser/terminal';
import {IThemeService} from 'vs/workbench/services/themes/common/themeService';
import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace';
import {KillTerminalAction, CreateNewTerminalAction, SwitchTerminalInstanceAction, SwitchTerminalInstanceActionItem, CopyTerminalSelectionAction, TerminalPasteAction} from 'vs/workbench/parts/terminal/electron-browser/terminalActions';
import {Panel} from 'vs/workbench/browser/panel';
import {Separator} from 'vs/base/browser/ui/actionbar/actionbar';
import {StandardMouseEvent} from 'vs/base/browser/mouseEvent';
import {TerminalInstance} from 'vs/workbench/parts/terminal/electron-browser/terminalInstance';
import {TPromise} from 'vs/base/common/winjs.base';
export class TerminalPanel extends Panel implements ITerminalPanel {
import { Action, IAction } from 'vs/base/common/actions';
import { Builder, Dimension } from 'vs/base/browser/builder';
import { IActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ITerminalFont } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper';
import { ITerminalService, TERMINAL_PANEL_ID } from 'vs/workbench/parts/terminal/electron-browser/terminal';
import { IThemeService } from 'vs/workbench/services/themes/common/themeService';
import { KillTerminalAction, CreateNewTerminalAction, SwitchTerminalInstanceAction, SwitchTerminalInstanceActionItem, CopyTerminalSelectionAction, TerminalPasteAction } from 'vs/workbench/parts/terminal/electron-browser/terminalActions';
import { Panel } from 'vs/workbench/browser/panel';
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { TPromise } from 'vs/base/common/winjs.base';
import { getBaseThemeId } from 'vs/platform/theme/common/themes';
export class TerminalPanel extends Panel {
private toDispose: lifecycle.IDisposable[] = [];
private terminalInstances: TerminalInstance[] = [];
private actions: IAction[];
private contextMenuActions: IAction[];
......@@ -42,31 +37,69 @@ export class TerminalPanel extends Panel implements ITerminalPanel {
private themeStyleElement: HTMLElement;
private fontStyleElement: HTMLElement;
private font: ITerminalFont;
private configurationHelper: TerminalConfigHelper;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IConfigurationService private configurationService: IConfigurationService,
@IContextMenuService private contextMenuService: IContextMenuService,
@IInstantiationService private instantiationService: IInstantiationService,
@IKeybindingService private keybindingService: IKeybindingService,
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@ITelemetryService telemetryService: ITelemetryService,
@ITerminalService private terminalService: ITerminalService,
@IThemeService private themeService: IThemeService,
@IMessageService private messageService: IMessageService
@IThemeService private themeService: IThemeService
) {
super(TERMINAL_PANEL_ID, telemetryService);
}
public create(parent: Builder): TPromise<any> {
super.create(parent);
this.parentDomElement = parent.getHTMLElement();
DOM.addClass(this.parentDomElement, 'integrated-terminal');
this.themeStyleElement = document.createElement('style');
this.fontStyleElement = document.createElement('style');
this.terminalContainer = document.createElement('div');
DOM.addClass(this.terminalContainer, 'terminal-outer-container');
this.parentDomElement.appendChild(this.themeStyleElement);
this.parentDomElement.appendChild(this.fontStyleElement);
this.parentDomElement.appendChild(this.terminalContainer);
this.attachEventListeners();
this.terminalService.setContainers(this.getContainer(), this.terminalContainer);
this.toDispose.push(this.themeService.onDidColorThemeChange(this.updateTheme.bind(this)));
this.toDispose.push(this.configurationService.onDidUpdateConfiguration(this.updateConfig.bind(this)));
this.updateTheme();
this.updateConfig();
return TPromise.as(void 0);
}
public layout(dimension?: Dimension): void {
if (!dimension) {
return;
}
this.terminalInstances.forEach((t) => {
this.terminalService.terminalInstances.forEach((t) => {
t.layout(dimension);
});
}
public setVisible(visible: boolean): TPromise<void> {
if (visible) {
if (this.terminalService.terminalInstances.length > 0) {
this.updateConfig();
this.updateTheme();
} else {
return super.setVisible(visible).then(() => {
this.terminalService.createInstance();
this.updateConfig();
this.updateTheme();
});
}
}
return super.setVisible(visible);
}
public getActions(): IAction[] {
if (!this.actions) {
this.actions = [
......@@ -104,37 +137,16 @@ export class TerminalPanel extends Panel implements ITerminalPanel {
return super.getActionItem(action);
}
public create(parent: Builder): TPromise<any> {
super.create(parent);
this.parentDomElement = parent.getHTMLElement();
this.terminalService.initConfigHelper(parent);
DOM.addClass(this.parentDomElement, 'integrated-terminal');
this.themeStyleElement = document.createElement('style');
this.fontStyleElement = document.createElement('style');
this.terminalContainer = document.createElement('div');
DOM.addClass(this.terminalContainer, 'terminal-outer-container');
this.parentDomElement.appendChild(this.themeStyleElement);
this.parentDomElement.appendChild(this.fontStyleElement);
this.parentDomElement.appendChild(this.terminalContainer);
this.attachEventListeners();
this.configurationHelper = new TerminalConfigHelper(platform.platform, this.configurationService, parent);
return TPromise.as(void 0);
}
private attachEventListeners(): void {
this.toDispose.push(DOM.addDisposableListener(this.parentDomElement, 'mousedown', (event: MouseEvent) => {
if (this.terminalInstances.length === 0) {
if (this.terminalService.terminalInstances.length === 0) {
return;
}
if (event.which === 2 && platform.isLinux) {
// Drop selection and focus terminal on Linux to enable middle button paste when click
// occurs on the selection itself.
this.terminalInstances[this.terminalService.getActiveTerminalIndex()].focus(true);
this.terminalService.getActiveInstance().focus();
} else if (event.which === 3) {
// Trigger the context menu on right click
let anchor: HTMLElement | { x: number, y: number } = this.parentDomElement;
......@@ -158,12 +170,12 @@ export class TerminalPanel extends Panel implements ITerminalPanel {
}
}));
this.toDispose.push(DOM.addDisposableListener(this.parentDomElement, 'mouseup', (event) => {
if (this.terminalInstances.length === 0) {
if (this.terminalService.terminalInstances.length === 0) {
return;
}
if (event.which !== 3) {
this.terminalInstances[this.terminalService.getActiveTerminalIndex()].focus();
this.terminalService.getActiveInstance().focus();
}
}));
this.toDispose.push(DOM.addDisposableListener(this.parentDomElement, 'keyup', (event: KeyboardEvent) => {
......@@ -174,104 +186,6 @@ export class TerminalPanel extends Panel implements ITerminalPanel {
}));
}
public createNewTerminalInstance(process: ITerminalProcess, focusContextKey: IContextKey<boolean>): TPromise<number> {
return this.createTerminal(process, focusContextKey).then((terminalInstance) => {
this.updateConfig();
this.focus();
return TPromise.as(terminalInstance.id);
});
}
public closeActiveTerminal(): TPromise<void> {
return this.closeTerminal(this.terminalService.getActiveTerminalIndex());
}
public closeTerminal(index: number): TPromise<void> {
this.onTerminalInstanceExit(this.terminalInstances[index]);
return TPromise.as(void 0);
}
public closeTerminalById(terminalId: number): TPromise<void> {
return this.closeTerminal(this.getTerminalIndexFromId(terminalId));
}
public setVisible(visible: boolean): TPromise<void> {
if (visible) {
if (this.terminalInstances.length > 0) {
this.updateConfig();
this.updateTheme();
} else {
return super.setVisible(visible).then(() => {
this.terminalService.createNew();
});
}
}
return super.setVisible(visible);
}
public sendTextToActiveTerminal(text: string, addNewLine: boolean): void {
let terminalInstance = this.terminalInstances[this.terminalService.getActiveTerminalIndex()];
terminalInstance.sendText(text, addNewLine);
}
private createTerminal(terminalProcess: ITerminalProcess, terminalFocusContextKey: IContextKey<boolean>): TPromise<TerminalInstance> {
return new TPromise<TerminalInstance>(resolve => {
var terminalInstance = new TerminalInstance(
terminalProcess,
this.terminalContainer,
this.contextMenuService,
this.contextService,
this.instantiationService,
this.keybindingService,
this.terminalService,
this.messageService,
terminalFocusContextKey,
this.onTerminalInstanceExit.bind(this));
this.terminalInstances.push(terminalInstance);
this.setActiveTerminal(this.terminalInstances.length - 1);
this.toDispose.push(this.themeService.onDidColorThemeChange(this.updateTheme.bind(this)));
this.toDispose.push(this.configurationService.onDidUpdateConfiguration(this.updateConfig.bind(this)));
this.updateTheme();
this.updateConfig();
resolve(terminalInstance);
});
}
public setActiveTerminal(newActiveIndex: number): void {
this.terminalInstances.forEach((terminalInstance, i) => {
terminalInstance.toggleVisibility(i === newActiveIndex);
});
}
private getTerminalIndexFromId(terminalId: number): number {
let terminalIndex = -1;
this.terminalInstances.forEach((terminalInstance, i) => {
if (terminalInstance.id === terminalId) {
terminalIndex = i;
}
});
if (terminalIndex === -1) {
throw new Error(`Terminal with ID ${terminalId} does not exist (has it already been disposed?)`);
}
return terminalIndex;
}
private onTerminalInstanceExit(terminalInstance: TerminalInstance): void {
let index = this.terminalInstances.indexOf(terminalInstance);
if (index !== -1) {
this.terminalInstances[index].dispose();
this.terminalInstances.splice(index, 1);
}
if (this.terminalInstances.length > 0) {
this.setActiveTerminal(this.terminalService.getActiveTerminalIndex());
}
if (this.terminalInstances.length === 0) {
this.terminalService.hide();
} else {
this.terminalService.show(true);
}
}
private updateTheme(themeId?: string): void {
if (!themeId) {
themeId = this.themeService.getColorTheme();
......@@ -283,7 +197,7 @@ export class TerminalPanel extends Panel implements ITerminalPanel {
}
this.currentBaseThemeId = baseThemeId;
let theme = this.configurationHelper.getTheme(baseThemeId);
let theme = this.terminalService.configHelper.getTheme(baseThemeId);
let css = '';
theme.forEach((color: string, index: number) => {
......@@ -297,14 +211,6 @@ export class TerminalPanel extends Panel implements ITerminalPanel {
this.themeStyleElement.innerHTML = css;
}
public scrollDown(): void {
this.terminalInstances[this.terminalService.getActiveTerminalIndex()].scrollDown();
}
public scrollUp(): void {
this.terminalInstances[this.terminalService.getActiveTerminalIndex()].scrollUp();
}
/**
* Converts a CSS hex color (#rrggbb) to a CSS rgba color (rgba(r, g, b, a)).
*/
......@@ -322,11 +228,11 @@ export class TerminalPanel extends Panel implements ITerminalPanel {
}
private updateFont(): void {
if (this.terminalInstances.length === 0) {
if (this.terminalService.terminalInstances.length === 0) {
return;
}
let newFont = this.configurationHelper.getFont();
DOM.toggleClass(this.parentDomElement, 'enable-ligatures', this.configurationHelper.getFontLigaturesEnabled());
let newFont = this.terminalService.configHelper.getFont();
DOM.toggleClass(this.parentDomElement, 'enable-ligatures', this.terminalService.configHelper.getFontLigaturesEnabled());
if (!this.font || this.fontsDiffer(this.font, newFont)) {
this.fontStyleElement.innerHTML = '.monaco-workbench .panel.integrated-terminal .xterm {' +
`font-family: ${newFont.fontFamily};` +
......@@ -335,7 +241,6 @@ export class TerminalPanel extends Panel implements ITerminalPanel {
'}';
this.font = newFont;
}
this.terminalInstances[this.terminalService.getActiveTerminalIndex()].setFont(newFont);
this.layout(new Dimension(this.parentDomElement.offsetWidth, this.parentDomElement.offsetHeight));
}
......@@ -348,29 +253,14 @@ export class TerminalPanel extends Panel implements ITerminalPanel {
}
private updateCursorBlink(): void {
this.terminalInstances.forEach((instance) => {
instance.setCursorBlink(this.configurationHelper.getCursorBlink());
this.terminalService.terminalInstances.forEach((instance) => {
instance.setCursorBlink(this.terminalService.configHelper.getCursorBlink());
});
}
private updateCommandsToSkipShell(): void {
this.terminalInstances.forEach((instance) => {
instance.setCommandsToSkipShell(this.configurationHelper.getCommandsToSkipShell());
this.terminalService.terminalInstances.forEach((instance) => {
instance.setCommandsToSkipShell(this.terminalService.configHelper.getCommandsToSkipShell());
});
}
public focus(): void {
let activeIndex = this.terminalService.getActiveTerminalIndex();
if (activeIndex !== -1 && this.terminalInstances.length > 0) {
this.terminalInstances[activeIndex].focus(true);
}
}
public dispose(): void {
this.toDispose = lifecycle.dispose(this.toDispose);
while (this.terminalInstances.length > 0) {
this.terminalInstances.pop().dispose();
}
super.dispose();
}
}
......@@ -3,375 +3,195 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import URI from 'vs/base/common/uri';
import Event, {Emitter} from 'vs/base/common/event';
import cp = require('child_process');
import nls = require('vs/nls');
import os = require('os');
import path = require('path');
import Event, { Emitter } from 'vs/base/common/event';
import platform = require('vs/base/common/platform');
import {Builder} from 'vs/base/browser/builder';
import {EndOfLinePreference} from 'vs/editor/common/editorCommon';
import {ICodeEditorService} from 'vs/editor/common/services/codeEditorService';
import {IConfigurationService} from 'vs/platform/configuration/common/configuration';
import {IContextKey, IContextKeyService} from 'vs/platform/contextkey/common/contextkey';
import {IMessageService, Severity} from 'vs/platform/message/common/message';
import {IPanelService} from 'vs/workbench/services/panel/common/panelService';
import {IPartService} from 'vs/workbench/services/part/common/partService';
import {IStringDictionary} from 'vs/base/common/collections';
import {ITerminalProcess, ITerminalService, KEYBINDING_CONTEXT_TERMINAL_FOCUS, TERMINAL_PANEL_ID} from 'vs/workbench/parts/terminal/electron-browser/terminal';
import {IWorkspaceContextService, IWorkspace} from 'vs/platform/workspace/common/workspace';
import {TPromise} from 'vs/base/common/winjs.base';
import {TerminalConfigHelper, IShell} from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper';
import {TerminalPanel} from 'vs/workbench/parts/terminal/electron-browser/terminalPanel';
import { Builder } from 'vs/base/browser/builder';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import { ITerminalInstance, ITerminalService, KEYBINDING_CONTEXT_TERMINAL_FOCUS, TERMINAL_PANEL_ID } from 'vs/workbench/parts/terminal/electron-browser/terminal';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { TPromise } from 'vs/base/common/winjs.base';
import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper';
import { TerminalInstance } from 'vs/workbench/parts/terminal/electron-browser/terminalInstance';
export class TerminalService implements ITerminalService {
public _serviceBrand: any;
private activeTerminalIndex: number = 0;
private terminalProcesses: ITerminalProcess[] = [];
private _nextTerminalProcessConfiguration: ITerminalProcessConfiguration = { name: null, shell: { executable: '', args: [] } };
protected _terminalFocusContextKey: IContextKey<boolean>;
private configHelper: TerminalConfigHelper;
private _activeTerminalInstanceIndex: number = 0;
private _configHelper: TerminalConfigHelper;
private _onActiveInstanceChanged: Emitter<string>;
private _onInstancesChanged: Emitter<string>;
private _onInstanceTitleChanged: Emitter<string>;
private _terminalInstances: ITerminalInstance[] = [];
public get activeTerminalInstanceIndex(): number { return this._activeTerminalInstanceIndex; }
public get configHelper(): TerminalConfigHelper { return this._configHelper; }
public get onActiveInstanceChanged(): Event<string> { return this._onActiveInstanceChanged.event; }
public get onInstancesChanged(): Event<string> { return this._onInstancesChanged.event; }
public get onInstanceTitleChanged(): Event<string> { return this._onInstanceTitleChanged.event; }
public get terminalInstances(): ITerminalInstance[] { return this._terminalInstances; }
private terminalContainer: HTMLElement;
private terminalFocusContextKey: IContextKey<boolean>;
constructor(
@ICodeEditorService private codeEditorService: ICodeEditorService,
@IConfigurationService private configurationService: IConfigurationService,
@IContextKeyService private contextKeyService: IContextKeyService,
@IMessageService private messageService: IMessageService,
@IInstantiationService private instantiationService: IInstantiationService,
@IPanelService private panelService: IPanelService,
@IPartService private partService: IPartService,
@IWorkspaceContextService private contextService: IWorkspaceContextService
@IWorkspaceContextService private workspaceContextService: IWorkspaceContextService
) {
this._onActiveInstanceChanged = new Emitter<string>();
this._onInstancesChanged = new Emitter<string>();
this._onInstanceTitleChanged = new Emitter<string>();
this._terminalFocusContextKey = KEYBINDING_CONTEXT_TERMINAL_FOCUS.bindTo(this.contextKeyService);
this.terminalFocusContextKey = KEYBINDING_CONTEXT_TERMINAL_FOCUS.bindTo(this.contextKeyService);
this._configHelper = <TerminalConfigHelper>this.instantiationService.createInstance(TerminalConfigHelper, platform.platform);
}
public createInstance(name?: string, shellPath?: string): ITerminalInstance {
let terminalInstance = <TerminalInstance>this.instantiationService.createInstance(TerminalInstance,
this.terminalFocusContextKey,
this.onTerminalInstanceDispose.bind(this),
this._configHelper,
this.terminalContainer,
this.workspaceContextService.getWorkspace(),
name,
shellPath);
terminalInstance.addDisposable(terminalInstance.onTitleChanged(this._onInstanceTitleChanged.fire, this._onInstanceTitleChanged));
this.terminalInstances.push(terminalInstance);
if (this.terminalInstances.length === 1) {
// It's the first instance so it should be made active automatically
this.setActiveInstanceByIndex(0);
}
this._onInstancesChanged.fire();
return terminalInstance;
}
public get onActiveInstanceChanged(): Event<string> {
return this._onActiveInstanceChanged.event;
public getInstanceLabels(): string[] {
return this._terminalInstances.map((instance, index) => `${index + 1}: ${instance.title}`);
}
public get onInstancesChanged(): Event<string> {
return this._onInstancesChanged.event;
private onTerminalInstanceDispose(terminalInstance: TerminalInstance): void {
let index = this.terminalInstances.indexOf(terminalInstance);
let wasActiveInstance = terminalInstance === this.getActiveInstance();
if (index !== -1) {
this.terminalInstances.splice(index, 1);
}
if (wasActiveInstance && this.terminalInstances.length > 0) {
let newIndex = index < this.terminalInstances.length ? index : this.terminalInstances.length - 1;
this.setActiveInstanceByIndex(newIndex);
}
if (this.terminalInstances.length === 0) {
this.hidePanel();
}
this._onInstancesChanged.fire();
if (wasActiveInstance) {
this._onActiveInstanceChanged.fire();
}
}
public get onInstanceTitleChanged(): Event<string> {
return this._onInstanceTitleChanged.event;
public getActiveInstance(): ITerminalInstance {
if (this.activeTerminalInstanceIndex < 0 || this.activeTerminalInstanceIndex >= this.terminalInstances.length) {
return null;
}
return this.terminalInstances[this.activeTerminalInstanceIndex];
}
public setActiveTerminal(index: number): TPromise<any> {
return this.show(false).then((terminalPanel) => {
this.activeTerminalIndex = index;
terminalPanel.setActiveTerminal(this.activeTerminalIndex);
this._onActiveInstanceChanged.fire();
});
public getInstanceFromId(terminalId: number): ITerminalInstance {
return this.terminalInstances[this.getIndexFromId(terminalId)];
}
public setActiveTerminalById(terminalId: number): void {
this.setActiveTerminal(this.getTerminalIndexFromId(terminalId));
public setActiveInstance(terminalInstance: ITerminalInstance): void {
this.setActiveInstanceByIndex(this.getIndexFromId(terminalInstance.id));
}
private getTerminalIndexFromId(terminalId: number): number {
let terminalIndex = -1;
this.terminalProcesses.forEach((terminalProcess, i) => {
if (terminalProcess.process.pid === terminalId) {
terminalIndex = i;
}
public setActiveInstanceByIndex(terminalIndex: number): void {
this._activeTerminalInstanceIndex = terminalIndex;
this._terminalInstances.forEach((terminalInstance, i) => {
terminalInstance.setVisible(i === terminalIndex);
});
if (terminalIndex === -1) {
throw new Error(`Terminal with ID ${terminalId} does not exist (has it already been disposed?)`);
}
return terminalIndex;
this._onActiveInstanceChanged.fire();
}
public focusNext(): TPromise<any> {
return this.focus().then((terminalPanel) => {
if (this.terminalProcesses.length <= 1) {
return;
}
this.activeTerminalIndex++;
if (this.activeTerminalIndex >= this.terminalProcesses.length) {
this.activeTerminalIndex = 0;
}
terminalPanel.setActiveTerminal(this.activeTerminalIndex);
terminalPanel.focus();
this._onActiveInstanceChanged.fire();
});
public setActiveInstanceToNext(): void {
if (this.terminalInstances.length <= 1) {
return;
}
let newIndex = this._activeTerminalInstanceIndex + 1;
if (newIndex >= this.terminalInstances.length) {
newIndex = 0;
}
this.setActiveInstanceByIndex(newIndex);
}
public focusPrevious(): TPromise<any> {
return this.focus().then((terminalPanel) => {
if (this.terminalProcesses.length <= 1) {
return;
}
this.activeTerminalIndex--;
if (this.activeTerminalIndex < 0) {
this.activeTerminalIndex = this.terminalProcesses.length - 1;
}
terminalPanel.setActiveTerminal(this.activeTerminalIndex);
terminalPanel.focus();
this._onActiveInstanceChanged.fire();
});
public setActiveInstanceToPrevious(): void {
if (this.terminalInstances.length <= 1) {
return;
}
let newIndex = this._activeTerminalInstanceIndex - 1;
if (newIndex < 0) {
newIndex = this.terminalInstances.length - 1;
}
this.setActiveInstanceByIndex(newIndex);
}
public runSelectedText(): TPromise<any> {
return this.focus().then((terminalPanel) => {
let editor = this.codeEditorService.getFocusedCodeEditor();
let selection = editor.getSelection();
let text: string;
if (selection.isEmpty()) {
text = editor.getValue();
} else {
let endOfLinePreference = os.EOL === '\n' ? EndOfLinePreference.LF : EndOfLinePreference.CRLF;
text = editor.getModel().getValueInRange(selection, endOfLinePreference);
}
terminalPanel.sendTextToActiveTerminal(text, true);
public setContainers(panelContainer: Builder, terminalContainer: HTMLElement): void {
this.terminalContainer = terminalContainer;
this._terminalInstances.forEach(terminalInstance => {
terminalInstance.attachToElement(this.terminalContainer);
});
this._configHelper.panelContainer = panelContainer;
}
public show(focus: boolean): TPromise<TerminalPanel> {
return new TPromise<TerminalPanel>((complete) => {
public showPanel(focus?: boolean): TPromise<void> {
return new TPromise<void>((complete) => {
let panel = this.panelService.getActivePanel();
if (!panel || panel.getId() !== TERMINAL_PANEL_ID) {
return this.panelService.openPanel(TERMINAL_PANEL_ID, focus).then(() => {
panel = this.panelService.getActivePanel();
complete(<TerminalPanel>panel);
if (focus) {
this.getActiveInstance().focus(true);
}
complete(void 0);
});
} else {
if (focus) {
panel.focus();
this.getActiveInstance().focus(true);
}
complete(<TerminalPanel>panel);
complete(void 0);
}
});
}
public focus(): TPromise<TerminalPanel> {
return this.show(true);
}
public hide(): TPromise<any> {
public hidePanel(): void {
const panel = this.panelService.getActivePanel();
if (panel && panel.getId() === TERMINAL_PANEL_ID) {
this.partService.setPanelHidden(true);
}
return TPromise.as(void 0);
}
public hideTerminalInstance(terminalId: number): TPromise<any> {
const panel = this.panelService.getActivePanel();
if (panel && panel.getId() === TERMINAL_PANEL_ID) {
if (this.terminalProcesses[this.getActiveTerminalIndex()].process.pid === terminalId) {
this.partService.setPanelHidden(true);
}
}
return TPromise.as(void 0);
}
public toggle(): TPromise<any> {
public togglePanel(): TPromise<any> {
const panel = this.panelService.getActivePanel();
if (panel && panel.getId() === TERMINAL_PANEL_ID) {
this.partService.setPanelHidden(true);
return TPromise.as(null);
}
return this.focus();
this.showPanel(true);
}
public createNew(name?: string, shellPath?: string): TPromise<number> {
let processCount = this.terminalProcesses.length;
// When there are 0 processes it means that the panel is not yet created, so the name needs
// to be stored for when createNew is called from TerminalPanel.create. This has to work
// like this as TerminalPanel.setVisible must create a terminal if there is none due to how
// the TerminalPanel is restored on launch if it was open previously.
if (processCount === 0 && !name) {
name = this._nextTerminalProcessConfiguration.name;
shellPath = this._nextTerminalProcessConfiguration.shell.executable;
this._nextTerminalProcessConfiguration = undefined;
} else {
this._nextTerminalProcessConfiguration.name = name;
this._nextTerminalProcessConfiguration.shell.executable = shellPath;
}
// If user doesn't specify a shell, set it to null and let terminalConfigHelper to getShell()
// later.
// Todo: Also let user specify shell args
const terminalProcessConfiguration: ITerminalProcessConfiguration = {
name: name ? name : '',
shell: shellPath ? { executable: shellPath, args: [] } : null
};
return this.focus().then((terminalPanel) => {
// If the terminal panel has not been initialized yet skip this, the terminal will be
// created via a call from TerminalPanel.setVisible
if (terminalPanel === null) {
return;
}
// Only create a new process if none have been created since toggling the terminal
// panel. This happens when createNew is called when the panel is either empty or no yet
// created.
if (processCount !== this.terminalProcesses.length) {
return TPromise.as(this.terminalProcesses[this.terminalProcesses.length - 1].process.pid);
}
this.initConfigHelper(terminalPanel.getContainer());
return terminalPanel.createNewTerminalInstance(this.createTerminalProcess(terminalProcessConfiguration), this._terminalFocusContextKey).then((terminalId) => {
this._onInstancesChanged.fire();
return TPromise.as(terminalId);
});
});
}
public close(): TPromise<any> {
return this.focus().then((terminalPanel) => {
return terminalPanel.closeActiveTerminal();
});
}
public closeById(terminalId: number): TPromise<any> {
return this.show(false).then((terminalPanel) => {
return terminalPanel.closeTerminalById(terminalId);
});
}
public copySelection(): TPromise<any> {
if (document.activeElement.classList.contains('xterm')) {
document.execCommand('copy');
} else {
this.messageService.show(Severity.Warning, nls.localize('terminal.integrated.copySelection.noSelection', 'Cannot copy terminal selection when terminal does not have focus'));
}
return TPromise.as(void 0);
}
public paste(): TPromise<any> {
return this.focus().then(() => {
document.execCommand('paste');
});
}
public scrollDown(): TPromise<any> {
return this.focus().then((terminalPanel) => {
terminalPanel.scrollDown();
});
}
public scrollUp(): TPromise<any> {
return this.focus().then((terminalPanel) => {
terminalPanel.scrollUp();
});
}
public getActiveTerminalIndex(): number {
return this.activeTerminalIndex;
}
public getTerminalInstanceTitles(): string[] {
return this.terminalProcesses.map((process, index) => `${index + 1}: ${process.title}`);
}
public initConfigHelper(panelElement: Builder): void {
if (!this.configHelper) {
this.configHelper = new TerminalConfigHelper(platform.platform, this.configurationService, panelElement);
}
}
public killTerminalProcess(terminalProcess: ITerminalProcess): void {
if (terminalProcess.process.connected) {
terminalProcess.process.disconnect();
terminalProcess.process.kill();
}
let index = this.terminalProcesses.indexOf(terminalProcess);
if (index >= 0) {
let wasActiveTerminal = (index === this.getActiveTerminalIndex());
// Push active index back if the closed process was before the active process
if (this.getActiveTerminalIndex() >= index) {
this.activeTerminalIndex = Math.max(0, this.activeTerminalIndex - 1);
}
this.terminalProcesses.splice(index, 1);
this._onInstancesChanged.fire();
if (wasActiveTerminal) {
this._onActiveInstanceChanged.fire();
private getIndexFromId(terminalId: number): number {
let terminalIndex = -1;
this.terminalInstances.forEach((terminalInstance, i) => {
if (terminalInstance.id === terminalId) {
terminalIndex = i;
}
}
}
private createTerminalProcess(terminalProcessConfiguration: ITerminalProcessConfiguration): ITerminalProcess {
const locale = this.configHelper.isSetLocaleVariables() ? platform.locale : undefined;
const shell = terminalProcessConfiguration.shell ? terminalProcessConfiguration.shell : this.configHelper.getShell();
const env = TerminalService.createTerminalEnv(process.env, shell, this.contextService.getWorkspace(), locale);
const terminalProcess = {
title: terminalProcessConfiguration.name,
process: cp.fork('./terminalProcess', [], {
env: env,
cwd: URI.parse(path.dirname(require.toUrl('./terminalProcess'))).fsPath
})
};
this.terminalProcesses.push(terminalProcess);
this._onInstancesChanged.fire();
this.activeTerminalIndex = this.terminalProcesses.length - 1;
this._onActiveInstanceChanged.fire();
if (!terminalProcessConfiguration.name) {
// Only listen for process title changes when a name is not provided
terminalProcess.process.on('message', (message) => {
if (message.type === 'title') {
terminalProcess.title = message.content ? message.content : '';
this._onInstanceTitleChanged.fire();
}
});
}
return terminalProcess;
}
public static createTerminalEnv(parentEnv: IStringDictionary<string>, shell: IShell, workspace: IWorkspace, locale?: string): IStringDictionary<string> {
let env = this.cloneEnv(parentEnv);
env['PTYPID'] = process.pid.toString();
env['PTYSHELL'] = shell.executable;
shell.args.forEach((arg, i) => {
env[`PTYSHELLARG${i}`] = arg;
});
env['PTYCWD'] = this.sanitizeCwd(workspace ? workspace.resource.fsPath : os.homedir());
if (locale) {
env['LANG'] = this.getLangEnvVariable(locale);
}
return env;
}
private static sanitizeCwd(cwd: 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;
}
private static cloneEnv(env: IStringDictionary<string>): IStringDictionary<string> {
let newEnv: IStringDictionary<string> = Object.create(null);
Object.keys(env).forEach((key) => {
newEnv[key] = env[key];
});
return newEnv;
}
private static getLangEnvVariable(locale: string) {
const parts = locale.split('-');
const n = parts.length;
if (n > 1) {
parts[n - 1] = parts[n - 1].toUpperCase();
if (terminalIndex === -1) {
throw new Error(`Terminal with ID ${terminalId} does not exist (has it already been disposed?)`);
}
return parts.join('_') + '.UTF-8';
return terminalIndex;
}
}
export interface ITerminalProcessConfiguration {
name: string;
shell: IShell;
}
\ No newline at end of file
......@@ -44,7 +44,8 @@ suite('Workbench - TerminalConfigHelper', () => {
}
}
});
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService, fixture);
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService);
configHelper.panelContainer = fixture;
assert.equal(configHelper.getFont().fontFamily, 'bar', 'terminal.integrated.fontFamily should be selected over editor.fontFamily');
configurationService = new MockConfigurationService({
......@@ -57,7 +58,8 @@ suite('Workbench - TerminalConfigHelper', () => {
}
}
});
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService, fixture);
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService);
configHelper.panelContainer = fixture;
assert.equal(configHelper.getFont().fontFamily, 'foo', 'editor.fontFamily should be the fallback when terminal.integrated.fontFamily not set');
});
......@@ -77,7 +79,8 @@ suite('Workbench - TerminalConfigHelper', () => {
}
}
});
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService, fixture);
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService);
configHelper.panelContainer = fixture;
assert.equal(configHelper.getFont().fontSize, '2px', 'terminal.integrated.fontSize should be selected over editor.fontSize');
configurationService = new MockConfigurationService({
......@@ -92,7 +95,8 @@ suite('Workbench - TerminalConfigHelper', () => {
}
}
});
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService, fixture);
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService);
configHelper.panelContainer = fixture;
assert.equal(configHelper.getFont().fontSize, '1px', 'editor.fontSize should be the fallback when terminal.integrated.fontSize not set');
configurationService = new MockConfigurationService({
......@@ -107,7 +111,8 @@ suite('Workbench - TerminalConfigHelper', () => {
}
}
});
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService, fixture);
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService);
configHelper.panelContainer = fixture;
assert.equal(configHelper.getFont().fontSize, `${DefaultConfig.editor.fontSize}px`, 'The default editor font size should be used when editor.fontSize is 0 and terminal.integrated.fontSize not set');
configurationService = new MockConfigurationService({
......@@ -122,7 +127,8 @@ suite('Workbench - TerminalConfigHelper', () => {
}
}
});
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService, fixture);
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService);
configHelper.panelContainer = fixture;
assert.equal(configHelper.getFont().fontSize, `${DefaultConfig.editor.fontSize}px`, 'The default editor font size should be used when editor.fontSize is < 0 and terminal.integrated.fontSize not set');
});
......@@ -142,7 +148,8 @@ suite('Workbench - TerminalConfigHelper', () => {
}
}
});
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService, fixture);
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService);
configHelper.panelContainer = fixture;
assert.equal(configHelper.getFont().lineHeight, 2, 'terminal.integrated.lineHeight should be selected over editor.lineHeight');
configurationService = new MockConfigurationService({
......@@ -157,7 +164,8 @@ suite('Workbench - TerminalConfigHelper', () => {
}
}
});
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService, fixture);
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService);
configHelper.panelContainer = fixture;
assert.equal(configHelper.getFont().lineHeight, 1.2, 'editor.lineHeight should be 1.2 when terminal.integrated.lineHeight not set');
});
......@@ -177,7 +185,8 @@ suite('Workbench - TerminalConfigHelper', () => {
}
}
});
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService, fixture);
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService);
configHelper.panelContainer = fixture;
assert.equal(configHelper.getShell().executable, 'foo', 'terminal.integrated.shell.linux should be selected on Linux');
configurationService = new MockConfigurationService({
......@@ -192,7 +201,8 @@ suite('Workbench - TerminalConfigHelper', () => {
}
}
});
configHelper = new TerminalConfigHelper(Platform.Mac, configurationService, fixture);
configHelper = new TerminalConfigHelper(Platform.Mac, configurationService);
configHelper.panelContainer = fixture;
assert.equal(configHelper.getShell().executable, 'foo', 'terminal.integrated.shell.osx should be selected on OS X');
configurationService = new MockConfigurationService({
......@@ -204,7 +214,8 @@ suite('Workbench - TerminalConfigHelper', () => {
}
}
});
configHelper = new TerminalConfigHelper(Platform.Windows, configurationService, fixture);
configHelper = new TerminalConfigHelper(Platform.Windows, configurationService);
configHelper.panelContainer = fixture;
assert.equal(configHelper.getShell().executable, 'foo', 'terminal.integrated.shell.windows should be selected on Windows');
});
......@@ -212,7 +223,8 @@ suite('Workbench - TerminalConfigHelper', () => {
let configurationService: IConfigurationService = new MockConfigurationService();
let configHelper: TerminalConfigHelper;
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService, fixture);
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService);
configHelper.panelContainer = fixture;
assert.deepEqual(configHelper.getTheme('hc-black'), [
'#000000',
'#cd0000',
......@@ -232,7 +244,8 @@ suite('Workbench - TerminalConfigHelper', () => {
'#ffffff'
], 'The high contrast terminal theme should be selected when the hc-black theme is active');
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService, fixture);
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService);
configHelper.panelContainer = fixture;
assert.deepEqual(configHelper.getTheme('vs'), [
'#000000',
'#cd3131',
......@@ -252,7 +265,8 @@ suite('Workbench - TerminalConfigHelper', () => {
'#a5a5a5'
], 'The light terminal theme should be selected when a vs theme is active');
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService, fixture);
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService);
configHelper.panelContainer = fixture;
assert.deepEqual(configHelper.getTheme('vs-dark'), [
'#000000',
'#cd3131',
......@@ -280,13 +294,15 @@ suite('Workbench - TerminalConfigHelper', () => {
configurationService = new MockConfigurationService({
terminal: { integrated: { fontLigatures: true } }
});
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService, fixture);
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService);
configHelper.panelContainer = fixture;
assert.equal(configHelper.getFontLigaturesEnabled(), true, 'terminal.integrated.fontLigatures should be true');
configurationService = new MockConfigurationService({
terminal: { integrated: { fontLigatures: false } }
});
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService, fixture);
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService);
configHelper.panelContainer = fixture;
assert.equal(configHelper.getFontLigaturesEnabled(), false, 'terminal.integrated.fontLigatures should be false');
});
......@@ -297,13 +313,15 @@ suite('Workbench - TerminalConfigHelper', () => {
configurationService = new MockConfigurationService({
terminal: { integrated: { cursorBlinking: true } }
});
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService, fixture);
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService);
configHelper.panelContainer = fixture;
assert.equal(configHelper.getCursorBlink(), true, 'terminal.integrated.cursorBlinking should be true');
configurationService = new MockConfigurationService({
terminal: { integrated: { cursorBlinking: false } }
});
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService, fixture);
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService);
configHelper.panelContainer = fixture;
assert.equal(configHelper.getCursorBlink(), false, 'terminal.integrated.cursorBlinking should be false');
});
......@@ -314,13 +332,15 @@ suite('Workbench - TerminalConfigHelper', () => {
configurationService = new MockConfigurationService({
terminal: { integrated: { setLocaleVariables: true } }
});
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService, fixture);
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService);
configHelper.panelContainer = fixture;
assert.equal(configHelper.isSetLocaleVariables(), true, 'terminal.integrated.setLocaleVariables should be true');
configurationService = new MockConfigurationService({
terminal: { integrated: { setLocaleVariables: false } }
});
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService, fixture);
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService);
configHelper.panelContainer = fixture;
assert.equal(configHelper.isSetLocaleVariables(), false, 'terminal.integrated.setLocaleVariables should be false');
});
......@@ -331,13 +351,15 @@ suite('Workbench - TerminalConfigHelper', () => {
configurationService = new MockConfigurationService({
terminal: { integrated: { commandsToSkipShell: [] } }
});
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService, fixture);
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService);
configHelper.panelContainer = fixture;
assert.deepEqual(configHelper.getCommandsToSkipShell(), [], 'terminal.integrated.commandsToSkipShell should be []');
configurationService = new MockConfigurationService({
terminal: { integrated: { commandsToSkipShell: ['foo'] } }
});
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService, fixture);
configHelper = new TerminalConfigHelper(Platform.Linux, configurationService);
configHelper.panelContainer = fixture;
assert.deepEqual(configHelper.getCommandsToSkipShell(), ['foo'], 'terminal.integrated.commandsToSkipShell should be [\'foo\']');
});
});
\ No newline at end of file
......@@ -7,14 +7,71 @@
import * as assert from 'assert';
import * as os from 'os';
import {IStringDictionary} from 'vs/base/common/collections';
import {IWorkspace} from 'vs/platform/workspace/common/workspace';
import {TerminalService} from 'vs/workbench/parts/terminal/electron-browser/terminalService';
import * as platform from 'vs/base/common/platform';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IMessageService, Severity } from 'vs/platform/message/common/message';
import { IStringDictionary } from 'vs/base/common/collections';
import { ITerminalInstance, ITerminalService } from 'vs/workbench/parts/terminal/electron-browser/terminal';
import { IWorkspaceContextService, IWorkspace } from 'vs/platform/workspace/common/workspace';
import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper';
import { TerminalInstance } from 'vs/workbench/parts/terminal/electron-browser/terminalInstance';
import { TestInstantiationService, stubFunction } from 'vs/test/utils/instantiationTestUtils';
import { TestConfigurationService, TestMessageService } from 'vs/test/utils/servicesTestUtils';
suite('Workbench - TerminalService', () => {
suite('Workbench - TerminalInstance', () => {
let instantiationService: TestInstantiationService;
setup(() => {
instantiationService = new TestInstantiationService();
instantiationService.stub(IKeybindingService);
instantiationService.stub(IMessageService, new TestMessageService());
});
test('TerminalInstance - onTitleChanged event is fired by the process on creation (Linux & OSX)', function (done) {
if (platform.platform !== platform.Platform.Linux && platform.platform !== platform.Platform.Mac) {
done();
return;
}
let terminalInstance = createTerminalInstance(instantiationService, {
shell: { linux: '/bin/bash', osx: '/bin/bash' },
shellArgs: { linux: [], osx: [] }
})
terminalInstance.onTitleChanged((title) => {
if (title === '/bin/bash') {
terminalInstance.dispose();
done();
}
});
});
test('TerminalInstance - onTitleChanged event is fired by the process on creation (Windows)', function (done) {
if (platform.platform !== platform.Platform.Windows) {
done();
return;
}
let terminalInstance = createTerminalInstance(instantiationService, {
shell: { windows: 'cmd.exe' },
shellArgs: { windows: [] }
})
terminalInstance.onTitleChanged((title) => {
if (title === 'cmd.exe') {
terminalInstance.dispose();
done();
}
});
});
test('TerminalInstance - createTerminalEnv', function () {
let terminalConfigHelper = instantiationService.createInstance(TerminalConfigHelper, platform.Platform.Linux);
test('TerminalService - createTerminalEnv', function () {
const shell1 = {
executable: '/bin/foosh',
args: ['-bar', 'baz']
......@@ -22,7 +79,7 @@ suite('Workbench - TerminalService', () => {
const parentEnv1: IStringDictionary<string> = <any>{
ok: true
};
const env1 = TerminalService.createTerminalEnv(parentEnv1, shell1, null, 'en-au');
const env1 = TerminalInstance.createTerminalEnv(parentEnv1, shell1, null, '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');
......@@ -45,15 +102,33 @@ suite('Workbench - TerminalService', () => {
fsPath: '/my/dev/folder'
}
};
const env2 = TerminalService.createTerminalEnv(parentEnv2, shell2, workspace2, 'en-au');
const env2 = TerminalInstance.createTerminalEnv(parentEnv2, shell2, workspace2, 'en-au');
assert.ok(!('PTYSHELLARG0' in env2), 'PTYSHELLARG0 is unset');
assert.equal(env2['PTYCWD'], '/my/dev/folder', 'PTYCWD is equal to the workspace folder');
assert.equal(env2['LANG'], 'en_AU.UTF-8', 'LANG is equal to the requested locale with UTF-8');
const env3 = TerminalService.createTerminalEnv(parentEnv1, shell1, null, null);
const env3 = TerminalInstance.createTerminalEnv(parentEnv1, shell1, null, null);
assert.ok(!('LANG' in env3), 'LANG is unset');
const env4 = TerminalService.createTerminalEnv(parentEnv2, shell1, null, null);
const env4 = TerminalInstance.createTerminalEnv(parentEnv2, shell1, null, null);
assert.equal(env4['LANG'], 'en_US.UTF-8', 'LANG is equal to the parent environment\'s LANG');
});
});
\ No newline at end of file
});
function createTerminalInstance(instantiationService: TestInstantiationService, terminalConfig: any): TerminalInstance {
let configService = new TestConfigurationService();
configService.setUserConfiguration('terminal', {
integrated: terminalConfig
});
instantiationService.stub(IConfigurationService, configService);
let terminalConfigHelper = new TerminalConfigHelper(platform.platform, configService);
return <TerminalInstance>instantiationService.createInstance(TerminalInstance,
/*terminalFocusContextKey*/null,
/*onExitCallback*/() => {},
/*configHelper*/terminalConfigHelper,
/*container*/null,
/*workspace*/null,
/*name*/'',
/*shellPath*/'');
}
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册