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

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

34 35 36 37 38 39 40 41 42
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
		});
	}
}

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

46
	private static _terminalProcessFactory: ITerminalProcessFactory = new StandardTerminalProcessFactory();
D
Daniel Imms 已提交
47 48
	private static _idCounter = 1;

49
	private _id: number;
D
Daniel Imms 已提交
50
	private _isExiting: boolean;
K
Kai Wood 已提交
51
	private _hadFocusOnExit: boolean;
52
	private _isLaunching: boolean;
D
Daniel Imms 已提交
53
	private _isVisible: boolean;
54
	private _isDisposed: boolean;
55
	private _onDisposed: Emitter<ITerminalInstance>;
56
	private _onProcessIdReady: Emitter<TerminalInstance>;
D
Daniel Imms 已提交
57
	private _onTitleChanged: Emitter<string>;
D
Daniel Imms 已提交
58
	private _process: cp.ChildProcess;
59
	private _processId: number;
D
Daniel Imms 已提交
60
	private _skipTerminalCommands: string[];
D
Daniel Imms 已提交
61
	private _title: string;
62 63
	private _instanceDisposables: lifecycle.IDisposable[];
	private _processDisposables: lifecycle.IDisposable[];
D
Daniel Imms 已提交
64 65 66
	private _wrapperElement: HTMLDivElement;
	private _xterm: any;
	private _xtermElement: HTMLDivElement;
67
	private _terminalHasTextContextKey: IContextKey<boolean>;
68 69
	private _cols: number;
	private _rows: number;
D
Daniel Imms 已提交
70

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

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

103
		this._onDisposed = new Emitter<TerminalInstance>();
104 105
		this._onProcessIdReady = new Emitter<TerminalInstance>();
		this._onTitleChanged = new Emitter<string>();
106

107
		this._initDimensions();
D
Daniel Imms 已提交
108
		this._createProcess(this._contextService.getWorkspace(), this._shellLaunchConfig);
109
		this._createXterm();
D
Daniel Imms 已提交
110

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

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

121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
	private _initDimensions(): void {
		// The terminal panel needs to have been created
		if (!this._container) {
			return;
		}

		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._evaluateColsAndRows(width, height);
	}

	/**
	 * Evaluates and sets the cols and rows of the terminal if possible.
	 * @param width The width of the container.
	 * @param height The height of the container.
	 * @return Whether cols and rows were set.
	 */
	private _evaluateColsAndRows(width: number, height: number): boolean {
		// The font needs to have been initialized
		const font = this._configHelper.getFont();
		if (!font || !font.charWidth || !font.charHeight) {
			return false;
		}

		// TODO: Fetch size from panel so initial size is correct
		// The panel is minimized
		if (!height) {
			return false;
		} 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
			if (this._xterm) {
				this._xterm.emit('scroll', this._xterm.ydisp);
			}
		}

		const padding = parseInt(getComputedStyle(document.querySelector('.terminal-outer-container')).paddingLeft.split('px')[0], 10);
		// Use left padding as right padding, right padding is not defined in CSS just in case
		// xterm.js causes an unexpected overflow.
		const innerWidth = width - padding * 2;
		this._cols = Math.floor(innerWidth / font.charWidth);
		this._rows = Math.floor(height / font.charHeight);
		return true;
	}

169 170 171 172
	/**
	 * Create xterm.js instance and attach data listeners.
	 */
	protected _createXterm(): void {
173 174 175
		this._xterm = xterm({
			scrollback: this._configHelper.getScrollback()
		});
176
		this._process.on('message', (message) => this._sendPtyDataToXterm(message));
D
Daniel Imms 已提交
177
		this._xterm.on('data', (data) => {
178 179 180 181 182 183
			if (this._process) {
				this._process.send({
					event: 'input',
					data: this._sanitizeInput(data)
				});
			}
D
Daniel Imms 已提交
184 185
			return false;
		});
186 187 188 189 190 191 192 193 194 195 196 197 198
	}

	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);
