window.ts 19.4 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 } from 'vs/platform/windows/common/windows';
29 30 31 32
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IConfigurationEditingService, ConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditing';
B
Benjamin Pasero 已提交
33
import { ITitleService } from 'vs/workbench/services/title/common/titleService';
34
import { IWorkbenchThemeService, VS_HC_THEME, VS_DARK_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService';
35 36 37
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 已提交
38
import { Position, IResourceInput, IUntitledResourceInput, IEditor } from 'vs/platform/editor/common/editor';
39
import { IExtensionService } from 'vs/platform/extensions/common/extensions';
A
Alex Dima 已提交
40
import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron-browser/keybindingService';
41
import { Themable } from 'vs/workbench/common/theme';
42
import { ipcRenderer as ipc, webFrame } from 'electron';
43
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
44
import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing';
45 46 47 48 49
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';
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 89
		@IMessageService private messageService: IMessageService,
		@IConfigurationEditingService private configurationEditingService: IConfigurationEditingService,
		@ICommandService private commandService: ICommandService,
		@IExtensionService private extensionService: IExtensionService,
		@IViewletService private viewletService: IViewletService,
		@IContextMenuService private contextMenuService: IContextMenuService,
		@IKeybindingService private keybindingService: IKeybindingService,
J
Johannes Rieken 已提交
90
		@IEnvironmentService private environmentService: IEnvironmentService,
91 92
		@ITelemetryService private telemetryService: ITelemetryService,
		@IWorkspaceContextService private contextService: IWorkspaceContextService,
93
		@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService,
94 95 96
		@IFileService private fileService: IFileService,
		@IMenuService private menuService: IMenuService,
		@IContextKeyService private contextKeyService: IContextKeyService
E
Erich Gamma 已提交
97
	) {
98 99
		super(themeService);

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

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

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

	private registerListeners(): void {

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

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

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

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

129 130
		// Support runAction event
		ipc.on('vscode:runAction', (event, actionId: string) => {
J
Johannes Rieken 已提交
131 132 133 134 135
			this.commandService.executeCommand(actionId, { from: 'menu' }).done(_ => {
				this.telemetryService.publicLog('commandExecuted', { id: actionId, from: 'menu' });
			}, err => {
				this.messageService.show(Severity.Error, err);
			});
136 137 138 139 140 141 142 143 144 145 146 147
		});

		// 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
148
			this.resolveKeybindings(actionIds).done(keybindings => {
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
				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));

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

169 170 171 172 173 174
		// Message support
		ipc.on('vscode:showInfoMessage', (event, message: string) => {
			this.messageService.show(Severity.Info, message);
		});

		// Support toggling auto save
175
		ipc.on('vscode.toggleAutoSave', event => {
176 177 178 179
			this.toggleAutoSave();
		});

		// Fullscreen Events
180
		ipc.on('vscode:enterFullScreen', event => {
181 182 183 184 185
			this.partService.joinCreation().then(() => {
				browser.setFullscreen(true);
			});
		});

186
		ipc.on('vscode:leaveFullScreen', event => {
187 188 189 190 191 192
			this.partService.joinCreation().then(() => {
				browser.setFullscreen(false);
			});
		});

		// High Contrast Events
193
		ipc.on('vscode:enterHighContrast', event => {
194
			const windowConfig = this.configurationService.getConfiguration<IWindowSettings>('window');
195
			if (windowConfig && windowConfig.autoDetectHighContrast) {
196
				this.partService.joinCreation().then(() => {
M
Martin Aeschlimann 已提交
197
					this.themeService.setColorTheme(VS_HC_THEME, null);
198 199
				});
			}
200 201
		});

202
		ipc.on('vscode:leaveHighContrast', event => {
203
			const windowConfig = this.configurationService.getConfiguration<IWindowSettings>('window');
204
			if (windowConfig && windowConfig.autoDetectHighContrast) {
205
				this.partService.joinCreation().then(() => {
M
Martin Aeschlimann 已提交
206
					this.themeService.setColorTheme(VS_DARK_THEME, null);
207 208
				});
			}
209 210
		});

A
Alex Dima 已提交
211
		// keyboard layout changed event
212 213
		ipc.on('vscode:keyboardLayoutChanged', (event, isISOKeyboard: boolean) => {
			KeyboardMapperFactory.INSTANCE._onKeyboardLayoutChanged(isISOKeyboard);
A
Alex Dima 已提交
214 215
		});

216 217
		// keyboard layout changed event
		ipc.on('vscode:accessibilitySupportChanged', (event, accessibilitySupportEnabled: boolean) => {
218
			browser.setAccessibilitySupport(accessibilitySupportEnabled ? platform.AccessibilitySupport.Enabled : platform.AccessibilitySupport.Disabled);
219 220
		});

221
		// Configuration changes
222
		this.toUnbind.push(this.configurationService.onDidUpdateConfiguration(e => this.onDidUpdateConfiguration(e)));
223

224 225 226
		// Context menu support in input/textarea
		window.document.addEventListener('contextmenu', e => this.onContextMenu(e));
	}
227

228 229 230 231 232 233
	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();
234

235 236 237 238
				this.contextMenuService.showContextMenu({
					getAnchor: () => e,
					getActions: () => TPromise.as(TextInputActions)
				});
239
			}
