Path: blob/main/src/vs/workbench/electron-browser/window.ts
3291 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './media/window.css';6import { localize } from '../../nls.js';7import { URI } from '../../base/common/uri.js';8import { equals } from '../../base/common/objects.js';9import { EventType, EventHelper, addDisposableListener, ModifierKeyEmitter, getActiveElement, hasWindow, getWindowById, getWindows, $ } from '../../base/browser/dom.js';10import { Action, Separator, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../base/common/actions.js';11import { IFileService } from '../../platform/files/common/files.js';12import { EditorResourceAccessor, IUntitledTextResourceEditorInput, SideBySideEditor, pathsToEditors, IResourceDiffEditorInput, IUntypedEditorInput, IEditorPane, isResourceEditorInput, IResourceMergeEditorInput } from '../common/editor.js';13import { IEditorService } from '../services/editor/common/editorService.js';14import { ITelemetryService } from '../../platform/telemetry/common/telemetry.js';15import { WindowMinimumSize, IOpenFileRequest, IAddRemoveFoldersRequest, INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, INativeOpenFileRequest, hasNativeTitlebar } from '../../platform/window/common/window.js';16import { ITitleService } from '../services/title/browser/titleService.js';17import { IWorkbenchThemeService } from '../services/themes/common/workbenchThemeService.js';18import { ApplyZoomTarget, applyZoom } from '../../platform/window/electron-browser/window.js';19import { setFullscreen, getZoomLevel, onDidChangeZoomLevel, getZoomFactor } from '../../base/browser/browser.js';20import { ICommandService, CommandsRegistry } from '../../platform/commands/common/commands.js';21import { IResourceEditorInput } from '../../platform/editor/common/editor.js';22import { ipcRenderer, process } from '../../base/parts/sandbox/electron-browser/globals.js';23import { IWorkspaceEditingService } from '../services/workspaces/common/workspaceEditing.js';24import { IMenuService, MenuId, IMenu, MenuItemAction, MenuRegistry } from '../../platform/actions/common/actions.js';25import { ICommandAction } from '../../platform/action/common/action.js';26import { getFlatActionBarActions } from '../../platform/actions/browser/menuEntryActionViewItem.js';27import { RunOnceScheduler } from '../../base/common/async.js';28import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../base/common/lifecycle.js';29import { LifecyclePhase, ILifecycleService, WillShutdownEvent, ShutdownReason, BeforeShutdownErrorEvent, BeforeShutdownEvent } from '../services/lifecycle/common/lifecycle.js';30import { IWorkspaceFolderCreationData } from '../../platform/workspaces/common/workspaces.js';31import { IIntegrityService } from '../services/integrity/common/integrity.js';32import { isWindows, isMacintosh } from '../../base/common/platform.js';33import { IProductService } from '../../platform/product/common/productService.js';34import { INotificationService, NotificationPriority, Severity } from '../../platform/notification/common/notification.js';35import { IKeybindingService } from '../../platform/keybinding/common/keybinding.js';36import { INativeWorkbenchEnvironmentService } from '../services/environment/electron-browser/environmentService.js';37import { IAccessibilityService, AccessibilitySupport } from '../../platform/accessibility/common/accessibility.js';38import { WorkbenchState, IWorkspaceContextService } from '../../platform/workspace/common/workspace.js';39import { coalesce } from '../../base/common/arrays.js';40import { ConfigurationTarget, IConfigurationService } from '../../platform/configuration/common/configuration.js';41import { IStorageService, StorageScope, StorageTarget } from '../../platform/storage/common/storage.js';42import { IOpenerService, IResolvedExternalUri, OpenOptions } from '../../platform/opener/common/opener.js';43import { Schemas } from '../../base/common/network.js';44import { INativeHostService } from '../../platform/native/common/native.js';45import { posix } from '../../base/common/path.js';46import { ITunnelService, RemoteTunnel, extractLocalHostUriMetaDataForPortMapping, extractQueryLocalHostUriMetaDataForPortMapping } from '../../platform/tunnel/common/tunnel.js';47import { IWorkbenchLayoutService, positionFromString, Position } from '../services/layout/browser/layoutService.js';48import { IWorkingCopyService } from '../services/workingCopy/common/workingCopyService.js';49import { WorkingCopyCapabilities } from '../services/workingCopy/common/workingCopy.js';50import { IFilesConfigurationService } from '../services/filesConfiguration/common/filesConfigurationService.js';51import { Event } from '../../base/common/event.js';52import { IRemoteAuthorityResolverService } from '../../platform/remote/common/remoteAuthorityResolver.js';53import { IAddressProvider, IAddress } from '../../platform/remote/common/remoteAgentConnection.js';54import { IEditorGroupsService, IEditorPart } from '../services/editor/common/editorGroupsService.js';55import { IDialogService } from '../../platform/dialogs/common/dialogs.js';56import { AuthInfo } from '../../base/parts/sandbox/electron-browser/electronTypes.js';57import { ILogService } from '../../platform/log/common/log.js';58import { IInstantiationService } from '../../platform/instantiation/common/instantiation.js';59import { whenEditorClosed } from '../browser/editor.js';60import { ISharedProcessService } from '../../platform/ipc/electron-browser/services.js';61import { IProgressService, ProgressLocation } from '../../platform/progress/common/progress.js';62import { toErrorMessage } from '../../base/common/errorMessage.js';63import { ILabelService } from '../../platform/label/common/label.js';64import { dirname } from '../../base/common/resources.js';65import { IBannerService } from '../services/banner/browser/bannerService.js';66import { Codicon } from '../../base/common/codicons.js';67import { IUriIdentityService } from '../../platform/uriIdentity/common/uriIdentity.js';68import { IPreferencesService } from '../services/preferences/common/preferences.js';69import { IUtilityProcessWorkerWorkbenchService } from '../services/utilityProcess/electron-browser/utilityProcessWorkerWorkbenchService.js';70import { registerWindowDriver } from '../services/driver/browser/driver.js';71import { mainWindow } from '../../base/browser/window.js';72import { BaseWindow } from '../browser/window.js';73import { IHostService } from '../services/host/browser/host.js';74import { IStatusbarService, ShowTooltipCommand, StatusbarAlignment } from '../services/statusbar/browser/statusbar.js';75import { ActionBar } from '../../base/browser/ui/actionbar/actionbar.js';76import { ThemeIcon } from '../../base/common/themables.js';77import { getWorkbenchContribution } from '../common/contributions.js';78import { DynamicWorkbenchSecurityConfiguration } from '../common/configuration.js';79import { nativeHoverDelegate } from '../../platform/hover/browser/hover.js';80import { WINDOW_ACTIVE_BORDER, WINDOW_INACTIVE_BORDER } from '../common/theme.js';8182export class NativeWindow extends BaseWindow {8384private readonly customTitleContextMenuDisposable = this._register(new DisposableStore());8586private readonly addRemoveFoldersScheduler = this._register(new RunOnceScheduler(() => this.doAddRemoveFolders(), 100));87private pendingFoldersToAdd: URI[] = [];88private pendingFoldersToRemove: URI[] = [];8990private isDocumentedEdited = false;9192constructor(93@IEditorService private readonly editorService: IEditorService,94@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,95@IConfigurationService private readonly configurationService: IConfigurationService,96@ITitleService private readonly titleService: ITitleService,97@IWorkbenchThemeService protected themeService: IWorkbenchThemeService,98@INotificationService private readonly notificationService: INotificationService,99@ICommandService private readonly commandService: ICommandService,100@IKeybindingService private readonly keybindingService: IKeybindingService,101@ITelemetryService private readonly telemetryService: ITelemetryService,102@IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService,103@IFileService private readonly fileService: IFileService,104@IMenuService private readonly menuService: IMenuService,105@ILifecycleService private readonly lifecycleService: ILifecycleService,106@IIntegrityService private readonly integrityService: IIntegrityService,107@INativeWorkbenchEnvironmentService private readonly nativeEnvironmentService: INativeWorkbenchEnvironmentService,108@IAccessibilityService private readonly accessibilityService: IAccessibilityService,109@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,110@IOpenerService private readonly openerService: IOpenerService,111@INativeHostService private readonly nativeHostService: INativeHostService,112@ITunnelService private readonly tunnelService: ITunnelService,113@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,114@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,115@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService,116@IProductService private readonly productService: IProductService,117@IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService,118@IDialogService private readonly dialogService: IDialogService,119@IStorageService private readonly storageService: IStorageService,120@ILogService private readonly logService: ILogService,121@IInstantiationService private readonly instantiationService: IInstantiationService,122@ISharedProcessService private readonly sharedProcessService: ISharedProcessService,123@IProgressService private readonly progressService: IProgressService,124@ILabelService private readonly labelService: ILabelService,125@IBannerService private readonly bannerService: IBannerService,126@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,127@IPreferencesService private readonly preferencesService: IPreferencesService,128@IUtilityProcessWorkerWorkbenchService private readonly utilityProcessWorkerWorkbenchService: IUtilityProcessWorkerWorkbenchService,129@IHostService hostService: IHostService130) {131super(mainWindow, undefined, hostService, nativeEnvironmentService);132133this.configuredWindowZoomLevel = this.resolveConfiguredWindowZoomLevel();134135this.registerListeners();136this.create();137}138139protected registerListeners(): void {140141// Layout142this._register(addDisposableListener(mainWindow, EventType.RESIZE, () => this.layoutService.layout()));143144// React to editor input changes145this._register(this.editorService.onDidActiveEditorChange(() => this.updateTouchbarMenu()));146147// Prevent opening a real URL inside the window148for (const event of [EventType.DRAG_OVER, EventType.DROP]) {149this._register(addDisposableListener(mainWindow.document.body, event, (e: DragEvent) => {150EventHelper.stop(e);151}));152}153154// Support `runAction` event155ipcRenderer.on('vscode:runAction', async (event: unknown, request: INativeRunActionInWindowRequest) => {156const args: unknown[] = request.args || [];157158// If we run an action from the touchbar, we fill in the currently active resource159// as payload because the touch bar items are context aware depending on the editor160if (request.from === 'touchbar') {161const activeEditor = this.editorService.activeEditor;162if (activeEditor) {163const resource = EditorResourceAccessor.getOriginalUri(activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });164if (resource) {165args.push(resource);166}167}168} else {169args.push({ from: request.from });170}171172try {173await this.commandService.executeCommand(request.id, ...args);174175this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: request.id, from: request.from });176} catch (error) {177this.notificationService.error(error);178}179});180181// Support runKeybinding event182ipcRenderer.on('vscode:runKeybinding', (event: unknown, request: INativeRunKeybindingInWindowRequest) => {183const activeElement = getActiveElement();184if (activeElement) {185this.keybindingService.dispatchByUserSettingsLabel(request.userSettingsLabel, activeElement);186}187});188189// Shared Process crash reported from main190ipcRenderer.on('vscode:reportSharedProcessCrash', (event: unknown, error: string) => {191this.notificationService.prompt(192Severity.Error,193localize('sharedProcessCrash', "A shared background process terminated unexpectedly. Please restart the application to recover."),194[{195label: localize('restart', "Restart"),196run: () => this.nativeHostService.relaunch()197}],198{199priority: NotificationPriority.URGENT200}201);202});203204// Support openFiles event for existing and new files205ipcRenderer.on('vscode:openFiles', (event: unknown, request: IOpenFileRequest) => { this.onOpenFiles(request); });206207// Support addRemoveFolders event for workspace management208ipcRenderer.on('vscode:addRemoveFolders', (event: unknown, request: IAddRemoveFoldersRequest) => this.onAddRemoveFoldersRequest(request));209210// Message support211ipcRenderer.on('vscode:showInfoMessage', (event: unknown, message: string) => this.notificationService.info(message));212213// Shell Environment Issue Notifications214ipcRenderer.on('vscode:showResolveShellEnvError', (event: unknown, message: string) => {215this.notificationService.prompt(216Severity.Error,217message,218[{219label: localize('restart', "Restart"),220run: () => this.nativeHostService.relaunch()221},222{223label: localize('configure', "Configure"),224run: () => this.preferencesService.openUserSettings({ query: 'application.shellEnvironmentResolutionTimeout' })225},226{227label: localize('learnMore', "Learn More"),228run: () => this.openerService.open('https://go.microsoft.com/fwlink/?linkid=2149667')229}]230);231});232233ipcRenderer.on('vscode:showCredentialsError', (event: unknown, message: string) => {234this.notificationService.prompt(235Severity.Error,236localize('keychainWriteError', "Writing login information to the keychain failed with error '{0}'.", message),237[{238label: localize('troubleshooting', "Troubleshooting Guide"),239run: () => this.openerService.open('https://go.microsoft.com/fwlink/?linkid=2190713')240}]241);242});243244ipcRenderer.on('vscode:showTranslatedBuildWarning', () => {245this.notificationService.prompt(246Severity.Warning,247localize("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),248[{249label: localize('downloadArmBuild', "Download"),250run: () => {251const quality = this.productService.quality;252const stableURL = 'https://code.visualstudio.com/docs/?dv=osx';253const insidersURL = 'https://code.visualstudio.com/docs/?dv=osx&build=insiders';254this.openerService.open(quality === 'stable' ? stableURL : insidersURL);255}256}],257{258priority: NotificationPriority.URGENT259}260);261});262263ipcRenderer.on('vscode:showArgvParseWarning', (event: unknown, message: string) => {264this.notificationService.prompt(265Severity.Warning,266localize("showArgvParseWarning", "The runtime arguments file 'argv.json' contains errors. Please correct them and restart."),267[{268label: localize('showArgvParseWarningAction', "Open File"),269run: () => this.editorService.openEditor({ resource: this.nativeEnvironmentService.argvResource })270}],271{272priority: NotificationPriority.URGENT273}274);275});276277// Fullscreen Events278ipcRenderer.on('vscode:enterFullScreen', () => setFullscreen(true, mainWindow));279ipcRenderer.on('vscode:leaveFullScreen', () => setFullscreen(false, mainWindow));280281// Proxy Login Dialog282ipcRenderer.on('vscode:openProxyAuthenticationDialog', async (event: unknown, payload: { authInfo: AuthInfo; username?: string; password?: string; replyChannel: string }) => {283const rememberCredentialsKey = 'window.rememberProxyCredentials';284const rememberCredentials = this.storageService.getBoolean(rememberCredentialsKey, StorageScope.APPLICATION);285const result = await this.dialogService.input({286type: 'warning',287message: localize('proxyAuthRequired', "Proxy Authentication Required"),288primaryButton: localize({ key: 'loginButton', comment: ['&& denotes a mnemonic'] }, "&&Log In"),289inputs:290[291{ placeholder: localize('username', "Username"), value: payload.username },292{ placeholder: localize('password', "Password"), type: 'password', value: payload.password }293],294detail: localize('proxyDetail', "The proxy {0} requires a username and password.", `${payload.authInfo.host}:${payload.authInfo.port}`),295checkbox: {296label: localize('rememberCredentials', "Remember my credentials"),297checked: rememberCredentials298}299});300301// Reply back to the channel without result to indicate302// that the login dialog was cancelled303if (!result.confirmed || !result.values) {304ipcRenderer.send(payload.replyChannel);305}306307// Other reply back with the picked credentials308else {309310// Update state based on checkbox311if (result.checkboxChecked) {312this.storageService.store(rememberCredentialsKey, true, StorageScope.APPLICATION, StorageTarget.MACHINE);313} else {314this.storageService.remove(rememberCredentialsKey, StorageScope.APPLICATION);315}316317// Reply back to main side with credentials318const [username, password] = result.values;319ipcRenderer.send(payload.replyChannel, { username, password, remember: !!result.checkboxChecked });320}321});322323// Accessibility support changed event324ipcRenderer.on('vscode:accessibilitySupportChanged', (event: unknown, accessibilitySupportEnabled: boolean) => {325this.accessibilityService.setAccessibilitySupport(accessibilitySupportEnabled ? AccessibilitySupport.Enabled : AccessibilitySupport.Disabled);326});327328// Allow to update security settings around allowed UNC Host329ipcRenderer.on('vscode:configureAllowedUNCHost', async (event: unknown, host: string) => {330if (!isWindows) {331return; // only supported on Windows332}333334const allowedUncHosts = new Set<string>();335336const configuredAllowedUncHosts = this.configurationService.getValue<string[] | undefined>('security.allowedUNCHosts',) ?? [];337if (Array.isArray(configuredAllowedUncHosts)) {338for (const configuredAllowedUncHost of configuredAllowedUncHosts) {339if (typeof configuredAllowedUncHost === 'string') {340allowedUncHosts.add(configuredAllowedUncHost);341}342}343}344345if (!allowedUncHosts.has(host)) {346allowedUncHosts.add(host);347348await getWorkbenchContribution<DynamicWorkbenchSecurityConfiguration>(DynamicWorkbenchSecurityConfiguration.ID).ready; // ensure this setting is registered349this.configurationService.updateValue('security.allowedUNCHosts', [...allowedUncHosts.values()], ConfigurationTarget.USER);350}351});352353// Allow to update security settings around protocol handlers354ipcRenderer.on('vscode:disablePromptForProtocolHandling', (event: unknown, kind: 'local' | 'remote') => {355const setting = kind === 'local' ? 'security.promptForLocalFileProtocolHandling' : 'security.promptForRemoteFileProtocolHandling';356this.configurationService.updateValue(setting, false);357});358359// Window Settings360this._register(this.configurationService.onDidChangeConfiguration(e => {361if (e.affectsConfiguration('window.zoomLevel') || (e.affectsConfiguration('window.zoomPerWindow') && this.configurationService.getValue('window.zoomPerWindow') === false)) {362this.onDidChangeConfiguredWindowZoomLevel();363} else if (e.affectsConfiguration('keyboard.touchbar.enabled') || e.affectsConfiguration('keyboard.touchbar.ignored')) {364this.updateTouchbarMenu();365} else if (e.affectsConfiguration('window.border')) {366this.updateWindowBorder();367}368}));369370this._register(onDidChangeZoomLevel(targetWindowId => this.handleOnDidChangeZoomLevel(targetWindowId)));371372for (const part of this.editorGroupService.parts) {373this.createWindowZoomStatusEntry(part);374}375376this._register(this.editorGroupService.onDidCreateAuxiliaryEditorPart(part => this.createWindowZoomStatusEntry(part)));377378// Listen to visible editor changes (debounced in case a new editor opens immediately after)379this._register(Event.debounce(this.editorService.onDidVisibleEditorsChange, () => undefined, 0, undefined, undefined, undefined, this._store)(() => this.maybeCloseWindow()));380381// Listen to editor closing (if we run with --wait)382const filesToWait = this.nativeEnvironmentService.filesToWait;383if (filesToWait) {384this.trackClosedWaitFiles(filesToWait.waitMarkerFileUri, coalesce(filesToWait.paths.map(path => path.fileUri)));385}386387// macOS OS integration: represented file name388if (isMacintosh) {389for (const part of this.editorGroupService.parts) {390this.handleRepresentedFilename(part);391}392393this._register(this.editorGroupService.onDidCreateAuxiliaryEditorPart(part => this.handleRepresentedFilename(part)));394}395396// Document edited: indicate for dirty working copies397this._register(this.workingCopyService.onDidChangeDirty(workingCopy => {398const gotDirty = workingCopy.isDirty();399if (gotDirty && !(workingCopy.capabilities & WorkingCopyCapabilities.Untitled) && this.filesConfigurationService.hasShortAutoSaveDelay(workingCopy.resource)) {400return; // do not indicate dirty of working copies that are auto saved after short delay401}402403this.updateDocumentEdited(gotDirty ? true : undefined);404}));405406this.updateDocumentEdited(undefined);407408// Detect minimize / maximize409this._register(Event.any(410Event.map(Event.filter(this.nativeHostService.onDidMaximizeWindow, windowId => !!hasWindow(windowId)), windowId => ({ maximized: true, windowId })),411Event.map(Event.filter(this.nativeHostService.onDidUnmaximizeWindow, windowId => !!hasWindow(windowId)), windowId => ({ maximized: false, windowId }))412)(e => this.layoutService.updateWindowMaximizedState(getWindowById(e.windowId)!.window, e.maximized)));413this.layoutService.updateWindowMaximizedState(mainWindow, this.nativeEnvironmentService.window.maximized ?? false);414415// Detect panel position to determine minimum width416this._register(this.layoutService.onDidChangePanelPosition(pos => this.onDidChangePanelPosition(positionFromString(pos))));417this.onDidChangePanelPosition(this.layoutService.getPanelPosition());418419// Border420this._register(this.themeService.onDidColorThemeChange(() => this.updateWindowBorder()));421this._register(this.hostService.onDidChangeActiveWindow(() => this.updateWindowBorder()));422this._register(this.hostService.onDidChangeFocus(() => this.updateWindowBorder()));423424// Lifecycle425this._register(this.lifecycleService.onBeforeShutdown(e => this.onBeforeShutdown(e)));426this._register(this.lifecycleService.onBeforeShutdownError(e => this.onBeforeShutdownError(e)));427this._register(this.lifecycleService.onWillShutdown(e => this.onWillShutdown(e)));428}429430private handleRepresentedFilename(part: IEditorPart): void {431const disposables = new DisposableStore();432Event.once(part.onWillDispose)(() => disposables.dispose());433434this.editorGroupService.getScopedInstantiationService(part).invokeFunction(accessor => {435const editorService = accessor.get(IEditorService);436disposables.add(editorService.onDidActiveEditorChange(() => this.updateRepresentedFilename(editorService, part.windowId)));437});438}439440private updateRepresentedFilename(editorService: IEditorService, targetWindowId: number): void {441const file = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY, filterByScheme: Schemas.file });442443// Represented Filename444this.nativeHostService.setRepresentedFilename(file?.fsPath ?? '', { targetWindowId });445446// Custom title menu (main window only currently)447if (targetWindowId === mainWindow.vscodeWindowId) {448this.provideCustomTitleContextMenu(file?.fsPath);449}450}451452//#region Window Lifecycle453454private onBeforeShutdown({ veto, reason }: BeforeShutdownEvent): void {455if (reason === ShutdownReason.CLOSE) {456const confirmBeforeCloseSetting = this.configurationService.getValue<'always' | 'never' | 'keyboardOnly'>('window.confirmBeforeClose');457458const confirmBeforeClose = confirmBeforeCloseSetting === 'always' || (confirmBeforeCloseSetting === 'keyboardOnly' && ModifierKeyEmitter.getInstance().isModifierPressed);459if (confirmBeforeClose) {460461// When we need to confirm on close or quit, veto the shutdown462// with a long running promise to figure out whether shutdown463// can proceed or not.464465return veto((async () => {466let actualReason: ShutdownReason = reason;467if (reason === ShutdownReason.CLOSE && !isMacintosh) {468const windowCount = await this.nativeHostService.getWindowCount();469if (windowCount === 1) {470actualReason = ShutdownReason.QUIT; // Windows/Linux: closing last window means to QUIT471}472}473474let confirmed = true;475if (confirmBeforeClose) {476confirmed = await this.instantiationService.invokeFunction(accessor => NativeWindow.confirmOnShutdown(accessor, actualReason));477}478479// Progress for long running shutdown480if (confirmed) {481this.progressOnBeforeShutdown(reason);482}483484return !confirmed;485})(), 'veto.confirmBeforeClose');486}487}488489// Progress for long running shutdown490this.progressOnBeforeShutdown(reason);491}492493private progressOnBeforeShutdown(reason: ShutdownReason): void {494this.progressService.withProgress({495location: ProgressLocation.Window, // use window progress to not be too annoying about this operation496delay: 800, // delay so that it only appears when operation takes a long time497title: this.toShutdownLabel(reason, false),498}, () => {499return Event.toPromise(Event.any(500this.lifecycleService.onWillShutdown, // dismiss this dialog when we shutdown501this.lifecycleService.onShutdownVeto, // or when shutdown was vetoed502this.dialogService.onWillShowDialog // or when a dialog asks for input503));504});505}506507private onBeforeShutdownError({ error, reason }: BeforeShutdownErrorEvent): void {508this.dialogService.error(this.toShutdownLabel(reason, true), localize('shutdownErrorDetail', "Error: {0}", toErrorMessage(error)));509}510511private onWillShutdown({ reason, force, joiners }: WillShutdownEvent): void {512513// Delay so that the dialog only appears after timeout514const shutdownDialogScheduler = new RunOnceScheduler(() => {515const pendingJoiners = joiners();516517this.progressService.withProgress({518location: ProgressLocation.Dialog, // use a dialog to prevent the user from making any more interactions now519buttons: [this.toForceShutdownLabel(reason)], // allow to force shutdown anyway520cancellable: false, // do not allow to cancel521sticky: true, // do not allow to dismiss522title: this.toShutdownLabel(reason, false),523detail: pendingJoiners.length > 0 ? localize('willShutdownDetail', "The following operations are still running: \n{0}", pendingJoiners.map(joiner => `- ${joiner.label}`).join('\n')) : undefined524}, () => {525return Event.toPromise(this.lifecycleService.onDidShutdown); // dismiss this dialog when we actually shutdown526}, () => {527force();528});529}, 1200);530shutdownDialogScheduler.schedule();531532// Dispose scheduler when we actually shutdown533Event.once(this.lifecycleService.onDidShutdown)(() => shutdownDialogScheduler.dispose());534}535536private toShutdownLabel(reason: ShutdownReason, isError: boolean): string {537if (isError) {538switch (reason) {539case ShutdownReason.CLOSE:540return localize('shutdownErrorClose', "An unexpected error prevented the window to close");541case ShutdownReason.QUIT:542return localize('shutdownErrorQuit', "An unexpected error prevented the application to quit");543case ShutdownReason.RELOAD:544return localize('shutdownErrorReload', "An unexpected error prevented the window to reload");545case ShutdownReason.LOAD:546return localize('shutdownErrorLoad', "An unexpected error prevented to change the workspace");547}548}549550switch (reason) {551case ShutdownReason.CLOSE:552return localize('shutdownTitleClose', "Closing the window is taking a bit longer...");553case ShutdownReason.QUIT:554return localize('shutdownTitleQuit', "Quitting the application is taking a bit longer...");555case ShutdownReason.RELOAD:556return localize('shutdownTitleReload', "Reloading the window is taking a bit longer...");557case ShutdownReason.LOAD:558return localize('shutdownTitleLoad', "Changing the workspace is taking a bit longer...");559}560}561562private toForceShutdownLabel(reason: ShutdownReason): string {563switch (reason) {564case ShutdownReason.CLOSE:565return localize('shutdownForceClose', "Close Anyway");566case ShutdownReason.QUIT:567return localize('shutdownForceQuit', "Quit Anyway");568case ShutdownReason.RELOAD:569return localize('shutdownForceReload', "Reload Anyway");570case ShutdownReason.LOAD:571return localize('shutdownForceLoad', "Change Anyway");572}573}574575//#endregion576577private updateDocumentEdited(documentEdited: true | undefined): void {578let setDocumentEdited: boolean;579if (typeof documentEdited === 'boolean') {580setDocumentEdited = documentEdited;581} else {582setDocumentEdited = this.workingCopyService.hasDirty;583}584585if ((!this.isDocumentedEdited && setDocumentEdited) || (this.isDocumentedEdited && !setDocumentEdited)) {586this.isDocumentedEdited = setDocumentEdited;587588this.nativeHostService.setDocumentEdited(setDocumentEdited);589}590}591592private getWindowMinimumWidth(panelPosition: Position = this.layoutService.getPanelPosition()): number {593594// if panel is on the side, then return the larger minwidth595const panelOnSide = panelPosition === Position.LEFT || panelPosition === Position.RIGHT;596if (panelOnSide) {597return WindowMinimumSize.WIDTH_WITH_VERTICAL_PANEL;598}599600return WindowMinimumSize.WIDTH;601}602603private onDidChangePanelPosition(pos: Position): void {604const minWidth = this.getWindowMinimumWidth(pos);605606this.nativeHostService.setMinimumSize(minWidth, undefined);607}608609private maybeCloseWindow(): void {610const closeWhenEmpty = this.configurationService.getValue('window.closeWhenEmpty') || this.nativeEnvironmentService.args.wait;611if (!closeWhenEmpty) {612return; // return early if configured to not close when empty613}614615// Close empty editor groups based on setting and environment616for (const editorPart of this.editorGroupService.parts) {617if (editorPart.groups.some(group => !group.isEmpty)) {618continue; // not empty619}620621if (editorPart === this.editorGroupService.mainPart && (622this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY || // only for empty windows623this.environmentService.isExtensionDevelopment || // not when developing an extension624this.editorService.visibleEditors.length > 0 // not when there are still editors open in other windows625)) {626continue;627}628629if (editorPart === this.editorGroupService.mainPart) {630this.nativeHostService.closeWindow();631} else {632editorPart.removeGroup(editorPart.activeGroup);633}634}635}636637private provideCustomTitleContextMenu(filePath: string | undefined): void {638639// Clear old menu640this.customTitleContextMenuDisposable.clear();641642// Only provide a menu when we have a file path and custom titlebar643if (!filePath || hasNativeTitlebar(this.configurationService)) {644return;645}646647// Split up filepath into segments648const segments = filePath.split(posix.sep);649for (let i = segments.length; i > 0; i--) {650const isFile = (i === segments.length);651652let pathOffset = i;653if (!isFile) {654pathOffset++; // for segments which are not the file name we want to open the folder655}656657const path = URI.file(segments.slice(0, pathOffset).join(posix.sep));658659let label: string;660if (!isFile) {661label = this.labelService.getUriBasenameLabel(dirname(path));662} else {663label = this.labelService.getUriBasenameLabel(path);664}665666const commandId = `workbench.action.revealPathInFinder${i}`;667this.customTitleContextMenuDisposable.add(CommandsRegistry.registerCommand(commandId, () => this.nativeHostService.showItemInFolder(path.fsPath)));668this.customTitleContextMenuDisposable.add(MenuRegistry.appendMenuItem(MenuId.TitleBarTitleContext, { command: { id: commandId, title: label || posix.sep }, order: -i, group: '1_file' }));669}670}671672protected create(): void {673674// Handle open calls675this.setupOpenHandlers();676677// Notify some services about lifecycle phases678this.lifecycleService.when(LifecyclePhase.Ready).then(() => this.nativeHostService.notifyReady());679this.lifecycleService.when(LifecyclePhase.Restored).then(() => {680this.sharedProcessService.notifyRestored();681this.utilityProcessWorkerWorkbenchService.notifyRestored();682});683684// Check for situations that are worth warning the user about685this.handleWarnings();686687// Touchbar menu (if enabled)688this.updateTouchbarMenu();689690// Window border691this.updateWindowBorder();692693// Smoke Test Driver694if (this.environmentService.enableSmokeTestDriver) {695registerWindowDriver(this.instantiationService);696}697}698699private async handleWarnings(): Promise<void> {700701// After restored phase is fine for the following ones702await this.lifecycleService.when(LifecyclePhase.Restored);703704// Integrity / Root warning705(async () => {706const isAdmin = await this.nativeHostService.isAdmin();707const { isPure } = await this.integrityService.isPure();708709// Update to title710this.titleService.updateProperties({ isPure, isAdmin });711712// Show warning message (unix only)713if (isAdmin && !isWindows) {714this.notificationService.warn(localize('runningAsRoot', "It is not recommended to run {0} as root user.", this.productService.nameShort));715}716})();717718// Installation Dir Warning719if (this.environmentService.isBuilt && !this.environmentService.extensionDevelopmentLocationURI?.length) {720let installLocationUri: URI;721if (isMacintosh) {722// appRoot = /Applications/Visual Studio Code - Insiders.app/Contents/Resources/app723installLocationUri = dirname(dirname(dirname(URI.file(this.nativeEnvironmentService.appRoot))));724} else {725// appRoot = C:\Users\<name>\AppData\Local\Programs\Microsoft VS Code Insiders\resources\app726// appRoot = /usr/share/code-insiders/resources/app727installLocationUri = dirname(dirname(URI.file(this.nativeEnvironmentService.appRoot)));728}729730for (const folder of this.contextService.getWorkspace().folders) {731if (this.uriIdentityService.extUri.isEqualOrParent(folder.uri, installLocationUri)) {732this.bannerService.show({733id: 'appRootWarning.banner',734message: 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)),735icon: Codicon.warning736});737738break;739}740}741}742743// Slow shell environment progress indicator744const shellEnv = process.shellEnv();745this.progressService.withProgress({746title: localize('resolveShellEnvironment', "Resolving shell environment..."),747location: ProgressLocation.Window,748delay: 1600,749buttons: [localize('learnMore', "Learn More")]750}, () => shellEnv, () => this.openerService.open('https://go.microsoft.com/fwlink/?linkid=2149667'));751}752753async resolveExternalUri(uri: URI, options?: OpenOptions): Promise<IResolvedExternalUri | undefined> {754let queryTunnel: RemoteTunnel | string | undefined;755if (options?.allowTunneling) {756const portMappingRequest = extractLocalHostUriMetaDataForPortMapping(uri);757const queryPortMapping = extractQueryLocalHostUriMetaDataForPortMapping(uri);758if (queryPortMapping) {759queryTunnel = await this.openTunnel(queryPortMapping.address, queryPortMapping.port);760if (queryTunnel && (typeof queryTunnel !== 'string')) {761// If the tunnel was mapped to a different port, dispose it, because some services762// validate the port number in the query string.763if (queryTunnel.tunnelRemotePort !== queryPortMapping.port) {764queryTunnel.dispose();765queryTunnel = undefined;766} else {767if (!portMappingRequest) {768const tunnel = queryTunnel;769return {770resolved: uri,771dispose: () => tunnel.dispose()772};773}774}775}776}777778if (portMappingRequest) {779const tunnel = await this.openTunnel(portMappingRequest.address, portMappingRequest.port);780if (tunnel && (typeof tunnel !== 'string')) {781const addressAsUri = URI.parse(tunnel.localAddress).with({ path: uri.path });782const resolved = addressAsUri.scheme.startsWith(uri.scheme) ? addressAsUri : uri.with({ authority: tunnel.localAddress });783return {784resolved,785dispose() {786tunnel.dispose();787if (queryTunnel && (typeof queryTunnel !== 'string')) {788queryTunnel.dispose();789}790}791};792}793}794}795796if (!options?.openExternal) {797const canHandleResource = await this.fileService.canHandleResource(uri);798if (canHandleResource) {799return {800resolved: URI.from({801scheme: this.productService.urlProtocol,802path: 'workspace',803query: uri.toString()804}),805dispose() { }806};807}808}809810return undefined;811}812813private async openTunnel(address: string, port: number): Promise<RemoteTunnel | string | undefined> {814const remoteAuthority = this.environmentService.remoteAuthority;815const addressProvider: IAddressProvider | undefined = remoteAuthority ? {816getAddress: async (): Promise<IAddress> => {817return (await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority)).authority;818}819} : undefined;820821const tunnel = await this.tunnelService.getExistingTunnel(address, port);822if (!tunnel || (typeof tunnel === 'string')) {823return this.tunnelService.openTunnel(addressProvider, address, port);824}825826return tunnel;827}828829private setupOpenHandlers(): void {830831// Handle external open() calls832this.openerService.setDefaultExternalOpener({833openExternal: async (href: string) => {834const success = await this.nativeHostService.openExternal(href, this.configurationService.getValue<string>('workbench.externalBrowser'));835if (!success) {836const fileCandidate = URI.parse(href);837if (fileCandidate.scheme === Schemas.file) {838// if opening failed, and this is a file, we can still try to reveal it839await this.nativeHostService.showItemInFolder(fileCandidate.fsPath);840}841}842843return true;844}845});846847// Register external URI resolver848this.openerService.registerExternalUriResolver({849resolveExternalUri: async (uri: URI, options?: OpenOptions) => {850return this.resolveExternalUri(uri, options);851}852});853}854855//#region Touchbar856857private touchBarMenu: IMenu | undefined;858private readonly touchBarDisposables = this._register(new DisposableStore());859private lastInstalledTouchedBar: ICommandAction[][] | undefined;860861private updateTouchbarMenu(): void {862if (!isMacintosh) {863return; // macOS only864}865866// Dispose old867this.touchBarDisposables.clear();868this.touchBarMenu = undefined;869870// Create new (delayed)871const scheduler: RunOnceScheduler = this.touchBarDisposables.add(new RunOnceScheduler(() => this.doUpdateTouchbarMenu(scheduler), 300));872scheduler.schedule();873}874875private doUpdateTouchbarMenu(scheduler: RunOnceScheduler): void {876if (!this.touchBarMenu) {877const scopedContextKeyService = this.editorService.activeEditorPane?.scopedContextKeyService || this.editorGroupService.activeGroup.scopedContextKeyService;878this.touchBarMenu = this.menuService.createMenu(MenuId.TouchBarContext, scopedContextKeyService);879this.touchBarDisposables.add(this.touchBarMenu);880this.touchBarDisposables.add(this.touchBarMenu.onDidChange(() => scheduler.schedule()));881}882883const disabled = this.configurationService.getValue('keyboard.touchbar.enabled') === false;884const touchbarIgnored = this.configurationService.getValue('keyboard.touchbar.ignored');885const ignoredItems = Array.isArray(touchbarIgnored) ? touchbarIgnored : [];886887// Fill actions into groups respecting order888const actions = getFlatActionBarActions(this.touchBarMenu.getActions());889890// Convert into command action multi array891const items: ICommandAction[][] = [];892let group: ICommandAction[] = [];893if (!disabled) {894for (const action of actions) {895896// Command897if (action instanceof MenuItemAction) {898if (ignoredItems.indexOf(action.item.id) >= 0) {899continue; // ignored900}901902group.push(action.item);903}904905// Separator906else if (action instanceof Separator) {907if (group.length) {908items.push(group);909}910911group = [];912}913}914915if (group.length) {916items.push(group);917}918}919920// Only update if the actions have changed921if (!equals(this.lastInstalledTouchedBar, items)) {922this.lastInstalledTouchedBar = items;923this.nativeHostService.updateTouchBar(items);924}925}926927//#endregion928929//#region Window Border930931private updateWindowBorder(): void {932if (!isWindows) {933return; // windows only934}935936const theme = this.themeService.getColorTheme();937938let activeBorder = theme.getColor(WINDOW_ACTIVE_BORDER)?.toString();939let inactiveBorder = theme.getColor(WINDOW_INACTIVE_BORDER)?.toString();940941const borderSetting = this.configurationService.getValue<string>('window.border');942if (borderSetting === 'off') {943activeBorder = 'off';944inactiveBorder = undefined;945} else if (borderSetting === 'default') {946activeBorder = activeBorder ?? 'default';947} else if (borderSetting === 'system') {948activeBorder = 'default';949inactiveBorder = undefined;950} else {951activeBorder = borderSetting;952inactiveBorder = undefined;953}954955this.nativeHostService.updateWindowAccentColor(activeBorder, inactiveBorder);956}957958//#endregion959960private onAddRemoveFoldersRequest(request: IAddRemoveFoldersRequest): void {961962// Buffer all pending requests963this.pendingFoldersToAdd.push(...request.foldersToAdd.map(folder => URI.revive(folder)));964this.pendingFoldersToRemove.push(...request.foldersToRemove.map(folder => URI.revive(folder)));965966// Delay the adding of folders a bit to buffer in case more requests are coming967if (!this.addRemoveFoldersScheduler.isScheduled()) {968this.addRemoveFoldersScheduler.schedule();969}970}971972private async doAddRemoveFolders(): Promise<void> {973const foldersToAdd: IWorkspaceFolderCreationData[] = this.pendingFoldersToAdd.map(folder => ({ uri: folder }));974const foldersToRemove = this.pendingFoldersToRemove.slice(0);975976this.pendingFoldersToAdd = [];977this.pendingFoldersToRemove = [];978979if (foldersToAdd.length) {980await this.workspaceEditingService.addFolders(foldersToAdd);981}982983if (foldersToRemove.length) {984await this.workspaceEditingService.removeFolders(foldersToRemove);985}986}987988private async onOpenFiles(request: INativeOpenFileRequest): Promise<void> {989const diffMode = !!(request.filesToDiff && (request.filesToDiff.length === 2));990const mergeMode = !!(request.filesToMerge && (request.filesToMerge.length === 4));991992const inputs = coalesce(await pathsToEditors(mergeMode ? request.filesToMerge : diffMode ? request.filesToDiff : request.filesToOpenOrCreate, this.fileService, this.logService));993if (inputs.length) {994const openedEditorPanes = await this.openResources(inputs, diffMode, mergeMode);995996if (request.filesToWait) {997998// In wait mode, listen to changes to the editors and wait until the files999// are closed that the user wants to wait for. When this happens we delete1000// the wait marker file to signal to the outside that editing is done.1001// However, it is possible that opening of the editors failed, as such we1002// check for whether editor panes got opened and otherwise delete the marker1003// right away.10041005if (openedEditorPanes.length) {1006return this.trackClosedWaitFiles(URI.revive(request.filesToWait.waitMarkerFileUri), coalesce(request.filesToWait.paths.map(path => URI.revive(path.fileUri))));1007} else {1008return this.fileService.del(URI.revive(request.filesToWait.waitMarkerFileUri));1009}1010}1011}1012}10131014private async trackClosedWaitFiles(waitMarkerFile: URI, resourcesToWaitFor: URI[]): Promise<void> {10151016// Wait for the resources to be closed in the text editor...1017await this.instantiationService.invokeFunction(accessor => whenEditorClosed(accessor, resourcesToWaitFor));10181019// ...before deleting the wait marker file1020await this.fileService.del(waitMarkerFile);1021}10221023private async openResources(resources: Array<IResourceEditorInput | IUntitledTextResourceEditorInput>, diffMode: boolean, mergeMode: boolean): Promise<readonly IEditorPane[]> {1024const editors: IUntypedEditorInput[] = [];10251026if (mergeMode && isResourceEditorInput(resources[0]) && isResourceEditorInput(resources[1]) && isResourceEditorInput(resources[2]) && isResourceEditorInput(resources[3])) {1027const mergeEditor: IResourceMergeEditorInput = {1028input1: { resource: resources[0].resource },1029input2: { resource: resources[1].resource },1030base: { resource: resources[2].resource },1031result: { resource: resources[3].resource },1032options: { pinned: true }1033};1034editors.push(mergeEditor);1035} else if (diffMode && isResourceEditorInput(resources[0]) && isResourceEditorInput(resources[1])) {1036const diffEditor: IResourceDiffEditorInput = {1037original: { resource: resources[0].resource },1038modified: { resource: resources[1].resource },1039options: { pinned: true }1040};1041editors.push(diffEditor);1042} else {1043editors.push(...resources);1044}10451046return this.editorService.openEditors(editors, undefined, { validateTrust: true });1047}10481049//#region Window Zoom10501051private readonly mapWindowIdToZoomStatusEntry = new Map<number, ZoomStatusEntry>();10521053private configuredWindowZoomLevel: number;10541055private resolveConfiguredWindowZoomLevel(): number {1056const windowZoomLevel = this.configurationService.getValue('window.zoomLevel');10571058return typeof windowZoomLevel === 'number' ? windowZoomLevel : 0;1059}10601061private handleOnDidChangeZoomLevel(targetWindowId: number): void {10621063// Zoom status entry1064this.updateWindowZoomStatusEntry(targetWindowId);10651066// Notify main process about a custom zoom level1067if (targetWindowId === mainWindow.vscodeWindowId) {1068const currentWindowZoomLevel = getZoomLevel(mainWindow);10691070let notifyZoomLevel: number | undefined = undefined;1071if (this.configuredWindowZoomLevel !== currentWindowZoomLevel) {1072notifyZoomLevel = currentWindowZoomLevel;1073}10741075ipcRenderer.invoke('vscode:notifyZoomLevel', notifyZoomLevel);1076}1077}10781079private createWindowZoomStatusEntry(part: IEditorPart): void {1080const disposables = new DisposableStore();1081Event.once(part.onWillDispose)(() => disposables.dispose());10821083const scopedInstantiationService = this.editorGroupService.getScopedInstantiationService(part);1084this.mapWindowIdToZoomStatusEntry.set(part.windowId, disposables.add(scopedInstantiationService.createInstance(ZoomStatusEntry)));1085disposables.add(toDisposable(() => this.mapWindowIdToZoomStatusEntry.delete(part.windowId)));10861087this.updateWindowZoomStatusEntry(part.windowId);1088}10891090private updateWindowZoomStatusEntry(targetWindowId: number): void {1091const targetWindow = getWindowById(targetWindowId);1092const entry = this.mapWindowIdToZoomStatusEntry.get(targetWindowId);1093if (entry && targetWindow) {1094const currentZoomLevel = getZoomLevel(targetWindow.window);10951096let text: string | undefined = undefined;1097if (currentZoomLevel < this.configuredWindowZoomLevel) {1098text = '$(zoom-out)';1099} else if (currentZoomLevel > this.configuredWindowZoomLevel) {1100text = '$(zoom-in)';1101}11021103entry.updateZoomEntry(text ?? false, targetWindowId);1104}1105}11061107private onDidChangeConfiguredWindowZoomLevel(): void {1108this.configuredWindowZoomLevel = this.resolveConfiguredWindowZoomLevel();11091110let applyZoomLevel = false;1111for (const { window } of getWindows()) {1112if (getZoomLevel(window) !== this.configuredWindowZoomLevel) {1113applyZoomLevel = true;1114break;1115}1116}11171118if (applyZoomLevel) {1119applyZoom(this.configuredWindowZoomLevel, ApplyZoomTarget.ALL_WINDOWS);1120}11211122for (const [windowId] of this.mapWindowIdToZoomStatusEntry) {1123this.updateWindowZoomStatusEntry(windowId);1124}1125}11261127//#endregion11281129override dispose(): void {1130super.dispose();11311132for (const [, entry] of this.mapWindowIdToZoomStatusEntry) {1133entry.dispose();1134}1135}1136}11371138class ZoomStatusEntry extends Disposable {11391140private readonly disposable = this._register(new MutableDisposable<DisposableStore>());11411142private zoomLevelLabel: Action | undefined = undefined;11431144constructor(1145@IStatusbarService private readonly statusbarService: IStatusbarService,1146@ICommandService private readonly commandService: ICommandService,1147@IKeybindingService private readonly keybindingService: IKeybindingService1148) {1149super();1150}11511152updateZoomEntry(visibleOrText: false | string, targetWindowId: number): void {1153if (typeof visibleOrText === 'string') {1154if (!this.disposable.value) {1155this.createZoomEntry(visibleOrText);1156}11571158this.updateZoomLevelLabel(targetWindowId);1159} else {1160this.disposable.clear();1161}1162}11631164private createZoomEntry(visibleOrText: string): void {1165const disposables = new DisposableStore();1166this.disposable.value = disposables;11671168const container = $('.zoom-status');11691170const left = $('.zoom-status-left');1171container.appendChild(left);11721173const zoomOutAction: Action = disposables.add(new Action('workbench.action.zoomOut', localize('zoomOut', "Zoom Out"), ThemeIcon.asClassName(Codicon.remove), true, () => this.commandService.executeCommand(zoomOutAction.id)));1174const zoomInAction: Action = disposables.add(new Action('workbench.action.zoomIn', localize('zoomIn', "Zoom In"), ThemeIcon.asClassName(Codicon.plus), true, () => this.commandService.executeCommand(zoomInAction.id)));1175const zoomResetAction: Action = disposables.add(new Action('workbench.action.zoomReset', localize('zoomReset', "Reset"), undefined, true, () => this.commandService.executeCommand(zoomResetAction.id)));1176zoomResetAction.tooltip = localize('zoomResetLabel', "{0} ({1})", zoomResetAction.label, this.keybindingService.lookupKeybinding(zoomResetAction.id)?.getLabel());1177const zoomSettingsAction: Action = disposables.add(new Action('workbench.action.openSettings', localize('zoomSettings', "Settings"), ThemeIcon.asClassName(Codicon.settingsGear), true, () => this.commandService.executeCommand(zoomSettingsAction.id, 'window.zoom')));1178const zoomLevelLabel = disposables.add(new Action('zoomLabel', undefined, undefined, false));11791180this.zoomLevelLabel = zoomLevelLabel;1181disposables.add(toDisposable(() => this.zoomLevelLabel = undefined));11821183const actionBarLeft = disposables.add(new ActionBar(left, { hoverDelegate: nativeHoverDelegate }));1184actionBarLeft.push(zoomOutAction, { icon: true, label: false, keybinding: this.keybindingService.lookupKeybinding(zoomOutAction.id)?.getLabel() });1185actionBarLeft.push(this.zoomLevelLabel, { icon: false, label: true });1186actionBarLeft.push(zoomInAction, { icon: true, label: false, keybinding: this.keybindingService.lookupKeybinding(zoomInAction.id)?.getLabel() });11871188const right = $('.zoom-status-right');1189container.appendChild(right);11901191const actionBarRight = disposables.add(new ActionBar(right, { hoverDelegate: nativeHoverDelegate }));11921193actionBarRight.push(zoomResetAction, { icon: false, label: true });1194actionBarRight.push(zoomSettingsAction, { icon: true, label: false, keybinding: this.keybindingService.lookupKeybinding(zoomSettingsAction.id)?.getLabel() });11951196const name = localize('status.windowZoom', "Window Zoom");1197disposables.add(this.statusbarService.addEntry({1198name,1199text: visibleOrText,1200tooltip: container,1201ariaLabel: name,1202command: ShowTooltipCommand,1203kind: 'prominent'1204}, 'status.windowZoom', StatusbarAlignment.RIGHT, 102));1205}12061207private updateZoomLevelLabel(targetWindowId: number): void {1208if (this.zoomLevelLabel) {1209const targetWindow = getWindowById(targetWindowId, true).window;1210const zoomFactor = Math.round(getZoomFactor(targetWindow) * 100);1211const zoomLevel = getZoomLevel(targetWindow);12121213this.zoomLevelLabel.label = `${zoomLevel}`;1214this.zoomLevelLabel.tooltip = localize('zoomNumber', "Zoom Level: {0} ({1}%)", zoomLevel, zoomFactor);1215}1216}1217}121812191220