window.ts 34.5 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';
9
import { equals, deepClone, assign } 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';
J
Johannes Rieken 已提交
16
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
17
import { IWindowSettings, IOpenFileRequest, IWindowsConfiguration, IAddFoldersRequest, IRunActionInWindowRequest, IRunKeybindingInWindowRequest, getTitleBarStyle } from 'vs/platform/windows/common/windows';
B
Benjamin Pasero 已提交
18
import { ITitleService } from 'vs/workbench/services/title/common/titleService';
19
import { IWorkbenchThemeService, VS_HC_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService';
20
import * as browser from 'vs/base/browser/browser';
21
import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands';
22
import { IResourceEditorInput } from 'vs/platform/editor/common/editor';
P
Peng Lyu 已提交
23
import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron-browser/nativeKeymapService';
R
Robo 已提交
24
import { ipcRenderer as ipc, webFrame, crashReporter, CrashReporterStartOptions, Event as IpcEvent } from 'electron';
B
Benjamin Pasero 已提交
25
import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing';
26
import { IMenuService, MenuId, IMenu, MenuItemAction, ICommandAction, SubmenuItemAction, MenuRegistry } from 'vs/platform/actions/common/actions';
27
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
28
import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
29
import { RunOnceScheduler } from 'vs/base/common/async';
30
import { IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
31
import { LifecyclePhase, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
32
import { IWorkspaceFolderCreationData, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
33
import { IIntegrityService } from 'vs/workbench/services/integrity/common/integrity';
34
import { isRootUser, isWindows, isMacintosh, isLinux } from 'vs/base/common/platform';
35
import product from 'vs/platform/product/common/product';
36
import { INotificationService } from 'vs/platform/notification/common/notification';
B
Benjamin Pasero 已提交
37
import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
38
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
39
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
40
import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility';
41
import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
B
Benjamin Pasero 已提交
42
import { coalesce } from 'vs/base/common/arrays';
43
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
B
Benjamin Pasero 已提交
44
import { isEqual } from 'vs/base/common/resources';
45 46 47 48 49 50 51
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';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IPreferencesService } from '../services/preferences/common/preferences';
import { IMenubarService, IMenubarData, IMenubarMenu, IMenubarKeybinding, IMenubarMenuItemSubmenu, IMenubarMenuItemAction, MenubarMenuItem } from 'vs/platform/menubar/node/menubar';
B
Benjamin Pasero 已提交
52
import { withNullAsUndefined, assertIsDefined } from 'vs/base/common/types';
M
Matt Bierner 已提交
53
import { IOpenerService, OpenOptions } from 'vs/platform/opener/common/opener';
B
Benjamin Pasero 已提交
54
import { Schemas } from 'vs/base/common/network';
55
import { IElectronService } from 'vs/platform/electron/node/electron';
56 57
import { posix, dirname } from 'vs/base/common/path';
import { getBaseLabel } from 'vs/base/common/labels';
58
import { ITunnelService, extractLocalHostUriMetaDataForPortMapping } from 'vs/platform/remote/common/tunnel';
59
import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService';
60
import { IHostService } from 'vs/workbench/services/host/browser/host';
61 62
import { IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
63
import { Event } from 'vs/base/common/event';
64
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
65
import { clearAllFontInfos } from 'vs/editor/browser/config/configuration';
S
SteVen Batten 已提交
66

67
export class NativeWindow extends Disposable {
68

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

73
	private readonly customTitleContextMenuDisposable = this._register(new DisposableStore());
74

B
Benjamin Pasero 已提交
75
	private previousConfiguredZoomLevel: number | undefined;
76

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

80 81 82
	private readonly closeEmptyWindowScheduler: RunOnceScheduler = this._register(new RunOnceScheduler(() => this.onAllEditorsClosed(), 50));

	private isDocumentedEdited = false;
83

E
Erich Gamma 已提交
84
	constructor(
85
		@IEditorService private readonly editorService: EditorServiceImpl,
86
		@IConfigurationService private readonly configurationService: IConfigurationService,
87
		@ITitleService private readonly titleService: ITitleService,
88
		@IWorkbenchThemeService protected themeService: IWorkbenchThemeService,
89 90 91 92 93 94 95 96
		@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,
97
		@IIntegrityService private readonly integrityService: IIntegrityService,
98
		@IWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService,
99
		@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
B
Benjamin Pasero 已提交
100
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
B
Benjamin Pasero 已提交
101
		@IInstantiationService private readonly instantiationService: IInstantiationService,
102
		@IOpenerService private readonly openerService: IOpenerService,
103 104
		@IElectronService private readonly electronService: IElectronService,
		@ITunnelService private readonly tunnelService: ITunnelService,
105
		@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
106 107
		@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
		@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService
E
Erich Gamma 已提交
108
	) {
109
		super();
110

E
Erich Gamma 已提交
111
		this.registerListeners();
112
		this.create();
E
Erich Gamma 已提交
113 114 115 116
	}

	private registerListeners(): void {

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

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

127
		// Support runAction event
128
		ipc.on('vscode:runAction', async (event: IpcEvent, request: IRunActionInWindowRequest) => {
129
			const args: unknown[] = request.args || [];
130 131 132 133

			// 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 已提交
134
				const activeEditor = this.editorService.activeEditor;
135
				if (activeEditor) {
B
Benjamin Pasero 已提交
136
					const resource = toResource(activeEditor, { supportSideBySide: SideBySideEditor.MASTER });
137 138 139 140 141 142 143 144
					if (resource) {
						args.push(resource);
					}
				}
			} else {
				args.push({ from: request.from }); // TODO@telemetry this is a bit weird to send this to every action?
			}

145 146 147
			try {
				await this.commandService.executeCommand(request.id, ...args);

148 149 150 151 152
				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 });
153 154 155
			} catch (error) {
				this.notificationService.error(error);
			}
156 157
		});

158
		// Support runKeybinding event
159
		ipc.on('vscode:runKeybinding', (event: IpcEvent, request: IRunKeybindingInWindowRequest) => {
160 161 162
			if (document.activeElement) {
				this.keybindingService.dispatchByUserSettingsLabel(request.userSettingsLabel, document.activeElement);
			}
163 164
		});

B
Benjamin Pasero 已提交
165
		// Error reporting from main
166
		ipc.on('vscode:reportError', (event: IpcEvent, error: string) => {
167
			if (error) {
168
				errors.onUnexpectedError(JSON.parse(error));
169 170 171 172
			}
		});

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

175
		// Support addFolders event if we have a workspace opened
176
		ipc.on('vscode:addFolders', (event: IpcEvent, request: IAddFoldersRequest) => this.onAddFoldersRequest(request));
177

178
		// Message support
179
		ipc.on('vscode:showInfoMessage', (event: IpcEvent, message: string) => {
180
			this.notificationService.info(message);
181 182
		});

183 184 185 186
		ipc.on('vscode:displayChanged', (event: IpcEvent) => {
			clearAllFontInfos();
		});

187
		// Fullscreen Events
188 189 190
		ipc.on('vscode:enterFullScreen', async () => {
			await this.lifecycleService.when(LifecyclePhase.Ready);
			browser.setFullscreen(true);
191 192
		});

193 194 195
		ipc.on('vscode:leaveFullScreen', async () => {
			await this.lifecycleService.when(LifecyclePhase.Ready);
			browser.setFullscreen(false);
196 197 198
		});

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

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

A
Alex Dima 已提交
215
		// keyboard layout changed event
216
		ipc.on('vscode:keyboardLayoutChanged', () => {
217
			KeyboardMapperFactory.INSTANCE._onKeyboardLayoutChanged();
A
Alex Dima 已提交
218 219
		});

I
isidor 已提交
220
		// accessibility support changed event
221
		ipc.on('vscode:accessibilitySupportChanged', (event: IpcEvent, accessibilitySupportEnabled: boolean) => {
222
			this.accessibilityService.setAccessibilitySupport(accessibilitySupportEnabled ? AccessibilitySupport.Enabled : AccessibilitySupport.Disabled);
223 224
		});

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

235 236 237 238
		// Listen to visible editor changes
		this._register(this.editorService.onDidVisibleEditorsChange(() => this.onDidVisibleEditorsChange()));

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

B
Benjamin Pasero 已提交
244
			this._register(this.trackClosedWaitFiles(waitMarkerFile, resourcesToWaitFor));
245
		}
246

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

				// Represented Filename
				this.updateRepresentedFilename(file ? file.fsPath : undefined);

				// Custom title menu
				this.provideCustomTitleContextMenu(file ? file.fsPath : undefined);
			}));
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

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

