window.ts 36.1 KB
Newer Older
E
Erich Gamma 已提交
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 * as nls from 'vs/nls';
7
import { URI } from 'vs/base/common/uri';
8
import * as errors from 'vs/base/common/errors';
R
Robo 已提交
9
import { equals, deepClone } from 'vs/base/common/objects';
10
import * as DOM from 'vs/base/browser/dom';
11
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
12
import { IAction } from 'vs/base/common/actions';
S
SteVen Batten 已提交
13
import { IFileService } from 'vs/platform/files/common/files';
14
import { toResource, IUntitledTextResourceEditorInput, SideBySideEditor, pathsToEditors } from 'vs/workbench/common/editor';
15
import { IEditorService, IResourceEditorInputType } from 'vs/workbench/services/editor/common/editorService';
R
Robo 已提交
16
import { ITelemetryService, crashReporterIdStorageKey } from 'vs/platform/telemetry/common/telemetry';
17 18
import { IWindowSettings, IOpenFileRequest, IWindowsConfiguration, getTitleBarStyle } from 'vs/platform/windows/common/windows';
import { IRunActionInWindowRequest, IRunKeybindingInWindowRequest, IAddFoldersRequest, INativeOpenFileRequest } from 'vs/platform/windows/node/window';
B
Benjamin Pasero 已提交
19
import { ITitleService } from 'vs/workbench/services/title/common/titleService';
20
import { IWorkbenchThemeService, VS_HC_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService';
21
import * as browser from 'vs/base/browser/browser';
22
import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands';
23
import { IResourceEditorInput } from 'vs/platform/editor/common/editor';
P
Peng Lyu 已提交
24
import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron-browser/nativeKeymapService';
25 26
import { CrashReporterStartOptions } from 'vs/base/parts/sandbox/common/electronTypes';
import { crashReporter, ipcRenderer, webFrame } from 'vs/base/parts/sandbox/electron-sandbox/globals';
B
Benjamin Pasero 已提交
27
import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing';
28
import { IMenuService, MenuId, IMenu, MenuItemAction, ICommandAction, SubmenuItemAction, MenuRegistry } from 'vs/platform/actions/common/actions';
29
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
30
import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
31
import { RunOnceScheduler } from 'vs/base/common/async';
32
import { IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
33
import { LifecyclePhase, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
34
import { IWorkspaceFolderCreationData, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
35
import { IIntegrityService } from 'vs/workbench/services/integrity/common/integrity';
36
import { isRootUser, isWindows, isMacintosh, isLinux } from 'vs/base/common/platform';
37
import product from 'vs/platform/product/common/product';
38
import { INotificationService } from 'vs/platform/notification/common/notification';
B
Benjamin Pasero 已提交
39
import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
40
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
41
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
42
import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility';
43
import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
B
Benjamin Pasero 已提交
44
import { coalesce } from 'vs/base/common/arrays';
45
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
B
Benjamin Pasero 已提交
46
import { isEqual } from 'vs/base/common/resources';
47 48 49 50
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { MenubarControl } from '../browser/parts/titlebar/menubarControl';
import { ILabelService } from 'vs/platform/label/common/label';
import { IUpdateService } from 'vs/platform/update/common/update';
R
Robo 已提交
51
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
52
import { IPreferencesService } from '../services/preferences/common/preferences';
B
Benjamin Pasero 已提交
53
import { IMenubarService, IMenubarData, IMenubarMenu, IMenubarKeybinding, IMenubarMenuItemSubmenu, IMenubarMenuItemAction, MenubarMenuItem } from 'vs/platform/menubar/common/menubar';
B
Benjamin Pasero 已提交
54
import { withNullAsUndefined, assertIsDefined } from 'vs/base/common/types';
M
Matt Bierner 已提交
55
import { IOpenerService, OpenOptions } from 'vs/platform/opener/common/opener';
B
Benjamin Pasero 已提交
56
import { Schemas } from 'vs/base/common/network';
57
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
58 59
import { posix, dirname } from 'vs/base/common/path';
import { getBaseLabel } from 'vs/base/common/labels';
60
import { ITunnelService, extractLocalHostUriMetaDataForPortMapping } from 'vs/platform/remote/common/tunnel';
61
import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService';
62
import { IHostService } from 'vs/workbench/services/host/browser/host';
63 64
import { IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
65
import { Event } from 'vs/base/common/event';
66
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
67
import { clearAllFontInfos } from 'vs/editor/browser/config/configuration';
S
SteVen Batten 已提交
68

69
export class NativeWindow extends Disposable {
70

B
Benjamin Pasero 已提交
71
	private touchBarMenu: IMenu | undefined;
72
	private readonly touchBarDisposables = this._register(new DisposableStore());
B
Benjamin Pasero 已提交
73
	private lastInstalledTouchedBar: ICommandAction[][] | undefined;
74

75
	private readonly customTitleContextMenuDisposable = this._register(new DisposableStore());
76

B
Benjamin Pasero 已提交
77
	private previousConfiguredZoomLevel: number | undefined;
78

79 80
	private readonly addFoldersScheduler = this._register(new RunOnceScheduler(() => this.doAddFolders(), 100));
	private pendingFoldersToAdd: URI[] = [];
81

B
Benjamin Pasero 已提交
82
	private readonly closeEmptyWindowScheduler = this._register(new RunOnceScheduler(() => this.onAllEditorsClosed(), 50));
83 84

	private isDocumentedEdited = false;
85

E
Erich Gamma 已提交
86
	constructor(
87
		@IEditorService private readonly editorService: EditorServiceImpl,
88
		@IConfigurationService private readonly configurationService: IConfigurationService,
89
		@ITitleService private readonly titleService: ITitleService,
90
		@IWorkbenchThemeService protected themeService: IWorkbenchThemeService,
91 92 93 94 95 96 97 98
		@INotificationService private readonly notificationService: INotificationService,
		@ICommandService private readonly commandService: ICommandService,
		@IKeybindingService private readonly keybindingService: IKeybindingService,
		@ITelemetryService private readonly telemetryService: ITelemetryService,
		@IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService,
		@IFileService private readonly fileService: IFileService,
		@IMenuService private readonly menuService: IMenuService,
		@ILifecycleService private readonly lifecycleService: ILifecycleService,
99
		@IIntegrityService private readonly integrityService: IIntegrityService,
100
		@IWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService,
101
		@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
B
Benjamin Pasero 已提交
102
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
B
Benjamin Pasero 已提交
103
		@IInstantiationService private readonly instantiationService: IInstantiationService,
104
		@IOpenerService private readonly openerService: IOpenerService,
105 106
		@IElectronService private readonly electronService: IElectronService,
		@ITunnelService private readonly tunnelService: ITunnelService,
107
		@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
108
		@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
R
Robo 已提交
109 110
		@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService,
		@IStorageService private readonly storageService: IStorageService,
E
Erich Gamma 已提交
111
	) {
112
		super();
113

E
Erich Gamma 已提交
114
		this.registerListeners();
115
		this.create();
E
Erich Gamma 已提交
116 117 118 119
	}

	private registerListeners(): void {

120
		// React to editor input changes
B
Benjamin Pasero 已提交
121
		this._register(this.editorService.onDidActiveEditorChange(() => this.updateTouchbarMenu()));
E
Erich Gamma 已提交
122

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

130
		// Support runAction event
131
		ipcRenderer.on('vscode:runAction', async (event: unknown, request: IRunActionInWindowRequest) => {
132
			const args: unknown[] = request.args || [];
133 134 135 136

			// 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') {
B
Benjamin Pasero 已提交
137
				const activeEditor = this.editorService.activeEditor;
138
				if (activeEditor) {
B
Benjamin Pasero 已提交
139
					const resource = toResource(activeEditor, { supportSideBySide: SideBySideEditor.MASTER });
140 141 142 143 144
					if (resource) {
						args.push(resource);
					}
				}
			} else {
B
Benjamin Pasero 已提交
145
				args.push({ from: request.from });
146 147
			}

148 149 150
			try {
				await this.commandService.executeCommand(request.id, ...args);

151 152 153 154 155
				type CommandExecutedClassifcation = {
					id: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
					from: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
				};
				this.telemetryService.publicLog2<{ id: String, from: String }, CommandExecutedClassifcation>('commandExecuted', { id: request.id, from: request.from });
156 157 158
			} catch (error) {
				this.notificationService.error(error);
			}
159 160
		});

161
		// Support runKeybinding event
162
		ipcRenderer.on('vscode:runKeybinding', (event: unknown, request: IRunKeybindingInWindowRequest) => {
163 164 165
			if (document.activeElement) {
				this.keybindingService.dispatchByUserSettingsLabel(request.userSettingsLabel, document.activeElement);
			}
166 167
		});

B
Benjamin Pasero 已提交
168
		// Error reporting from main
169
		ipcRenderer.on('vscode:reportError', (event: unknown, error: string) => {
170
			if (error) {
171
				errors.onUnexpectedError(JSON.parse(error));
172 173 174 175
			}
		});

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

178
		// Support addFolders event if we have a workspace opened
179
		ipcRenderer.on('vscode:addFolders', (event: unknown, request: IAddFoldersRequest) => this.onAddFoldersRequest(request));
180

181
		// Message support
182
		ipcRenderer.on('vscode:showInfoMessage', (event: unknown, message: string) => {
183
			this.notificationService.info(message);
184 185
		});

186
		ipcRenderer.on('vscode:displayChanged', () => {
187 188 189
			clearAllFontInfos();
		});

190
		// Fullscreen Events
191
		ipcRenderer.on('vscode:enterFullScreen', async () => {
192 193
			await this.lifecycleService.when(LifecyclePhase.Ready);
			browser.setFullscreen(true);
194 195
		});

196
		ipcRenderer.on('vscode:leaveFullScreen', async () => {
197 198
			await this.lifecycleService.when(LifecyclePhase.Ready);
			browser.setFullscreen(false);
199 200 201
		});

		// High Contrast Events
202
		ipcRenderer.on('vscode:enterHighContrast', async () => {
203
			const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
B
Benjamin Pasero 已提交
204
			if (windowConfig?.autoDetectHighContrast) {
205 206
				await this.lifecycleService.when(LifecyclePhase.Ready);
				this.themeService.setColorTheme(VS_HC_THEME, undefined);
207
			}
208 209
		});

210
		ipcRenderer.on('vscode:leaveHighContrast', async () => {
211
			const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
B
Benjamin Pasero 已提交
212
			if (windowConfig?.autoDetectHighContrast) {
213 214
				await this.lifecycleService.when(LifecyclePhase.Ready);
				this.themeService.restoreColorTheme();
215
			}
216 217
		});

A
Alex Dima 已提交
218
		// keyboard layout changed event
219
		ipcRenderer.on('vscode:keyboardLayoutChanged', () => {
220
			KeyboardMapperFactory.INSTANCE._onKeyboardLayoutChanged();
A
Alex Dima 已提交
221 222
		});

I
isidor 已提交
223
		// accessibility support changed event
224
		ipcRenderer.on('vscode:accessibilitySupportChanged', (event: unknown, accessibilitySupportEnabled: boolean) => {
225
			this.accessibilityService.setAccessibilitySupport(accessibilitySupportEnabled ? AccessibilitySupport.Enabled : AccessibilitySupport.Disabled);
226 227
		});

S
Sandeep Somavarapu 已提交
228 229
		// Zoom level changes
		this.updateWindowZoomLevel();
B
Benjamin Pasero 已提交
230
		this._register(this.configurationService.onDidChangeConfiguration(e => {
S
Sandeep Somavarapu 已提交
231 232
			if (e.affectsConfiguration('window.zoomLevel')) {
				this.updateWindowZoomLevel();
233
			} else if (e.affectsConfiguration('keyboard.touchbar.enabled') || e.affectsConfiguration('keyboard.touchbar.ignored')) {
234
				this.updateTouchbarMenu();
S
Sandeep Somavarapu 已提交
235 236
			}
		}));
237

238 239 240 241
		// Listen to visible editor changes
		this._register(this.editorService.onDidVisibleEditorsChange(() => this.onDidVisibleEditorsChange()));

		// Listen to editor closing (if we run with --wait)
242
		const filesToWait = this.environmentService.configuration.filesToWait;
243
		if (filesToWait) {
M
Martin Aeschlimann 已提交
244
			const waitMarkerFile = filesToWait.waitMarkerFileUri;
B
Benjamin Pasero 已提交
245
			const resourcesToWaitFor = coalesce(filesToWait.paths.map(p => p.fileUri));
246

B
Benjamin Pasero 已提交
247
			this._register(this.trackClosedWaitFiles(waitMarkerFile, resourcesToWaitFor));
248
		}
249

250
		// macOS OS integration
251
		if (isMacintosh) {
252 253 254 255
			this._register(this.editorService.onDidActiveEditorChange(() => {
				const file = toResource(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER, filterByScheme: Schemas.file });

				// Represented Filename
256
				this.updateRepresentedFilename(file?.fsPath);
257 258

				// Custom title menu
259
				this.provideCustomTitleContextMenu(file?.fsPath);
260
			}));
261
		}
262 263 264

		// Maximize/Restore on doubleclick (for macOS custom title)
		if (isMacintosh && getTitleBarStyle(this.configurationService, this.environmentService) === 'custom') {
B
Benjamin Pasero 已提交
265
			const titlePart = assertIsDefined(this.layoutService.getContainer(Parts.TITLEBAR_PART));
266 267 268 269 270 271 272

			this._register(DOM.addDisposableListener(titlePart, DOM.EventType.DBLCLICK, e => {
				DOM.EventHelper.stop(e);

				this.electronService.handleTitleDoubleClick();
			}));
		}
273

274 275 276 277 278 279
		// Document edited: indicate for dirty working copies
		this._register(this.workingCopyService.onDidChangeDirty(workingCopy => {
			const gotDirty = workingCopy.isDirty();
			if (gotDirty && !(workingCopy.capabilities & WorkingCopyCapabilities.Untitled) && this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) {
				return; // do not indicate dirty of working copies that are auto saved after short delay
			}
280

281 282
			this.updateDocumentEdited(gotDirty);
		}));
283

284
		this.updateDocumentEdited();
285

286
		// Detect minimize / maximize
287
		this._register(Event.any(
288 289
			Event.map(Event.filter(this.electronService.onWindowMaximize, id => id === this.electronService.windowId), () => true),
			Event.map(Event.filter(this.electronService.onWindowUnmaximize, id => id === this.electronService.windowId), () => false)
290 291 292 293 294
		)(e => this.onDidChangeMaximized(e)));

		this.onDidChangeMaximized(this.environmentService.configuration.maximized ?? false);
	}

295 296
	private updateDocumentEdited(isDirty = this.workingCopyService.hasDirty): void {
		if ((!this.isDocumentedEdited && isDirty) || (this.isDocumentedEdited && !isDirty)) {
297
			this.isDocumentedEdited = isDirty;
298

299
			this.electronService.setDocumentEdited(isDirty);
300 301 302
		}
	}

303 304
	private onDidChangeMaximized(maximized: boolean): void {
		this.layoutService.updateWindowMaximizedState(maximized);
305 306 307 308 309 310 311
	}

	private onDidVisibleEditorsChange(): void {

		// Close when empty: check if we should close the window based on the setting
		// Overruled by: window has a workspace opened or this window is for extension development
		// or setting is disabled. Also enabled when running with --wait from the command line.
312 313
		const visibleEditorPanes = this.editorService.visibleEditorPanes;
		if (visibleEditorPanes.length === 0 && this.contextService.getWorkbenchState() === WorkbenchState.EMPTY && !this.environmentService.isExtensionDevelopment) {
314
			const closeWhenEmpty = this.configurationService.getValue<boolean>('window.closeWhenEmpty');
315 316 317 318 319 320 321
			if (closeWhenEmpty || this.environmentService.args.wait) {
				this.closeEmptyWindowScheduler.schedule();
			}
		}
	}

	private onAllEditorsClosed(): void {
322 323
		const visibleEditorPanes = this.editorService.visibleEditorPanes.length;
		if (visibleEditorPanes === 0) {
324
			this.electronService.closeWindow();
325 326 327
		}
	}

S
Sandeep Somavarapu 已提交
328
	private updateWindowZoomLevel(): void {
329
		const windowConfig: IWindowsConfiguration = this.configurationService.getValue<IWindowsConfiguration>();
330 331 332 333

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

335 336 337
			// Leave early if the configured zoom level did not change (https://github.com/Microsoft/vscode/issues/1536)
			if (this.previousConfiguredZoomLevel === newZoomLevel) {
				return;
338
			}
339 340 341 342 343 344 345 346 347 348 349 350 351 352

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

353 354 355 356 357
	private updateRepresentedFilename(filePath: string | undefined): void {
		this.electronService.setRepresentedFilename(filePath ? filePath : '');
	}

	private provideCustomTitleContextMenu(filePath: string | undefined): void {
358 359 360 361 362

		// Clear old menu
		this.customTitleContextMenuDisposable.clear();

		// Provide new menu if a file is opened and we are on a custom title
363
		if (!filePath || getTitleBarStyle(this.configurationService, this.environmentService) !== 'custom') {
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391
			return;
		}

		// Split up filepath into segments
		const segments = filePath.split(posix.sep);
		for (let i = segments.length; i > 0; i--) {
			const isFile = (i === segments.length);

			let pathOffset = i;
			if (!isFile) {
				pathOffset++; // for segments which are not the file name we want to open the folder
			}

			const path = segments.slice(0, pathOffset).join(posix.sep);

			let label: string;
			if (!isFile) {
				label = getBaseLabel(dirname(path));
			} else {
				label = getBaseLabel(path);
			}

			const commandId = `workbench.action.revealPathInFinder${i}`;
			this.customTitleContextMenuDisposable.add(CommandsRegistry.registerCommand(commandId, () => this.electronService.showItemInFolder(path)));
			this.customTitleContextMenuDisposable.add(MenuRegistry.appendMenuItem(MenuId.TitleBarContext, { command: { id: commandId, title: label || posix.sep }, order: -i }));
		}
	}

392 393
	private create(): void {

394 395 396 397 398
		// Native menu controller
		if (isMacintosh || getTitleBarStyle(this.configurationService, this.environmentService) === 'native') {
			this._register(this.instantiationService.createInstance(NativeMenubarControl));
		}

B
Benjamin Pasero 已提交
399 400
		// Handle open calls
		this.setupOpenHandlers();
401

B
Benjamin Pasero 已提交
402
		// Emit event when vscode is ready
403
		this.lifecycleService.when(LifecyclePhase.Ready).then(() => ipcRenderer.send('vscode:workbenchReady', this.electronService.windowId));
404

405 406 407 408
		// Integrity warning
		this.integrityService.isPure().then(res => this.titleService.updateProperties({ isPure: res.isPure }));

		// Root warning
409 410
		this.lifecycleService.when(LifecyclePhase.Restored).then(async () => {
			let isAdmin: boolean;
411
			if (isWindows) {
412
				isAdmin = (await import('native-is-elevated'))();
413
			} else {
414
				isAdmin = isRootUser();
415 416
			}

417 418
			// Update title
			this.titleService.updateProperties({ isAdmin });
419

420 421 422 423
			// Show warning message (unix only)
			if (isAdmin && !isWindows) {
				this.notificationService.warn(nls.localize('runningAsRoot', "It is not recommended to run {0} as root user.", product.nameShort));
			}
424
		});
425 426 427

		// Touchbar menu (if enabled)
		this.updateTouchbarMenu();
428 429

		// Crash reporter (if enabled)
430 431 432 433 434 435 436 437 438 439 440 441 442 443
		if (!this.environmentService.disableCrashReporter && this.configurationService.getValue('telemetry.enableCrashReporter')) {
			const companyName = product.crashReporter?.companyName || 'Microsoft';
			const productName = product.crashReporter?.productName || product.nameShort;

			// With appCenter enabled, crashes will be uploaded
			if (product.appCenter) {
				this.setupCrashReporter(companyName, productName, product.appCenter, undefined);
			}

			// With a provided crash reporter directory, crashes
			// will be stored only locally in that folder
			else if (this.environmentService.crashReporterDirectory) {
				this.setupCrashReporter(companyName, productName, undefined, this.environmentService.crashReporterDirectory);
			}
444
		}
445 446
	}

B
Benjamin Pasero 已提交
447 448
	private setupOpenHandlers(): void {

449 450
		// Block window.open() calls
		window.open = function (): Window | null {
B
Benjamin Pasero 已提交
451
			throw new Error('Prevented call to window.open(). Use IOpenerService instead!');
B
Benjamin Pasero 已提交
452 453
		};

454 455 456 457 458 459 460
		// Handle external open() calls
		this.openerService.setExternalOpener({
			openExternal: async (href: string) => {
				const success = await this.electronService.openExternal(href);
				if (!success) {
					const fileCandidate = URI.parse(href);
					if (fileCandidate.scheme === Schemas.file) {
461
						// if opening failed, and this is a file, we can still try to reveal it
462
						await this.electronService.showItemInFolder(fileCandidate.fsPath);
463
					}
B
Benjamin Pasero 已提交
464 465
				}

466
				return true;
B
Benjamin Pasero 已提交
467 468
			}
		});
469

470
		// Register external URI resolver
471
		this.openerService.registerExternalUriResolver({
472
			resolveExternalUri: async (uri: URI, options?: OpenOptions) => {
B
Benjamin Pasero 已提交
473
				if (options?.allowTunneling) {
474 475
					const portMappingRequest = extractLocalHostUriMetaDataForPortMapping(uri);
					if (portMappingRequest) {
476
						const tunnel = await this.tunnelService.openTunnel(undefined, portMappingRequest.port);
477
						if (tunnel) {
478 479 480 481
							return {
								resolved: uri.with({ authority: `127.0.0.1:${tunnel.tunnelLocalPort}` }),
								dispose: () => tunnel.dispose(),
							};
482 483 484
						}
					}
				}
485
				return undefined;
486 487
			}
		});
B
Benjamin Pasero 已提交
488 489
	}

490
	private updateTouchbarMenu(): void {
491 492
		if (!isMacintosh) {
			return; // macOS only
493 494
		}

495
		// Dispose old
496
		this.touchBarDisposables.clear();
R
Rob Lourens 已提交
497
		this.touchBarMenu = undefined;
498

499
		// Create new (delayed)
B
Benjamin Pasero 已提交
500 501
		const scheduler: RunOnceScheduler = this.touchBarDisposables.add(new RunOnceScheduler(() => this.doUpdateTouchbarMenu(scheduler), 300));
		scheduler.schedule();
502 503
	}

B
Benjamin Pasero 已提交
504
	private doUpdateTouchbarMenu(scheduler: RunOnceScheduler): void {
505 506
		if (!this.touchBarMenu) {
			this.touchBarMenu = this.editorService.invokeWithinEditorContext(accessor => this.menuService.createMenu(MenuId.TouchBarContext, accessor.get(IContextKeyService)));
507
			this.touchBarDisposables.add(this.touchBarMenu);
B
Benjamin Pasero 已提交
508
			this.touchBarDisposables.add(this.touchBarMenu.onDidChange(() => scheduler.schedule()));
509 510
		}

511
		const actions: Array<MenuItemAction | Separator> = [];
512

513 514 515
		const disabled = this.configurationService.getValue<boolean>('keyboard.touchbar.enabled') === false;
		const ignoredItems = this.configurationService.getValue<string[]>('keyboard.touchbar.ignored') || [];

516
		// Fill actions into groups respecting order
517
		this.touchBarDisposables.add(createAndFillInActionBarActions(this.touchBarMenu, undefined, actions));
518 519 520 521

		// Convert into command action multi array
		const items: ICommandAction[][] = [];
		let group: ICommandAction[] = [];
522 523 524 525 526 527 528 529
		if (!disabled) {
			for (const action of actions) {

				// Command
				if (action instanceof MenuItemAction) {
					if (ignoredItems.indexOf(action.item.id) >= 0) {
						continue; // ignored
					}
530

531
					group.push(action.item);
532 533
				}

534 535 536 537 538
				// Separator
				else if (action instanceof Separator) {
					if (group.length) {
						items.push(group);
					}
539

540
					group = [];
541 542
				}
			}
543

544 545 546
			if (group.length) {
				items.push(group);
			}
547 548 549
		}

		// Only update if the actions have changed
550
		if (!equals(this.lastInstalledTouchedBar, items)) {
551
			this.lastInstalledTouchedBar = items;
552
			this.electronService.updateTouchBar(items);
553
		}
554 555
	}

556 557 558 559 560 561
	private async setupCrashReporter(companyName: string, productName: string, appCenter: typeof product.appCenter, crashesDirectory: undefined): Promise<void>;
	private async setupCrashReporter(companyName: string, productName: string, appCenter: undefined, crashesDirectory: string): Promise<void>;
	private async setupCrashReporter(companyName: string, productName: string, appCenter: typeof product.appCenter | undefined, crashesDirectory: string | undefined): Promise<void> {
		let submitURL: string | undefined = undefined;
		if (appCenter) {
			submitURL = isWindows ? appCenter[process.arch === 'ia32' ? 'win32-ia32' : 'win32-x64'] : isLinux ? appCenter[`linux-x64`] : appCenter.darwin;
562
		}
563

R
Robo 已提交
564 565 566
		const info = await this.telemetryService.getTelemetryInfo();
		const crashReporterId = this.storageService.get(crashReporterIdStorageKey, StorageScope.GLOBAL)!;

567
		// base options with product info
R
Robo 已提交
568
		const options: CrashReporterStartOptions = {
569 570
			companyName,
			productName,
571
			submitURL: (submitURL?.concat('&uid=', crashReporterId, '&iid=', crashReporterId, '&sid=', info.sessionId)) || '',
572
			extra: {
573
				vscode_version: product.version,
R
Robo 已提交
574
				vscode_commit: product.commit || ''
575 576 577 578
			},

			// If `crashesDirectory` is specified, we do not upload
			uploadToServer: !crashesDirectory,
579 580
		};

R
Robo 已提交
581 582 583 584 585 586
		// start crash reporter in the main process first.
		// On windows crashpad excepts a name pipe for the client to connect,
		// this pipe is created by crash reporter initialization from the main process,
		// changing this order of initialization will cause issues.
		// For more info: https://chromium.googlesource.com/crashpad/crashpad/+/HEAD/doc/overview_design.md#normal-registration
		await this.electronService.startCrashReporter(options);
587

588 589
		// start crash reporter right here
		crashReporter.start(deepClone(options));
590 591
	}

592 593 594
	private onAddFoldersRequest(request: IAddFoldersRequest): void {

		// Buffer all pending requests
595
		this.pendingFoldersToAdd.push(...request.foldersToAdd.map(f => URI.revive(f)));
596 597 598 599 600 601 602 603 604 605

		// Delay the adding of folders a bit to buffer in case more requests are coming
		if (!this.addFoldersScheduler.isScheduled()) {
			this.addFoldersScheduler.schedule();
		}
	}

	private doAddFolders(): void {
		const foldersToAdd: IWorkspaceFolderCreationData[] = [];

606 607
		this.pendingFoldersToAdd.forEach(folder => {
			foldersToAdd.push(({ uri: folder }));
608 609 610
		});

		this.pendingFoldersToAdd = [];
611

612
		this.workspaceEditingService.addFolders(foldersToAdd);
613 614
	}

615
	private async onOpenFiles(request: INativeOpenFileRequest): Promise<void> {
616
		const inputs: IResourceEditorInputType[] = [];
617
		const diffMode = !!(request.filesToDiff && (request.filesToDiff.length === 2));
618

619 620
		if (!diffMode && request.filesToOpenOrCreate) {
			inputs.push(...(await pathsToEditors(request.filesToOpenOrCreate, this.fileService)));
621 622
		}

623
		if (diffMode && request.filesToDiff) {
624
			inputs.push(...(await pathsToEditors(request.filesToDiff, this.fileService)));
625 626 627
		}

		if (inputs.length) {
628
			this.openResources(inputs, diffMode);
629
		}
630 631 632 633 634

		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.
M
Martin Aeschlimann 已提交
635
			const waitMarkerFile = URI.revive(request.filesToWait.waitMarkerFileUri);
B
Benjamin Pasero 已提交
636 637 638 639 640 641
			const resourcesToWaitFor = coalesce(request.filesToWait.paths.map(p => URI.revive(p.fileUri)));
			this.trackClosedWaitFiles(waitMarkerFile, resourcesToWaitFor);
		}
	}

	private trackClosedWaitFiles(waitMarkerFile: URI, resourcesToWaitFor: URI[]): IDisposable {
642 643
		let remainingResourcesToWaitFor = resourcesToWaitFor.slice(0);

644 645 646
		// 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.
647
		const listener = this.editorService.onDidCloseEditor(async event => {
B
Benjamin Pasero 已提交
648 649 650 651 652
			const detailsResource = toResource(event.editor, { supportSideBySide: SideBySideEditor.DETAILS });
			const masterResource = toResource(event.editor, { supportSideBySide: SideBySideEditor.MASTER });

			// Remove from resources to wait for based on the
			// resources from editors that got closed
653
			remainingResourcesToWaitFor = remainingResourcesToWaitFor.filter(resourceToWaitFor => {
B
Benjamin Pasero 已提交
654 655 656
				if (isEqual(resourceToWaitFor, masterResource) || isEqual(resourceToWaitFor, detailsResource)) {
					return false; // remove - the closing editor matches this resource
				}
657

B
Benjamin Pasero 已提交
658 659
				return true; // keep - not yet closed
			});
660

661
			if (remainingResourcesToWaitFor.length === 0) {
B
Benjamin Pasero 已提交
662 663 664 665
				// If auto save is configured with the default delay (1s) it is possible
				// to close the editor while the save still continues in the background. As such
				// we have to also check if the files to wait for are dirty and if so wait
				// for them to get saved before deleting the wait marker file.
666
				const dirtyFilesToWait = resourcesToWaitFor.filter(resourceToWaitFor => this.workingCopyService.isDirty(resourceToWaitFor));
B
Benjamin Pasero 已提交
667 668 669 670 671 672 673 674 675 676 677 678 679 680
				if (dirtyFilesToWait.length > 0) {
					await Promise.all(dirtyFilesToWait.map(async dirtyFileToWait => await this.joinResourceSaved(dirtyFileToWait)));
				}

				listener.dispose();
				await this.fileService.del(waitMarkerFile);
			}
		});

		return listener;
	}

	private joinResourceSaved(resource: URI): Promise<void> {
		return new Promise(resolve => {
681
			if (!this.workingCopyService.isDirty(resource)) {
B
Benjamin Pasero 已提交
682 683 684 685
				return resolve(); // return early if resource is not dirty
			}

			// Otherwise resolve promise when resource is saved
686 687
			const listener = this.workingCopyService.onDidChangeDirty(workingCopy => {
				if (!workingCopy.isDirty() && isEqual(resource, workingCopy.resource)) {
B
Benjamin Pasero 已提交
688 689 690
					listener.dispose();

					resolve();
691 692
				}
			});
B
Benjamin Pasero 已提交
693
		});
694 695
	}

696
	private async openResources(resources: Array<IResourceEditorInput | IUntitledTextResourceEditorInput>, diffMode: boolean): Promise<unknown> {
697
		await this.lifecycleService.when(LifecyclePhase.Ready);
N
Nick Snyder 已提交
698

699
		// In diffMode we open 2 resources as diff
700 701
		if (diffMode && resources.length === 2 && resources[0].resource && resources[1].resource) {
			return this.editorService.openEditor({ leftResource: resources[0].resource, rightResource: resources[1].resource, options: { pinned: true } });
702
		}
703

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

709 710
		// Otherwise open all
		return this.editorService.openEditors(resources);
711
	}
J
Johannes Rieken 已提交
712
}
713 714 715 716

class NativeMenubarControl extends MenubarControl {
	constructor(
		@IMenuService menuService: IMenuService,
717
		@IWorkspacesService workspacesService: IWorkspacesService,
718 719 720 721 722 723 724 725
		@IContextKeyService contextKeyService: IContextKeyService,
		@IKeybindingService keybindingService: IKeybindingService,
		@IConfigurationService configurationService: IConfigurationService,
		@ILabelService labelService: ILabelService,
		@IUpdateService updateService: IUpdateService,
		@IStorageService storageService: IStorageService,
		@INotificationService notificationService: INotificationService,
		@IPreferencesService preferencesService: IPreferencesService,
726
		@IWorkbenchEnvironmentService protected readonly environmentService: INativeWorkbenchEnvironmentService,
727
		@IAccessibilityService accessibilityService: IAccessibilityService,
728
		@IMenubarService private readonly menubarService: IMenubarService,
729
		@IHostService hostService: IHostService,
730
		@IElectronService private readonly electronService: IElectronService
731 732 733
	) {
		super(
			menuService,
734
			workspacesService,
735 736 737 738 739 740 741 742 743
			contextKeyService,
			keybindingService,
			configurationService,
			labelService,
			updateService,
			storageService,
			notificationService,
			preferencesService,
			environmentService,
744 745 746
			accessibilityService,
			hostService
		);
747

748
		if (isMacintosh) {
749 750 751 752 753 754 755 756 757 758 759
			this.menus['Preferences'] = this._register(this.menuService.createMenu(MenuId.MenubarPreferencesMenu, this.contextKeyService));
			this.topLevelTitles['Preferences'] = nls.localize('mPreferences', "Preferences");
		}

		for (const topLevelMenuName of Object.keys(this.topLevelTitles)) {
			const menu = this.menus[topLevelMenuName];
			if (menu) {
				this._register(menu.onDidChange(() => this.updateMenubar()));
			}
		}

760
		(async () => {
761
			this.recentlyOpened = await this.workspacesService.getRecentlyOpened();
762 763

			this.doUpdateMenubar(true);
764
		})();
765 766 767 768 769

		this.registerListeners();
	}

	protected doUpdateMenubar(firstTime: boolean): void {
S
SteVen Batten 已提交
770 771 772 773 774
		// Since the native menubar is shared between windows (main process)
		// only allow the focused window to update the menubar
		if (!this.hostService.hasFocus) {
			return;
		}
775 776 777 778

		// Send menus to main process to be rendered by Electron
		const menubarData = { menus: {}, keybindings: {} };
		if (this.getMenubarMenus(menubarData)) {
779
			this.menubarService.updateMenubar(this.electronService.windowId, menubarData);
780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813
		}
	}

	private getMenubarMenus(menubarData: IMenubarData): boolean {
		if (!menubarData) {
			return false;
		}

		menubarData.keybindings = this.getAdditionalKeybindings();
		for (const topLevelMenuName of Object.keys(this.topLevelTitles)) {
			const menu = this.menus[topLevelMenuName];
			if (menu) {
				const menubarMenu: IMenubarMenu = { items: [] };
				this.populateMenuItems(menu, menubarMenu, menubarData.keybindings);
				if (menubarMenu.items.length === 0) {
					return false; // Menus are incomplete
				}
				menubarData.menus[topLevelMenuName] = menubarMenu;
			}
		}

		return true;
	}

	private populateMenuItems(menu: IMenu, menuToPopulate: IMenubarMenu, keybindings: { [id: string]: IMenubarKeybinding | undefined }) {
		let groups = menu.getActions();
		for (let group of groups) {
			const [, actions] = group;

			actions.forEach(menuItem => {

				if (menuItem instanceof SubmenuItemAction) {
					const submenu = { items: [] };

J
Johannes Rieken 已提交
814 815
					if (!this.menus[menuItem.item.submenu.id]) {
						const menu = this.menus[menuItem.item.submenu.id] = this.menuService.createMenu(menuItem.item.submenu, this.contextKeyService);
816
						this._register(menu.onDidChange(() => this.updateMenubar()));
817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907
					}

					const menuToDispose = this.menuService.createMenu(menuItem.item.submenu, this.contextKeyService);
					this.populateMenuItems(menuToDispose, submenu, keybindings);

					let menubarSubmenuItem: IMenubarMenuItemSubmenu = {
						id: menuItem.id,
						label: menuItem.label,
						submenu: submenu
					};

					menuToPopulate.items.push(menubarSubmenuItem);
					menuToDispose.dispose();
				} else {
					if (menuItem.id === 'workbench.action.openRecent') {
						const actions = this.getOpenRecentActions().map(this.transformOpenRecentAction);
						menuToPopulate.items.push(...actions);
					}

					let menubarMenuItem: IMenubarMenuItemAction = {
						id: menuItem.id,
						label: menuItem.label
					};

					if (menuItem.checked) {
						menubarMenuItem.checked = true;
					}

					if (!menuItem.enabled) {
						menubarMenuItem.enabled = false;
					}

					menubarMenuItem.label = this.calculateActionLabel(menubarMenuItem);
					keybindings[menuItem.id] = this.getMenubarKeybinding(menuItem.id);
					menuToPopulate.items.push(menubarMenuItem);
				}
			});

			menuToPopulate.items.push({ id: 'vscode.menubar.separator' });
		}

		if (menuToPopulate.items.length > 0) {
			menuToPopulate.items.pop();
		}
	}

	private transformOpenRecentAction(action: Separator | (IAction & { uri: URI })): MenubarMenuItem {
		if (action instanceof Separator) {
			return { id: 'vscode.menubar.separator' };
		}

		return {
			id: action.id,
			uri: action.uri,
			enabled: action.enabled,
			label: action.label
		};
	}

	private getAdditionalKeybindings(): { [id: string]: IMenubarKeybinding } {
		const keybindings: { [id: string]: IMenubarKeybinding } = {};
		if (isMacintosh) {
			const keybinding = this.getMenubarKeybinding('workbench.action.quit');
			if (keybinding) {
				keybindings['workbench.action.quit'] = keybinding;
			}
		}

		return keybindings;
	}

	private getMenubarKeybinding(id: string): IMenubarKeybinding | undefined {
		const binding = this.keybindingService.lookupKeybinding(id);
		if (!binding) {
			return undefined;
		}

		// first try to resolve a native accelerator
		const electronAccelerator = binding.getElectronAccelerator();
		if (electronAccelerator) {
			return { label: electronAccelerator, userSettingsLabel: withNullAsUndefined(binding.getUserSettingsLabel()) };
		}

		// we need this fallback to support keybindings that cannot show in electron menus (e.g. chords)
		const acceleratorLabel = binding.getLabel();
		if (acceleratorLabel) {
			return { label: acceleratorLabel, isNative: false, userSettingsLabel: withNullAsUndefined(binding.getUserSettingsLabel()) };
		}

		return undefined;
	}
B
Benjamin Pasero 已提交
908
}