240 241 242 243 244 245 246 247 248
		}
	}

	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;
249

250 251 252
			// Leave early if the configured zoom level did not change (https://github.com/Microsoft/vscode/issues/1536)
			if (this.previousConfiguredZoomLevel === newZoomLevel) {
				return;
253
			}
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280

			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 }; })));
281 282
		});

283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 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
		// 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);
335
				}
336 337

				group = [];
338
			}
339 340 341 342 343 344 345 346 347 348 349
		}

		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);
		}
350 351
	}

352
	private resolveKeybindings(actionIds: string[]): TPromise<{ id: string; label: string, isNative: boolean; }[]> {
353
		return TPromise.join([this.partService.joinCreation(), this.extensionService.onReady()]).then(() => {
354
			return arrays.coalesce(actionIds.map(id => {
355
				const binding = this.keybindingService.lookupKeybinding(id);
356 357 358
				if (!binding) {
					return null;
				}
359

360 361 362 363 364
				// first try to resolve a native accelerator
				const electronAccelerator = binding.getElectronAccelerator();
				if (electronAccelerator) {
					return { id, label: electronAccelerator, isNative: true };
				}
365

366 367 368 369
				// 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 };
370 371 372 373 374 375 376
				}

				return null;
			}));
		});
	}

377 378 379
	private onAddFolders(request: IAddFoldersRequest): void {
		const foldersToAdd = request.foldersToAdd.map(folderToAdd => URI.file(folderToAdd.filePath));

380
		// Workspace: just add to workspace config
381
		if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) {
S
Sandeep Somavarapu 已提交
382
			this.workspaceEditingService.addFolders(foldersToAdd).done(null, errors.onUnexpectedError);
383 384
		}

385
		// Single folder or no workspace: create workspace and open
386
		else {
387
			const workspaceFolders: URI[] = [...this.contextService.getWorkspace().folders.map(folder => folder.uri)];
388 389 390 391 392

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

			// Create workspace and open (ensure no duplicates)
393
			this.workspaceEditingService.createAndEnterWorkspace(arrays.distinct(workspaceFolders.map(folder => folder.fsPath), folder => platform.isLinux ? folder : folder.toLowerCase()));
394 395 396
		}
	}

397
	private onOpenFiles(request: IOpenFileRequest): void {
398 399
		const inputs: IResourceInputType[] = [];
		const diffMode = (request.filesToDiff.length === 2);
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415

		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);
		}
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430

		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);
				}
			});
		}
431 432
	}

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

436
			// In diffMode we open 2 resources as diff
437
			if (diffMode && resources.length === 2) {
438
				return this.editorService.openEditor({ leftResource: resources[0].resource, rightResource: resources[1].resource, options: { pinned: true } });
439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456
			}

			// 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
				};
			}));
		});
	}

457
	private toInputs(paths: IPath[], isNew: boolean): IResourceInputType[] {
458
		return paths.map(p => {
459 460 461 462 463 464 465
			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;
			}
466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491

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

			return input;
		});
	}

	private toggleAutoSave(): void {
		const setting = this.configurationService.lookup(ElectronWindow.AUTO_SAVE_SETTING);
		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;
		}

S
Sandeep Somavarapu 已提交
492
		this.configurationEditingService.writeConfiguration(ConfigurationTarget.USER, { key: ElectronWindow.AUTO_SAVE_SETTING, value: newAutoSaveValue });
493
	}
494 495 496 497 498 499

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

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