279
				this.updateDocumentEdited(gotDirty);
280
			}));
281 282

			this.updateDocumentEdited();
283
		}
284

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

391 392
	private create(): void {

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

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

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

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

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

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

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

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

		// Crash reporter (if enabled)
		if (!this.environmentService.disableCrashReporter && product.crashReporter && product.hockeyApp && this.configurationService.getValue('telemetry.enableCrashReporter')) {
430
			this.setupCrashReporter(product.crashReporter.companyName, product.crashReporter.productName, product.hockeyApp);
431
		}
432 433
	}

B
Benjamin Pasero 已提交
434 435
	private setupOpenHandlers(): void {

436 437
		// Block window.open() calls
		window.open = function (): Window | null {
B
Benjamin Pasero 已提交
438
			throw new Error('Prevented call to window.open(). Use IOpenerService instead!');
B
Benjamin Pasero 已提交
439 440
		};

441 442 443 444 445 446 447
		// 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) {
448
						// if opening failed, and this is a file, we can still try to reveal it
449
						await this.electronService.showItemInFolder(fileCandidate.fsPath);
450
					}
B
Benjamin Pasero 已提交
451 452
				}

453
				return true;
B
Benjamin Pasero 已提交
454 455
			}
		});
456

457
		// Register external URI resolver
