window.ts 20.0 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

8
import nls = require('vs/nls');
E
Erich Gamma 已提交
9
import platform = require('vs/base/common/platform');
10
import URI from 'vs/base/common/uri';
11 12
import errors = require('vs/base/common/errors');
import types = require('vs/base/common/types');
J
Johannes Rieken 已提交
13
import { TPromise } from 'vs/base/common/winjs.base';
14
import arrays = require('vs/base/common/arrays');
15
import objects = require('vs/base/common/objects');
16
import DOM = require('vs/base/browser/dom');
17 18 19
import Severity from 'vs/base/common/severity';
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { IAction, Action } from 'vs/base/common/actions';
J
Johannes Rieken 已提交
20
import { IPartService } from 'vs/workbench/services/part/common/partService';
21
import { AutoSaveConfiguration, IFileService } from 'vs/platform/files/common/files';
22
import { toResource } from 'vs/workbench/common/editor';
23
import { IWorkbenchEditorService, IResourceInputType } from 'vs/workbench/services/editor/common/editorService';
J
Johannes Rieken 已提交
24
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
25
import { IMessageService } from 'vs/platform/message/common/message';
J
Johannes Rieken 已提交
26
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
27
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
28
import { IWindowsService, IWindowService, IWindowSettings, IPath, IOpenFileRequest, IWindowsConfiguration, IAddFoldersRequest, IRunActionInWindowRequest } from 'vs/platform/windows/common/windows';
29 30 31
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
B
Benjamin Pasero 已提交
32
import { ITitleService } from 'vs/workbench/services/title/common/titleService';
33
import { IWorkbenchThemeService, VS_HC_THEME, VS_DARK_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService';
34 35 36
import * as browser from 'vs/base/browser/browser';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
N
Nick Snyder 已提交
37
import { Position, IResourceInput, IUntitledResourceInput, IEditor } from 'vs/platform/editor/common/editor';
38
import { IExtensionService } from 'vs/platform/extensions/common/extensions';
A
Alex Dima 已提交
39
import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron-browser/keybindingService';
40
import { Themable } from 'vs/workbench/common/theme';
41
import { ipcRenderer as ipc, webFrame } from 'electron';
42
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
43
import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing';
44 45 46 47 48
import { IMenuService, MenuId, IMenu, MenuItemAction, ICommandAction } from 'vs/platform/actions/common/actions';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { fillInActions } from 'vs/platform/actions/browser/menuItemActionItem';
import { RunOnceScheduler } from 'vs/base/common/async';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
49
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
E
Erich Gamma 已提交
50

51 52 53 54 55 56 57 58 59 60 61
const TextInputActions: IAction[] = [
	new Action('undo', nls.localize('undo', "Undo"), null, true, () => document.execCommand('undo') && TPromise.as(true)),
	new Action('redo', nls.localize('redo', "Redo"), null, true, () => document.execCommand('redo') && TPromise.as(true)),
	new Separator(),
	new Action('editor.action.clipboardCutAction', nls.localize('cut', "Cut"), null, true, () => document.execCommand('cut') && TPromise.as(true)),
	new Action('editor.action.clipboardCopyAction', nls.localize('copy', "Copy"), null, true, () => document.execCommand('copy') && TPromise.as(true)),
	new Action('editor.action.clipboardPasteAction', nls.localize('paste', "Paste"), null, true, () => document.execCommand('paste') && TPromise.as(true)),
	new Separator(),
	new Action('editor.action.selectAll', nls.localize('selectAll', "Select All"), null, true, () => document.execCommand('selectAll') && TPromise.as(true))
];

62
export class ElectronWindow extends Themable {
63 64 65

	private static AUTO_SAVE_SETTING = 'files.autoSave';

66 67 68 69 70 71 72
	private touchBarUpdater: RunOnceScheduler;
	private touchBarMenu: IMenu;
	private touchBarDisposables: IDisposable[];
	private lastInstalledTouchedBar: ICommandAction[][];

	private previousConfiguredZoomLevel: number;

E
Erich Gamma 已提交
73 74 75
	constructor(
		shellContainer: HTMLElement,
		@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
76
		@IEditorGroupService private editorGroupService: IEditorGroupService,
77
		@IPartService private partService: IPartService,
78
		@IWindowsService private windowsService: IWindowsService,
B
Benjamin Pasero 已提交
79
		@IWindowService private windowService: IWindowService,
80 81
		@IWorkspaceConfigurationService private configurationService: IWorkspaceConfigurationService,
		@ITitleService private titleService: ITitleService,
82
		@IWorkbenchThemeService protected themeService: IWorkbenchThemeService,
83 84 85 86 87 88
		@IMessageService private messageService: IMessageService,
		@ICommandService private commandService: ICommandService,
		@IExtensionService private extensionService: IExtensionService,
		@IViewletService private viewletService: IViewletService,
		@IContextMenuService private contextMenuService: IContextMenuService,
		@IKeybindingService private keybindingService: IKeybindingService,
J
Johannes Rieken 已提交
89
		@IEnvironmentService private environmentService: IEnvironmentService,
90 91
		@ITelemetryService private telemetryService: ITelemetryService,
		@IWorkspaceContextService private contextService: IWorkspaceContextService,
92
		@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService,
93 94 95
		@IFileService private fileService: IFileService,
		@IMenuService private menuService: IMenuService,
		@IContextKeyService private contextKeyService: IContextKeyService
E
Erich Gamma 已提交
96
	) {
97 98
		super(themeService);

99 100 101 102 103
		this.touchBarDisposables = [];

		this.touchBarUpdater = new RunOnceScheduler(() => this.doSetupTouchbar(), 300);
		this.toUnbind.push(this.touchBarUpdater);

E
Erich Gamma 已提交
104
		this.registerListeners();
105
		this.create();
E
Erich Gamma 已提交
106 107 108 109
	}

	private registerListeners(): void {

110
		// React to editor input changes
111
		this.toUnbind.push(this.editorGroupService.onEditorsChanged(() => {
E
Erich Gamma 已提交
112

113 114
			// Represented File Name
			const file = toResource(this.editorService.getActiveEditorInput(), { supportSideBySide: true, filter: 'file' });
115
			this.titleService.setRepresentedFilename(file ? file.fsPath : '');
116 117 118 119

			// Touch Bar
			this.updateTouchbarMenu();
		}));
E
Erich Gamma 已提交
120

121
		// prevent opening a real URL inside the shell
122 123 124 125
		[DOM.EventType.DRAG_OVER, DOM.EventType.DROP].forEach(event => {
			window.document.body.addEventListener(event, (e: DragEvent) => {
				DOM.EventHelper.stop(e);
			});
E
Erich Gamma 已提交
126 127
		});

128
		// Support runAction event
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
		ipc.on('vscode:runAction', (event, request: IRunActionInWindowRequest) => {
			const args: any[] = [];

			// If we run an action from the touchbar, we fill in the currently active resource
			// as payload because the touch bar items are context aware depending on the editor
			if (request.from === 'touchbar') {
				const activeEditor = this.editorService.getActiveEditor();
				if (activeEditor) {
					const resource = toResource(activeEditor.input, { supportSideBySide: true });
					if (resource) {
						args.push(resource);
					}
				}
			} else {
				args.push({ from: request.from }); // TODO@telemetry this is a bit weird to send this to every action?
			}

			this.commandService.executeCommand(request.id, ...args).done(_ => {
K
kieferrm 已提交
147
				/* __GDPR__
K
kieferrm 已提交
148 149 150 151 152
					"commandExecuted" : {
						"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
						"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
					}
				*/
153
				this.telemetryService.publicLog('commandExecuted', { id: request.id, from: request.from });
J
Johannes Rieken 已提交
154 155 156
			}, err => {
				this.messageService.show(Severity.Error, err);
			});
157 158 159 160 161 162 163 164 165 166 167 168
		});

		// Support resolve keybindings event
		ipc.on('vscode:resolveKeybindings', (event, rawActionIds: string) => {
			let actionIds: string[] = [];
			try {
				actionIds = JSON.parse(rawActionIds);
			} catch (error) {
				// should not happen
			}

			// Resolve keys using the keybinding service and send back to browser process
169
			this.resolveKeybindings(actionIds).done(keybindings => {
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
				if (keybindings.length) {
					ipc.send('vscode:keybindingsResolved', JSON.stringify(keybindings));
				}
			}, () => errors.onUnexpectedError);
		});

		ipc.on('vscode:reportError', (event, error) => {
			if (error) {
				const errorParsed = JSON.parse(error);
				errorParsed.mainProcess = true;
				errors.onUnexpectedError(errorParsed);
			}
		});

		// Support openFiles event for existing and new files
		ipc.on('vscode:openFiles', (event, request: IOpenFileRequest) => this.onOpenFiles(request));

187 188 189
		// Support addFolders event if we have a workspace opened
		ipc.on('vscode:addFolders', (event, request: IAddFoldersRequest) => this.onAddFolders(request));

190 191 192 193 194 195
		// Message support
		ipc.on('vscode:showInfoMessage', (event, message: string) => {
			this.messageService.show(Severity.Info, message);
		});

		// Support toggling auto save
196
		ipc.on('vscode.toggleAutoSave', event => {
197 198 199 200
			this.toggleAutoSave();
		});

		// Fullscreen Events
201
		ipc.on('vscode:enterFullScreen', event => {
202 203 204 205 206
			this.partService.joinCreation().then(() => {
				browser.setFullscreen(true);
			});
		});

207
		ipc.on('vscode:leaveFullScreen', event => {
208 209 210 211 212 213
			this.partService.joinCreation().then(() => {
				browser.setFullscreen(false);
			});
		});

		// High Contrast Events
214
		ipc.on('vscode:enterHighContrast', event => {
215
			const windowConfig = this.configurationService.getConfiguration<IWindowSettings>('window');
216
			if (windowConfig && windowConfig.autoDetectHighContrast) {
217
				this.partService.joinCreation().then(() => {
M
Martin Aeschlimann 已提交
218
					this.themeService.setColorTheme(VS_HC_THEME, null);
219 220
				});
			}
221 222
		});

223
		ipc.on('vscode:leaveHighContrast', event => {
224
			const windowConfig = this.configurationService.getConfiguration<IWindowSettings>('window');
225
			if (windowConfig && windowConfig.autoDetectHighContrast) {
226
				this.partService.joinCreation().then(() => {
M
Martin Aeschlimann 已提交
227
					this.themeService.setColorTheme(VS_DARK_THEME, null);
228 229
				});
			}
230 231
		});

A
Alex Dima 已提交
232
		// keyboard layout changed event
233 234
		ipc.on('vscode:keyboardLayoutChanged', event => {
			KeyboardMapperFactory.INSTANCE._onKeyboardLayoutChanged();
A
Alex Dima 已提交
235 236
		});

237 238
		// keyboard layout changed event
		ipc.on('vscode:accessibilitySupportChanged', (event, accessibilitySupportEnabled: boolean) => {
239
			browser.setAccessibilitySupport(accessibilitySupportEnabled ? platform.AccessibilitySupport.Enabled : platform.AccessibilitySupport.Disabled);
240 241
		});

242
		// Configuration changes
243
		this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => this.onDidUpdateConfiguration(e)));
244

245 246 247
		// Context menu support in input/textarea
		window.document.addEventListener('contextmenu', e => this.onContextMenu(e));
	}
