terminalInstance.ts 29.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.
 *--------------------------------------------------------------------------------------------*/

D
Daniel Imms 已提交
6 7
import * as cp from 'child_process';
import * as os from 'os';
8
import * as path from 'path';
D
Daniel Imms 已提交
9 10 11 12
import * as lifecycle from 'vs/base/common/lifecycle';
import * as nls from 'vs/nls';
import * as platform from 'vs/base/common/platform';
import * as dom from 'vs/base/browser/dom';
J
Johannes Rieken 已提交
13
import Event, { Emitter } from 'vs/base/common/event';
D
Daniel Imms 已提交
14
import Uri from 'vs/base/common/uri';
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 { ILegacyWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
25
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
26
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
27 28
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { TabFocus } from 'vs/editor/common/config/commonEditorConfig';
29
import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper';
30
import { TerminalLinkHandler } from 'vs/workbench/parts/terminal/electron-browser/terminalLinkHandler';
31
import { TerminalWidgetManager } from 'vs/workbench/parts/terminal/browser/terminalWidgetManager';
32 33
import { registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
import { scrollbarSliderBackground, scrollbarSliderHoverBackground, scrollbarSliderActiveBackground } from 'vs/platform/theme/common/colorRegistry';
D
Daniel Imms 已提交
34 35
import { TPromise } from 'vs/base/common/winjs.base';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
36

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

40 41 42 43
class StandardTerminalProcessFactory implements ITerminalProcessFactory {
	public create(env: { [key: string]: string }): cp.ChildProcess {
		return cp.fork('./terminalProcess', [], {
			env,
D
Daniel Imms 已提交
44
			cwd: Uri.parse(path.dirname(require.toUrl('./terminalProcess'))).fsPath
45 46 47 48
		});
	}
}

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

52
	private static _terminalProcessFactory: ITerminalProcessFactory = new StandardTerminalProcessFactory();
53
	private static _lastKnownDimensions: Dimension = null;
D
Daniel Imms 已提交
54 55
	private static _idCounter = 1;

56
	private _id: number;
D
Daniel Imms 已提交
57
	private _isExiting: boolean;
K
Kai Wood 已提交
58
	private _hadFocusOnExit: boolean;
59
	private _isLaunching: boolean;
D
Daniel Imms 已提交
60
	private _isVisible: boolean;
61
	private _processReady: TPromise<void>;
62
	private _isDisposed: boolean;
63
	private _onDisposed: Emitter<ITerminalInstance>;
D
Daniel Imms 已提交
64
	private _onDataForApi: Emitter<{ instance: ITerminalInstance, data: string }>;
65
	private _onProcessIdReady: Emitter<TerminalInstance>;
D
Daniel Imms 已提交
66
	private _onTitleChanged: Emitter<string>;
D
Daniel Imms 已提交
67
	private _process: cp.ChildProcess;
68
	private _processId: number;
D
Daniel Imms 已提交
69
	private _skipTerminalCommands: string[];
D
Daniel Imms 已提交
70
	private _title: string;
71 72
	private _instanceDisposables: lifecycle.IDisposable[];
	private _processDisposables: lifecycle.IDisposable[];
D
Daniel Imms 已提交
73 74 75
	private _wrapperElement: HTMLDivElement;
	private _xterm: any;
	private _xtermElement: HTMLDivElement;
76
	private _terminalHasTextContextKey: IContextKey<boolean>;
77 78
	private _cols: number;
	private _rows: number;
D
Daniel Imms 已提交
79
	private _messageTitleListener: (message: { type: string, content: string }) => void;
D
Daniel Imms 已提交
80

81 82 83
	private _widgetManager: TerminalWidgetManager;
	private _linkHandler: TerminalLinkHandler;

84
	public get id(): number { return this._id; }
85
	public get processId(): number { return this._processId; }
86
	public get onDisposed(): Event<ITerminalInstance> { return this._onDisposed.event; }
D
Daniel Imms 已提交
87
	public get onDataForApi(): Event<{ instance: ITerminalInstance, data: string }> { return this._onDataForApi.event; }
88
	public get onProcessIdReady(): Event<TerminalInstance> { return this._onProcessIdReady.event; }
D
Daniel Imms 已提交
89
	public get onTitleChanged(): Event<string> { return this._onTitleChanged.event; }
D
Daniel Imms 已提交
90
	public get title(): string { return this._title; }
K
Kai Wood 已提交
91
	public get hadFocusOnExit(): boolean { return this._hadFocusOnExit; }
92 93

	public constructor(
D
Daniel Imms 已提交
94 95 96
		private _terminalFocusContextKey: IContextKey<boolean>,
		private _configHelper: TerminalConfigHelper,
		private _container: HTMLElement,
97
		private _shellLaunchConfig: IShellLaunchConfig,
98
		@IContextKeyService private _contextKeyService: IContextKeyService,
D
Daniel Imms 已提交
99
		@IKeybindingService private _keybindingService: IKeybindingService,
100
		@IMessageService private _messageService: IMessageService,
101
		@IPanelService private _panelService: IPanelService,
102
		@IWorkspaceContextService private _contextService: IWorkspaceContextService,
103
		@IWorkbenchEditorService private _editorService: IWorkbenchEditorService,
D
Daniel Imms 已提交
104 105
		@IInstantiationService private _instantiationService: IInstantiationService,
		@IClipboardService private _clipboardService: IClipboardService
106
	) {
107 108
		this._instanceDisposables = [];
		this._processDisposables = [];
D
Daniel Imms 已提交
109
		this._skipTerminalCommands = [];
D
Daniel Imms 已提交
110
		this._isExiting = false;
K
Kai Wood 已提交
111
		this._hadFocusOnExit = false;
112
		this._isLaunching = true;
D
Daniel Imms 已提交
113
		this._isVisible = false;
114
		this._isDisposed = false;
D
Daniel Imms 已提交
115
		this._id = TerminalInstance._idCounter++;
116
		this._terminalHasTextContextKey = KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED.bindTo(this._contextKeyService);
D
Daniel Imms 已提交
117

118
		this._onDisposed = new Emitter<TerminalInstance>();
D
Daniel Imms 已提交
119
		this._onDataForApi = new Emitter<{ instance: ITerminalInstance, data: string }>();
120 121
		this._onProcessIdReady = new Emitter<TerminalInstance>();
		this._onTitleChanged = new Emitter<string>();
122

123 124 125 126 127
		// Create a promise that resolves when the pty is ready
		this._processReady = new TPromise<void>(c => {
			this.onProcessIdReady(() => c(void 0));
		});

128
		this._initDimensions();
D
Daniel Imms 已提交
129
		this._createProcess(this._contextService.getWorkspace(), this._shellLaunchConfig);
130
		this._createXterm();
D
Daniel Imms 已提交
131

132
		// Only attach xterm.js to the DOM if the terminal panel has been opened before.
D
Daniel Imms 已提交
133 134
		if (_container) {
			this.attachToElement(_container);
135 136 137
		}
	}

D
Daniel Imms 已提交
138
	public addDisposable(disposable: lifecycle.IDisposable): void {
139
		this._instanceDisposables.push(disposable);
D
Daniel Imms 已提交
140 141
	}

142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
	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.
D
Daniel Imms 已提交
158
	 * @return The terminal's width if it requires a layout.
159
	 */
D
Daniel Imms 已提交
160
	private _evaluateColsAndRows(width: number, height: number): number {
161 162 163 164 165 166 167 168 169 170 171
		const dimension = this._getDimension(width, height);
		if (!dimension) {
			return null;
		}
		const font = this._configHelper.getFont();
		this._cols = Math.floor(dimension.width / font.charWidth);
		this._rows = Math.floor(dimension.height / font.charHeight);
		return dimension.width;
	}

	private _getDimension(width: number, height: number): Dimension {
172 173 174
		// The font needs to have been initialized
		const font = this._configHelper.getFont();
		if (!font || !font.charWidth || !font.charHeight) {
D
Daniel Imms 已提交
175
			return null;
176 177 178 179
		}

		// The panel is minimized
		if (!height) {
180
			return TerminalInstance._lastKnownDimensions;
181 182 183 184 185 186 187 188 189 190 191 192 193 194
		} 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;
195 196
		TerminalInstance._lastKnownDimensions = new Dimension(innerWidth, height);
		return TerminalInstance._lastKnownDimensions;
197 198
	}

199 200 201 202
	/**
	 * Create xterm.js instance and attach data listeners.
	 */
	protected _createXterm(): void {
203
		this._xterm = xterm({
204
			scrollback: this._configHelper.config.scrollback
205
		});
206 207 208
		if (this._shellLaunchConfig.initialText) {
			this._xterm.writeln(this._shellLaunchConfig.initialText);
		}
209
		this._process.on('message', (message) => this._sendPtyDataToXterm(message));
D
Daniel Imms 已提交
210
		this._xterm.on('data', (data) => {
211 212 213 214 215 216
			if (this._process) {
				this._process.send({
					event: 'input',
					data: this._sanitizeInput(data)
				});
			}
D
Daniel Imms 已提交
217 218
			return false;
		});
219 220
		this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, this._xterm, platform.platform);
		this._linkHandler.registerLocalLinkHandler();
221 222 223 224 225 226 227 228 229
	}

	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');
D
Daniel Imms 已提交
230
		dom.addClass(this._wrapperElement, 'terminal-wrapper');
231 232
		this._xtermElement = document.createElement('div');

233
		this._xterm.open(this._xtermElement, false);
D
Daniel Imms 已提交
234
		this._xterm.attachCustomKeydownHandler((event: KeyboardEvent) => {
235 236 237 238 239
			// Disable all input if the terminal is exiting
			if (this._isExiting) {
				return false;
			}

240 241
			// Skip processing by xterm.js of keyboard events that resolve to commands described
			// within commandsToSkipShell
D
Daniel Imms 已提交
242
			const standardKeyboardEvent = new StandardKeyboardEvent(event);
243
			const resolveResult = this._keybindingService.softDispatch(standardKeyboardEvent, standardKeyboardEvent.target);
D
Daniel Imms 已提交
244
			if (resolveResult && this._skipTerminalCommands.some(k => k === resolveResult.commandId)) {
D
Daniel Imms 已提交
245 246 247 248 249 250 251 252
				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;
			}
253
			return undefined;
D
Daniel Imms 已提交
254
		});
D
Daniel Imms 已提交
255
		this._instanceDisposables.push(dom.addDisposableListener(this._xterm.element, 'mouseup', (event: KeyboardEvent) => {
256 257 258
			// Wait until mouseup has propogated through the DOM before evaluating the new selection
			// state.
			setTimeout(() => {
259 260
				this._refreshSelectionContextKey();
			}, 0);
261
		}));
262 263

		// xterm.js currently drops selection on keyup as we need to handle this case.
D
Daniel Imms 已提交
264
		this._instanceDisposables.push(dom.addDisposableListener(this._xterm.element, 'keyup', (event: KeyboardEvent) => {
265 266 267 268
			// Wait until keyup has propogated through the DOM before evaluating the new selection
			// state.
			setTimeout(() => {
				this._refreshSelectionContextKey();
269
			}, 0);
270
		}));
D
Daniel Imms 已提交
271

D
Daniel Imms 已提交
272 273
		const xtermHelper: HTMLElement = this._xterm.element.querySelector('.xterm-helpers');
		const focusTrap: HTMLElement = document.createElement('div');
274
		focusTrap.setAttribute('tabindex', '0');
D
Daniel Imms 已提交
275 276
		dom.addClass(focusTrap, 'focus-trap');
		this._instanceDisposables.push(dom.addDisposableListener(focusTrap, 'focus', (event: FocusEvent) => {
277
			let currentElement = focusTrap;
D
Daniel Imms 已提交
278
			while (!dom.hasClass(currentElement, 'part')) {
279 280
				currentElement = currentElement.parentElement;
			}
D
Daniel Imms 已提交
281
			const hidePanelElement = <HTMLElement>currentElement.querySelector('.hide-panel-action');
282
			hidePanelElement.focus();
283
		}));
D
Daniel Imms 已提交
284
		xtermHelper.insertBefore(focusTrap, this._xterm.textarea);
285

D
Daniel Imms 已提交
286
		this._instanceDisposables.push(dom.addDisposableListener(this._xterm.textarea, 'focus', (event: KeyboardEvent) => {
D
Daniel Imms 已提交
287
			this._terminalFocusContextKey.set(true);
288
		}));
D
Daniel Imms 已提交
289
		this._instanceDisposables.push(dom.addDisposableListener(this._xterm.textarea, 'blur', (event: KeyboardEvent) => {
D
Daniel Imms 已提交
290
			this._terminalFocusContextKey.reset();
291
			this._refreshSelectionContextKey();
292
		}));
D
Daniel Imms 已提交
293
		this._instanceDisposables.push(dom.addDisposableListener(this._xterm.element, 'focus', (event: KeyboardEvent) => {
D
Daniel Imms 已提交
294
			this._terminalFocusContextKey.set(true);
295
		}));
D
Daniel Imms 已提交
296
		this._instanceDisposables.push(dom.addDisposableListener(this._xterm.element, 'blur', (event: KeyboardEvent) => {
D
Daniel Imms 已提交
297
			this._terminalFocusContextKey.reset();
298
			this._refreshSelectionContextKey();
299 300
		}));

D
Daniel Imms 已提交
301
		this._wrapperElement.appendChild(this._xtermElement);
302
		this._widgetManager = new TerminalWidgetManager(this._configHelper, this._wrapperElement);
303
		this._linkHandler.setWidgetManager(this._widgetManager);
D
Daniel Imms 已提交
304
		this._container.appendChild(this._wrapperElement);
305

306 307 308 309
		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 已提交
310
		this.setVisible(this._isVisible);
311
		this.updateConfig();
312 313 314 315 316 317

		// If IShellLaunchConfig.waitOnExit was true and the process finished before the terminal
		// panel was initialized.
		if (this._xterm.getOption('disableStdin')) {
			this._attachPressAnyKeyToCloseListener();
		}
318 319
	}

