terminalService.ts 12.2 KB
Newer Older
D
Daniel Imms 已提交
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 URI from 'vs/base/common/uri';
7
import Event, {Emitter} from 'vs/base/common/event';
8
import cp = require('child_process');
9
import nls = require('vs/nls');
10 11 12
import os = require('os');
import path = require('path');
import platform = require('vs/base/common/platform');
D
Daniel Imms 已提交
13
import {Builder} from 'vs/base/browser/builder';
14
import {EndOfLinePreference} from 'vs/editor/common/editorCommon';
15
import {ICodeEditorService} from 'vs/editor/common/services/codeEditorService';
16
import {IConfigurationService} from 'vs/platform/configuration/common/configuration';
A
Alex Dima 已提交
17
import {IContextKey, IContextKeyService} from 'vs/platform/contextkey/common/contextkey';
18
import {IMessageService, Severity} from 'vs/platform/message/common/message';
D
Daniel Imms 已提交
19
import {IPanelService} from 'vs/workbench/services/panel/common/panelService';
20
import {IPartService} from 'vs/workbench/services/part/common/partService';
21
import {IStringDictionary} from 'vs/base/common/collections';
22
import {ITerminalProcess, ITerminalService, KEYBINDING_CONTEXT_TERMINAL_FOCUS, TERMINAL_PANEL_ID} from 'vs/workbench/parts/terminal/electron-browser/terminal';
23
import {IWorkspaceContextService, IWorkspace} from 'vs/platform/workspace/common/workspace';
24
import {TPromise} from 'vs/base/common/winjs.base';
25
import {TerminalConfigHelper, IShell} from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper';
26
import {TerminalPanel} from 'vs/workbench/parts/terminal/electron-browser/terminalPanel';
D
Daniel Imms 已提交
27 28

export class TerminalService implements ITerminalService {
29
	public _serviceBrand: any;
D
Daniel Imms 已提交
30

31
	private activeTerminalIndex: number = 0;
D
Daniel Imms 已提交
32
	private terminalProcesses: ITerminalProcess[] = [];
33
	private nextTerminalName: string;
A
Alex Dima 已提交
34
	protected _terminalFocusContextKey: IContextKey<boolean>;
35

36
	private configHelper: TerminalConfigHelper;
37 38 39
	private _onActiveInstanceChanged: Emitter<string>;
	private _onInstancesChanged: Emitter<string>;
	private _onInstanceTitleChanged: Emitter<string>;
40

D
Daniel Imms 已提交
41
	constructor(
42
		@ICodeEditorService private codeEditorService: ICodeEditorService,
43
		@IConfigurationService private configurationService: IConfigurationService,
44
		@IContextKeyService private contextKeyService: IContextKeyService,
45
		@IMessageService private messageService: IMessageService,
46
		@IPanelService private panelService: IPanelService,
47 48
		@IPartService private partService: IPartService,
		@IWorkspaceContextService private contextService: IWorkspaceContextService
D
Daniel Imms 已提交
49
	) {
50 51 52
		this._onActiveInstanceChanged = new Emitter<string>();
		this._onInstancesChanged = new Emitter<string>();
		this._onInstanceTitleChanged = new Emitter<string>();
53
		this._terminalFocusContextKey = KEYBINDING_CONTEXT_TERMINAL_FOCUS.bindTo(this.contextKeyService);
54 55 56 57 58 59 60 61 62 63 64 65
	}

	public get onActiveInstanceChanged(): Event<string> {
		return this._onActiveInstanceChanged.event;
	}

	public get onInstancesChanged(): Event<string> {
		return this._onInstancesChanged.event;
	}