248

249 250 251 252 253 254
	private onContextMenu(e: PointerEvent): void {
		if (e.target instanceof HTMLElement) {
			const target = <HTMLElement>e.target;
			if (target.nodeName && (target.nodeName.toLowerCase() === 'input' || target.nodeName.toLowerCase() === 'textarea')) {
				e.preventDefault();
				e.stopPropagation();
255

256 257 258 259
				this.contextMenuService.showContextMenu({
					getAnchor: () => e,
					getActions: () => TPromise.as(TextInputActions)
				});
260
			}
261 262 263 264 265 266 267 268 269
		}
	}

	private onDidUpdateConfiguration(e): void {
		const windowConfig: IWindowsConfiguration = this.configurationService.getConfiguration<IWindowsConfiguration>();

		let newZoomLevel = 0;
		if (windowConfig.window && typeof windowConfig.window.zoomLevel === 'number') {
			newZoomLevel = windowConfig.window.zoomLevel;
270

271 272 273
			// Leave early if the configured zoom level did not change (https://github.com/Microsoft/vscode/issues/1536)
			if (this.previousConfiguredZoomLevel === newZoomLevel) {
				return;
274
			}
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301

			this.previousConfiguredZoomLevel = newZoomLevel;
		}

		if (webFrame.getZoomLevel() !== newZoomLevel) {
			webFrame.setZoomLevel(newZoomLevel);
			browser.setZoomFactor(webFrame.getZoomFactor());
			// See https://github.com/Microsoft/vscode/issues/26151
			// Cannot be trusted because the webFrame might take some time
			// until it really applies the new zoom level
			browser.setZoomLevel(webFrame.getZoomLevel(), /*isTrusted*/false);
		}
	}

	private create(): void {

		// Handle window.open() calls
		const $this = this;
		(<any>window).open = function (url: string, target: string, features: string, replace: boolean): any {
			$this.windowsService.openExternal(url);

			return null;
		};

		// Send over all extension viewlets when extensions are ready
		this.extensionService.onReady().then(() => {
			ipc.send('vscode:extensionViewlets', JSON.stringify(this.viewletService.getViewlets().filter(v => !!v.extensionId).map(v => { return { id: v.id, label: v.name }; })));
302 303
		});

304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
		// Emit event when vscode has loaded
		this.partService.joinCreation().then(() => {
			ipc.send('vscode:workbenchLoaded', this.windowService.getCurrentWindowId());
		});

		// Touchbar Support
		this.updateTouchbarMenu();
	}

	private updateTouchbarMenu(): void {
		if (!platform.isMacintosh) {
			return; // macOS only
		}

		// Dispose old
		this.touchBarDisposables = dispose(this.touchBarDisposables);

		// Create new
		this.touchBarMenu = this.editorGroupService.invokeWithinEditorContext(accessor => this.menuService.createMenu(MenuId.TouchBarContext, accessor.get(IContextKeyService)));
		this.touchBarDisposables.push(this.touchBarMenu);
		this.touchBarDisposables.push(this.touchBarMenu.onDidChange(() => {
			this.scheduleSetupTouchbar();
		}));

		this.scheduleSetupTouchbar();
	}

	private scheduleSetupTouchbar(): void {
		this.touchBarUpdater.schedule();
	}

	private doSetupTouchbar(): void {
		const actions: (MenuItemAction | Separator)[] = [];

		// Fill actions into groups respecting order
		fillInActions(this.touchBarMenu, void 0, actions);

		// Convert into command action multi array
		const items: ICommandAction[][] = [];
		let group: ICommandAction[] = [];
		for (let i = 0; i < actions.length; i++) {
			const action = actions[i];

			// Command
			if (action instanceof MenuItemAction) {
				group.push(action.item);
			}

			// Separator
			else if (action instanceof Separator) {
				if (group.length) {
					items.push(group);
356
				}
357 358

				group = [];
359
			}
360 361 362 363 364 365 366 367 368 369 370
		}

		if (group.length) {
			items.push(group);
		}

		// Only update if the actions have changed
		if (!objects.equals(this.lastInstalledTouchedBar, items)) {
			this.lastInstalledTouchedBar = items;
			this.windowService.updateTouchBar(items);
		}
371 372
	}