D
Daniel Imms 已提交
320
	public registerLinkMatcher(regex: RegExp, handler: (url: string) => void, matchIndex?: number, validationCallback?: (uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => void): number {
321
		return this._linkHandler.registerCustomLinkHandler(regex, handler, matchIndex, validationCallback);
322 323 324 325 326 327
	}

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

328
	public hasSelection(): boolean {
D
Daniel Imms 已提交
329
		return this._xterm.hasSelection();
330 331
	}

D
Daniel Imms 已提交
332
	public copySelection(): void {
D
Daniel Imms 已提交
333 334
		if (this.hasSelection()) {
			this._clipboardService.writeText(this._xterm.getSelection());
D
Daniel Imms 已提交
335
		} else {
D
Daniel Imms 已提交
336
			this._messageService.show(Severity.Warning, nls.localize('terminal.integrated.copySelection.noSelection', 'The terminal has no selection to copy'));
D
Daniel Imms 已提交
337
		}
D
Daniel Imms 已提交
338 339
	}

340
	public clearSelection(): void {
341 342 343 344
		this._xterm.clearSelection();
	}

	public selectAll(): void {
345 346
		// Focus here to ensure the terminal context key is set
		this._xterm.focus();
347
		this._xterm.selectAll();
348 349
	}

R
rebornix 已提交
350 351 352 353 354 355 356 357
	public findNext(term: string): boolean {
		return this._xterm.findNext(term);
	}

	public findPrevious(term: string): boolean {
		return this._xterm.findPrevious(term);
	}

D
Daniel Imms 已提交
358
	public dispose(): void {
359 360 361
		if (this._linkHandler) {
			this._linkHandler.dispose();
		}
K
Kai Wood 已提交
362
		if (this._xterm && this._xterm.element) {
D
Daniel Imms 已提交
363
			this._hadFocusOnExit = dom.hasClass(this._xterm.element, 'focus');
K
Kai Wood 已提交
364
		}
D
Daniel Imms 已提交
365 366 367
		if (this._wrapperElement) {
			this._container.removeChild(this._wrapperElement);
			this._wrapperElement = null;
D
Daniel Imms 已提交
368
		}
D
Daniel Imms 已提交
369 370 371
		if (this._xterm) {
			this._xterm.destroy();
			this._xterm = null;
D
Daniel Imms 已提交
372
		}
D
Daniel Imms 已提交
373 374 375
		if (this._process) {
			if (this._process.connected) {
				this._process.kill();
D
Daniel Imms 已提交
376
			}
D
Daniel Imms 已提交
377
			this._process = null;
D
Daniel Imms 已提交
378
		}
379 380 381 382
		if (!this._isDisposed) {
			this._isDisposed = true;
			this._onDisposed.fire(this);
		}
383 384
		this._processDisposables = lifecycle.dispose(this._processDisposables);
		this._instanceDisposables = lifecycle.dispose(this._instanceDisposables);
D
Daniel Imms 已提交
385 386
	}

D
Daniel Imms 已提交
387
	public focus(force?: boolean): void {
D
Daniel Imms 已提交
388
		if (!this._xterm) {
D
Daniel Imms 已提交
389 390
			return;
		}
D
Daniel Imms 已提交
391
		const text = window.getSelection().toString();
D
Daniel Imms 已提交
392
		if (!text || force) {
D
Daniel Imms 已提交
393
			this._xterm.focus();
D
Daniel Imms 已提交
394
		}
D
Daniel Imms 已提交
395 396 397
	}

	public paste(): void {
D
Daniel Imms 已提交
398 399
		this.focus();
		document.execCommand('paste');
D
Daniel Imms 已提交
400 401 402
	}

	public sendText(text: string, addNewLine: boolean): void {
403 404 405 406 407 408 409 410 411
		this._processReady.then(() => {
			text = this._sanitizeInput(text);
			if (addNewLine && text.substr(text.length - 1) !== '\r') {
				text += '\r';
			}
			this._process.send({
				event: 'input',
				data: text
			});
D
Daniel Imms 已提交
412
		});
D
Daniel Imms 已提交
413
	}
414 415

	public setVisible(visible: boolean): void {
D
Daniel Imms 已提交
416 417
		this._isVisible = visible;
		if (this._wrapperElement) {
D
Daniel Imms 已提交
418
			dom.toggleClass(this._wrapperElement, 'active', visible);
419
		}
D
Daniel Imms 已提交
420
		if (visible && this._xterm) {
421 422 423 424 425 426
			// 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);
		}
427 428
	}

429
	public scrollDownLine(): void {
D
Daniel Imms 已提交
430
		this._xterm.scrollDisp(1);
D
Daniel Imms 已提交
431 432
	}

433
	public scrollDownPage(): void {
434
		this._xterm.scrollPages(1);
435 436
	}

437 438 439 440
	public scrollToBottom(): void {
		this._xterm.scrollToBottom();
	}

441
	public scrollUpLine(): void {
D
Daniel Imms 已提交
442
		this._xterm.scrollDisp(-1);
D
Daniel Imms 已提交
443
	}
444

445
	public scrollUpPage(): void {
446
		this._xterm.scrollPages(-1);
447 448
	}

449 450 451 452
	public scrollToTop(): void {
		this._xterm.scrollToTop();
	}

D
Daniel Imms 已提交
453 454 455 456
	public clear(): void {
		this._xterm.clear();
	}

457
	private _refreshSelectionContextKey() {
458
		const activePanel = this._panelService.getActivePanel();
D
Daniel Imms 已提交
459 460
		const isActive = activePanel && activePanel.getId() === TERMINAL_PANEL_ID;
		this._terminalHasTextContextKey.set(isActive && this.hasSelection());
461 462
	}

D
Daniel Imms 已提交
463
	private _sanitizeInput(data: any) {
464
		return typeof data === 'string' ? data.replace(TerminalInstance.WINDOWS_EOL_REGEX, '\r') : data;
C
Christof Marti 已提交
465 466
	}

467
	protected _getCwd(shell: IShellLaunchConfig, workspace: ILegacyWorkspace): string {
468 469 470 471
		if (shell.cwd) {
			return shell.cwd;
		}

B
Benjamin Pasero 已提交
472
		let cwd: string;
D
Daniel Imms 已提交
473 474

		// TODO: Handle non-existent customCwd
475
		if (!shell.ignoreConfigurationCwd) {
476
			// Evaluate custom cwd first
477
			const customCwd = this._configHelper.config.cwd;
478 479 480 481 482 483
			if (customCwd) {
				if (path.isAbsolute(customCwd)) {
					cwd = customCwd;
				} else if (workspace) {
					cwd = path.normalize(path.join(workspace.resource.fsPath, customCwd));
				}
D
Daniel Imms 已提交
484 485 486 487 488
			}
		}

		// If there was no custom cwd or it was relative with no workspace
		if (!cwd) {
489
			cwd = workspace ? workspace.resource.fsPath : os.homedir();
D
Daniel Imms 已提交
490 491 492 493 494
		}

		return TerminalInstance._sanitizeCwd(cwd);
	}

495
	protected _createProcess(workspace: ILegacyWorkspace, shell: IShellLaunchConfig): void {
496
		const locale = this._configHelper.config.setLocaleVariables ? platform.locale : undefined;
D
Daniel Imms 已提交
497
		if (!shell.executable) {
498
			this._configHelper.mergeDefaultShellPathAndArgs(shell);
P
Pine Wu 已提交
499
		}
500
		const env = TerminalInstance.createTerminalEnv(process.env, shell, this._getCwd(shell, workspace), locale, this._cols, this._rows);
D
Daniel Imms 已提交
501
		this._title = shell.name || '';
D
Daniel Imms 已提交
502
		this._process = cp.fork(Uri.parse(require.toUrl('bootstrap')).fsPath, ['--type=terminal'], {
503
			env,
D
Daniel Imms 已提交
504
			cwd: Uri.parse(path.dirname(require.toUrl('../node/terminalProcess'))).fsPath
505
		});
D
Daniel Imms 已提交
506
		if (!shell.name) {
507
			// Only listen for process title changes when a name is not provided
D
Daniel Imms 已提交
508
			this._messageTitleListener = (message) => {
509
				if (message.type === 'title') {
D
Daniel Imms 已提交
510
					this._title = message.content ? message.content : '';
511
					this._onTitleChanged.fire(this._title);
512
				}
513
			};
D
Daniel Imms 已提交
514
			this._process.on('message', this._messageTitleListener);
515
		}
516 517 518 519 520 521
		this._process.on('message', (message) => {
			if (message.type === 'pid') {
				this._processId = message.content;
				this._onProcessIdReady.fire(this);
			}
		});
522 523 524 525 526 527
		this._process.on('exit', exitCode => this._onPtyProcessExit(exitCode));
		setTimeout(() => {
			this._isLaunching = false;
		}, LAUNCHING_DURATION);
	}

528 529
	private _sendPtyDataToXterm(message: { type: string, content: string }): void {
		if (message.type === 'data') {
D
Daniel Imms 已提交
530 531 532
			if (this._widgetManager) {
				this._widgetManager.closeMessage();
			}
D
Daniel Imms 已提交
533 534 535
			if (this._xterm) {
				this._xterm.write(message.content);
			}
536 537 538
		}
	}

539 540 541 542 543 544 545
	private _onPtyProcessExit(exitCode: number): void {
		// Prevent dispose functions being triggered multiple times
		if (this._isExiting) {
			return;
		}

		this._isExiting = true;
546
		this._process = null;
547
		let exitCodeMessage: string;
548
		if (exitCode) {
549 550 551
			exitCodeMessage = nls.localize('terminal.integrated.exitedWithCode', 'The terminal process terminated with exit code: {0}', exitCode);
		}

552 553 554 555 556
		// 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) {
557
			if (exitCode) {
558 559
				this._xterm.writeln(exitCodeMessage);
			}
560 561 562
			let message = typeof this._shellLaunchConfig.waitOnExit === 'string'
				? this._shellLaunchConfig.waitOnExit
				: nls.localize('terminal.integrated.waitOnExit', 'Press any key to close the terminal');
D
Daniel Imms 已提交
563 564
			// Bold the message and add an extra new line to make it stand out from the rest of the output
			message = `\n\x1b[1m${message}\x1b[0m`;
565
			this._xterm.writeln(message);
566 567
			// Disable all input if the terminal is exiting and listen for next keypress
			this._xterm.setOption('disableStdin', true);
568
			if (this._xterm.textarea) {
569
				this._attachPressAnyKeyToCloseListener();
570
			}
571 572
		} else {
			this.dispose();
573
			if (exitCode) {
574 575
				if (this._isLaunching) {
					let args = '';
576 577 578
					if (typeof this._shellLaunchConfig.args === 'string') {
						args = this._shellLaunchConfig.args;
					} else if (this._shellLaunchConfig.args && this._shellLaunchConfig.args.length) {
579 580 581 582 583 584
						args = ' ' + this._shellLaunchConfig.args.map(a => {
							if (a.indexOf(' ') !== -1) {
								return `'${a}'`;
							}
							return a;
						}).join(' ');
585
					}
586 587 588
					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);
589 590
				}
			}
591
		}