	public get onInstanceTitleChanged(): Event<string> {
		return this._onInstanceTitleChanged.event;
D
Daniel Imms 已提交
66 67
	}

68
	public setActiveTerminal(index: number): TPromise<any> {
69
		return this.show(false).then((terminalPanel) => {
70 71 72
			this.activeTerminalIndex = index;
			terminalPanel.setActiveTerminal(this.activeTerminalIndex);
			this._onActiveInstanceChanged.fire();
73 74 75
		});
	}

76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
	public setActiveTerminalById(terminalId: number): void {
		this.setActiveTerminal(this.getTerminalIndexFromId(terminalId));
	}

	private getTerminalIndexFromId(terminalId: number): number {
		let terminalIndex = -1;
		this.terminalProcesses.forEach((terminalProcess, i) => {
			if (terminalProcess.process.pid === terminalId) {
				terminalIndex = i;
			}
		});
		if (terminalIndex === -1) {
			throw new Error(`Terminal with ID ${terminalId} does not exist (has it already been disposed?)`);
		}
		return terminalIndex;
	}

93
	public focusNext(): TPromise<any> {
94 95 96 97 98 99 100 101 102 103 104
		return this.focus().then((terminalPanel) => {
			if (this.terminalProcesses.length <= 1) {
				return;
			}
			this.activeTerminalIndex++;
			if (this.activeTerminalIndex >= this.terminalProcesses.length) {
				this.activeTerminalIndex = 0;
			}
			terminalPanel.setActiveTerminal(this.activeTerminalIndex);
			terminalPanel.focus();
			this._onActiveInstanceChanged.fire();
105 106 107 108
		});
	}

	public focusPrevious(): TPromise<any> {
109 110 111 112 113 114 115 116 117 118 119
		return this.focus().then((terminalPanel) => {
			if (this.terminalProcesses.length <= 1) {
				return;
			}
			this.activeTerminalIndex--;
			if (this.activeTerminalIndex < 0) {
				this.activeTerminalIndex = this.terminalProcesses.length - 1;
			}
			terminalPanel.setActiveTerminal(this.activeTerminalIndex);
			terminalPanel.focus();
			this._onActiveInstanceChanged.fire();
120 121 122
		});
	}

123
	public runSelectedText(): TPromise<any> {
124
		return this.focus().then((terminalPanel) => {
125
			let editor = this.codeEditorService.getFocusedCodeEditor();
126
			let selection = editor.getSelection();
D
Daniel Imms 已提交
127 128 129 130 131 132 133 134
			let text: string;
			if (selection.isEmpty()) {
				text = editor.getValue();
			} else {
				let endOfLinePreference = os.EOL === '\n' ? EndOfLinePreference.LF : EndOfLinePreference.CRLF;
				text = editor.getModel().getValueInRange(selection, endOfLinePreference);
			}
			terminalPanel.sendTextToActiveTerminal(text, true);
135 136 137
		});
	}

138 139 140 141 142 143 144 145 146 147 148 149
	public show(focus: boolean): TPromise<TerminalPanel> {
		return new TPromise<TerminalPanel>((complete) => {
			let panel = this.panelService.getActivePanel();
			if (!panel || panel.getId() !== TERMINAL_PANEL_ID) {
				return this.panelService.openPanel(TERMINAL_PANEL_ID, focus).then(() => {
					panel = this.panelService.getActivePanel();
					complete(<TerminalPanel>panel);
				});
			} else {
				complete(<TerminalPanel>panel);
			}
		});
150 151
	}

152 153
	public focus(): TPromise<TerminalPanel> {
		return this.show(true);
D
Daniel Imms 已提交
154
	}
155

D
Daniel Imms 已提交
156 157 158 159 160
	public hide(): TPromise<any> {
		const panel = this.panelService.getActivePanel();
		if (panel && panel.getId() === TERMINAL_PANEL_ID) {
			this.partService.setPanelHidden(true);
		}
161 162 163 164 165 166 167 168 169 170 171
		return TPromise.as(void 0);
	}