199
		this._xterm.registerLinkMatcher(this._linkHandler.localLinkRegex, (url) => this._linkHandler.handleLocalLink(url), 1);
D
Daniel Imms 已提交
200
		this._xterm.attachCustomKeydownHandler((event: KeyboardEvent) => {
201 202 203 204 205
			// Disable all input if the terminal is exiting
			if (this._isExiting) {
				return false;
			}

206 207
			// Skip processing by xterm.js of keyboard events that resolve to commands described
			// within commandsToSkipShell
D
Daniel Imms 已提交
208
			const standardKeyboardEvent = new StandardKeyboardEvent(event);
209
			const keybinding = standardKeyboardEvent.toKeybinding();
D
Daniel Imms 已提交
210 211
			const resolveResult = this._keybindingService.resolve(keybinding, standardKeyboardEvent.target);
			if (resolveResult && this._skipTerminalCommands.some(k => k === resolveResult.commandId)) {
D
Daniel Imms 已提交
212 213 214 215 216 217 218 219
				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;
			}
220
			return undefined;
D
Daniel Imms 已提交
221
		});
222
		this._instanceDisposables.push(DOM.addDisposableListener(this._xterm.element, 'mouseup', (event: KeyboardEvent) => {
223 224 225
			// Wait until mouseup has propogated through the DOM before evaluating the new selection
			// state.
			setTimeout(() => {
226 227
				this._refreshSelectionContextKey();
			}, 0);
228
		}));
229 230

		// xterm.js currently drops selection on keyup as we need to handle this case.
231
		this._instanceDisposables.push(DOM.addDisposableListener(this._xterm.element, 'keyup', (event: KeyboardEvent) => {
232 233 234 235
			// Wait until keyup has propogated through the DOM before evaluating the new selection
			// state.
			setTimeout(() => {
				this._refreshSelectionContextKey();
236
			}, 0);
237
		}));
D
Daniel Imms 已提交
238

D
Daniel Imms 已提交
239 240
		const xtermHelper: HTMLElement = this._xterm.element.querySelector('.xterm-helpers');
		const focusTrap: HTMLElement = document.createElement('div');
241 242
		focusTrap.setAttribute('tabindex', '0');
		DOM.addClass(focusTrap, 'focus-trap');
243
		this._instanceDisposables.push(DOM.addDisposableListener(focusTrap, 'focus', (event: FocusEvent) => {
244 245 246 247
			let currentElement = focusTrap;
			while (!DOM.hasClass(currentElement, 'part')) {
				currentElement = currentElement.parentElement;
			}
D
Daniel Imms 已提交
248
			const hidePanelElement = <HTMLElement>currentElement.querySelector('.hide-panel-action');
249
			hidePanelElement.focus();
250
		}));
D
Daniel Imms 已提交
251
		xtermHelper.insertBefore(focusTrap, this._xterm.textarea);
252

253
		this._instanceDisposables.push(DOM.addDisposableListener(this._xterm.textarea, 'focus', (event: KeyboardEvent) => {
D
Daniel Imms 已提交
254
			this._terminalFocusContextKey.set(true);
255
		}));
256
		this._instanceDisposables.push(DOM.addDisposableListener(this._xterm.textarea, 'blur', (event: KeyboardEvent) => {
D
Daniel Imms 已提交
257
			this._terminalFocusContextKey.reset();
258
			this._refreshSelectionContextKey();
259
		}));
260
		this._instanceDisposables.push(DOM.addDisposableListener(this._xterm.element, 'focus', (event: KeyboardEvent) => {
D
Daniel Imms 已提交
261
			this._terminalFocusContextKey.set(true);
262
		}));
263
		this._instanceDisposables.push(DOM.addDisposableListener(this._xterm.element, 'blur', (event: KeyboardEvent) => {
D
Daniel Imms 已提交
264
			this._terminalFocusContextKey.reset();
265
			this._refreshSelectionContextKey();
266 267
		}));

D
Daniel Imms 已提交
268 269
		this._wrapperElement.appendChild(this._xtermElement);
		this._container.appendChild(this._wrapperElement);
270

