Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/electron-browser/window.ts
5221 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import './media/window.css';
7
import { localize } from '../../nls.js';
8
import { URI } from '../../base/common/uri.js';
9
import { equals } from '../../base/common/objects.js';
10
import { EventType, EventHelper, addDisposableListener, ModifierKeyEmitter, getActiveElement, hasWindow, getWindowById, getWindows, $ } from '../../base/browser/dom.js';
11
import { Action, Separator, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../base/common/actions.js';
12
import { IFileService } from '../../platform/files/common/files.js';
13
import { EditorResourceAccessor, IUntitledTextResourceEditorInput, SideBySideEditor, pathsToEditors, IResourceDiffEditorInput, IUntypedEditorInput, IEditorPane, isResourceEditorInput, IResourceMergeEditorInput } from '../common/editor.js';
14
import { IEditorService } from '../services/editor/common/editorService.js';
15
import { ITelemetryService } from '../../platform/telemetry/common/telemetry.js';
16
import { WindowMinimumSize, IOpenFileRequest, IAddRemoveFoldersRequest, INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, INativeOpenFileRequest, hasNativeTitlebar } from '../../platform/window/common/window.js';
17
import { ITitleService } from '../services/title/browser/titleService.js';
18
import { IWorkbenchThemeService } from '../services/themes/common/workbenchThemeService.js';
19
import { ApplyZoomTarget, applyZoom } from '../../platform/window/electron-browser/window.js';
20
import { setFullscreen, getZoomLevel, onDidChangeZoomLevel, getZoomFactor } from '../../base/browser/browser.js';
21
import { ICommandService, CommandsRegistry } from '../../platform/commands/common/commands.js';
22
import { IResourceEditorInput } from '../../platform/editor/common/editor.js';
23
import { ipcRenderer, process } from '../../base/parts/sandbox/electron-browser/globals.js';
24
import { IWorkspaceEditingService } from '../services/workspaces/common/workspaceEditing.js';
25
import { IMenuService, MenuId, IMenu, MenuItemAction, MenuRegistry } from '../../platform/actions/common/actions.js';
26
import { ICommandAction } from '../../platform/action/common/action.js';
27
import { getFlatActionBarActions } from '../../platform/actions/browser/menuEntryActionViewItem.js';
28
import { RunOnceScheduler } from '../../base/common/async.js';
29
import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../base/common/lifecycle.js';
30
import { LifecyclePhase, ILifecycleService, WillShutdownEvent, ShutdownReason, BeforeShutdownErrorEvent, BeforeShutdownEvent } from '../services/lifecycle/common/lifecycle.js';
31
import { IWorkspaceFolderCreationData } from '../../platform/workspaces/common/workspaces.js';
32
import { IIntegrityService } from '../services/integrity/common/integrity.js';
33
import { isWindows, isMacintosh } from '../../base/common/platform.js';
34
import { IProductService } from '../../platform/product/common/productService.js';
35
import { INotificationService, NeverShowAgainScope, NotificationPriority, Severity } from '../../platform/notification/common/notification.js';
36
import { IKeybindingService } from '../../platform/keybinding/common/keybinding.js';
37
import { INativeWorkbenchEnvironmentService } from '../services/environment/electron-browser/environmentService.js';
38
import { IAccessibilityService, AccessibilitySupport } from '../../platform/accessibility/common/accessibility.js';
39
import { WorkbenchState, IWorkspaceContextService } from '../../platform/workspace/common/workspace.js';
40
import { coalesce } from '../../base/common/arrays.js';
41
import { ConfigurationTarget, IConfigurationService } from '../../platform/configuration/common/configuration.js';
42
import { IStorageService, StorageScope, StorageTarget } from '../../platform/storage/common/storage.js';
43
import { IOpenerService, IResolvedExternalUri, OpenOptions } from '../../platform/opener/common/opener.js';
44
import { Schemas } from '../../base/common/network.js';
45
import { INativeHostService } from '../../platform/native/common/native.js';
46
import { posix } from '../../base/common/path.js';
47
import { ITunnelService, RemoteTunnel, extractLocalHostUriMetaDataForPortMapping, extractQueryLocalHostUriMetaDataForPortMapping } from '../../platform/tunnel/common/tunnel.js';
48
import { IWorkbenchLayoutService, positionFromString, Position } from '../services/layout/browser/layoutService.js';
49
import { IWorkingCopyService } from '../services/workingCopy/common/workingCopyService.js';
50
import { WorkingCopyCapabilities } from '../services/workingCopy/common/workingCopy.js';
51
import { IFilesConfigurationService } from '../services/filesConfiguration/common/filesConfigurationService.js';
52
import { Event } from '../../base/common/event.js';
53
import { IRemoteAuthorityResolverService } from '../../platform/remote/common/remoteAuthorityResolver.js';
54
import { IAddressProvider, IAddress } from '../../platform/remote/common/remoteAgentConnection.js';
55
import { IEditorGroupsService, IEditorPart } from '../services/editor/common/editorGroupsService.js';
56
import { IDialogService } from '../../platform/dialogs/common/dialogs.js';
57
import { AuthInfo } from '../../base/parts/sandbox/electron-browser/electronTypes.js';
58
import { ILogService } from '../../platform/log/common/log.js';
59
import { IInstantiationService } from '../../platform/instantiation/common/instantiation.js';
60
import { whenEditorClosed } from '../browser/editor.js';
61
import { ISharedProcessService } from '../../platform/ipc/electron-browser/services.js';
62
import { IProgressService, ProgressLocation } from '../../platform/progress/common/progress.js';
63
import { toErrorMessage } from '../../base/common/errorMessage.js';
64
import { ILabelService } from '../../platform/label/common/label.js';
65
import { dirname } from '../../base/common/resources.js';
66
import { IBannerService } from '../services/banner/browser/bannerService.js';
67
import { Codicon } from '../../base/common/codicons.js';
68
import { IUriIdentityService } from '../../platform/uriIdentity/common/uriIdentity.js';
69
import { IPreferencesService } from '../services/preferences/common/preferences.js';
70
import { IUtilityProcessWorkerWorkbenchService } from '../services/utilityProcess/electron-browser/utilityProcessWorkerWorkbenchService.js';
71
import { registerWindowDriver } from '../services/driver/browser/driver.js';
72
import { mainWindow } from '../../base/browser/window.js';
73
import { BaseWindow } from '../browser/window.js';
74
import { IHostService } from '../services/host/browser/host.js';
75
import { IStatusbarService, ShowTooltipCommand, StatusbarAlignment } from '../services/statusbar/browser/statusbar.js';
76
import { ActionBar } from '../../base/browser/ui/actionbar/actionbar.js';
77
import { ThemeIcon } from '../../base/common/themables.js';
78
import { getWorkbenchContribution } from '../common/contributions.js';
79
import { DynamicWorkbenchSecurityConfiguration } from '../common/configuration.js';
80
import { nativeHoverDelegate } from '../../platform/hover/browser/hover.js';
81
import { WINDOW_ACTIVE_BORDER, WINDOW_INACTIVE_BORDER } from '../common/theme.js';
82
import { IContextMenuService } from '../../platform/contextview/browser/contextView.js';
83
84
export class NativeWindow extends BaseWindow {
85
86
private readonly customTitleContextMenuDisposable = this._register(new DisposableStore());
87
88
private readonly addRemoveFoldersScheduler = this._register(new RunOnceScheduler(() => this.doAddRemoveFolders(), 100));
89
private pendingFoldersToAdd: URI[] = [];
90
private pendingFoldersToRemove: URI[] = [];
91
92
private isDocumentedEdited = false;
93
94
constructor(
95
@IEditorService private readonly editorService: IEditorService,
96
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
97
@IConfigurationService private readonly configurationService: IConfigurationService,
98
@ITitleService private readonly titleService: ITitleService,
99
@IWorkbenchThemeService protected themeService: IWorkbenchThemeService,
100
@INotificationService private readonly notificationService: INotificationService,
101
@ICommandService private readonly commandService: ICommandService,
102
@IKeybindingService private readonly keybindingService: IKeybindingService,
103
@ITelemetryService private readonly telemetryService: ITelemetryService,
104
@IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService,
105
@IFileService private readonly fileService: IFileService,
106
@IMenuService private readonly menuService: IMenuService,
107
@ILifecycleService private readonly lifecycleService: ILifecycleService,
108
@IIntegrityService private readonly integrityService: IIntegrityService,
109
@INativeWorkbenchEnvironmentService private readonly nativeEnvironmentService: INativeWorkbenchEnvironmentService,
110
@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
111
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
112
@IOpenerService private readonly openerService: IOpenerService,
113
@INativeHostService private readonly nativeHostService: INativeHostService,
114
@ITunnelService private readonly tunnelService: ITunnelService,
115
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
116
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
117
@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService,
118
@IProductService private readonly productService: IProductService,
119
@IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService,
120
@IDialogService private readonly dialogService: IDialogService,
121
@IStorageService private readonly storageService: IStorageService,
122
@ILogService private readonly logService: ILogService,
123
@IInstantiationService private readonly instantiationService: IInstantiationService,
124
@ISharedProcessService private readonly sharedProcessService: ISharedProcessService,
125
@IProgressService private readonly progressService: IProgressService,
126
@ILabelService private readonly labelService: ILabelService,
127
@IBannerService private readonly bannerService: IBannerService,
128
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
129
@IPreferencesService private readonly preferencesService: IPreferencesService,
130
@IUtilityProcessWorkerWorkbenchService private readonly utilityProcessWorkerWorkbenchService: IUtilityProcessWorkerWorkbenchService,
131
@IHostService hostService: IHostService,
132
@IContextMenuService contextMenuService: IContextMenuService,
133
) {
134
super(mainWindow, undefined, hostService, nativeEnvironmentService, contextMenuService, layoutService);
135
136
this.configuredWindowZoomLevel = this.resolveConfiguredWindowZoomLevel();
137
138
this.registerListeners();
139
this.create();
140
}
141
142
protected registerListeners(): void {
143
144
// Layout
145
this._register(addDisposableListener(mainWindow, EventType.RESIZE, () => this.layoutService.layout()));
146
147
// React to editor input changes
148
this._register(this.editorService.onDidActiveEditorChange(() => this.updateTouchbarMenu()));
149
150
// Prevent opening a real URL inside the window
151
for (const event of [EventType.DRAG_OVER, EventType.DROP]) {
152
this._register(addDisposableListener(mainWindow.document.body, event, (e: DragEvent) => {
153
EventHelper.stop(e);
154
}));
155
}
156
157
// Support `runAction` event
158
ipcRenderer.on('vscode:runAction', async (event: unknown, ...argsRaw: unknown[]) => {
159
const request = argsRaw[0] as INativeRunActionInWindowRequest;
160
const args: unknown[] = request.args || [];
161
162
// If we run an action from the touchbar, we fill in the currently active resource
163
// as payload because the touch bar items are context aware depending on the editor
164
if (request.from === 'touchbar') {
165
const activeEditor = this.editorService.activeEditor;
166
if (activeEditor) {
167
const resource = EditorResourceAccessor.getOriginalUri(activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });
168
if (resource) {
169
args.push(resource);
170
}
171
}
172
} else {
173
args.push({ from: request.from });
174
}
175
176
try {
177
await this.commandService.executeCommand(request.id, ...args);
178
179
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: request.id, from: request.from });
180
} catch (error) {
181
this.notificationService.error(error);
182
}
183
});
184
185
// Support runKeybinding event
186
ipcRenderer.on('vscode:runKeybinding', (event: unknown, ...argsRaw: unknown[]) => {
187
const request = argsRaw[0] as INativeRunKeybindingInWindowRequest;
188
const activeElement = getActiveElement();
189
if (activeElement) {
190
this.keybindingService.dispatchByUserSettingsLabel(request.userSettingsLabel, activeElement);
191
}
192
});
193
194
// Shared Process crash reported from main
195
ipcRenderer.on('vscode:reportSharedProcessCrash', (event: unknown, ...argsRaw: unknown[]) => {
196
this.notificationService.prompt(
197
Severity.Error,
198
localize('sharedProcessCrash', "A shared background process terminated unexpectedly. Please restart the application to recover."),
199
[{
200
label: localize('restart', "Restart"),
201
run: () => this.nativeHostService.relaunch()
202
}],
203
{
204
priority: NotificationPriority.URGENT
205
}
206
);
207
});
208
209
// Support openFiles event for existing and new files
210
ipcRenderer.on('vscode:openFiles', (event: unknown, ...argsRaw: unknown[]) => { this.onOpenFiles(argsRaw[0] as IOpenFileRequest); });
211
212
// Support addRemoveFolders event for workspace management
213
ipcRenderer.on('vscode:addRemoveFolders', (event: unknown, ...argsRaw: unknown[]) => this.onAddRemoveFoldersRequest(argsRaw[0] as IAddRemoveFoldersRequest));
214
215
// Message support
216
ipcRenderer.on('vscode:showInfoMessage', (event: unknown, ...argsRaw: unknown[]) => this.notificationService.info(argsRaw[0] as string));
217
218
// Shell Environment Issue Notifications
219
ipcRenderer.on('vscode:showResolveShellEnvError', (event: unknown, ...argsRaw: unknown[]) => {
220
const message = argsRaw[0] as string;
221
this.notificationService.prompt(
222
Severity.Error,
223
message,
224
[{
225
label: localize('restart', "Restart"),
226
run: () => this.nativeHostService.relaunch()
227
},
228
{
229
label: localize('configure', "Configure"),
230
run: () => this.preferencesService.openUserSettings({ query: 'application.shellEnvironmentResolutionTimeout' })
231
},
232
{
233
label: localize('learnMore', "Learn More"),
234
run: () => this.openerService.open('https://go.microsoft.com/fwlink/?linkid=2149667')
235
}]
236
);
237
});
238
239
ipcRenderer.on('vscode:showCredentialsError', (event: unknown, ...argsRaw: unknown[]) => {
240
const message = argsRaw[0] as string;
241
this.notificationService.prompt(
242
Severity.Error,
243
localize('keychainWriteError', "Writing login information to the keychain failed with error '{0}'.", message),
244
[{
245
label: localize('troubleshooting', "Troubleshooting Guide"),
246
run: () => this.openerService.open('https://go.microsoft.com/fwlink/?linkid=2190713')
247
}]
248
);
249
});
250
251
ipcRenderer.on('vscode:showTranslatedBuildWarning', () => {
252
this.notificationService.prompt(
253
Severity.Warning,
254
localize("runningTranslated", "You are running an emulated version of {0}. For better performance download the native arm64 version of {0} build for your machine.", this.productService.nameLong),
255
[{
256
label: localize('downloadArmBuild', "Download"),
257
run: () => {
258
const quality = this.productService.quality;
259
const stableURL = 'https://code.visualstudio.com/docs/?dv=osx';
260
const insidersURL = 'https://code.visualstudio.com/docs/?dv=osx&build=insiders';
261
this.openerService.open(quality === 'stable' ? stableURL : insidersURL);
262
}
263
}],
264
{
265
priority: NotificationPriority.URGENT
266
}
267
);
268
});
269
270
ipcRenderer.on('vscode:showArgvParseWarning', () => {
271
this.notificationService.prompt(
272
Severity.Warning,
273
localize("showArgvParseWarning", "The runtime arguments file 'argv.json' contains errors. Please correct them and restart."),
274
[{
275
label: localize('showArgvParseWarningAction', "Open File"),
276
run: () => this.editorService.openEditor({ resource: this.nativeEnvironmentService.argvResource })
277
}],
278
{
279
priority: NotificationPriority.URGENT
280
}
281
);
282
});
283
284
// Fullscreen Events
285
ipcRenderer.on('vscode:enterFullScreen', () => setFullscreen(true, mainWindow));
286
ipcRenderer.on('vscode:leaveFullScreen', () => setFullscreen(false, mainWindow));
287
288
// Proxy Login Dialog
289
ipcRenderer.on('vscode:openProxyAuthenticationDialog', async (event: unknown, ...argsRaw: unknown[]) => {
290
const payload = argsRaw[0] as { authInfo: AuthInfo; username?: string; password?: string; replyChannel: string };
291
const rememberCredentialsKey = 'window.rememberProxyCredentials';
292
const rememberCredentials = this.storageService.getBoolean(rememberCredentialsKey, StorageScope.APPLICATION);
293
const result = await this.dialogService.input({
294
type: 'warning',
295
message: localize('proxyAuthRequired', "Proxy Authentication Required"),
296
primaryButton: localize({ key: 'loginButton', comment: ['&& denotes a mnemonic'] }, "&&Log In"),
297
inputs:
298
[
299
{ placeholder: localize('username', "Username"), value: payload.username },
300
{ placeholder: localize('password', "Password"), type: 'password', value: payload.password }
301
],
302
detail: localize('proxyDetail', "The proxy {0} requires a username and password.", `${payload.authInfo.host}:${payload.authInfo.port}`),
303
checkbox: {
304
label: localize('rememberCredentials', "Remember my credentials"),
305
checked: rememberCredentials
306
}
307
});
308
309
// Reply back to the channel without result to indicate
310
// that the login dialog was cancelled
311
if (!result.confirmed || !result.values) {
312
ipcRenderer.send(payload.replyChannel);
313
}
314
315
// Other reply back with the picked credentials
316
else {
317
318
// Update state based on checkbox
319
if (result.checkboxChecked) {
320
this.storageService.store(rememberCredentialsKey, true, StorageScope.APPLICATION, StorageTarget.MACHINE);
321
} else {
322
this.storageService.remove(rememberCredentialsKey, StorageScope.APPLICATION);
323
}
324
325
// Reply back to main side with credentials
326
const [username, password] = result.values;
327
ipcRenderer.send(payload.replyChannel, { username, password, remember: !!result.checkboxChecked });
328
}
329
});
330
331
// Accessibility support changed event
332
ipcRenderer.on('vscode:accessibilitySupportChanged', (event: unknown, ...argsRaw: unknown[]) => {
333
const accessibilitySupportEnabled = argsRaw[0] as boolean;
334
this.accessibilityService.setAccessibilitySupport(accessibilitySupportEnabled ? AccessibilitySupport.Enabled : AccessibilitySupport.Disabled);
335
});
336
337
// Allow to update security settings around allowed UNC Host
338
ipcRenderer.on('vscode:configureAllowedUNCHost', async (event: unknown, ...argsRaw: unknown[]) => {
339
const host = argsRaw[0] as string;
340
if (!isWindows) {
341
return; // only supported on Windows
342
}
343
344
const allowedUncHosts = new Set<string>();
345
346
const configuredAllowedUncHosts = this.configurationService.getValue<string[] | undefined>('security.allowedUNCHosts',) ?? [];
347
if (Array.isArray(configuredAllowedUncHosts)) {
348
for (const configuredAllowedUncHost of configuredAllowedUncHosts) {
349
if (typeof configuredAllowedUncHost === 'string') {
350
allowedUncHosts.add(configuredAllowedUncHost);
351
}
352
}
353
}
354
355
if (!allowedUncHosts.has(host)) {
356
allowedUncHosts.add(host);
357
358
await getWorkbenchContribution<DynamicWorkbenchSecurityConfiguration>(DynamicWorkbenchSecurityConfiguration.ID).ready; // ensure this setting is registered
359
this.configurationService.updateValue('security.allowedUNCHosts', [...allowedUncHosts.values()], ConfigurationTarget.USER);
360
}
361
});
362
363
// Allow to update security settings around protocol handlers
364
ipcRenderer.on('vscode:disablePromptForProtocolHandling', (event: unknown, ...argsRaw: unknown[]) => {
365
const kind = argsRaw[0] as 'local' | 'remote';
366
const setting = kind === 'local' ? 'security.promptForLocalFileProtocolHandling' : 'security.promptForRemoteFileProtocolHandling';
367
this.configurationService.updateValue(setting, false);
368
});
369
370
// Window Settings
371
this._register(this.configurationService.onDidChangeConfiguration(e => {
372
if (e.affectsConfiguration('window.zoomLevel') || (e.affectsConfiguration('window.zoomPerWindow') && this.configurationService.getValue('window.zoomPerWindow') === false)) {
373
this.onDidChangeConfiguredWindowZoomLevel();
374
} else if (e.affectsConfiguration('keyboard.touchbar.enabled') || e.affectsConfiguration('keyboard.touchbar.ignored')) {
375
this.updateTouchbarMenu();
376
} else if (e.affectsConfiguration('window.border')) {
377
this.updateWindowBorder();
378
}
379
}));
380
381
this._register(onDidChangeZoomLevel(targetWindowId => this.handleOnDidChangeZoomLevel(targetWindowId)));
382
383
for (const part of this.editorGroupService.parts) {
384
this.createWindowZoomStatusEntry(part);
385
}
386
387
this._register(this.editorGroupService.onDidCreateAuxiliaryEditorPart(part => this.createWindowZoomStatusEntry(part)));
388
389
// Listen to visible editor changes (debounced in case a new editor opens immediately after)
390
this._register(Event.debounce(this.editorService.onDidVisibleEditorsChange, () => undefined, 0, undefined, undefined, undefined, this._store)(() => this.maybeCloseWindow()));
391
392
// Listen to editor closing (if we run with --wait)
393
const filesToWait = this.nativeEnvironmentService.filesToWait;
394
if (filesToWait) {
395
this.trackClosedWaitFiles(filesToWait.waitMarkerFileUri, coalesce(filesToWait.paths.map(path => path.fileUri)));
396
}
397
398
// macOS OS integration: represented file name
399
if (isMacintosh) {
400
for (const part of this.editorGroupService.parts) {
401
this.handleRepresentedFilename(part);
402
}
403
404
this._register(this.editorGroupService.onDidCreateAuxiliaryEditorPart(part => this.handleRepresentedFilename(part)));
405
}
406
407
// Document edited: indicate for dirty working copies
408
this._register(this.workingCopyService.onDidChangeDirty(workingCopy => {
409
const gotDirty = workingCopy.isDirty();
410
if (gotDirty && !(workingCopy.capabilities & WorkingCopyCapabilities.Untitled) && this.filesConfigurationService.hasShortAutoSaveDelay(workingCopy.resource)) {
411
return; // do not indicate dirty of working copies that are auto saved after short delay
412
}
413
414
this.updateDocumentEdited(gotDirty ? true : undefined);
415
}));
416
417
this.updateDocumentEdited(undefined);
418
419
// Detect minimize / maximize
420
this._register(Event.any(
421
Event.map(Event.filter(this.nativeHostService.onDidMaximizeWindow, windowId => !!hasWindow(windowId)), windowId => ({ maximized: true, windowId })),
422
Event.map(Event.filter(this.nativeHostService.onDidUnmaximizeWindow, windowId => !!hasWindow(windowId)), windowId => ({ maximized: false, windowId }))
423
)(e => this.layoutService.updateWindowMaximizedState(getWindowById(e.windowId)!.window, e.maximized)));
424
this.layoutService.updateWindowMaximizedState(mainWindow, this.nativeEnvironmentService.window.maximized ?? false);
425
426
// Detect panel position to determine minimum width
427
this._register(this.layoutService.onDidChangePanelPosition(pos => this.onDidChangePanelPosition(positionFromString(pos))));
428
this.onDidChangePanelPosition(this.layoutService.getPanelPosition());
429
430
// Border
431
this._register(this.themeService.onDidColorThemeChange(() => this.updateWindowBorder()));
432
this._register(this.hostService.onDidChangeActiveWindow(() => this.updateWindowBorder()));
433
this._register(this.hostService.onDidChangeFocus(() => this.updateWindowBorder()));
434
435
// Lifecycle
436
this._register(this.lifecycleService.onBeforeShutdown(e => this.onBeforeShutdown(e)));
437
this._register(this.lifecycleService.onBeforeShutdownError(e => this.onBeforeShutdownError(e)));
438
this._register(this.lifecycleService.onWillShutdown(e => this.onWillShutdown(e)));
439
}
440
441
private handleRepresentedFilename(part: IEditorPart): void {
442
const disposables = new DisposableStore();
443
Event.once(part.onWillDispose)(() => disposables.dispose());
444
445
this.editorGroupService.getScopedInstantiationService(part).invokeFunction(accessor => {
446
const editorService = accessor.get(IEditorService);
447
disposables.add(editorService.onDidActiveEditorChange(() => this.updateRepresentedFilename(editorService, part.windowId)));
448
});
449
}
450
451
private updateRepresentedFilename(editorService: IEditorService, targetWindowId: number): void {
452
const file = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY, filterByScheme: Schemas.file });
453
454
// Represented Filename
455
this.nativeHostService.setRepresentedFilename(file?.fsPath ?? '', { targetWindowId });
456
457
// Custom title menu (main window only currently)
458
if (targetWindowId === mainWindow.vscodeWindowId) {
459
this.provideCustomTitleContextMenu(file?.fsPath);
460
}
461
}
462
463
//#region Window Lifecycle
464
465
private onBeforeShutdown({ veto, reason }: BeforeShutdownEvent): void {
466
if (reason === ShutdownReason.CLOSE) {
467
const confirmBeforeCloseSetting = this.configurationService.getValue<'always' | 'never' | 'keyboardOnly'>('window.confirmBeforeClose');
468
469
const confirmBeforeClose = confirmBeforeCloseSetting === 'always' || (confirmBeforeCloseSetting === 'keyboardOnly' && ModifierKeyEmitter.getInstance().isModifierPressed);
470
if (confirmBeforeClose) {
471
472
// When we need to confirm on close or quit, veto the shutdown
473
// with a long running promise to figure out whether shutdown
474
// can proceed or not.
475
476
return veto((async () => {
477
let actualReason: ShutdownReason = reason;
478
if (reason === ShutdownReason.CLOSE && !isMacintosh) {
479
const windowCount = await this.nativeHostService.getWindowCount();
480
if (windowCount === 1) {
481
actualReason = ShutdownReason.QUIT; // Windows/Linux: closing last window means to QUIT
482
}
483
}
484
485
let confirmed = true;
486
if (confirmBeforeClose) {
487
confirmed = await this.instantiationService.invokeFunction(accessor => NativeWindow.confirmOnShutdown(accessor, actualReason));
488
}
489
490
// Progress for long running shutdown
491
if (confirmed) {
492
this.progressOnBeforeShutdown(reason);
493
}
494
495
return !confirmed;
496
})(), 'veto.confirmBeforeClose');
497
}
498
}
499
500
// Progress for long running shutdown
501
this.progressOnBeforeShutdown(reason);
502
}
503
504
private progressOnBeforeShutdown(reason: ShutdownReason): void {
505
this.progressService.withProgress({
506
location: ProgressLocation.Window, // use window progress to not be too annoying about this operation
507
delay: 800, // delay so that it only appears when operation takes a long time
508
title: this.toShutdownLabel(reason, false),
509
}, () => {
510
return Event.toPromise(Event.any(
511
this.lifecycleService.onWillShutdown, // dismiss this dialog when we shutdown
512
this.lifecycleService.onShutdownVeto, // or when shutdown was vetoed
513
this.dialogService.onWillShowDialog // or when a dialog asks for input
514
));
515
});
516
}
517
518
private onBeforeShutdownError({ error, reason }: BeforeShutdownErrorEvent): void {
519
this.dialogService.error(this.toShutdownLabel(reason, true), localize('shutdownErrorDetail', "Error: {0}", toErrorMessage(error)));
520
}
521
522
private onWillShutdown({ reason, force, joiners }: WillShutdownEvent): void {
523
524
// Delay so that the dialog only appears after timeout
525
const shutdownDialogScheduler = new RunOnceScheduler(() => {
526
const pendingJoiners = joiners();
527
528
this.progressService.withProgress({
529
location: ProgressLocation.Dialog, // use a dialog to prevent the user from making any more interactions now
530
buttons: [this.toForceShutdownLabel(reason)], // allow to force shutdown anyway
531
cancellable: false, // do not allow to cancel
532
sticky: true, // do not allow to dismiss
533
title: this.toShutdownLabel(reason, false),
534
detail: pendingJoiners.length > 0 ? localize('willShutdownDetail', "The following operations are still running: \n{0}", pendingJoiners.map(joiner => `- ${joiner.label}`).join('\n')) : undefined
535
}, () => {
536
return Event.toPromise(this.lifecycleService.onDidShutdown); // dismiss this dialog when we actually shutdown
537
}, () => {
538
force();
539
});
540
}, 1200);
541
shutdownDialogScheduler.schedule();
542
543
// Dispose scheduler when we actually shutdown
544
Event.once(this.lifecycleService.onDidShutdown)(() => shutdownDialogScheduler.dispose());
545
}
546
547
private toShutdownLabel(reason: ShutdownReason, isError: boolean): string {
548
if (isError) {
549
switch (reason) {
550
case ShutdownReason.CLOSE:
551
return localize('shutdownErrorClose', "An unexpected error prevented the window to close");
552
case ShutdownReason.QUIT:
553
return localize('shutdownErrorQuit', "An unexpected error prevented the application to quit");
554
case ShutdownReason.RELOAD:
555
return localize('shutdownErrorReload', "An unexpected error prevented the window to reload");
556
case ShutdownReason.LOAD:
557
return localize('shutdownErrorLoad', "An unexpected error prevented to change the workspace");
558
}
559
}
560
561
switch (reason) {
562
case ShutdownReason.CLOSE:
563
return localize('shutdownTitleClose', "Closing the window is taking a bit longer...");
564
case ShutdownReason.QUIT:
565
return localize('shutdownTitleQuit', "Quitting the application is taking a bit longer...");
566
case ShutdownReason.RELOAD:
567
return localize('shutdownTitleReload', "Reloading the window is taking a bit longer...");
568
case ShutdownReason.LOAD:
569
return localize('shutdownTitleLoad', "Changing the workspace is taking a bit longer...");
570
}
571
}
572
573
private toForceShutdownLabel(reason: ShutdownReason): string {
574
switch (reason) {
575
case ShutdownReason.CLOSE:
576
return localize('shutdownForceClose', "Close Anyway");
577
case ShutdownReason.QUIT:
578
return localize('shutdownForceQuit', "Quit Anyway");
579
case ShutdownReason.RELOAD:
580
return localize('shutdownForceReload', "Reload Anyway");
581
case ShutdownReason.LOAD:
582
return localize('shutdownForceLoad', "Change Anyway");
583
}
584
}
585
586
//#endregion
587
588
private updateDocumentEdited(documentEdited: true | undefined): void {
589
let setDocumentEdited: boolean;
590
if (typeof documentEdited === 'boolean') {
591
setDocumentEdited = documentEdited;
592
} else {
593
setDocumentEdited = this.workingCopyService.hasDirty;
594
}
595
596
if ((!this.isDocumentedEdited && setDocumentEdited) || (this.isDocumentedEdited && !setDocumentEdited)) {
597
this.isDocumentedEdited = setDocumentEdited;
598
599
this.nativeHostService.setDocumentEdited(setDocumentEdited);
600
}
601
}
602
603
private getWindowMinimumWidth(panelPosition: Position = this.layoutService.getPanelPosition()): number {
604
605
// if panel is on the side, then return the larger minwidth
606
const panelOnSide = panelPosition === Position.LEFT || panelPosition === Position.RIGHT;
607
if (panelOnSide) {
608
return WindowMinimumSize.WIDTH_WITH_VERTICAL_PANEL;
609
}
610
611
return WindowMinimumSize.WIDTH;
612
}
613
614
private onDidChangePanelPosition(pos: Position): void {
615
const minWidth = this.getWindowMinimumWidth(pos);
616
617
this.nativeHostService.setMinimumSize(minWidth, undefined);
618
}
619
620
private maybeCloseWindow(): void {
621
const closeWhenEmpty = this.configurationService.getValue('window.closeWhenEmpty') || this.nativeEnvironmentService.args.wait;
622
if (!closeWhenEmpty) {
623
return; // return early if configured to not close when empty
624
}
625
626
// Close empty editor groups based on setting and environment
627
for (const editorPart of this.editorGroupService.parts) {
628
if (editorPart.groups.some(group => !group.isEmpty)) {
629
continue; // not empty
630
}
631
632
if (editorPart === this.editorGroupService.mainPart && (
633
this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY || // only for empty windows
634
this.environmentService.isExtensionDevelopment || // not when developing an extension
635
this.editorService.visibleEditors.length > 0 // not when there are still editors open in other windows
636
)) {
637
continue;
638
}
639
640
if (editorPart === this.editorGroupService.mainPart) {
641
this.nativeHostService.closeWindow();
642
} else {
643
editorPart.removeGroup(editorPart.activeGroup);
644
}
645
}
646
}
647
648
private provideCustomTitleContextMenu(filePath: string | undefined): void {
649
650
// Clear old menu
651
this.customTitleContextMenuDisposable.clear();
652
653
// Only provide a menu when we have a file path and custom titlebar
654
if (!filePath || hasNativeTitlebar(this.configurationService)) {
655
return;
656
}
657
658
// Split up filepath into segments
659
const segments = filePath.split(posix.sep);
660
for (let i = segments.length; i > 0; i--) {
661
const isFile = (i === segments.length);
662
663
let pathOffset = i;
664
if (!isFile) {
665
pathOffset++; // for segments which are not the file name we want to open the folder
666
}
667
668
const path = URI.file(segments.slice(0, pathOffset).join(posix.sep));
669
670
let label: string;
671
if (!isFile) {
672
label = this.labelService.getUriBasenameLabel(dirname(path));
673
} else {
674
label = this.labelService.getUriBasenameLabel(path);
675
}
676
677
const commandId = `workbench.action.revealPathInFinder${i}`;
678
this.customTitleContextMenuDisposable.add(CommandsRegistry.registerCommand(commandId, () => this.nativeHostService.showItemInFolder(path.fsPath)));
679
this.customTitleContextMenuDisposable.add(MenuRegistry.appendMenuItem(MenuId.TitleBarTitleContext, { command: { id: commandId, title: label || posix.sep }, order: -i, group: '1_file' }));
680
}
681
}
682
683
protected create(): void {
684
685
// Handle open calls
686
this.setupOpenHandlers();
687
688
// Notify some services about lifecycle phases
689
this.lifecycleService.when(LifecyclePhase.Ready).then(() => this.nativeHostService.notifyReady());
690
this.lifecycleService.when(LifecyclePhase.Restored).then(() => {
691
this.sharedProcessService.notifyRestored();
692
this.utilityProcessWorkerWorkbenchService.notifyRestored();
693
});
694
695
// Check for situations that are worth warning the user about
696
this.handleWarnings();
697
698
// Touchbar menu (if enabled)
699
this.updateTouchbarMenu();
700
701
// Window border
702
this.updateWindowBorder();
703
704
// Smoke Test Driver
705
if (this.environmentService.enableSmokeTestDriver) {
706
registerWindowDriver(this.instantiationService);
707
}
708
}
709
710
private async handleWarnings(): Promise<void> {
711
712
// After restored phase is fine for the following ones
713
await this.lifecycleService.when(LifecyclePhase.Restored);
714
715
// Integrity / Root warning
716
(async () => {
717
const isAdmin = await this.nativeHostService.isAdmin();
718
const { isPure } = await this.integrityService.isPure();
719
720
// Update to title
721
this.titleService.updateProperties({ isPure, isAdmin });
722
723
// Show warning message (unix only)
724
if (isAdmin && !isWindows) {
725
this.notificationService.warn(localize('runningAsRoot', "It is not recommended to run {0} as root user.", this.productService.nameShort));
726
}
727
})();
728
729
// Installation Dir Warning
730
if (this.environmentService.isBuilt && !this.environmentService.extensionDevelopmentLocationURI?.length) {
731
let installLocationUri: URI;
732
if (isMacintosh) {
733
// appRoot = /Applications/Visual Studio Code - Insiders.app/Contents/Resources/app
734
installLocationUri = dirname(dirname(dirname(URI.file(this.nativeEnvironmentService.appRoot))));
735
} else {
736
// appRoot = C:\Users\<name>\AppData\Local\Programs\Microsoft VS Code Insiders\resources\app
737
// appRoot = /usr/share/code-insiders/resources/app
738
installLocationUri = dirname(dirname(URI.file(this.nativeEnvironmentService.appRoot)));
739
}
740
741
for (const folder of this.contextService.getWorkspace().folders) {
742
if (this.uriIdentityService.extUri.isEqualOrParent(folder.uri, installLocationUri)) {
743
this.bannerService.show({
744
id: 'appRootWarning.banner',
745
message: localize('appRootWarning.banner', "Files you store within the installation folder ('{0}') may be OVERWRITTEN or DELETED IRREVERSIBLY without warning at update time.", this.labelService.getUriLabel(installLocationUri)),
746
icon: Codicon.warning
747
});
748
749
break;
750
}
751
}
752
}
753
754
// macOS 11 warning
755
if (isMacintosh) {
756
const majorVersion = this.nativeEnvironmentService.os.release.split('.')[0];
757
const eolReleases = new Map<string, string>([
758
['20', 'macOS Big Sur'],
759
]);
760
761
if (eolReleases.has(majorVersion)) {
762
const message = localize('macoseolmessage', "{0} on {1} will soon stop receiving updates. Consider upgrading your macOS version.", this.productService.nameLong, eolReleases.get(majorVersion));
763
764
this.notificationService.prompt(
765
Severity.Warning,
766
message,
767
[{
768
label: localize('learnMore', "Learn More"),
769
run: () => this.openerService.open(URI.parse('https://aka.ms/vscode-faq-old-macOS'))
770
}],
771
{
772
neverShowAgain: { id: 'macoseol', isSecondary: true, scope: NeverShowAgainScope.APPLICATION },
773
priority: NotificationPriority.URGENT,
774
sticky: true
775
}
776
);
777
}
778
}
779
780
// Slow shell environment progress indicator
781
const shellEnv = process.shellEnv();
782
this.progressService.withProgress({
783
title: localize('resolveShellEnvironment', "Resolving shell environment..."),
784
location: ProgressLocation.Window,
785
delay: 1600,
786
buttons: [localize('learnMore', "Learn More")]
787
}, () => shellEnv, () => this.openerService.open('https://go.microsoft.com/fwlink/?linkid=2149667'));
788
}
789
790
async resolveExternalUri(uri: URI, options?: OpenOptions): Promise<IResolvedExternalUri | undefined> {
791
let queryTunnel: RemoteTunnel | string | undefined;
792
if (options?.allowTunneling) {
793
const portMappingRequest = extractLocalHostUriMetaDataForPortMapping(uri);
794
const queryPortMapping = extractQueryLocalHostUriMetaDataForPortMapping(uri);
795
if (queryPortMapping) {
796
queryTunnel = await this.openTunnel(queryPortMapping.address, queryPortMapping.port);
797
if (queryTunnel && (typeof queryTunnel !== 'string')) {
798
// If the tunnel was mapped to a different port, dispose it, because some services
799
// validate the port number in the query string.
800
if (queryTunnel.tunnelRemotePort !== queryPortMapping.port) {
801
queryTunnel.dispose();
802
queryTunnel = undefined;
803
} else {
804
if (!portMappingRequest) {
805
const tunnel = queryTunnel;
806
return {
807
resolved: uri,
808
dispose: () => tunnel.dispose()
809
};
810
}
811
}
812
}
813
}
814
815
if (portMappingRequest) {
816
const tunnel = await this.openTunnel(portMappingRequest.address, portMappingRequest.port);
817
if (tunnel && (typeof tunnel !== 'string')) {
818
const addressAsUri = URI.parse(tunnel.localAddress).with({ path: uri.path });
819
const resolved = addressAsUri.scheme.startsWith(uri.scheme) ? addressAsUri : uri.with({ authority: tunnel.localAddress });
820
return {
821
resolved,
822
dispose() {
823
tunnel.dispose();
824
if (queryTunnel && (typeof queryTunnel !== 'string')) {
825
queryTunnel.dispose();
826
}
827
}
828
};
829
}
830
}
831
}
832
833
if (!options?.openExternal) {
834
const canHandleResource = await this.fileService.canHandleResource(uri);
835
if (canHandleResource) {
836
return {
837
resolved: URI.from({
838
scheme: this.productService.urlProtocol,
839
path: 'workspace',
840
query: uri.toString()
841
}),
842
dispose() { }
843
};
844
}
845
}
846
847
return undefined;
848
}
849
850
private async openTunnel(address: string, port: number): Promise<RemoteTunnel | string | undefined> {
851
const remoteAuthority = this.environmentService.remoteAuthority;
852
const addressProvider: IAddressProvider | undefined = remoteAuthority ? {
853
getAddress: async (): Promise<IAddress> => {
854
return (await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority)).authority;
855
}
856
} : undefined;
857
858
const tunnel = await this.tunnelService.getExistingTunnel(address, port);
859
if (!tunnel || (typeof tunnel === 'string')) {
860
return this.tunnelService.openTunnel(addressProvider, address, port);
861
}
862
863
return tunnel;
864
}
865
866
private setupOpenHandlers(): void {
867
868
// Handle external open() calls
869
this.openerService.setDefaultExternalOpener({
870
openExternal: async (href: string) => {
871
const success = await this.nativeHostService.openExternal(href, this.configurationService.getValue<string>('workbench.externalBrowser'));
872
if (!success) {
873
const fileCandidate = URI.parse(href);
874
if (fileCandidate.scheme === Schemas.file) {
875
// if opening failed, and this is a file, we can still try to reveal it
876
await this.nativeHostService.showItemInFolder(fileCandidate.fsPath);
877
}
878
}
879
880
return true;
881
}
882
});
883
884
// Register external URI resolver
885
this.openerService.registerExternalUriResolver({
886
resolveExternalUri: async (uri: URI, options?: OpenOptions) => {
887
return this.resolveExternalUri(uri, options);
888
}
889
});
890
}
891
892
//#region Touchbar
893
894
private touchBarMenu: IMenu | undefined;
895
private readonly touchBarDisposables = this._register(new DisposableStore());
896
private lastInstalledTouchedBar: ICommandAction[][] | undefined;
897
898
private updateTouchbarMenu(): void {
899
if (!isMacintosh) {
900
return; // macOS only
901
}
902
903
// Dispose old
904
this.touchBarDisposables.clear();
905
this.touchBarMenu = undefined;
906
907
// Create new (delayed)
908
const scheduler: RunOnceScheduler = this.touchBarDisposables.add(new RunOnceScheduler(() => this.doUpdateTouchbarMenu(scheduler), 300));
909
scheduler.schedule();
910
}
911
912
private doUpdateTouchbarMenu(scheduler: RunOnceScheduler): void {
913
if (!this.touchBarMenu) {
914
const scopedContextKeyService = this.editorService.activeEditorPane?.scopedContextKeyService || this.editorGroupService.activeGroup.scopedContextKeyService;
915
this.touchBarMenu = this.menuService.createMenu(MenuId.TouchBarContext, scopedContextKeyService);
916
this.touchBarDisposables.add(this.touchBarMenu);
917
this.touchBarDisposables.add(this.touchBarMenu.onDidChange(() => scheduler.schedule()));
918
}
919
920
const disabled = this.configurationService.getValue('keyboard.touchbar.enabled') === false;
921
const touchbarIgnored = this.configurationService.getValue('keyboard.touchbar.ignored');
922
const ignoredItems = Array.isArray(touchbarIgnored) ? touchbarIgnored : [];
923
924
// Fill actions into groups respecting order
925
const actions = getFlatActionBarActions(this.touchBarMenu.getActions());
926
927
// Convert into command action multi array
928
const items: ICommandAction[][] = [];
929
let group: ICommandAction[] = [];
930
if (!disabled) {
931
for (const action of actions) {
932
933
// Command
934
if (action instanceof MenuItemAction) {
935
if (ignoredItems.indexOf(action.item.id) >= 0) {
936
continue; // ignored
937
}
938
939
group.push(action.item);
940
}
941
942
// Separator
943
else if (action instanceof Separator) {
944
if (group.length) {
945
items.push(group);
946
}
947
948
group = [];
949
}
950
}
951
952
if (group.length) {
953
items.push(group);
954
}
955
}
956
957
// Only update if the actions have changed
958
if (!equals(this.lastInstalledTouchedBar, items)) {
959
this.lastInstalledTouchedBar = items;
960
this.nativeHostService.updateTouchBar(items);
961
}
962
}
963
964
//#endregion
965
966
//#region Window Border
967
968
private updateWindowBorder(): void {
969
if (!isWindows) {
970
return; // windows only
971
}
972
973
const theme = this.themeService.getColorTheme();
974
975
let activeBorder = theme.getColor(WINDOW_ACTIVE_BORDER)?.toString();
976
let inactiveBorder = theme.getColor(WINDOW_INACTIVE_BORDER)?.toString();
977
978
const borderSetting = this.configurationService.getValue<string>('window.border');
979
if (borderSetting === 'off') {
980
activeBorder = 'off';
981
inactiveBorder = undefined;
982
} else if (borderSetting === 'default') {
983
activeBorder = activeBorder ?? 'default';
984
} else if (borderSetting === 'system') {
985
activeBorder = 'default';
986
inactiveBorder = undefined;
987
} else {
988
activeBorder = borderSetting;
989
inactiveBorder = undefined;
990
}
991
992
this.nativeHostService.updateWindowAccentColor(activeBorder, inactiveBorder);
993
}
994
995
//#endregion
996
997
private onAddRemoveFoldersRequest(request: IAddRemoveFoldersRequest): void {
998
999
// Buffer all pending requests
1000
this.pendingFoldersToAdd.push(...request.foldersToAdd.map(folder => URI.revive(folder)));
1001
this.pendingFoldersToRemove.push(...request.foldersToRemove.map(folder => URI.revive(folder)));
1002
1003
// Delay the adding of folders a bit to buffer in case more requests are coming
1004
if (!this.addRemoveFoldersScheduler.isScheduled()) {
1005
this.addRemoveFoldersScheduler.schedule();
1006
}
1007
}
1008
1009
private async doAddRemoveFolders(): Promise<void> {
1010
const foldersToAdd: IWorkspaceFolderCreationData[] = this.pendingFoldersToAdd.map(folder => ({ uri: folder }));
1011
const foldersToRemove = this.pendingFoldersToRemove.slice(0);
1012
1013
this.pendingFoldersToAdd = [];
1014
this.pendingFoldersToRemove = [];
1015
1016
if (foldersToAdd.length) {
1017
await this.workspaceEditingService.addFolders(foldersToAdd);
1018
}
1019
1020
if (foldersToRemove.length) {
1021
await this.workspaceEditingService.removeFolders(foldersToRemove);
1022
}
1023
}
1024
1025
private async onOpenFiles(request: INativeOpenFileRequest): Promise<void> {
1026
const diffMode = !!(request.filesToDiff && (request.filesToDiff.length === 2));
1027
const mergeMode = !!(request.filesToMerge && (request.filesToMerge.length === 4));
1028
1029
const inputs = coalesce(await pathsToEditors(mergeMode ? request.filesToMerge : diffMode ? request.filesToDiff : request.filesToOpenOrCreate, this.fileService, this.logService));
1030
if (inputs.length) {
1031
const openedEditorPanes = await this.openResources(inputs, diffMode, mergeMode);
1032
1033
if (request.filesToWait) {
1034
1035
// In wait mode, listen to changes to the editors and wait until the files
1036
// are closed that the user wants to wait for. When this happens we delete
1037
// the wait marker file to signal to the outside that editing is done.
1038
// However, it is possible that opening of the editors failed, as such we
1039
// check for whether editor panes got opened and otherwise delete the marker
1040
// right away.
1041
1042
if (openedEditorPanes.length) {
1043
return this.trackClosedWaitFiles(URI.revive(request.filesToWait.waitMarkerFileUri), coalesce(request.filesToWait.paths.map(path => URI.revive(path.fileUri))));
1044
} else {
1045
return this.fileService.del(URI.revive(request.filesToWait.waitMarkerFileUri));
1046
}
1047
}
1048
}
1049
}
1050
1051
private async trackClosedWaitFiles(waitMarkerFile: URI, resourcesToWaitFor: URI[]): Promise<void> {
1052
1053
// Wait for the resources to be closed in the text editor...
1054
await this.instantiationService.invokeFunction(accessor => whenEditorClosed(accessor, resourcesToWaitFor));
1055
1056
// ...before deleting the wait marker file
1057
await this.fileService.del(waitMarkerFile);
1058
}
1059
1060
private async openResources(resources: Array<IResourceEditorInput | IUntitledTextResourceEditorInput>, diffMode: boolean, mergeMode: boolean): Promise<readonly IEditorPane[]> {
1061
const editors: IUntypedEditorInput[] = [];
1062
1063
if (mergeMode && isResourceEditorInput(resources[0]) && isResourceEditorInput(resources[1]) && isResourceEditorInput(resources[2]) && isResourceEditorInput(resources[3])) {
1064
const mergeEditor: IResourceMergeEditorInput = {
1065
input1: { resource: resources[0].resource },
1066
input2: { resource: resources[1].resource },
1067
base: { resource: resources[2].resource },
1068
result: { resource: resources[3].resource },
1069
options: { pinned: true }
1070
};
1071
editors.push(mergeEditor);
1072
} else if (diffMode && isResourceEditorInput(resources[0]) && isResourceEditorInput(resources[1])) {
1073
const diffEditor: IResourceDiffEditorInput = {
1074
original: { resource: resources[0].resource },
1075
modified: { resource: resources[1].resource },
1076
options: { pinned: true }
1077
};
1078
editors.push(diffEditor);
1079
} else {
1080
editors.push(...resources);
1081
}
1082
1083
return this.editorService.openEditors(editors, undefined, { validateTrust: true });
1084
}
1085
1086
//#region Window Zoom
1087
1088
private readonly mapWindowIdToZoomStatusEntry = new Map<number, ZoomStatusEntry>();
1089
1090
private configuredWindowZoomLevel: number;
1091
1092
private resolveConfiguredWindowZoomLevel(): number {
1093
const windowZoomLevel = this.configurationService.getValue('window.zoomLevel');
1094
1095
return typeof windowZoomLevel === 'number' ? windowZoomLevel : 0;
1096
}
1097
1098
private handleOnDidChangeZoomLevel(targetWindowId: number): void {
1099
1100
// Zoom status entry
1101
this.updateWindowZoomStatusEntry(targetWindowId);
1102
1103
// Notify main process about a custom zoom level
1104
if (targetWindowId === mainWindow.vscodeWindowId) {
1105
const currentWindowZoomLevel = getZoomLevel(mainWindow);
1106
1107
let notifyZoomLevel: number | undefined = undefined;
1108
if (this.configuredWindowZoomLevel !== currentWindowZoomLevel) {
1109
notifyZoomLevel = currentWindowZoomLevel;
1110
}
1111
1112
ipcRenderer.invoke('vscode:notifyZoomLevel', notifyZoomLevel);
1113
}
1114
}
1115
1116
private createWindowZoomStatusEntry(part: IEditorPart): void {
1117
const disposables = new DisposableStore();
1118
Event.once(part.onWillDispose)(() => disposables.dispose());
1119
1120
const scopedInstantiationService = this.editorGroupService.getScopedInstantiationService(part);
1121
this.mapWindowIdToZoomStatusEntry.set(part.windowId, disposables.add(scopedInstantiationService.createInstance(ZoomStatusEntry)));
1122
disposables.add(toDisposable(() => this.mapWindowIdToZoomStatusEntry.delete(part.windowId)));
1123
1124
this.updateWindowZoomStatusEntry(part.windowId);
1125
}
1126
1127
private updateWindowZoomStatusEntry(targetWindowId: number): void {
1128
const targetWindow = getWindowById(targetWindowId);
1129
const entry = this.mapWindowIdToZoomStatusEntry.get(targetWindowId);
1130
if (entry && targetWindow) {
1131
const currentZoomLevel = getZoomLevel(targetWindow.window);
1132
1133
let text: string | undefined = undefined;
1134
if (currentZoomLevel < this.configuredWindowZoomLevel) {
1135
text = '$(zoom-out)';
1136
} else if (currentZoomLevel > this.configuredWindowZoomLevel) {
1137
text = '$(zoom-in)';
1138
}
1139
1140
entry.updateZoomEntry(text ?? false, targetWindowId);
1141
}
1142
}
1143
1144
private onDidChangeConfiguredWindowZoomLevel(): void {
1145
this.configuredWindowZoomLevel = this.resolveConfiguredWindowZoomLevel();
1146
1147
let applyZoomLevel = false;
1148
for (const { window } of getWindows()) {
1149
if (getZoomLevel(window) !== this.configuredWindowZoomLevel) {
1150
applyZoomLevel = true;
1151
break;
1152
}
1153
}
1154
1155
if (applyZoomLevel) {
1156
applyZoom(this.configuredWindowZoomLevel, ApplyZoomTarget.ALL_WINDOWS);
1157
}
1158
1159
for (const [windowId] of this.mapWindowIdToZoomStatusEntry) {
1160
this.updateWindowZoomStatusEntry(windowId);
1161
}
1162
}
1163
1164
//#endregion
1165
1166
override dispose(): void {
1167
super.dispose();
1168
1169
for (const [, entry] of this.mapWindowIdToZoomStatusEntry) {
1170
entry.dispose();
1171
}
1172
}
1173
}
1174
1175
class ZoomStatusEntry extends Disposable {
1176
1177
private readonly disposable = this._register(new MutableDisposable<DisposableStore>());
1178
1179
private zoomLevelLabel: Action | undefined = undefined;
1180
1181
constructor(
1182
@IStatusbarService private readonly statusbarService: IStatusbarService,
1183
@ICommandService private readonly commandService: ICommandService,
1184
@IKeybindingService private readonly keybindingService: IKeybindingService
1185
) {
1186
super();
1187
}
1188
1189
updateZoomEntry(visibleOrText: false | string, targetWindowId: number): void {
1190
if (typeof visibleOrText === 'string') {
1191
if (!this.disposable.value) {
1192
this.createZoomEntry(visibleOrText);
1193
}
1194
1195
this.updateZoomLevelLabel(targetWindowId);
1196
} else {
1197
this.disposable.clear();
1198
}
1199
}
1200
1201
private createZoomEntry(visibleOrText: string): void {
1202
const disposables = new DisposableStore();
1203
this.disposable.value = disposables;
1204
1205
const container = $('.zoom-status');
1206
1207
const left = $('.zoom-status-left');
1208
container.appendChild(left);
1209
1210
const zoomOutAction: Action = disposables.add(new Action('workbench.action.zoomOut', localize('zoomOut', "Zoom Out"), ThemeIcon.asClassName(Codicon.remove), true, () => this.commandService.executeCommand(zoomOutAction.id)));
1211
const zoomInAction: Action = disposables.add(new Action('workbench.action.zoomIn', localize('zoomIn', "Zoom In"), ThemeIcon.asClassName(Codicon.plus), true, () => this.commandService.executeCommand(zoomInAction.id)));
1212
const zoomResetAction: Action = disposables.add(new Action('workbench.action.zoomReset', localize('zoomReset', "Reset"), undefined, true, () => this.commandService.executeCommand(zoomResetAction.id)));
1213
zoomResetAction.tooltip = this.keybindingService.appendKeybinding(zoomResetAction.label, zoomResetAction.id);
1214
const zoomSettingsAction: Action = disposables.add(new Action('workbench.action.openSettings', localize('zoomSettings', "Settings"), ThemeIcon.asClassName(Codicon.settingsGear), true, () => this.commandService.executeCommand(zoomSettingsAction.id, 'window.zoom')));
1215
const zoomLevelLabel = disposables.add(new Action('zoomLabel', undefined, undefined, false));
1216
1217
this.zoomLevelLabel = zoomLevelLabel;
1218
disposables.add(toDisposable(() => this.zoomLevelLabel = undefined));
1219
1220
const actionBarLeft = disposables.add(new ActionBar(left, { hoverDelegate: nativeHoverDelegate }));
1221
actionBarLeft.push(zoomOutAction, { icon: true, label: false, keybinding: this.keybindingService.lookupKeybinding(zoomOutAction.id)?.getLabel() });
1222
actionBarLeft.push(this.zoomLevelLabel, { icon: false, label: true });
1223
actionBarLeft.push(zoomInAction, { icon: true, label: false, keybinding: this.keybindingService.lookupKeybinding(zoomInAction.id)?.getLabel() });
1224
1225
const right = $('.zoom-status-right');
1226
container.appendChild(right);
1227
1228
const actionBarRight = disposables.add(new ActionBar(right, { hoverDelegate: nativeHoverDelegate }));
1229
1230
actionBarRight.push(zoomResetAction, { icon: false, label: true });
1231
actionBarRight.push(zoomSettingsAction, { icon: true, label: false, keybinding: this.keybindingService.lookupKeybinding(zoomSettingsAction.id)?.getLabel() });
1232
1233
const name = localize('status.windowZoom', "Window Zoom");
1234
disposables.add(this.statusbarService.addEntry({
1235
name,
1236
text: visibleOrText,
1237
tooltip: container,
1238
ariaLabel: name,
1239
command: ShowTooltipCommand,
1240
kind: 'prominent'
1241
}, 'status.windowZoom', StatusbarAlignment.RIGHT, 102));
1242
}
1243
1244
private updateZoomLevelLabel(targetWindowId: number): void {
1245
if (this.zoomLevelLabel) {
1246
const targetWindow = getWindowById(targetWindowId, true).window;
1247
const zoomFactor = Math.round(getZoomFactor(targetWindow) * 100);
1248
const zoomLevel = getZoomLevel(targetWindow);
1249
1250
this.zoomLevelLabel.label = `${zoomLevel}`;
1251
this.zoomLevelLabel.tooltip = localize('zoomNumber', "Zoom Level: {0} ({1}%)", zoomLevel, zoomFactor);
1252
}
1253
}
1254
}
1255
1256