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

import DOM = require('vs/base/browser/dom');
J
Johannes Rieken 已提交
7
import Event, { Emitter } from 'vs/base/common/event';
8 9
import URI from 'vs/base/common/uri';
import cp = require('child_process');
10
import lifecycle = require('vs/base/common/lifecycle');
C
Christof Marti 已提交
11
import nls = require('vs/nls');
C
Christof Marti 已提交
12
import os = require('os');
13 14
import path = require('path');
import platform = require('vs/base/common/platform');
15
import xterm = require('xterm');
16 17 18 19 20 21 22
import { Dimension } from 'vs/base/browser/builder';
import { IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IMessageService, Severity } from 'vs/platform/message/common/message';
import { IStringDictionary } from 'vs/base/common/collections';
import { ITerminalInstance } from 'vs/workbench/parts/terminal/electron-browser/terminal';
import { IWorkspace } from 'vs/platform/workspace/common/workspace';
A
Alexandru Dima 已提交
23
import { Keybinding } from 'vs/base/common/keybinding';
24 25 26
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { TabFocus } from 'vs/editor/common/config/commonEditorConfig';
import { TerminalConfigHelper, IShell } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper';
27

28 29
export class TerminalInstance implements ITerminalInstance {
	private static EOL_REGEX = /\r?\n/g;
C
Christof Marti 已提交
30

D
Daniel Imms 已提交
31 32
	private static _idCounter = 1;

33
	private _id: number;
D
Daniel Imms 已提交
34 35
	private _isExiting: boolean;
	private _isVisible: boolean;
36
	private _onDisposed: Emitter<TerminalInstance>;
37
	private _onProcessIdReady: Emitter<TerminalInstance>;
D
Daniel Imms 已提交
38
	private _onTitleChanged: Emitter<string>;
D
Daniel Imms 已提交
39
	private _process: cp.ChildProcess;
40
	private _processId: number;
D
Daniel Imms 已提交
41 42 43 44 45 46 47
	private _skipTerminalKeybindings: Keybinding[];
	private _title: string;
	private _toDispose: lifecycle.IDisposable[];
	private _wrapperElement: HTMLDivElement;
	private _xterm: any;
	private _xtermElement: HTMLDivElement;

48
	public get id(): number { return this._id; }
49
	public get processId(): number { return this._processId; }
50
	public get onClosed(): Event<TerminalInstance> { return this._onDisposed.event; }
51
	public get onProcessIdReady(): Event<TerminalInstance> { return this._onProcessIdReady.event; }
D
Daniel Imms 已提交
52
	public get onTitleChanged(): Event<string> { return this._onTitleChanged.event; }
D
Daniel Imms 已提交
53
	public get title(): string { return this._title; }
54 55