373
	private resolveKeybindings(actionIds: string[]): TPromise<{ id: string; label: string, isNative: boolean; }[]> {
374
		return TPromise.join([this.partService.joinCreation(), this.extensionService.onReady()]).then(() => {
375
			return arrays.coalesce(actionIds.map(id => {
376
				const binding = this.keybindingService.lookupKeybinding(id);
377 378 379
				if (!binding) {
					return null;
				}
380

381 382 383 384 385
				// first try to resolve a native accelerator
				const electronAccelerator = binding.getElectronAccelerator();
				if (electronAccelerator) {
					return { id, label: electronAccelerator, isNative: true };
				}
386

387 388 389 390
				// we need this fallback to support keybindings that cannot show in electron menus (e.g. chords)
				const acceleratorLabel = binding.getLabel();
				if (acceleratorLabel) {
					return { id, label: acceleratorLabel, isNative: false };
391 392 393 394 395 396 397
				}

				return null;
			}));
		});
	}

398 399 400
	private onAddFolders(request: IAddFoldersRequest): void {
		const foldersToAdd = request.foldersToAdd.map(folderToAdd => URI.file(folderToAdd.filePath));

401
		// Workspace: just add to workspace config
402
		if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) {
S
Sandeep Somavarapu 已提交
403
			this.contextService.addFolders(foldersToAdd).done(null, errors.onUnexpectedError);
404 405
		}