458
		this.openerService.registerExternalUriResolver({
459
			resolveExternalUri: async (uri: URI, options?: OpenOptions) => {
B
Benjamin Pasero 已提交
460
				if (options?.allowTunneling) {
461 462
					const portMappingRequest = extractLocalHostUriMetaDataForPortMapping(uri);
					if (portMappingRequest) {
463
						const tunnel = await this.tunnelService.openTunnel(undefined, portMappingRequest.port);
464
						if (tunnel) {
465 466 467 468
							return {
								resolved: uri.with({ authority: `127.0.0.1:${tunnel.tunnelLocalPort}` }),
								dispose: () => tunnel.dispose(),
							};
469 470 471
						}
					}
				}
472
				return undefined;
473 474
			}
		});
B
Benjamin Pasero 已提交
475 476
	}

477
	private updateTouchbarMenu(): void {
478 479
		if (!isMacintosh) {
			return; // macOS only
480 481
		}

482
		// Dispose old
483
		this.touchBarDisposables.clear();
R
Rob Lourens 已提交
484
		this.touchBarMenu = undefined;
485

486
		// Create new (delayed)
B
Benjamin Pasero 已提交
487 488
		const scheduler: RunOnceScheduler = this.touchBarDisposables.add(new RunOnceScheduler(() => this.doUpdateTouchbarMenu(scheduler), 300));
		scheduler.schedule();
489 490
	}

B
Benjamin Pasero 已提交
491
	private doUpdateTouchbarMenu(scheduler: RunOnceScheduler): void {
492 493
		if (!this.touchBarMenu) {
			this.touchBarMenu = this.editorService.invokeWithinEditorContext(accessor => this.menuService.createMenu(MenuId.TouchBarContext, accessor.get(IContextKeyService)));
494
			this.touchBarDisposables.add(this.touchBarMenu);
B
Benjamin Pasero 已提交
495
			this.touchBarDisposables.add(this.touchBarMenu.onDidChange(() => scheduler.schedule()));
496 497
		}

498
		const actions: Array<MenuItemAction | Separator> = [];
499

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

503
		// Fill actions into groups respecting order
504
		this.touchBarDisposables.add(createAndFillInActionBarActions(this.touchBarMenu, undefined, actions));
505 506 507 508

		// Convert into command action multi array
		const items: ICommandAction[][] = [];
		let group: ICommandAction[] = [];
509 510 511 512 513 514 515 516
		if (!disabled) {
			for (const action of actions) {

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

518
					group.push(action.item);
519 520
				}

521 522 523 524 525
				// Separator
				else if (action instanceof Separator) {
					if (group.length) {
						items.push(group);
					}
526

527
					group = [];
528 529
				}
			}
530

531 532 533
			if (group.length) {
				items.push(group);
			}
534 535 536
		}

		// Only update if the actions have changed