	public hideTerminalInstance(terminalId: number): TPromise<any> {
		const panel = this.panelService.getActivePanel();
		if (panel && panel.getId() === TERMINAL_PANEL_ID) {
			if (this.terminalProcesses[this.getActiveTerminalIndex()].process.pid === terminalId) {
				this.partService.setPanelHidden(true);
			}
		}
		return TPromise.as(void 0);
D
Daniel Imms 已提交
172 173
	}

174 175 176 177 178 179 180 181 182
	public toggle(): TPromise<any> {
		const panel = this.panelService.getActivePanel();
		if (panel && panel.getId() === TERMINAL_PANEL_ID) {
			this.partService.setPanelHidden(true);
			return TPromise.as(null);
		}
		return this.focus();
	}

D
Daniel Imms 已提交
183
	public createNew(name?: string): TPromise<number> {
184 185
		let processCount = this.terminalProcesses.length;

186
		// When there are 0 processes it means that the panel is not yet created, so the name needs
D
Daniel Imms 已提交
187 188 189
		// to be stored for when createNew is called from TerminalPanel.create. This has to work
		// like this as TerminalPanel.setVisible must create a terminal if there is none due to how
		// the TerminalPanel is restored on launch if it was open previously.
190

191 192
		if (processCount === 0 && !name) {
			name = this.nextTerminalName;
D
Daniel Imms 已提交
193 194 195
			this.nextTerminalName = undefined;
		} else {
			this.nextTerminalName = name;
196 197
		}

198
		return this.focus().then((terminalPanel) => {
199 200 201 202 203 204
			// If the terminal panel has not been initialized yet skip this, the terminal will be
			// created via a call from TerminalPanel.setVisible
			if (terminalPanel === null) {
				return;
			}

D
Daniel Imms 已提交
205 206 207
			// Only create a new process if none have been created since toggling the terminal
			// panel. This happens when createNew is called when the panel is either empty or no yet
			// created.
208
			if (processCount !== this.terminalProcesses.length) {
D
Daniel Imms 已提交
209
				return TPromise.as(this.terminalProcesses[this.terminalProcesses.length - 1].process.pid);
210 211
			}

D
Daniel Imms 已提交
212 213 214
			this.initConfigHelper(terminalPanel.getContainer());
			return terminalPanel.createNewTerminalInstance(this.createTerminalProcess(name), this._terminalFocusContextKey).then((terminalId) => {
				this._onInstancesChanged.fire();
215 216
				return TPromise.as(terminalId);
			});
217
		});
218
	}
D
Daniel Imms 已提交
219 220

	public close(): TPromise<any> {
221
		return this.focus().then((terminalPanel) => {
222
			return terminalPanel.closeActiveTerminal();
223 224 225
		});
	}

D
Daniel Imms 已提交
226 227 228 229 230 231
	public closeById(terminalId: number): TPromise<any> {
		return this.show(false).then((terminalPanel) => {
			return terminalPanel.closeTerminalById(terminalId);
		});
	}

232 233 234 235 236 237 238 239 240 241
	public copySelection(): TPromise<any> {
		if (document.activeElement.classList.contains('xterm')) {
			document.execCommand('copy');
		} else {
			this.messageService.show(Severity.Warning, nls.localize('terminal.integrated.copySelection.noSelection', 'Cannot copy terminal selection when terminal does not have focus'));
		}
		return TPromise.as(void 0);
	}

	public paste(): TPromise<any> {
242 243
		return this.focus().then(() => {
			document.execCommand('paste');
244 245 246
		});
	}

247
	public scrollDown(): TPromise<any> {
248
		return this.focus().then((terminalPanel) => {
249 250 251 252 253
			terminalPanel.scrollDown();
		});
	}