592 593
	}

594
	private _attachPressAnyKeyToCloseListener() {
D
Daniel Imms 已提交
595
		this._processDisposables.push(dom.addDisposableListener(this._xterm.textarea, 'keypress', (event: KeyboardEvent) => {
596 597 598 599 600
			this.dispose();
			event.preventDefault();
		}));
	}

601 602
	public reuseTerminal(shell?: IShellLaunchConfig): void {
		// Kill and clean up old process
603 604 605 606 607 608 609
		if (this._process) {
			this._process.removeAllListeners('exit');
			if (this._process.connected) {
				this._process.kill();
			}
			this._process = null;
		}
610 611 612
		lifecycle.dispose(this._processDisposables);
		this._processDisposables = [];

613 614
		// Ensure new processes' output starts at start of new line
		this._xterm.write('\n\x1b[G');
615

616 617 618 619 620
		// Print initialText if specified
		if (shell.initialText) {
			this._xterm.writeln(shell.initialText);
		}

621
		// Initialize new process
622
		const oldTitle = this._title;
D
Daniel Imms 已提交
623
		this._createProcess(this._contextService.getWorkspace(), shell);
624 625 626
		if (oldTitle !== this._title) {
			this._onTitleChanged.fire(this._title);
		}
627
		this._process.on('message', (message) => this._sendPtyDataToXterm(message));
628 629

		// Clean up waitOnExit state
630 631
		if (this._isExiting && this._shellLaunchConfig.waitOnExit) {
			this._xterm.setOption('disableStdin', false);
632
			this._isExiting = false;
633
		}
634

635 636 637 638
		// Set the new shell launch config
		this._shellLaunchConfig = shell;
	}