271 272 273 274
		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 已提交
275
		this.setVisible(this._isVisible);
276
		this.updateConfig();
277 278
	}

279 280 281 282 283 284 285 286
	public registerLinkMatcher(regex: RegExp, handler: (url: string) => void, matchIndex?: number): number {
		return this._xterm.registerLinkMatcher(regex, handler, matchIndex);
	}

	public deregisterLinkMatcher(linkMatcherId: number): void {
		this._xterm.deregisterLinkMatcher(linkMatcherId);
	}

287 288 289 290
	public hasSelection(): boolean {
		return !document.getSelection().isCollapsed;
	}

D
Daniel Imms 已提交
291
	public copySelection(): void {
D
Daniel Imms 已提交
292 293 294
		if (document.activeElement.classList.contains('xterm')) {
			document.execCommand('copy');
		} else {
D
Daniel Imms 已提交
295
			this._messageService.show(Severity.Warning, nls.localize('terminal.integrated.copySelection.noSelection', 'Cannot copy terminal selection when terminal does not have focus'));
D
Daniel Imms 已提交
296
		}
D
Daniel Imms 已提交
297 298
	}

299 300 301 302
	public clearSelection(): void {
		document.getSelection().empty();
	}

D
Daniel Imms 已提交
303
	public dispose(): void {
K
Kai Wood 已提交
304 305 306
		if (this._xterm && this._xterm.element) {
			this._hadFocusOnExit = DOM.hasClass(this._xterm.element, 'focus');
		}
D
Daniel Imms 已提交
307 308 309
		if (this._wrapperElement) {
			this._container.removeChild(this._wrapperElement);
			this._wrapperElement = null;
D
Daniel Imms 已提交
310
		}
D
Daniel Imms 已提交
311 312 313
		if (this._xterm) {
			this._xterm.destroy();
			this._xterm = null;
D
Daniel Imms 已提交
314
		}
D
Daniel Imms 已提交
315 316 317
		if (this._process) {
			if (this._process.connected) {
				this._process.kill();
D
Daniel Imms 已提交
318
			}
D
Daniel Imms 已提交
319
			this._process = null;
D
Daniel Imms 已提交
320
		}
321 322 323 324
		if (!this._isDisposed) {
			this._isDisposed = true;
			this._onDisposed.fire(this);
		}
325 326
		this._processDisposables = lifecycle.dispose(this._processDisposables);
		this._instanceDisposables = lifecycle.dispose(this._instanceDisposables);
D
Daniel Imms 已提交
327 328
	}

D
Daniel Imms 已提交
329
	public focus(force?: boolean): void {
D
Daniel Imms 已提交
330
		if (!this._xterm) {
D
Daniel Imms 已提交
331 332
			return;
		}
D
Daniel Imms 已提交
333
		const text = window.getSelection().toString();
D
Daniel Imms 已提交
334
		if (!text || force) {
D
Daniel Imms 已提交
335
			this._xterm.focus();
D
Daniel Imms 已提交
336
		}
D
Daniel Imms 已提交
337 338 339
	}

	public paste(): void {
D
Daniel Imms 已提交
340 341
		this.focus();
		document.execCommand('paste');
D
Daniel Imms 已提交
342 343 344
	}

	public sendText(text: string, addNewLine: boolean): void {
D
Daniel Imms 已提交
345 346 347
		if (addNewLine && text.substr(text.length - os.EOL.length) !== os.EOL) {
			text += os.EOL;
		}
D
Daniel Imms 已提交
348
		this._process.send({
D
Daniel Imms 已提交
349 350 351
			event: 'input',
			data: text
		});
D
Daniel Imms 已提交
352
	}
353 354

	public setVisible(visible: boolean): void {
D
Daniel Imms 已提交
355 356 357
		this._isVisible = visible;
		if (this._wrapperElement) {
			DOM.toggleClass(this._wrapperElement, 'active', visible);
358
		}
D
Daniel Imms 已提交
359
		if (visible && this._xterm) {
360 361 362 363 364 365
			// 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);
		}
366 367
	}