	public constructor(
D
Daniel Imms 已提交
56 57 58
		private _terminalFocusContextKey: IContextKey<boolean>,
		private _configHelper: TerminalConfigHelper,
		private _container: HTMLElement,
D
Daniel Imms 已提交
59
		workspace: IWorkspace,
60
		name: string,
P
Pine Wu 已提交
61
		shell: IShell,
D
Daniel Imms 已提交
62 63
		@IKeybindingService private _keybindingService: IKeybindingService,
		@IMessageService private _messageService: IMessageService
64
	) {
D
Daniel Imms 已提交
65 66 67 68 69 70
		this._toDispose = [];
		this._skipTerminalKeybindings = [];
		this._isExiting = false;
		this._isVisible = false;
		this._id = TerminalInstance._idCounter++;

71
		this._onDisposed = new Emitter<TerminalInstance>();
72 73
		this._onProcessIdReady = new Emitter<TerminalInstance>();
		this._onTitleChanged = new Emitter<string>();
74

D
Daniel Imms 已提交
75
		this._createProcess(workspace, name, shell);
D
Daniel Imms 已提交
76 77 78

		if (_container) {
			this.attachToElement(_container);
79 80 81
		}
	}

D
Daniel Imms 已提交
82
	public addDisposable(disposable: lifecycle.IDisposable): void {
D
Daniel Imms 已提交
83
		this._toDispose.push(disposable);
D
Daniel Imms 已提交
84 85
	}

86
	public attachToElement(container: HTMLElement): void {
D
Daniel Imms 已提交
87
		if (this._wrapperElement) {
88 89 90
			throw new Error('The terminal instance has already been attached to a container');
		}

D
Daniel Imms 已提交
91 92 93 94
		this._container = container;
		this._wrapperElement = document.createElement('div');
		DOM.addClass(this._wrapperElement, 'terminal-wrapper');
		this._xtermElement = document.createElement('div');
95

D
Daniel Imms 已提交
96 97
		this._xterm = xterm();
		this._xterm.open(this._xtermElement);
98

D
Daniel Imms 已提交
99
		this._process.on('message', (message) => {
D
Daniel Imms 已提交
100
			if (message.type === 'data') {
D
Daniel Imms 已提交
101
				this._xterm.write(message.content);
D
Daniel Imms 已提交
102 103
			}
		});
D
Daniel Imms 已提交
104 105
		this._xterm.on('data', (data) => {
			this._process.send({
D
Daniel Imms 已提交
106 107 108 109 110
				event: 'input',
				data: this.sanitizeInput(data)
			});
			return false;
		});
D
Daniel Imms 已提交
111
		this._xterm.attachCustomKeydownHandler((event: KeyboardEvent) => {
D
Daniel Imms 已提交
112 113 114
			// Allow the toggle tab mode keybinding to pass through the terminal so that focus can
			// be escaped
			let standardKeyboardEvent = new StandardKeyboardEvent(event);
D
Daniel Imms 已提交
115
			if (this._skipTerminalKeybindings.some((k) => standardKeyboardEvent.equals(k.value))) {
D
Daniel Imms 已提交
116 117 118 119 120 121 122 123 124 125
				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;
			}
		});

D
Daniel Imms 已提交
126
		let xtermHelper: HTMLElement = this._xterm.element.querySelector('.xterm-helpers');
127 128 129 130 131 132 133 134 135 136 137
		let focusTrap: HTMLElement = document.createElement('div');
		focusTrap.setAttribute('tabindex', '0');
		DOM.addClass(focusTrap, 'focus-trap');
		focusTrap.addEventListener('focus', function (event: FocusEvent) {
			let currentElement = focusTrap;
			while (!DOM.hasClass(currentElement, 'part')) {
				currentElement = currentElement.parentElement;
			}
			let hidePanelElement = <HTMLElement>currentElement.querySelector('.hide-panel-action');
			hidePanelElement.focus();
		});
D
Daniel Imms 已提交
138
		xtermHelper.insertBefore(focusTrap, this._xterm.textarea);
139

D
Daniel Imms 已提交
140 141
		this._toDispose.push(DOM.addDisposableListener(this._xterm.textarea, 'focus', (event: KeyboardEvent) => {
			this._terminalFocusContextKey.set(true);
142
		}));
D
Daniel Imms 已提交
143 144
		this._toDispose.push(DOM.addDisposableListener(this._xterm.textarea, 'blur', (event: KeyboardEvent) => {
			this._terminalFocusContextKey.reset();
145
		}));
D
Daniel Imms 已提交
146 147
		this._toDispose.push(DOM.addDisposableListener(this._xterm.element, 'focus', (event: KeyboardEvent) => {
			this._terminalFocusContextKey.set(true);
148
		}));
D
Daniel Imms 已提交
149 150
		this._toDispose.push(DOM.addDisposableListener(this._xterm.element, 'blur', (event: KeyboardEvent) => {
			this._terminalFocusContextKey.reset();
151 152
		}));

D
Daniel Imms 已提交
153 154
		this._wrapperElement.appendChild(this._xtermElement);
		this._container.appendChild(this._wrapperElement);
155

156 157 158 159
		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 已提交
160
		this.setVisible(this._isVisible);
161 162
	}

D
Daniel Imms 已提交
163
	public copySelection(): void {
D
Daniel Imms 已提交
164 165 166
		if (document.activeElement.classList.contains('xterm')) {
			document.execCommand('copy');
		} else {
D
Daniel Imms 已提交
167
			this._messageService.show(Severity.Warning, nls.localize('terminal.integrated.copySelection.noSelection', 'Cannot copy terminal selection when terminal does not have focus'));
D
Daniel Imms 已提交
168
		}
D
Daniel Imms 已提交
169 170 171
	}