406
		// Single folder or no workspace: create workspace and open
407
		else {
408
			const workspaceFolders: URI[] = [...this.contextService.getWorkspace().folders.map(folder => folder.uri)];
409 410 411 412 413

			// Fill in remaining ones from request
			workspaceFolders.push(...request.foldersToAdd.map(folderToAdd => URI.file(folderToAdd.filePath)));

			// Create workspace and open (ensure no duplicates)
414
			this.workspaceEditingService.createAndEnterWorkspace(arrays.distinct(workspaceFolders.map(folder => folder.fsPath), folder => platform.isLinux ? folder : folder.toLowerCase()));
415 416 417
		}
	}

418
	private onOpenFiles(request: IOpenFileRequest): void {
419 420
		const inputs: IResourceInputType[] = [];
		const diffMode = (request.filesToDiff.length === 2);
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436

		if (!diffMode && request.filesToOpen) {
			inputs.push(...this.toInputs(request.filesToOpen, false));
		}

		if (!diffMode && request.filesToCreate) {
			inputs.push(...this.toInputs(request.filesToCreate, true));
		}

		if (diffMode) {
			inputs.push(...this.toInputs(request.filesToDiff, false));
		}

		if (inputs.length) {
			this.openResources(inputs, diffMode).done(null, errors.onUnexpectedError);
		}
437 438 439 440 441 442 443 444 445 446 447 448 449 450 451

		if (request.filesToWait && inputs.length) {
			// In wait mode, listen to changes to the editors and wait until the files
			// are closed that the user wants to wait for. When this happens we delete
			// the wait marker file to signal to the outside that editing is done.
			const resourcesToWaitFor = request.filesToWait.paths.map(p => URI.file(p.filePath));
			const waitMarkerFile = URI.file(request.filesToWait.waitMarkerFilePath);
			const stacks = this.editorGroupService.getStacksModel();
			const unbind = stacks.onEditorClosed(() => {
				if (resourcesToWaitFor.every(r => !stacks.isOpen(r))) {
					unbind.dispose();
					this.fileService.del(waitMarkerFile).done(null, errors.onUnexpectedError);
				}
			});
		}
452 453
	}