368
	public scrollDownLine(): void {
D
Daniel Imms 已提交
369
		this._xterm.scrollDisp(1);
D
Daniel Imms 已提交
370 371
	}

372
	public scrollDownPage(): void {
373
		this._xterm.scrollPages(1);
374 375
	}

376 377 378 379
	public scrollToBottom(): void {
		this._xterm.scrollToBottom();
	}

380
	public scrollUpLine(): void {
D
Daniel Imms 已提交
381
		this._xterm.scrollDisp(-1);
D
Daniel Imms 已提交
382
	}
383

384
	public scrollUpPage(): void {
385
		this._xterm.scrollPages(-1);
386 387
	}

388 389 390 391
	public scrollToTop(): void {
		this._xterm.scrollToTop();
	}

D
Daniel Imms 已提交
392 393 394 395
	public clear(): void {
		this._xterm.clear();
	}

396
	private _refreshSelectionContextKey() {
397 398 399
		const activePanel = this._panelService.getActivePanel();
		const isFocused = activePanel && activePanel.getId() === TERMINAL_PANEL_ID;
		this._terminalHasTextContextKey.set(isFocused && !window.getSelection().isCollapsed);
400 401
	}

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

406 407 408 409 410
	protected _getCwd(shell: IShellLaunchConfig, workspace: IWorkspace): string {
		if (shell.cwd) {
			return shell.cwd;
		}

B
Benjamin Pasero 已提交
411
		let cwd: string;
D
Daniel Imms 已提交
412 413

		// TODO: Handle non-existent customCwd
414
		if (!shell.ignoreConfigurationCwd) {
415
			// Evaluate custom cwd first
D
Daniel Imms 已提交
416
			const customCwd = this._configHelper.getCwd();
417 418 419 420 421 422
			if (customCwd) {
				if (path.isAbsolute(customCwd)) {
					cwd = customCwd;
				} else if (workspace) {
					cwd = path.normalize(path.join(workspace.resource.fsPath, customCwd));
				}
D
Daniel Imms 已提交
423 424 425 426 427
			}
		}

		// If there was no custom cwd or it was relative with no workspace
		if (!cwd) {
428
			cwd = workspace ? workspace.resource.fsPath : os.homedir();
D
Daniel Imms 已提交
429 430 431 432 433
		}

		return TerminalInstance._sanitizeCwd(cwd);
	}

434
	protected _createProcess(workspace: IWorkspace, shell: IShellLaunchConfig): void {
D
Daniel Imms 已提交
435
		const locale = this._configHelper.isSetLocaleVariables() ? platform.locale : undefined;
D
Daniel Imms 已提交
436
		if (!shell.executable) {
437
			this._configHelper.mergeDefaultShellPathAndArgs(shell);
P
Pine Wu 已提交
438
		}
439
		const env = TerminalInstance.createTerminalEnv(process.env, shell, this._getCwd(shell, workspace), locale, this._cols, this._rows);
D
Daniel Imms 已提交
440
		this._title = shell.name || '';
D
Daniel Imms 已提交
441
		this._process = cp.fork('./terminalProcess', [], {
442 443 444
			env: env,
			cwd: URI.parse(path.dirname(require.toUrl('./terminalProcess'))).fsPath
		});
D
Daniel Imms 已提交
445
		if (!shell.name) {
446
			// Only listen for process title changes when a name is not provided
D
Daniel Imms 已提交
447
			this._process.on('message', (message) => {
448
				if (message.type === 'title') {
D
Daniel Imms 已提交
449
					this._title = message.content ? message.content : '';
450
					this._onTitleChanged.fire(this._title);
451
				}
452 453
			});
		}
454 455 456 457 458 459
		this._process.on('message', (message) => {
			if (message.type === 'pid') {
				this._processId = message.content;
				this._onProcessIdReady.fire(this);
			}
		});
460 461 462 463 464 465
		this._process.on('exit', exitCode => this._onPtyProcessExit(exitCode));
		setTimeout(() => {
			this._isLaunching = false;
		}, LAUNCHING_DURATION);
	}