639 640
	// TODO: This should be private/protected
	// TODO: locale should not be optional
641
	public static createTerminalEnv(parentEnv: IStringDictionary<string>, shell: IShellLaunchConfig, cwd: string, locale?: string, cols?: number, rows?: number): IStringDictionary<string> {
642
		const env = shell.env ? shell.env : TerminalInstance._cloneEnv(parentEnv);
643 644
		env['PTYPID'] = process.pid.toString();
		env['PTYSHELL'] = shell.executable;
D
Daniel Imms 已提交
645
		if (shell.args) {
646 647 648 649 650
			if (typeof shell.args === 'string') {
				env[`PTYSHELLCMDLINE`] = shell.args;
			} else {
				shell.args.forEach((arg, i) => env[`PTYSHELLARG${i}`] = arg);
			}
D
Daniel Imms 已提交
651
		}
D
Daniel Imms 已提交
652
		env['PTYCWD'] = cwd;
653
		env['LANG'] = TerminalInstance._getLangEnvVariable(locale);
654 655 656 657
		if (cols && rows) {
			env['PTYCOLS'] = cols.toString();
			env['PTYROWS'] = rows.toString();
		}
658
		env['AMD_ENTRYPOINT'] = 'vs/workbench/parts/terminal/node/terminalProcess';
659
		return env;
660 661
	}