537
		if (!equals(this.lastInstalledTouchedBar, items)) {
538
			this.lastInstalledTouchedBar = items;
539
			this.electronService.updateTouchBar(items);
540
		}
541 542
	}

543 544 545 546
	private async setupCrashReporter(companyName: string, productName: string, hockeyAppConfig: typeof product.hockeyApp): Promise<void> {
		if (!hockeyAppConfig) {
			return;
		}
547 548

		// base options with product info
R
Robo 已提交
549
		const options: CrashReporterStartOptions = {
550 551 552
			companyName,
			productName,
			submitURL: isWindows ? hockeyAppConfig[process.arch === 'ia32' ? 'win32-ia32' : 'win32-x64'] : isLinux ? hockeyAppConfig[`linux-x64`] : hockeyAppConfig.darwin,
553
			extra: {
554
				vscode_version: product.version,
R
Robo 已提交
555
				vscode_commit: product.commit || ''
556 557 558 559
			}
		};

		// mixin telemetry info
560 561
		const info = await this.telemetryService.getTelemetryInfo();
		assign(options.extra, { vscode_sessionId: info.sessionId });
562

563 564
		// start crash reporter right here
		crashReporter.start(deepClone(options));
565

566
		// start crash reporter in the main process
567
		return this.electronService.startCrashReporter(options);
568 569
	}

570 571 572
	private onAddFoldersRequest(request: IAddFoldersRequest): void {

		// Buffer all pending requests
573
		this.pendingFoldersToAdd.push(...request.foldersToAdd.map(f => URI.revive(f)));
574 575 576 577 578 579 580 581 582 583

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

584 585
		this.pendingFoldersToAdd.forEach(folder => {
			foldersToAdd.push(({ uri: folder }));
586 587 588
		});

		this.pendingFoldersToAdd = [];
589

590
		this.workspaceEditingService.addFolders(foldersToAdd);
591 592
	}

593
	private async onOpenFiles(request: IOpenFileRequest): Promise<void> {
594
		const inputs: IResourceEditorInputType[] = [];
595
		const diffMode = !!(request.filesToDiff && (request.filesToDiff.length === 2));
596

597 598
		if (!diffMode && request.filesToOpenOrCreate) {
			inputs.push(...(await pathsToEditors(request.filesToOpenOrCreate, this.fileService)));
599 600
		}

601
		if (diffMode && request.filesToDiff) {
602
			inputs.push(...(await pathsToEditors(request.filesToDiff, this.fileService)));
603 604 605
		}

		if (inputs.length) {
606
			this.openResources(inputs, diffMode);
607
		}
608 609 610 611 612

		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 已提交
613
			const waitMarkerFile = URI.revive(request.filesToWait.waitMarkerFileUri);
B
Benjamin Pasero 已提交
614 615 616 617 618 619
			const resourcesToWaitFor = coalesce(request.filesToWait.paths.map(p => URI.revive(p.fileUri)));
			this.trackClosedWaitFiles(waitMarkerFile, resourcesToWaitFor);
		}
	}

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

622 623 624
		// 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.