466 467 468 469 470 471
	private _sendPtyDataToXterm(message: { type: string, content: string }): void {
		if (message.type === 'data') {
			this._xterm.write(message.content);
		}
	}

472 473 474 475 476 477 478 479
	private _onPtyProcessExit(exitCode: number): void {
		// Prevent dispose functions being triggered multiple times
		if (this._isExiting) {
			return;
		}

		this._isExiting = true;
		let exitCodeMessage: string;
480
		if (exitCode) {
481 482 483
			exitCodeMessage = nls.localize('terminal.integrated.exitedWithCode', 'The terminal process terminated with exit code: {0}', exitCode);
		}

484 485 486 487 488
		// 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) {
489
			if (exitCode) {
490 491 492 493 494
				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);
495
			this._processDisposables.push(DOM.addDisposableListener(this._xterm.textarea, 'keypress', (event: KeyboardEvent) => {
496
				this.dispose();
497
				event.preventDefault();
498
			}));
499 500
		} else {
			this.dispose();
501
			if (exitCode) {
502 503 504 505 506 507 508 509 510
				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(' ');
511
					}
512 513 514
					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);
515 516
				}
			}
517
		}
518 519
	}

520 521
	public reuseTerminal(shell?: IShellLaunchConfig): void {
		// Kill and clean up old process
522 523 524 525 526 527 528
		if (this._process) {
			this._process.removeAllListeners('exit');
			if (this._process.connected) {
				this._process.kill();
			}
			this._process = null;
		}
529 530 531
		lifecycle.dispose(this._processDisposables);
		this._processDisposables = [];

532 533
		// Ensure new processes' output starts at start of new line
		this._xterm.write('\n\x1b[G');
534 535

		// Initialize new process
536
		const oldTitle = this._title;
D
Daniel Imms 已提交
537
		this._createProcess(this._contextService.getWorkspace(), shell);
538 539 540
		if (oldTitle !== this._title) {
			this._onTitleChanged.fire(this._title);
		}
541
		this._process.on('message', (message) => this._sendPtyDataToXterm(message));
542 543

		// Clean up waitOnExit state
544 545
		if (this._isExiting && this._shellLaunchConfig.waitOnExit) {
			this._xterm.setOption('disableStdin', false);
546
			this._isExiting = false;
547
		}
548

549 550 551 552
		// Set the new shell launch config
		this._shellLaunchConfig = shell;
	}

553 554
	// TODO: This should be private/protected
	// TODO: locale should not be optional
555
	public static createTerminalEnv(parentEnv: IStringDictionary<string>, shell: IShellLaunchConfig, cwd: string, locale?: string, cols?: number, rows?: number): IStringDictionary<string> {
556
		const env = shell.env ? shell.env : TerminalInstance._cloneEnv(parentEnv);
557 558
		env['PTYPID'] = process.pid.toString();
		env['PTYSHELL'] = shell.executable;
D
Daniel Imms 已提交
559 560 561 562 563
		if (shell.args) {
			shell.args.forEach((arg, i) => {
				env[`PTYSHELLARG${i}`] = arg;
			});
		}
D
Daniel Imms 已提交
564
		env['PTYCWD'] = cwd;
565
		env['LANG'] = TerminalInstance._getLangEnvVariable(locale);
566 567 568 569
		if (cols && rows) {
			env['PTYCOLS'] = cols.toString();
			env['PTYROWS'] = rows.toString();
		}
570
		return env;
571 572
	}

D
Dirk Baeumer 已提交
573 574
	public onData(listener: (data: string) => void): lifecycle.IDisposable {
		let callback = (message) => {
575 576 577
			if (message.type === 'data') {
				listener(message.content);
			}
D
Dirk Baeumer 已提交
578 579 580 581
		};
		this._process.on('message', callback);
		return {
			dispose: () => {
582 583 584
				if (this._process) {
					this._process.removeListener('message', callback);
				}
D
Dirk Baeumer 已提交
585 586
			}
		};
587 588
	}