D
Dirk Baeumer 已提交
662 663
	public onData(listener: (data: string) => void): lifecycle.IDisposable {
		let callback = (message) => {
664 665 666
			if (message.type === 'data') {
				listener(message.content);
			}
D
Dirk Baeumer 已提交
667 668 669 670
		};
		this._process.on('message', callback);
		return {
			dispose: () => {
671 672 673
				if (this._process) {
					this._process.removeListener('message', callback);
				}
D
Dirk Baeumer 已提交
674 675
			}
		};
676 677
	}

D
Dirk Baeumer 已提交
678
	public onExit(listener: (exitCode: number) => void): lifecycle.IDisposable {
679
		this._process.on('exit', listener);
D
Dirk Baeumer 已提交
680 681
		return {
			dispose: () => {
682 683 684
				if (this._process) {
					this._process.removeListener('exit', listener);
				}
D
Dirk Baeumer 已提交
685 686
			}
		};
687 688
	}

D
Daniel Imms 已提交
689
	private static _sanitizeCwd(cwd: string) {
690 691 692
		// 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 已提交
693
		}
694
		return cwd;
D
Daniel Imms 已提交
695 696
	}

D
Daniel Imms 已提交
697
	private static _cloneEnv(env: IStringDictionary<string>): IStringDictionary<string> {
D
Daniel Imms 已提交
698
		const newEnv: IStringDictionary<string> = Object.create(null);
699 700
		Object.keys(env).forEach((key) => {
			newEnv[key] = env[key];
701
		});
702
		return newEnv;
703 704
	}

