window.ts 33.9 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, IAddFoldersRequest } from 'vs/platform/windows/common/windows';
import { IRunActionInWindowRequest, IRunKeybindingInWindowRequest, 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';
B
Benjamin Pasero 已提交
32
import { 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';
46 47 48 49
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 已提交
50
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
51
import { IPreferencesService } from '../services/preferences/common/preferences';
B
Benjamin Pasero 已提交
52 53
import { IMenubarData, IMenubarMenu, IMenubarKeybinding, IMenubarMenuItemSubmenu, IMenubarMenuItemAction, MenubarMenuItem } from 'vs/platform/menubar/common/menubar';
import { IMenubarService } from 'vs/platform/menubar/electron-sandbox/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) {
B
Benjamin Pasero 已提交
244
			this.trackClosedWaitFiles(filesToWait.waitMarkerFileUri, coalesce(filesToWait.paths.map(path => path.fileUri)));
245
		}
246

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

				// Represented Filename
253
				this.updateRepresentedFilename(file?.fsPath);
254 255

				// Custom title menu
256
				this.provideCustomTitleContextMenu(file?.fsPath);
257
			}));
258
		}
259 260 261

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

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

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

271 272 273 274 275 276
		// 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
			}
277

278 279
			this.updateDocumentEdited(gotDirty);
		}));
280

281
		this.updateDocumentEdited();
282

283
		// Detect minimize / maximize
284
		this._register(Event.any(
285 286
			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)
287 288 289 290 291
		)(e => this.onDidChangeMaximized(e)));

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

292 293
	private updateDocumentEdited(isDirty = this.workingCopyService.hasDirty): void {
		if ((!this.isDocumentedEdited && isDirty) || (this.isDocumentedEdited && !isDirty)) {
294
			this.isDocumentedEdited = isDirty;
295

296
			this.electronService.setDocumentEdited(isDirty);
297 298 299
		}
	}

300 301
	private onDidChangeMaximized(maximized: boolean): void {
		this.layoutService.updateWindowMaximizedState(maximized);
302 303 304 305 306 307 308
	}

	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.
309 310
		const visibleEditorPanes = this.editorService.visibleEditorPanes;
		if (visibleEditorPanes.length === 0 && this.contextService.getWorkbenchState() === WorkbenchState.EMPTY && !this.environmentService.isExtensionDevelopment) {
311
			const closeWhenEmpty = this.configurationService.getValue<boolean>('window.closeWhenEmpty');
312 313 314 315 316 317 318
			if (closeWhenEmpty || this.environmentService.args.wait) {
				this.closeEmptyWindowScheduler.schedule();
			}
		}
	}

	private onAllEditorsClosed(): void {
319 320
		const visibleEditorPanes = this.editorService.visibleEditorPanes.length;
		if (visibleEditorPanes === 0) {
321
			this.electronService.closeWindow();
322 323 324
		}
	}

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

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

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

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

350 351 352 353 354
	private updateRepresentedFilename(filePath: string | undefined): void {
		this.electronService.setRepresentedFilename(filePath ? filePath : '');
	}

	private provideCustomTitleContextMenu(filePath: string | undefined): void {
355 356 357 358 359

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

		// Provide new menu if a file is opened and we are on a custom title
360
		if (!filePath || getTitleBarStyle(this.configurationService, this.environmentService) !== 'custom') {
361 362 363 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
			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 }));
		}
	}

389 390
	private create(): void {

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

B
Benjamin Pasero 已提交
396 397
		// Handle open calls
		this.setupOpenHandlers();
398

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

402 403 404 405
		// Integrity warning
		this.integrityService.isPure().then(res => this.titleService.updateProperties({ isPure: res.isPure }));

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

414 415
			// Update title
			this.titleService.updateProperties({ isAdmin });
416

417 418 419 420
			// 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));
			}
421
		});
422 423 424

		// Touchbar menu (if enabled)
		this.updateTouchbarMenu();
425 426

		// Crash reporter (if enabled)
427 428 429 430 431 432
		if (!this.environmentService.disableCrashReporter && this.configurationService.getValue('telemetry.enableCrashReporter')) {
			const companyName = product.crashReporter?.companyName || 'Microsoft';
			const productName = product.crashReporter?.productName || product.nameShort;

			// With a provided crash reporter directory, crashes
			// will be stored only locally in that folder
433
			if (this.environmentService.crashReporterDirectory) {
434 435
				this.setupCrashReporter(companyName, productName, undefined, this.environmentService.crashReporterDirectory);
			}
436 437 438 439 440

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

B
Benjamin Pasero 已提交
444 445
	private setupOpenHandlers(): void {

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

451 452 453 454 455 456 457
		// 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) {
458
						// if opening failed, and this is a file, we can still try to reveal it
459
						await this.electronService.showItemInFolder(fileCandidate.fsPath);
460
					}
B
Benjamin Pasero 已提交
461 462
				}

463
				return true;
B
Benjamin Pasero 已提交
464 465
			}
		});
466

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

487
	private updateTouchbarMenu(): void {
488 489
		if (!isMacintosh) {
			return; // macOS only
490 491
		}

492
		// Dispose old
493
		this.touchBarDisposables.clear();
R
Rob Lourens 已提交
494
		this.touchBarMenu = undefined;
495

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

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

508
		const actions: Array<MenuItemAction | Separator> = [];
509

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

513
		// Fill actions into groups respecting order
514
		this.touchBarDisposables.add(createAndFillInActionBarActions(this.touchBarMenu, undefined, actions));
515 516 517 518

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

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

528
					group.push(action.item);
529 530
				}

