terminalInstance.ts 23.6 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6 7
import * as path from 'path';
import * as pfs from 'vs/base/node/pfs';
8
import DOM = require('vs/base/browser/dom');
J
Johannes Rieken 已提交
9
import Event, { Emitter } from 'vs/base/common/event';
10 11
import URI from 'vs/base/common/uri';
import cp = require('child_process');
12
import lifecycle = require('vs/base/common/lifecycle');
C
Christof Marti 已提交
13
import nls = require('vs/nls');
C
Christof Marti 已提交
14
import os = require('os');
15
import platform = require('vs/base/common/platform');
16
import xterm = require('xterm');
17
import { TPromise } from 'vs/base/common/winjs.base';
18
import { Dimension } from 'vs/base/browser/builder';
19
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
20 21
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IMessageService, Severity } from 'vs/platform/message/common/message';
22
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
23
import { IStringDictionary } from 'vs/base/common/collections';
24
import { ITerminalInstance, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_PANEL_ID, IShellLaunchConfig } from 'vs/workbench/parts/terminal/common/terminal';
25
import { ITerminalProcessFactory } from 'vs/workbench/parts/terminal/electron-browser/terminal';
26
import { IWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
27
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
28 29
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { TabFocus } from 'vs/editor/common/config/commonEditorConfig';
30
import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper';
31

D
Daniel Imms 已提交
32 33 34
/** The amount of time to consider terminal errors to be related to the launch */
const LAUNCHING_DURATION = 500;

35 36 37 38 39 40 41
/** A regex that matches paths in the form /path, ~/path, ./path, ../path */
const pathPrefix = '(\\.\\.?|\\~)';
const pathStartClause = '\\/';
const excludedPathCharactersClause = '[^\\0\\s!$`&*()+\'":;]'; // '":; are allowed in paths but they are often separators so ignore them
const escapedExcludedPathCharactersClause = '(\\\\s|\\\\!|\\\\$|\\\\`|\\\\&|\\\\*|(|)|\\+)';
const LOCAL_UNIX_LIKE_LINK_REGEX = new RegExp('(' + pathPrefix + '?(' + pathStartClause + '(' + excludedPathCharactersClause + '|' + escapedExcludedPathCharactersClause + ')+)+)');

42 43 44 45 46 47 48 49 50
class StandardTerminalProcessFactory implements ITerminalProcessFactory {
	public create(env: { [key: string]: string }): cp.ChildProcess {
		return cp.fork('./terminalProcess', [], {
			env,
			cwd: URI.parse(path.dirname(require.toUrl('./terminalProcess'))).fsPath
		});
	}
}

51
export class TerminalInstance implements ITerminalInstance {
52
	private static readonly EOL_REGEX = /\r?\n/g;
C
Christof Marti 已提交
53

54
	private static _terminalProcessFactory: ITerminalProcessFactory = new StandardTerminalProcessFactory();
D
Daniel Imms 已提交
55 56
	private static _idCounter = 1;

57
	private _id: number;
D
Daniel Imms 已提交
58
	private _isExiting: boolean;
K
Kai Wood 已提交
59
	private _hadFocusOnExit: boolean;
60
	private _isLaunching: boolean;
D
Daniel Imms 已提交
61
	private _isVisible: boolean;
62
	private _onDisposed: Emitter<ITerminalInstance>;
63
	private _onProcessIdReady: Emitter<TerminalInstance>;
D
Daniel Imms 已提交
64
	private _onTitleChanged: Emitter<string>;
D
Daniel Imms 已提交
65
	private _process: cp.ChildProcess;
66
	private _processId: number;
D
Daniel Imms 已提交
67
	private _skipTerminalCommands: string[];
D
Daniel Imms 已提交
68
	private _title: string;
69 70
	private _instanceDisposables: lifecycle.IDisposable[];
	private _processDisposables: lifecycle.IDisposable[];
D
Daniel Imms 已提交
71 72 73
	private _wrapperElement: HTMLDivElement;
	private _xterm: any;
	private _xtermElement: HTMLDivElement;
74
	private _terminalHasTextContextKey: IContextKey<boolean>;
D
Daniel Imms 已提交
75

76
	public get id(): number { return this._id; }
77
	public get processId(): number { return this._processId; }
78
	public get onDisposed(): Event<ITerminalInstance> { return this._onDisposed.event; }
79
	public get onProcessIdReady(): Event<TerminalInstance> { return this._onProcessIdReady.event; }
D
Daniel Imms 已提交
80
	public get onTitleChanged(): Event<string> { return this._onTitleChanged.event; }
D
Daniel Imms 已提交
81
	public get title(): string { return this._title; }
K
Kai Wood 已提交
82
	public get hadFocusOnExit(): boolean { return this._hadFocusOnExit; }
83 84

	public constructor(
D
Daniel Imms 已提交
85 86 87
		private _terminalFocusContextKey: IContextKey<boolean>,
		private _configHelper: TerminalConfigHelper,
		private _container: HTMLElement,
88
		private _shellLaunchConfig: IShellLaunchConfig,
89
		@IContextKeyService private _contextKeyService: IContextKeyService,
D
Daniel Imms 已提交
90
		@IKeybindingService private _keybindingService: IKeybindingService,
91
		@IMessageService private _messageService: IMessageService,
92
		@IPanelService private _panelService: IPanelService,
93 94
		@IWorkspaceContextService private _contextService: IWorkspaceContextService,
		@IWorkbenchEditorService private _editorService: IWorkbenchEditorService
95
	) {
96 97
		this._instanceDisposables = [];
		this._processDisposables = [];
D
Daniel Imms 已提交
98
		this._skipTerminalCommands = [];
D
Daniel Imms 已提交
99
		this._isExiting = false;
K
Kai Wood 已提交
100
		this._hadFocusOnExit = false;
101
		this._isLaunching = true;
D
Daniel Imms 已提交
102 103
		this._isVisible = false;
		this._id = TerminalInstance._idCounter++;
104
		this._terminalHasTextContextKey = KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED.bindTo(this._contextKeyService);
D
Daniel Imms 已提交
105

106
		this._onDisposed = new Emitter<TerminalInstance>();
107 108
		this._onProcessIdReady = new Emitter<TerminalInstance>();
		this._onTitleChanged = new Emitter<string>();
109

D
Daniel Imms 已提交
110
		this._createProcess(this._contextService.getWorkspace(), this._shellLaunchConfig);
111
		this._createXterm();
D
Daniel Imms 已提交
112

113
		// Only attach xterm.js to the DOM if the terminal panel has been opened before.
D
Daniel Imms 已提交
114 115
		if (_container) {
			this.attachToElement(_container);
116 117 118
		}
	}

D
Daniel Imms 已提交
119
	public addDisposable(disposable: lifecycle.IDisposable): void {
120
		this._instanceDisposables.push(disposable);
D
Daniel Imms 已提交
121 122
	}

123 124 125 126
	/**
	 * Create xterm.js instance and attach data listeners.
	 */
	protected _createXterm(): void {
127 128 129
		this._xterm = xterm({
			scrollback: this._configHelper.getScrollback()
		});
130
		this._process.on('message', (message) => this._sendPtyDataToXterm(message));
D
Daniel Imms 已提交
131
		this._xterm.on('data', (data) => {
132 133 134 135 136 137
			if (this._process) {
				this._process.send({
					event: 'input',
					data: this._sanitizeInput(data)
				});
			}
D
Daniel Imms 已提交
138 139
			return false;
		});
140 141 142 143 144 145 146 147 148 149 150 151 152
	}

	public attachToElement(container: HTMLElement): void {
		if (this._wrapperElement) {
			throw new Error('The terminal instance has already been attached to a container');
		}

		this._container = container;
		this._wrapperElement = document.createElement('div');
		DOM.addClass(this._wrapperElement, 'terminal-wrapper');
		this._xtermElement = document.createElement('div');

		this._xterm.open(this._xtermElement);
153 154 155
		if (!platform.isWindows) {
			this._xterm.registerLinkMatcher(LOCAL_UNIX_LIKE_LINK_REGEX, (url) => this._openRelativeUnixLikeLink(url), 1);
		}
D
Daniel Imms 已提交
156
		this._xterm.attachCustomKeydownHandler((event: KeyboardEvent) => {
157 158 159 160 161
			// Disable all input if the terminal is exiting
			if (this._isExiting) {
				return false;
			}

162 163
			// Skip processing by xterm.js of keyboard events that resolve to commands described
			// within commandsToSkipShell
D
Daniel Imms 已提交
164
			const standardKeyboardEvent = new StandardKeyboardEvent(event);
165
			const keybinding = standardKeyboardEvent.toKeybinding();
D
Daniel Imms 已提交
166 167
			const resolveResult = this._keybindingService.resolve(keybinding, standardKeyboardEvent.target);
			if (resolveResult && this._skipTerminalCommands.some(k => k === resolveResult.commandId)) {
D
Daniel Imms 已提交
168 169 170 171 172 173 174 175
				event.preventDefault();
				return false;
			}

			// If tab focus mode is on, tab is not passed to the terminal
			if (TabFocus.getTabFocusMode() && event.keyCode === 9) {
				return false;
			}
176
			return undefined;
D
Daniel Imms 已提交
177
		});
178
		this._instanceDisposables.push(DOM.addDisposableListener(this._xterm.element, 'mouseup', (event: KeyboardEvent) => {
179 180 181
			// Wait until mouseup has propogated through the DOM before evaluating the new selection
			// state.
			setTimeout(() => {
182 183
				this._refreshSelectionContextKey();
			}, 0);
184
		}));
185 186

		// xterm.js currently drops selection on keyup as we need to handle this case.
187
		this._instanceDisposables.push(DOM.addDisposableListener(this._xterm.element, 'keyup', (event: KeyboardEvent) => {
188 189 190 191
			// Wait until keyup has propogated through the DOM before evaluating the new selection
			// state.
			setTimeout(() => {
				this._refreshSelectionContextKey();
192
			}, 0);
193
		}));
D
Daniel Imms 已提交
194

D
Daniel Imms 已提交
195 196
		const xtermHelper: HTMLElement = this._xterm.element.querySelector('.xterm-helpers');
		const focusTrap: HTMLElement = document.createElement('div');
197 198
		focusTrap.setAttribute('tabindex', '0');
		DOM.addClass(focusTrap, 'focus-trap');
199
		this._instanceDisposables.push(DOM.addDisposableListener(focusTrap, 'focus', (event: FocusEvent) => {
200 201 202 203
			let currentElement = focusTrap;
			while (!DOM.hasClass(currentElement, 'part')) {
				currentElement = currentElement.parentElement;
			}
D
Daniel Imms 已提交
204
			const hidePanelElement = <HTMLElement>currentElement.querySelector('.hide-panel-action');
205
			hidePanelElement.focus();
206
		}));
D
Daniel Imms 已提交
207
		xtermHelper.insertBefore(focusTrap, this._xterm.textarea);
208

209
		this._instanceDisposables.push(DOM.addDisposableListener(this._xterm.textarea, 'focus', (event: KeyboardEvent) => {
D
Daniel Imms 已提交
210
			this._terminalFocusContextKey.set(true);
211
		}));
212
		this._instanceDisposables.push(DOM.addDisposableListener(this._xterm.textarea, 'blur', (event: KeyboardEvent) => {
D
Daniel Imms 已提交
213
			this._terminalFocusContextKey.reset();
214
			this._refreshSelectionContextKey();
215
		}));
216
		this._instanceDisposables.push(DOM.addDisposableListener(this._xterm.element, 'focus', (event: KeyboardEvent) => {
D
Daniel Imms 已提交
217
			this._terminalFocusContextKey.set(true);
218
		}));
219
		this._instanceDisposables.push(DOM.addDisposableListener(this._xterm.element, 'blur', (event: KeyboardEvent) => {
D
Daniel Imms 已提交
220
			this._terminalFocusContextKey.reset();
221
			this._refreshSelectionContextKey();
222 223
		}));

D
Daniel Imms 已提交
224 225
		this._wrapperElement.appendChild(this._xtermElement);
		this._container.appendChild(this._wrapperElement);
226

227 228 229 230
		const computedStyle = window.getComputedStyle(this._container);
		const width = parseInt(computedStyle.getPropertyValue('width').replace('px', ''), 10);
		const height = parseInt(computedStyle.getPropertyValue('height').replace('px', ''), 10);
		this.layout(new Dimension(width, height));
D
Daniel Imms 已提交
231
		this.setVisible(this._isVisible);
232
		this.updateConfig();
233 234
	}

235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
	private _openRelativeUnixLikeLink(link: string): TPromise<void> {
		// Resolve ~ -> $HOME
		if (link.charAt(0) === '~') {
			link = process.env.HOME + link.substring(1);
		}

		// Resolve workspace path . / .. -> <path>/. / <path/..
		if (link.charAt(0) === '.') {
			if (!this._contextService.hasWorkspace) {
				// Abort if no workspace is open
				return TPromise.as(void 0);
			}
			link = path.join(this._contextService.getWorkspace().resource.fsPath, link);
		}

		// Clean up the path
		const resource = URI.file(path.normalize(path.resolve(link)));

		// Open an editor if the path exists
		return pfs.fileExists(link).then(isFile => {
			if (!isFile) {
				return void 0;
			}
			return this._editorService.openEditor({ resource }).then(() => void 0);
		});
	}

262 263 264 265
	public hasSelection(): boolean {
		return !document.getSelection().isCollapsed;
	}

D
Daniel Imms 已提交
266
	public copySelection(): void {
D
Daniel Imms 已提交
267 268 269
		if (document.activeElement.classList.contains('xterm')) {
			document.execCommand('copy');
		} else {
D
Daniel Imms 已提交
270
			this._messageService.show(Severity.Warning, nls.localize('terminal.integrated.copySelection.noSelection', 'Cannot copy terminal selection when terminal does not have focus'));
D
Daniel Imms 已提交
271
		}
D
Daniel Imms 已提交
272 273
	}

274 275 276 277
	public clearSelection(): void {
		document.getSelection().empty();
	}

D
Daniel Imms 已提交
278
	public dispose(): void {
K
Kai Wood 已提交
279 280 281
		if (this._xterm && this._xterm.element) {
			this._hadFocusOnExit = DOM.hasClass(this._xterm.element, 'focus');
		}
D
Daniel Imms 已提交
282 283 284
		if (this._wrapperElement) {
			this._container.removeChild(this._wrapperElement);
			this._wrapperElement = null;
D
Daniel Imms 已提交
285
		}
D
Daniel Imms 已提交
286 287 288
		if (this._xterm) {
			this._xterm.destroy();
			this._xterm = null;
D
Daniel Imms 已提交
289
		}
D
Daniel Imms 已提交
290 291 292
		if (this._process) {
			if (this._process.connected) {
				this._process.kill();
D
Daniel Imms 已提交
293
			}
D
Daniel Imms 已提交
294
			this._process = null;
D
Daniel Imms 已提交
295
		}
296
		this._onDisposed.fire(this);
297 298
		this._processDisposables = lifecycle.dispose(this._processDisposables);
		this._instanceDisposables = lifecycle.dispose(this._instanceDisposables);
D
Daniel Imms 已提交
299 300
	}

D
Daniel Imms 已提交
301
	public focus(force?: boolean): void {
D
Daniel Imms 已提交
302
		if (!this._xterm) {
D
Daniel Imms 已提交
303 304
			return;
		}
D
Daniel Imms 已提交
305
		const text = window.getSelection().toString();
D
Daniel Imms 已提交
306
		if (!text || force) {
D
Daniel Imms 已提交
307
			this._xterm.focus();
D
Daniel Imms 已提交
308
		}
D
Daniel Imms 已提交
309 310 311
	}

	public paste(): void {
D
Daniel Imms 已提交
312 313
		this.focus();
		document.execCommand('paste');
D
Daniel Imms 已提交
314 315 316
	}

	public sendText(text: string, addNewLine: boolean): void {
D
Daniel Imms 已提交
317 318 319
		if (addNewLine && text.substr(text.length - os.EOL.length) !== os.EOL) {
			text += os.EOL;
		}
D
Daniel Imms 已提交
320
		this._process.send({
D
Daniel Imms 已提交
321 322 323
			event: 'input',
			data: text
		});
D
Daniel Imms 已提交
324
	}
325 326

	public setVisible(visible: boolean): void {
D
Daniel Imms 已提交
327 328 329
		this._isVisible = visible;
		if (this._wrapperElement) {
			DOM.toggleClass(this._wrapperElement, 'active', visible);
330
		}
D
Daniel Imms 已提交
331
		if (visible && this._xterm) {
332 333 334 335 336 337
			// Trigger a manual scroll event which will sync the viewport and scroll bar. This is
			// necessary if the number of rows in the terminal has decreased while it was in the
			// background since scrollTop changes take no effect but the terminal's position does
			// change since the number of visible rows decreases.
			this._xterm.emit('scroll', this._xterm.ydisp);
		}
338 339
	}

340
	public scrollDownLine(): void {
D
Daniel Imms 已提交
341
		this._xterm.scrollDisp(1);
D
Daniel Imms 已提交
342 343
	}

344
	public scrollDownPage(): void {
345
		this._xterm.scrollPages(1);
346 347
	}

348 349 350 351
	public scrollToBottom(): void {
		this._xterm.scrollToBottom();
	}

352
	public scrollUpLine(): void {
D
Daniel Imms 已提交
353
		this._xterm.scrollDisp(-1);
D
Daniel Imms 已提交
354
	}
355

356
	public scrollUpPage(): void {
357
		this._xterm.scrollPages(-1);
358 359
	}

360 361 362 363
	public scrollToTop(): void {
		this._xterm.scrollToTop();
	}

D
Daniel Imms 已提交
364 365 366 367
	public clear(): void {
		this._xterm.clear();
	}

368
	private _refreshSelectionContextKey() {
369 370 371
		const activePanel = this._panelService.getActivePanel();
		const isFocused = activePanel && activePanel.getId() === TERMINAL_PANEL_ID;
		this._terminalHasTextContextKey.set(isFocused && !window.getSelection().isCollapsed);
372 373
	}

D
Daniel Imms 已提交
374
	private _sanitizeInput(data: any) {
375
		return typeof data === 'string' ? data.replace(TerminalInstance.EOL_REGEX, os.EOL) : data;
C
Christof Marti 已提交
376 377
	}

378 379 380 381 382
	protected _getCwd(shell: IShellLaunchConfig, workspace: IWorkspace): string {
		if (shell.cwd) {
			return shell.cwd;
		}

B
Benjamin Pasero 已提交
383
		let cwd: string;
D
Daniel Imms 已提交
384 385

		// TODO: Handle non-existent customCwd
386
		if (!shell.ignoreConfigurationCwd) {
387
			// Evaluate custom cwd first
D
Daniel Imms 已提交
388
			const customCwd = this._configHelper.getCwd();
389 390 391 392 393 394
			if (customCwd) {
				if (path.isAbsolute(customCwd)) {
					cwd = customCwd;
				} else if (workspace) {
					cwd = path.normalize(path.join(workspace.resource.fsPath, customCwd));
				}
D
Daniel Imms 已提交
395 396 397 398 399
			}
		}

		// If there was no custom cwd or it was relative with no workspace
		if (!cwd) {
400
			cwd = workspace ? workspace.resource.fsPath : os.homedir();
D
Daniel Imms 已提交
401 402 403 404 405
		}

		return TerminalInstance._sanitizeCwd(cwd);
	}

406
	protected _createProcess(workspace: IWorkspace, shell: IShellLaunchConfig): void {
D
Daniel Imms 已提交
407
		const locale = this._configHelper.isSetLocaleVariables() ? platform.locale : undefined;
D
Daniel Imms 已提交
408
		if (!shell.executable) {
409
			this._configHelper.mergeDefaultShellPathAndArgs(shell);
P
Pine Wu 已提交
410
		}
411
		const env = TerminalInstance.createTerminalEnv(process.env, shell, this._getCwd(shell, workspace), locale);
D
Daniel Imms 已提交
412
		this._title = shell.name || '';
D
Daniel Imms 已提交
413
		this._process = cp.fork('./terminalProcess', [], {
414 415 416
			env: env,
			cwd: URI.parse(path.dirname(require.toUrl('./terminalProcess'))).fsPath
		});
D
Daniel Imms 已提交
417
		if (!shell.name) {
418
			// Only listen for process title changes when a name is not provided
D
Daniel Imms 已提交
419
			this._process.on('message', (message) => {
420
				if (message.type === 'title') {
D
Daniel Imms 已提交
421
					this._title = message.content ? message.content : '';
422
					this._onTitleChanged.fire(this._title);
423
				}
424 425
			});
		}
426 427 428 429 430 431
		this._process.on('message', (message) => {
			if (message.type === 'pid') {
				this._processId = message.content;
				this._onProcessIdReady.fire(this);
			}
		});
432 433 434 435 436 437
		this._process.on('exit', exitCode => this._onPtyProcessExit(exitCode));
		setTimeout(() => {
			this._isLaunching = false;
		}, LAUNCHING_DURATION);
	}

438 439 440 441 442 443
	private _sendPtyDataToXterm(message: { type: string, content: string }): void {
		if (message.type === 'data') {
			this._xterm.write(message.content);
		}
	}

444 445 446 447 448 449 450 451
	private _onPtyProcessExit(exitCode: number): void {
		// Prevent dispose functions being triggered multiple times
		if (this._isExiting) {
			return;
		}

		this._isExiting = true;
		let exitCodeMessage: string;
452
		if (exitCode) {
453 454 455
			exitCodeMessage = nls.localize('terminal.integrated.exitedWithCode', 'The terminal process terminated with exit code: {0}', exitCode);
		}

456 457 458 459 460
		// Only trigger wait on exit when the exit was triggered by the process, not through the
		// `workbench.action.terminal.kill` command
		const triggeredByProcess = exitCode !== null;

		if (triggeredByProcess && this._shellLaunchConfig.waitOnExit) {
461
			if (exitCode) {
462 463 464 465 466
				this._xterm.writeln(exitCodeMessage);
			}
			this._xterm.writeln(nls.localize('terminal.integrated.waitOnExit', 'Press any key to close the terminal'));
			// Disable all input if the terminal is exiting and listen for next keypress
			this._xterm.setOption('disableStdin', true);
467
			this._processDisposables.push(DOM.addDisposableListener(this._xterm.textarea, 'keypress', (event: KeyboardEvent) => {
468
				this.dispose();
469
				event.preventDefault();
470
			}));
471 472
		} else {
			this.dispose();
473
			if (exitCode) {
474 475 476 477 478 479 480 481 482
				if (this._isLaunching) {
					let args = '';
					if (this._shellLaunchConfig.args && this._shellLaunchConfig.args.length) {
						args = ' ' + this._shellLaunchConfig.args.map(a => {
							if (a.indexOf(' ') !== -1) {
								return `'${a}'`;
							}
							return a;
						}).join(' ');
483
					}
484 485 486
					this._messageService.show(Severity.Error, nls.localize('terminal.integrated.launchFailed', 'The terminal process command `{0}{1}` failed to launch (exit code: {2})', this._shellLaunchConfig.executable, args, exitCode));
				} else {
					this._messageService.show(Severity.Error, exitCodeMessage);
487 488
				}
			}
489
		}
490 491
	}

492 493
	public reuseTerminal(shell?: IShellLaunchConfig): void {
		// Kill and clean up old process
494 495 496 497 498 499 500
		if (this._process) {
			this._process.removeAllListeners('exit');
			if (this._process.connected) {
				this._process.kill();
			}
			this._process = null;
		}
501 502 503
		lifecycle.dispose(this._processDisposables);
		this._processDisposables = [];

504 505
		// Ensure new processes' output starts at start of new line
		this._xterm.write('\n\x1b[G');
506 507

		// Initialize new process
D
Daniel Imms 已提交
508
		this._createProcess(this._contextService.getWorkspace(), shell);
509
		this._process.on('message', (message) => this._sendPtyDataToXterm(message));
510 511

		// Clean up waitOnExit state
512 513
		if (this._isExiting && this._shellLaunchConfig.waitOnExit) {
			this._xterm.setOption('disableStdin', false);
514
			this._isExiting = false;
515
		}
516

517 518 519 520
		// Set the new shell launch config
		this._shellLaunchConfig = shell;
	}

521 522
	// TODO: This should be private/protected
	// TODO: locale should not be optional
523
	public static createTerminalEnv(parentEnv: IStringDictionary<string>, shell: IShellLaunchConfig, cwd: string, locale?: string): IStringDictionary<string> {
524
		const env = shell.env ? shell.env : TerminalInstance._cloneEnv(parentEnv);
525 526
		env['PTYPID'] = process.pid.toString();
		env['PTYSHELL'] = shell.executable;
D
Daniel Imms 已提交
527 528 529 530 531
		if (shell.args) {
			shell.args.forEach((arg, i) => {
				env[`PTYSHELLARG${i}`] = arg;
			});
		}
D
Daniel Imms 已提交
532
		env['PTYCWD'] = cwd;
533
		if (locale) {
D
Daniel Imms 已提交
534
			env['LANG'] = TerminalInstance._getLangEnvVariable(locale);
535 536
		}
		return env;
537 538
	}

D
Dirk Baeumer 已提交
539 540
	public onData(listener: (data: string) => void): lifecycle.IDisposable {
		let callback = (message) => {
541 542 543
			if (message.type === 'data') {
				listener(message.content);
			}
D
Dirk Baeumer 已提交
544 545 546 547
		};
		this._process.on('message', callback);
		return {
			dispose: () => {
548 549 550
				if (this._process) {
					this._process.removeListener('message', callback);
				}
D
Dirk Baeumer 已提交
551 552
			}
		};
553 554
	}

D
Dirk Baeumer 已提交
555
	public onExit(listener: (exitCode: number) => void): lifecycle.IDisposable {
556
		this._process.on('exit', listener);
D
Dirk Baeumer 已提交
557 558
		return {
			dispose: () => {
559 560 561
				if (this._process) {
					this._process.removeListener('exit', listener);
				}
D
Dirk Baeumer 已提交
562 563
			}
		};
564 565
	}

D
Daniel Imms 已提交
566
	private static _sanitizeCwd(cwd: string) {
567 568 569
		// 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);
D
Daniel Imms 已提交
570
		}
571
		return cwd;
D
Daniel Imms 已提交
572 573
	}

D
Daniel Imms 已提交
574
	private static _cloneEnv(env: IStringDictionary<string>): IStringDictionary<string> {
D
Daniel Imms 已提交
575
		const newEnv: IStringDictionary<string> = Object.create(null);
576 577
		Object.keys(env).forEach((key) => {
			newEnv[key] = env[key];
578
		});
579
		return newEnv;
580 581
	}

D
Daniel Imms 已提交
582
	private static _getLangEnvVariable(locale: string) {
583 584
		const parts = locale.split('-');
		const n = parts.length;
585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600
		const language = parts[0];
		if (n === 0) {
			return '';
		}
		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',
				fr: 'FR',
				it: 'IT',
				ja: 'JP',
				ko: 'KR',
				ru: 'RU',
601
				zh: 'CN'
602 603 604 605 606 607 608
			};
			if (language in languageVariants) {
				parts.push(languageVariants[language]);
			}
		} else {
			// Ensure the variant is uppercase
			parts[1] = parts[1].toUpperCase();
D
Daniel Imms 已提交
609
		}