705 706
	private static _getLangEnvVariable(locale?: string) {
		const parts = locale ? locale.split('-') : [];
707
		const n = parts.length;
708
		if (n === 0) {
D
Daniel Imms 已提交
709 710
			// Fallback to en_US to prevent possible encoding issues.
			return 'en_US.UTF-8';
711 712 713 714 715 716 717 718 719 720 721 722 723
		}
		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',
724
				zh: 'CN'
725
			};
D
Daniel Imms 已提交
726 727
			if (parts[0] in languageVariants) {
				parts.push(languageVariants[parts[0]]);
728 729 730 731
			}
		} else {
			// Ensure the variant is uppercase
			parts[1] = parts[1].toUpperCase();
D
Daniel Imms 已提交
732
		}
733
		return parts.join('_') + '.UTF-8';
D
Daniel Imms 已提交
734
	}
D
Daniel Imms 已提交
735

736
	public updateConfig(): void {
737 738 739 740
		this._setCursorBlink(this._configHelper.config.cursorBlinking);
		this._setCursorStyle(this._configHelper.config.cursorStyle);
		this._setCommandsToSkipShell(this._configHelper.config.commandsToSkipShell);
		this._setScrollback(this._configHelper.config.scrollback);
741 742 743
	}

	private _setCursorBlink(blink: boolean): void {
D
Daniel Imms 已提交
744
		if (this._xterm && this._xterm.getOption('cursorBlink') !== blink) {
D
Daniel Imms 已提交
745
			this._xterm.setOption('cursorBlink', blink);
D
Daniel Imms 已提交
746
			this._xterm.refresh(0, this._xterm.rows - 1);
D
Daniel Imms 已提交
747 748 749
		}
	}