531 532 533 534 535
				// Separator
				else if (action instanceof Separator) {
					if (group.length) {
						items.push(group);
					}
536

537
					group = [];
538 539
				}
			}
540

541 542 543
			if (group.length) {
				items.push(group);
			}
544 545 546
		}

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

553 554 555 556 557 558
	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;
559
		}
560

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

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

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

R
Robo 已提交
578 579 580 581 582 583
		// 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);
584

585 586
		// start crash reporter right here
		crashReporter.start(deepClone(options));
587 588
	}

589 590 591
	private onAddFoldersRequest(request: IAddFoldersRequest): void {

		// Buffer all pending requests
B
Benjamin Pasero 已提交
592
		this.pendingFoldersToAdd.push(...request.foldersToAdd.map(folder => URI.revive(folder)));
593 594 595 596 597 598 599 600 601 602

		// 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[] = [];

603 604
		this.pendingFoldersToAdd.forEach(folder => {
			foldersToAdd.push(({ uri: folder }));
605 606 607
		});

		this.pendingFoldersToAdd = [];
608

609
		this.workspaceEditingService.addFolders(foldersToAdd);
610 611
	}

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

616 617
		if (!diffMode && request.filesToOpenOrCreate) {
			inputs.push(...(await pathsToEditors(request.filesToOpenOrCreate, this.fileService)));
618 619
		}

620
		if (diffMode && request.filesToDiff) {
621
			inputs.push(...(await pathsToEditors(request.filesToDiff, this.fileService)));
622 623 624
		}

		if (inputs.length) {
625
			this.openResources(inputs, diffMode);
626
		}
627 628 629 630 631

		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.
B
Benjamin Pasero 已提交
632
			this.trackClosedWaitFiles(URI.revive(request.filesToWait.waitMarkerFileUri), coalesce(request.filesToWait.paths.map(p => URI.revive(p.fileUri))));
B
Benjamin Pasero 已提交
633 634 635
		}
	}

B
Benjamin Pasero 已提交
636
	private async trackClosedWaitFiles(waitMarkerFile: URI, resourcesToWaitFor: URI[]): Promise<void> {
B
Benjamin Pasero 已提交
637

B
Benjamin Pasero 已提交
638 639
		// Wait for the resources to be closed in the editor...
		await this.editorService.whenClosed(resourcesToWaitFor);
B
Benjamin Pasero 已提交
640

B
Benjamin Pasero 已提交
641 642
		// ...before deleting the wait marker file
		await this.fileService.del(waitMarkerFile);
643 644
	}

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

648
		// In diffMode we open 2 resources as diff
649 650
		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 } });
651
		}
652

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

658 659
		// Otherwise open all
		return this.editorService.openEditors(resources);
660
	}
J
Johannes Rieken 已提交
661
}
662 663 664 665

class NativeMenubarControl extends MenubarControl {
	constructor(
		@IMenuService menuService: IMenuService,
666
		@IWorkspacesService workspacesService: IWorkspacesService,
667 668 669 670 671 672 673 674
		@IContextKeyService contextKeyService: IContextKeyService,
		@IKeybindingService keybindingService: IKeybindingService,
		@IConfigurationService configurationService: IConfigurationService,
		@ILabelService labelService: ILabelService,
		@IUpdateService updateService: IUpdateService,
		@IStorageService storageService: IStorageService,
		@INotificationService notificationService: INotificationService,
		@IPreferencesService preferencesService: IPreferencesService,
675
		@IWorkbenchEnvironmentService protected readonly environmentService: INativeWorkbenchEnvironmentService,
676
		@IAccessibilityService accessibilityService: IAccessibilityService,
677
		@IMenubarService private readonly menubarService: IMenubarService,
678
		@IHostService hostService: IHostService,
679
		@IElectronService private readonly electronService: IElectronService
680 681 682
	) {
		super(
			menuService,
683
			workspacesService,
684 685 686 687 688 689 690 691 692
			contextKeyService,
			keybindingService,
			configurationService,
			labelService,
			updateService,
			storageService,
			notificationService,
			preferencesService,
			environmentService,
693 694 695
			accessibilityService,
			hostService
		);
696

697
		if (isMacintosh) {
698 699 700 701 702 703 704 705 706 707 708
			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()));
			}
		}

709
		(async () => {
710
			this.recentlyOpened = await this.workspacesService.getRecentlyOpened();
711 712

			this.doUpdateMenubar(true);
713
		})();
714 715 716 717 718

		this.registerListeners();
	}

	protected doUpdateMenubar(firstTime: boolean): void {
S
SteVen Batten 已提交
719 720 721 722 723
		// Since the native menubar is shared between windows (main process)
		// only allow the focused window to update the menubar
		if (!this.hostService.hasFocus) {
			return;
		}
724 725 726 727

		// Send menus to main process to be rendered by Electron
		const menubarData = { menus: {}, keybindings: {} };
		if (this.getMenubarMenus(menubarData)) {
728
			this.menubarService.updateMenubar(this.electronService.windowId, menubarData);
729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762
		}
	}

	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 已提交
763 764
					if (!this.menus[menuItem.item.submenu.id]) {
						const menu = this.menus[menuItem.item.submenu.id] = this.menuService.createMenu(menuItem.item.submenu, this.contextKeyService);
765
						this._register(menu.onDidChange(() => this.updateMenubar()));
766 767 768 769 770 771 772 773 774 775 776 777 778 779 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 814 815 816 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
					}

					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 已提交
857
}