N
Nick Snyder 已提交
454 455 456
	private openResources(resources: (IResourceInput | IUntitledResourceInput)[], diffMode: boolean): TPromise<IEditor | IEditor[]> {
		return this.partService.joinCreation().then((): TPromise<IEditor | IEditor[]> => {

457
			// In diffMode we open 2 resources as diff
458
			if (diffMode && resources.length === 2) {
459
				return this.editorService.openEditor({ leftResource: resources[0].resource, rightResource: resources[1].resource, options: { pinned: true } });
460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477
			}

			// For one file, just put it into the current active editor
			if (resources.length === 1) {
				return this.editorService.openEditor(resources[0]);
			}

			// Otherwise open all
			const activeEditor = this.editorService.getActiveEditor();
			return this.editorService.openEditors(resources.map((r, index) => {
				return {
					input: r,
					position: activeEditor ? activeEditor.position : Position.ONE
				};
			}));
		});
	}

478
	private toInputs(paths: IPath[], isNew: boolean): IResourceInputType[] {
479
		return paths.map(p => {
480 481 482 483 484 485 486
			const resource = URI.file(p.filePath);
			let input: IResourceInput | IUntitledResourceInput;
			if (isNew) {
				input = { filePath: resource.fsPath, options: { pinned: true } } as IUntitledResourceInput;
			} else {
				input = { resource, options: { pinned: true } } as IResourceInput;
			}
487 488 489 490 491 492 493 494 495 496 497 498 499

			if (!isNew && p.lineNumber) {
				input.options.selection = {
					startLineNumber: p.lineNumber,
					startColumn: p.columnNumber
				};
			}

			return input;
		});
	}

	private toggleAutoSave(): void {
500
		const setting = this.configurationService.inspect(ElectronWindow.AUTO_SAVE_SETTING);
501 502 503 504 505 506 507 508 509 510 511 512
		let userAutoSaveConfig = setting.user;
		if (types.isUndefinedOrNull(userAutoSaveConfig)) {
			userAutoSaveConfig = setting.default; // use default if setting not defined
		}

		let newAutoSaveValue: string;
		if ([AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE].some(s => s === userAutoSaveConfig)) {
			newAutoSaveValue = AutoSaveConfiguration.OFF;
		} else {
			newAutoSaveValue = AutoSaveConfiguration.AFTER_DELAY;
		}

513
		this.configurationService.updateValue(ElectronWindow.AUTO_SAVE_SETTING, newAutoSaveValue, ConfigurationTarget.USER);
514
	}
515 516 517 518 519 520

	public dispose(): void {
		this.touchBarDisposables = dispose(this.touchBarDisposables);

		super.dispose();
	}
J
Johannes Rieken 已提交
521
}