750 751 752 753 754 755 756 757
	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);
		}
	}

758
	private _setCommandsToSkipShell(commands: string[]): void {
D
Daniel Imms 已提交
759
		this._skipTerminalCommands = commands;
D
Daniel Imms 已提交
760 761
	}

762
	private _setScrollback(lineCount: number): void {
D
Daniel Imms 已提交
763 764 765 766 767
		if (this._xterm && this._xterm.getOption('scrollback') !== lineCount) {
			this._xterm.setOption('scrollback', lineCount);
		}
	}

768
	public layout(dimension: Dimension): void {
D
Daniel Imms 已提交
769 770
		const terminalWidth = this._evaluateColsAndRows(dimension.width, dimension.height);
		if (!terminalWidth) {
D
Daniel Imms 已提交
771 772
			return;
		}
D
Daniel Imms 已提交
773
		if (this._xterm) {
774
			this._xterm.resize(this._cols, this._rows);
D
Daniel Imms 已提交
775
			this._xterm.element.style.width = terminalWidth + 'px';
D
Daniel Imms 已提交
776
		}
777 778
		this._processReady.then(() => {
			if (this._process) {
779 780 781 782 783 784 785 786 787 788 789 790 791
				// The child process could aready be terminated
				try {
					this._process.send({
						event: 'resize',
						cols: this._cols,
						rows: this._rows
					});
				} catch (error) {
					// We tried to write to a closed pipe / channel.
					if (error.code !== 'EPIPE' && error.code !== 'ERR_IPC_CHANNEL_CLOSED') {
						throw (error);
					}
				}
792 793
			}
		});
D
Daniel Imms 已提交
794
	}