610
		return parts.join('_') + '.UTF-8';
D
Daniel Imms 已提交
611
	}
D
Daniel Imms 已提交
612

613
	public updateConfig(): void {
614
		this._setFlowControl(this._configHelper.getFlowControl());
615
		this._setCursorBlink(this._configHelper.getCursorBlink());
616
		this._setCursorStyle(this._configHelper.getCursorStyle());
617 618 619 620
		this._setCommandsToSkipShell(this._configHelper.getCommandsToSkipShell());
		this._setScrollback(this._configHelper.getScrollback());
	}

621 622 623 624 625 626
	private _setFlowControl(flowControl: boolean): void {
		if (this._xterm && this._xterm.getOption('useFlowControl') !== flowControl) {
			this._xterm.setOption('useFlowControl', flowControl);
		}
	}

627
	private _setCursorBlink(blink: boolean): void {
D
Daniel Imms 已提交
628
		if (this._xterm && this._xterm.getOption('cursorBlink') !== blink) {
D
Daniel Imms 已提交
629
			this._xterm.setOption('cursorBlink', blink);
D
Daniel Imms 已提交
630
			this._xterm.refresh(0, this._xterm.rows - 1);
D
Daniel Imms 已提交
631 632 633
		}
	}

634 635 636 637 638 639 640 641
	private _setCursorStyle(style: string): void {
		if (this._xterm && this._xterm.getOption('cursorStyle') !== style) {
			// 'line' is used instead of bar in VS Code to be consistent with editor.cursorStyle
			const xtermOption = style === 'line' ? 'bar' : style;
			this._xterm.setOption('cursorStyle', xtermOption);
		}
	}