D
Dirk Baeumer 已提交
589
	public onExit(listener: (exitCode: number) => void): lifecycle.IDisposable {
590
		this._process.on('exit', listener);
D
Dirk Baeumer 已提交
591 592
		return {
			dispose: () => {
593 594 595
				if (this._process) {
					this._process.removeListener('exit', listener);
				}
D
Dirk Baeumer 已提交
596 597
			}
		};
598 599
	}

D
Daniel Imms 已提交
600
	private static _sanitizeCwd(cwd: string) {
601 602 603
		// 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 已提交
604
		}
605
		return cwd;
D
Daniel Imms 已提交
606 607
	}

D
Daniel Imms 已提交
608
	private static _cloneEnv(env: IStringDictionary<string>): IStringDictionary<string> {
D
Daniel Imms 已提交
609
		const newEnv: IStringDictionary<string> = Object.create(null);
610 611
		Object.keys(env).forEach((key) => {
			newEnv[key] = env[key];
612
		});
613
		return newEnv;
614 615
	}

616 617
	private static _getLangEnvVariable(locale?: string) {
		const parts = locale ? locale.split('-') : [];
618
		const n = parts.length;
619
		if (n === 0) {
D
Daniel Imms 已提交
620 621
			// Fallback to en_US to prevent possible encoding issues.
			return 'en_US.UTF-8';
622 623 624 625 626 627 628 629 630 631 632 633 634
		}
		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',
635
				zh: 'CN'
636
			};
D
Daniel Imms 已提交
637 638
			if (parts[0] in languageVariants) {
				parts.push(languageVariants[parts[0]]);
639 640 641 642
			}
		} else {
			// Ensure the variant is uppercase
			parts[1] = parts[1].toUpperCase();
D
Daniel Imms 已提交
643
		}
644
		return parts.join('_') + '.UTF-8';
D
Daniel Imms 已提交
645
	}
D
Daniel Imms 已提交
646

647 648
	public updateConfig(): void {
		this._setCursorBlink(this._configHelper.getCursorBlink());
649
		this._setCursorStyle(this._configHelper.getCursorStyle());
650 651 652 653 654
		this._setCommandsToSkipShell(this._configHelper.getCommandsToSkipShell());
		this._setScrollback(this._configHelper.getScrollback());
	}

	private _setCursorBlink(blink: boolean): void {
D
Daniel Imms 已提交
655
		if (this._xterm && this._xterm.getOption('cursorBlink') !== blink) {
D
Daniel Imms 已提交
656
			this._xterm.setOption('cursorBlink', blink);
D
Daniel Imms 已提交
657
			this._xterm.refresh(0, this._xterm.rows - 1);
D
Daniel Imms 已提交
658 659 660
		}
	}

661 662 663 664 665 666 667 668
	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);
		}
	}

669
	private _setCommandsToSkipShell(commands: string[]): void {
D
Daniel Imms 已提交
670
		this._skipTerminalCommands = commands;
D
Daniel Imms 已提交
671 672
	}

673
	private _setScrollback(lineCount: number): void {
D
Daniel Imms 已提交
674 675 676 677 678
		if (this._xterm && this._xterm.getOption('scrollback') !== lineCount) {
			this._xterm.setOption('scrollback', lineCount);
		}
	}

679 680 681
	public layout(dimension: Dimension): void {
		const needsLayout = this._evaluateColsAndRows(dimension.width, dimension.height);
		if (!needsLayout) {
D
Daniel Imms 已提交
682 683
			return;
		}
D
Daniel Imms 已提交
684
		if (this._xterm) {
685
			this._xterm.resize(this._cols, this._rows);
D
Daniel Imms 已提交
686
			this._xterm.element.style.width = innerWidth + 'px';
D
Daniel Imms 已提交
687
		}
D
Daniel Imms 已提交
688 689
		if (this._process.connected) {
			this._process.send({
D
Daniel Imms 已提交
690
				event: 'resize',
691 692
				cols: this._cols,
				rows: this._rows
D
Daniel Imms 已提交
693 694 695
			});
		}
	}
696 697 698 699

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