	public dispose(): void {
172
		this._isExiting = true;
D
Daniel Imms 已提交
173 174 175
		if (this._wrapperElement) {
			this._container.removeChild(this._wrapperElement);
			this._wrapperElement = null;
D
Daniel Imms 已提交
176
		}
D
Daniel Imms 已提交
177 178 179
		if (this._xterm) {
			this._xterm.destroy();
			this._xterm = null;
D
Daniel Imms 已提交
180
		}
D
Daniel Imms 已提交
181 182 183
		if (this._process) {
			if (this._process.connected) {
				this._process.kill();
D
Daniel Imms 已提交
184
			}
D
Daniel Imms 已提交
185
			this._process = null;
D
Daniel Imms 已提交
186
		}
187
		this._onDisposed.fire(this);
D
Daniel Imms 已提交
188
		this._toDispose = lifecycle.dispose(this._toDispose);
D
Daniel Imms 已提交
189 190
	}

D
Daniel Imms 已提交
191
	public focus(force?: boolean): void {
D
Daniel Imms 已提交
192
		if (!this._xterm) {
D
Daniel Imms 已提交
193 194 195 196
			return;
		}
		let text = window.getSelection().toString();
		if (!text || force) {
D
Daniel Imms 已提交
197
			this._xterm.focus();
D
Daniel Imms 已提交
198
		}
D
Daniel Imms 已提交
199 200 201
	}

	public paste(): void {
D
Daniel Imms 已提交
202 203
		this.focus();
		document.execCommand('paste');
D
Daniel Imms 已提交
204 205 206
	}

	public sendText(text: string, addNewLine: boolean): void {
D
Daniel Imms 已提交
207 208 209
		if (addNewLine && text.substr(text.length - os.EOL.length) !== os.EOL) {
			text += os.EOL;
		}
D
Daniel Imms 已提交
210
		this._process.send({
D
Daniel Imms 已提交
211 212 213
			event: 'input',
			data: text
		});
D
Daniel Imms 已提交
214
	}
215 216

	public setVisible(visible: boolean): void {
D
Daniel Imms 已提交
217 218 219
		this._isVisible = visible;
		if (this._wrapperElement) {
			DOM.toggleClass(this._wrapperElement, 'active', visible);
220
		}
221 222
	}

223
	public scrollDownLine(): void {
D
Daniel Imms 已提交
224
		this._xterm.scrollDisp(1);
D
Daniel Imms 已提交
225 226
	}

227
	public scrollDownPage(): void {
228
		this._xterm.scrollPages(1);
229 230
	}

231 232 233 234
	public scrollToBottom(): void {
		this._xterm.scrollToBottom();
	}

235
	public scrollUpLine(): void {
D
Daniel Imms 已提交
236
		this._xterm.scrollDisp(-1);
D
Daniel Imms 已提交
237
	}
238

239
	public scrollUpPage(): void {
240
		this._xterm.scrollPages(-1);
241 242
	}

243 244 245 246
	public scrollToTop(): void {
		this._xterm.scrollToTop();
	}

D
Daniel Imms 已提交
247 248 249 250
	public clear(): void {
		this._xterm.clear();
	}

C
Christof Marti 已提交
251
	private sanitizeInput(data: any) {
252
		return typeof data === 'string' ? data.replace(TerminalInstance.EOL_REGEX, os.EOL) : data;
C
Christof Marti 已提交
253 254
	}

D
Daniel Imms 已提交
255 256
	private _createProcess(workspace: IWorkspace, name: string, shell: IShell) {
		let locale = this._configHelper.isSetLocaleVariables() ? platform.locale : undefined;
D
Daniel Imms 已提交
257
		if (!shell.executable) {
D
Daniel Imms 已提交
258
			shell = this._configHelper.getShell();
P
Pine Wu 已提交
259
		}
D
Daniel Imms 已提交
260
		let env = TerminalInstance.createTerminalEnv(process.env, shell, workspace, locale);
261
		this._title = name ? name : '';
D
Daniel Imms 已提交
262
		this._process = cp.fork('./terminalProcess', [], {
263 264 265 266 267
			env: env,
			cwd: URI.parse(path.dirname(require.toUrl('./terminalProcess'))).fsPath
		});
		if (!name) {
			// Only listen for process title changes when a name is not provided
D
Daniel Imms 已提交
268
			this._process.on('message', (message) => {
269
				if (message.type === 'title') {
D
Daniel Imms 已提交
270
					this._title = message.content ? message.content : '';
271
					this._onTitleChanged.fire(this._title);
272
				}
273 274
			});
		}
275 276 277 278 279 280
		this._process.on('message', (message) => {
			if (message.type === 'pid') {
				this._processId = message.content;
				this._onProcessIdReady.fire(this);
			}
		});
D
Daniel Imms 已提交
281
		this._process.on('exit', (exitCode) => {
282
			// Prevent dispose functions being triggered multiple times
D
Daniel Imms 已提交
283
			if (!this._isExiting) {
284 285
				this.dispose();
				if (exitCode) {
D
Daniel Imms 已提交
286
					this._messageService.show(Severity.Error, nls.localize('terminal.integrated.exitedWithCode', 'The terminal process terminated with exit code: {0}', exitCode));
287 288 289
				}
			}
		});
290 291
	}

D
Daniel Imms 已提交
292
	public static createTerminalEnv(parentEnv: IStringDictionary<string>, shell: IShell, workspace: IWorkspace, locale?: string): IStringDictionary<string> {
D
Daniel Imms 已提交
293
		let env = TerminalInstance._cloneEnv(parentEnv);
294 295
		env['PTYPID'] = process.pid.toString();
		env['PTYSHELL'] = shell.executable;
D
Daniel Imms 已提交
296 297 298 299 300
		if (shell.args) {
			shell.args.forEach((arg, i) => {
				env[`PTYSHELLARG${i}`] = arg;
			});
		}
D
Daniel Imms 已提交
301
		env['PTYCWD'] = TerminalInstance._sanitizeCwd(workspace ? workspace.resource.fsPath : os.homedir());
302
		if (locale) {
D
Daniel Imms 已提交
303
			env['LANG'] = TerminalInstance._getLangEnvVariable(locale);
304 305
		}
		return env;
306 307
	}

D
Daniel Imms 已提交
308
	private static _sanitizeCwd(cwd: string) {
309 310 311
		// 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 已提交
312
		}
313
		return cwd;
D
Daniel Imms 已提交
314 315
	}

