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