625
		const listener = this.editorService.onDidCloseEditor(async event => {
B
Benjamin Pasero 已提交
626 627 628 629 630
			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
631
			remainingResourcesToWaitFor = remainingResourcesToWaitFor.filter(resourceToWaitFor => {
B
Benjamin Pasero 已提交
632 633 634
				if (isEqual(resourceToWaitFor, masterResource) || isEqual(resourceToWaitFor, detailsResource)) {
					return false; // remove - the closing editor matches this resource
				}
635

B
Benjamin Pasero 已提交
636 637
				return true; // keep - not yet closed
			});
638

639
			if (remainingResourcesToWaitFor.length === 0) {
B
Benjamin Pasero 已提交
640 641 642 643
				// 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.
644
				const dirtyFilesToWait = resourcesToWaitFor.filter(resourceToWaitFor => this.workingCopyService.isDirty(resourceToWaitFor));
B
Benjamin Pasero 已提交
645 646 647 648 649 650 651 652 653 654 655 656 657 658
				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 => {
659
			if (!this.workingCopyService.isDirty(resource)) {
B
Benjamin Pasero 已提交
660 661 662 663
				return resolve(); // return early if resource is not dirty
			}

			// Otherwise resolve promise when resource is saved
664 665
			const listener = this.workingCopyService.onDidChangeDirty(workingCopy => {
				if (!workingCopy.isDirty() && isEqual(resource, workingCopy.resource)) {
B
Benjamin Pasero 已提交
666 667 668
					listener.dispose();

					resolve();
669 670
				}
			});
B
Benjamin Pasero 已提交
671
		});
672 673
	}

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

677
		// In diffMode we open 2 resources as diff
678 679
		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 } });
680
		}
681

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

687 688
		// Otherwise open all
		return this.editorService.openEditors(resources);
689
	}
J
Johannes Rieken 已提交
690
}
691 692 693 694

class NativeMenubarControl extends MenubarControl {
	constructor(
		@IMenuService menuService: IMenuService,
695
		@IWorkspacesService workspacesService: IWorkspacesService,
696 697 698 699 700 701 702 703
		@IContextKeyService contextKeyService: IContextKeyService,
		@IKeybindingService keybindingService: IKeybindingService,
		@IConfigurationService configurationService: IConfigurationService,
		@ILabelService labelService: ILabelService,
		@IUpdateService updateService: IUpdateService,
		@IStorageService storageService: IStorageService,
		@INotificationService notificationService: INotificationService,
		@IPreferencesService preferencesService: IPreferencesService,
704
		@IWorkbenchEnvironmentService protected readonly environmentService: INativeWorkbenchEnvironmentService,
705
		@IAccessibilityService accessibilityService: IAccessibilityService,
706
		@IMenubarService private readonly menubarService: IMenubarService,
707
		@IHostService hostService: IHostService,
708 709 710
	) {
		super(
			menuService,
711
			workspacesService,
712 713 714 715 716 717 718 719 720
			contextKeyService,
			keybindingService,
			configurationService,
			labelService,
			updateService,
			storageService,
			notificationService,
			preferencesService,
			environmentService,
721 722 723
			accessibilityService,
			hostService
		);
724

725
		if (isMacintosh) {
726 727 728 729 730 731 732 733 734 735 736
			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()));
			}
		}

737
		(async () => {
738
			this.recentlyOpened = await this.workspacesService.getRecentlyOpened();
739 740

			this.doUpdateMenubar(true);
741
		})();
742 743 744 745 746

		this.registerListeners();
	}

	protected doUpdateMenubar(firstTime: boolean): void {
S
SteVen Batten 已提交
747 748 749 750 751
		// Since the native menubar is shared between windows (main process)
		// only allow the focused window to update the menubar
		if (!this.hostService.hasFocus) {
			return;
		}
752 753 754 755

		// Send menus to main process to be rendered by Electron
		const menubarData = { menus: {}, keybindings: {} };
		if (this.getMenubarMenus(menubarData)) {
756
			this.menubarService.updateMenubar(this.environmentService.configuration.windowId, menubarData);
757 758 759 760 761 762 763 764 765 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
		}
	}

	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 已提交
791 792
					if (!this.menus[menuItem.item.submenu.id]) {
						const menu = this.menus[menuItem.item.submenu.id] = this.menuService.createMenu(menuItem.item.submenu, this.contextKeyService);
793
						this._register(menu.onDidChange(() => this.updateMenubar()));
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 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
					}

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