	public scrollUp(): TPromise<any> {
254
		return this.focus().then((terminalPanel) => {
255 256 257 258
			terminalPanel.scrollUp();
		});
	}

259
	public getActiveTerminalIndex(): number {
260
		return this.activeTerminalIndex;
261 262
	}

263
	public getTerminalInstanceTitles(): string[] {
264
		return this.terminalProcesses.map((process, index) => `${index + 1}: ${process.title}`);
265 266
	}

D
Daniel Imms 已提交
267
	public initConfigHelper(panelElement: Builder): void {
268 269 270 271 272 273
		if (!this.configHelper) {
			this.configHelper = new TerminalConfigHelper(platform.platform, this.configurationService, panelElement);
		}
	}

	public killTerminalProcess(terminalProcess: ITerminalProcess): void {
274 275 276
		if (terminalProcess.process.connected) {
			terminalProcess.process.disconnect();
			terminalProcess.process.kill();
277 278 279
		}

		let index = this.terminalProcesses.indexOf(terminalProcess);
280 281 282 283
		if (index >= 0) {
			let wasActiveTerminal = (index === this.getActiveTerminalIndex());
			// Push active index back if the closed process was before the active process
			if (this.getActiveTerminalIndex() >= index) {
D
Daniel Imms 已提交
284
				this.activeTerminalIndex = Math.max(0, this.activeTerminalIndex - 1);
285 286 287 288 289 290
			}
			this.terminalProcesses.splice(index, 1);
			this._onInstancesChanged.fire();
			if (wasActiveTerminal) {
				this._onActiveInstanceChanged.fire();
			}
291
		}
292 293
	}

D
Daniel Imms 已提交
294
	private createTerminalProcess(name?: string): ITerminalProcess {
295 296
		let locale = this.configHelper.isSetLocaleVariables() ? platform.locale : undefined;
		let env = TerminalService.createTerminalEnv(process.env, this.configHelper.getShell(), this.contextService.getWorkspace(), locale);
297
		let terminalProcess = {
298
			title: name ? name : '',
299 300 301 302 303
			process: cp.fork('./terminalProcess', [], {
				env: env,
				cwd: URI.parse(path.dirname(require.toUrl('./terminalProcess'))).fsPath
			})
		};
D
Daniel Imms 已提交
304
		this.terminalProcesses.push(terminalProcess);
305
		this._onInstancesChanged.fire();
306 307
		this.activeTerminalIndex = this.terminalProcesses.length - 1;
		this._onActiveInstanceChanged.fire();
D
Daniel Imms 已提交
308 309 310 311
		if (!name) {
			// Only listen for process title changes when a name is not provided
			terminalProcess.process.on('message', (message) => {
				if (message.type === 'title') {
312
					terminalProcess.title = message.content ? message.content : '';
D
Daniel Imms 已提交
313 314 315 316
					this._onInstanceTitleChanged.fire();
				}
			});
		}
317 318 319
		return terminalProcess;
	}

320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
	public static createTerminalEnv(parentEnv: IStringDictionary<string>, shell: IShell, workspace: IWorkspace, locale?: string): IStringDictionary<string> {
		let env = this.cloneEnv(parentEnv);
		env['PTYPID'] = process.pid.toString();
		env['PTYSHELL'] = shell.executable;
		shell.args.forEach((arg, i) => {
			env[`PTYSHELLARG${i}`] = arg;
		});
		env['PTYCWD'] = workspace ? workspace.resource.fsPath : os.homedir();
		if (locale) {
			env['LANG'] = this.getLangEnvVariable(locale);
		}
		return env;
	}

	private static cloneEnv(env: IStringDictionary<string>): IStringDictionary<string> {
335
		let newEnv: IStringDictionary<string> = Object.create(null);
336 337
		Object.keys(env).forEach((key) => {
			newEnv[key] = env[key];
338
		});
339 340
		return newEnv;
	}
341 342 343 344 345 346 347 348 349

	private static getLangEnvVariable(locale: string) {
		const parts = locale.split('-');
		const n = parts.length;
		if (n > 1) {
			parts[n - 1] = parts[n - 1].toUpperCase();
		}
		return parts.join('_') + '.UTF-8';
	}
D
Daniel Imms 已提交
350
}