795

D
Daniel Imms 已提交
796 797 798 799 800
	public enableApiOnData(): void {
		// Only send data through IPC if the API explicitly requests it.
		this.onData(data => this._onDataForApi.fire({ instance: this, data }));
	}

801 802 803
	public static setTerminalProcessFactory(factory: ITerminalProcessFactory): void {
		this._terminalProcessFactory = factory;
	}
B
Ben Stein 已提交
804 805

	public setTitle(title: string): void {
D
Daniel Imms 已提交
806
		const didTitleChange = title !== this._title;
D
Daniel Imms 已提交
807
		this._title = title;
D
Daniel Imms 已提交
808
		if (didTitleChange) {
B
Ben Stein 已提交
809 810 811
			this._onTitleChanged.fire(title);
		}

D
Daniel Imms 已提交
812 813 814 815 816
		// If the title was not set by the API, unregister the handler that
		// automatically updates the terminal name
		if (this._process && this._messageTitleListener) {
			this._process.removeListener('message', this._messageTitleListener);
			this._messageTitleListener = null;
B
Ben Stein 已提交
817 818
		}
	}
819
}
820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836

registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {

	// Scrollbar
	const scrollbarSliderBackgroundColor = theme.getColor(scrollbarSliderBackground);
	if (scrollbarSliderBackgroundColor) {
		collector.addRule(`
			.monaco-workbench .panel.integrated-terminal .xterm.focus .xterm-viewport,
			.monaco-workbench .panel.integrated-terminal .xterm:focus .xterm-viewport,
			.monaco-workbench .panel.integrated-terminal .xterm:hover .xterm-viewport { background-color: ${scrollbarSliderBackgroundColor}; }`
		);
	}

	const scrollbarSliderHoverBackgroundColor = theme.getColor(scrollbarSliderHoverBackground);
	if (scrollbarSliderHoverBackgroundColor) {
		collector.addRule(`.monaco-workbench .panel.integrated-terminal .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover { background-color: ${scrollbarSliderHoverBackgroundColor}; }`);
	}
837 838 839 840 841

	const scrollbarSliderActiveBackgroundColor = theme.getColor(scrollbarSliderActiveBackground);
	if (scrollbarSliderActiveBackgroundColor) {
		collector.addRule(`.monaco-workbench .panel.integrated-terminal .xterm .xterm-viewport::-webkit-scrollbar-thumb:active { background-color: ${scrollbarSliderActiveBackgroundColor}; }`);
	}
842
});