D
Daniel Imms 已提交
316
	private static _cloneEnv(env: IStringDictionary<string>): IStringDictionary<string> {
317 318 319
		let newEnv: IStringDictionary<string> = Object.create(null);
		Object.keys(env).forEach((key) => {
			newEnv[key] = env[key];
320
		});
321
		return newEnv;
322 323
	}

D
Daniel Imms 已提交
324
	private static _getLangEnvVariable(locale: string) {
325 326 327 328
		const parts = locale.split('-');
		const n = parts.length;
		if (n > 1) {
			parts[n - 1] = parts[n - 1].toUpperCase();
D
Daniel Imms 已提交
329
		}
330
		return parts.join('_') + '.UTF-8';
D
Daniel Imms 已提交
331
	}
D
Daniel Imms 已提交
332 333

	public setCursorBlink(blink: boolean): void {
D
Daniel Imms 已提交
334
		if (this._xterm && this._xterm.getOption('cursorBlink') !== blink) {
D
Daniel Imms 已提交
335
			this._xterm.setOption('cursorBlink', blink);
D
Daniel Imms 已提交
336
			this._xterm.refresh(0, this._xterm.rows - 1);
D
Daniel Imms 已提交
337 338 339 340
		}
	}

	public setCommandsToSkipShell(commands: string[]): void {
D
Daniel Imms 已提交
341 342
		this._skipTerminalKeybindings = commands.map((c) => {
			return this._keybindingService.lookupKeybindings(c);
D
Daniel Imms 已提交
343 344
		}).reduce((prev, curr) => {
			return prev.concat(curr);
345
		}, []);
D
Daniel Imms 已提交
346 347 348
	}

	public layout(dimension: Dimension): void {
D
Daniel Imms 已提交
349
		let font = this._configHelper.getFont();
350
		if (!font || !font.charWidth || !font.charHeight) {
D
Daniel Imms 已提交
351 352 353 354
			return;
		}
		if (!dimension.height) { // Minimized
			return;
355 356 357 358 359 360
		} 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 已提交
361 362 363
		}
		let leftPadding = parseInt(getComputedStyle(document.querySelector('.terminal-outer-container')).paddingLeft.split('px')[0], 10);
		let innerWidth = dimension.width - leftPadding;
364 365
		let cols = Math.floor(innerWidth / font.charWidth);
		let rows = Math.floor(dimension.height / font.charHeight);
D
Daniel Imms 已提交
366 367 368
		if (this._xterm) {
			this._xterm.resize(cols, rows);
			this._xterm.element.style.width = innerWidth + 'px';
D
Daniel Imms 已提交
369
		}
D
Daniel Imms 已提交
370 371
		if (this._process.connected) {
			this._process.send({
D
Daniel Imms 已提交
372 373 374 375 376 377
				event: 'resize',
				cols: cols,
				rows: rows
			});
		}
	}
378
}