642
	private _setCommandsToSkipShell(commands: string[]): void {
D
Daniel Imms 已提交
643
		this._skipTerminalCommands = commands;
D
Daniel Imms 已提交
644 645
	}

646
	private _setScrollback(lineCount: number): void {
D
Daniel Imms 已提交
647
		if (this._xterm && this._xterm.getOption('scrollback') !== lineCount) {
648
			console.log('set scrollback to: ' + lineCount);
D
Daniel Imms 已提交
649 650 651 652
			this._xterm.setOption('scrollback', lineCount);
		}
	}

653
	public layout(dimension: { width: number, height: number }): void {
D
Daniel Imms 已提交
654
		const font = this._configHelper.getFont();
655
		if (!font || !font.charWidth || !font.charHeight) {
D
Daniel Imms 已提交
656 657 658 659
			return;
		}
		if (!dimension.height) { // Minimized
			return;
660 661 662 663 664 665
		} else {
			// Trigger scroll event manually so that the viewport's scroll area is synced. This
			// needs to happen otherwise its scrollTop value is invalid when the panel is toggled as
			// it gets removed and then added back to the DOM (resetting scrollTop to 0).
			// Upstream issue: https://github.com/sourcelair/xterm.js/issues/291
			this._xterm.emit('scroll', this._xterm.ydisp);
D
Daniel Imms 已提交
666
		}
667
		const padding = parseInt(getComputedStyle(document.querySelector('.terminal-outer-container')).paddingLeft.split('px')[0], 10);
D
Daniel Imms 已提交
668 669 670
		// Use left padding as right padding, right padding is not defined in CSS just in case
		// xterm.js causes an unexpected overflow.
		const innerWidth = dimension.width - padding * 2;
D
Daniel Imms 已提交
671 672
		const cols = Math.floor(innerWidth / font.charWidth);
		const rows = Math.floor(dimension.height / font.charHeight);
D
Daniel Imms 已提交
673 674 675
		if (this._xterm) {
			this._xterm.resize(cols, rows);
			this._xterm.element.style.width = innerWidth + 'px';
D
Daniel Imms 已提交
676
		}
D
Daniel Imms 已提交
677 678
		if (this._process.connected) {
			this._process.send({
D
Daniel Imms 已提交
679 680 681 682 683 684
				event: 'resize',
				cols: cols,
				rows: rows
			});
		}
	}
685 686 687 688

	public static setTerminalProcessFactory(factory: ITerminalProcessFactory): void {
		this._terminalProcessFactory = factory;
	}
689
}