Path: blob/main/src/vs/workbench/electron-browser/window.ts
5221 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, NeverShowAgainScope, 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';81import { IContextMenuService } from '../../platform/contextview/browser/contextView.js';8283export class NativeWindow extends BaseWindow {8485private readonly customTitleContextMenuDisposable = this._register(new DisposableStore());8687private readonly addRemoveFoldersScheduler = this._register(new RunOnceScheduler(() => this.doAddRemoveFolders(), 100));88private pendingFoldersToAdd: URI[] = [];89private pendingFoldersToRemove: URI[] = [];9091private isDocumentedEdited = false;9293constructor(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 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@IContextMenuService contextMenuService: IContextMenuService,132) {133super(mainWindow, undefined, hostService, nativeEnvironmentService, contextMenuService, layoutService);134135this.configuredWindowZoomLevel = this.resolveConfiguredWindowZoomLevel();136137this.registerListeners();138this.create();139}140141protected registerListeners(): void {142143// Layout144this._register(addDisposableListener(mainWindow, EventType.RESIZE, () => this.layoutService.layout()));145146// React to editor input changes147this._register(this.editorService.onDidActiveEditorChange(() => this.updateTouchbarMenu()));148149// Prevent opening a real URL inside the window150for (const event of [EventType.DRAG_OVER, EventType.DROP]) {151this._register(addDisposableListener(mainWindow.document.body, event, (e: DragEvent) => {152EventHelper.stop(e);153}));154}155156// Support `runAction` event157ipcRenderer.on('vscode:runAction', async (event: unknown, ...argsRaw: unknown[]) => {158const request = argsRaw[0] as INativeRunActionInWindowRequest;159const args: unknown[] = request.args || [];160161// If we run an action from the touchbar, we fill in the currently active resource162// as payload because the touch bar items are context aware depending on the editor163if (request.from === 'touchbar') {164const activeEditor = this.editorService.activeEditor;165if (activeEditor) {166const resource = EditorResourceAccessor.getOriginalUri(activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });167if (resource) {168args.push(resource);169}170}171} else {172args.push({ from: request.from });173}174175try {176await this.commandService.executeCommand(request.id, ...args);177178this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: request.id, from: request.from });179} catch (error) {180this.notificationService.error(error);181}182});183184// Support runKeybinding event185ipcRenderer.on('vscode:runKeybinding', (event: unknown, ...argsRaw: unknown[]) => {186const request = argsRaw[0] as INativeRunKeybindingInWindowRequest;187const activeElement = getActiveElement();188if (activeElement) {189this.keybindingService.dispatchByUserSettingsLabel(request.userSettingsLabel, activeElement);190}191});192193// Shared Process crash reported from main194ipcRenderer.on('vscode:reportSharedProcessCrash', (event: unknown, ...argsRaw: unknown[]) => {195this.notificationService.prompt(196Severity.Error,197localize('sharedProcessCrash', "A shared background process terminated unexpectedly. Please restart the application to recover."),198[{199label: localize('restart', "Restart"),200run: () => this.nativeHostService.relaunch()201}],202{203priority: NotificationPriority.URGENT204}205);206});207208// Support openFiles event for existing and new files209ipcRenderer.on('vscode:openFiles', (event: unknown, ...argsRaw: unknown[]) => { this.onOpenFiles(argsRaw[0] as IOpenFileRequest); });210211// Support addRemoveFolders event for workspace management212ipcRenderer.on('vscode:addRemoveFolders', (event: unknown, ...argsRaw: unknown[]) => this.onAddRemoveFoldersRequest(argsRaw[0] as IAddRemoveFoldersRequest));213214// Message support215ipcRenderer.on('vscode:showInfoMessage', (event: unknown, ...argsRaw: unknown[]) => this.notificationService.info(argsRaw[0] as string));216217// Shell Environment Issue Notifications218ipcRenderer.on('vscode:showResolveShellEnvError', (event: unknown, ...argsRaw: unknown[]) => {219const message = argsRaw[0] as string;220this.notificationService.prompt(221Severity.Error,222message,223[{224label: localize('restart', "Restart"),225run: () => this.nativeHostService.relaunch()226},227{228label: localize('configure', "Configure"),229run: () => this.preferencesService.openUserSettings({ query: 'application.shellEnvironmentResolutionTimeout' })230},231{232label: localize('learnMore', "Learn More"),233run: () => this.openerService.open('https://go.microsoft.com/fwlink/?linkid=2149667')234}]235);236});237238ipcRenderer.on('vscode:showCredentialsError', (event: unknown, ...argsRaw: unknown[]) => {239const message = argsRaw[0] as string;240this.notificationService.prompt(241Severity.Error,242localize('keychainWriteError', "Writing login information to the keychain failed with error '{0}'.", message),243[{244label: localize('troubleshooting', "Troubleshooting Guide"),245run: () => this.openerService.open('https://go.microsoft.com/fwlink/?linkid=2190713')246}]247);248});249250ipcRenderer.on('vscode:showTranslatedBuildWarning', () => {251this.notificationService.prompt(252Severity.Warning,253localize("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),254[{255label: localize('downloadArmBuild', "Download"),256run: () => {257const quality = this.productService.quality;258const stableURL = 'https://code.visualstudio.com/docs/?dv=osx';259const insidersURL = 'https://code.visualstudio.com/docs/?dv=osx&build=insiders';260this.openerService.open(quality === 'stable' ? stableURL : insidersURL);261}262}],263{264priority: NotificationPriority.URGENT265}266);267});268269ipcRenderer.on('vscode:showArgvParseWarning', () => {270this.notificationService.prompt(271Severity.Warning,272localize("showArgvParseWarning", "The runtime arguments file 'argv.json' contains errors. Please correct them and restart."),273[{274label: localize('showArgvParseWarningAction', "Open File"),275run: () => this.editorService.openEditor({ resource: this.nativeEnvironmentService.argvResource })276}],277{278priority: NotificationPriority.URGENT279}280);281});282283// Fullscreen Events284ipcRenderer.on('vscode:enterFullScreen', () => setFullscreen(true, mainWindow));285ipcRenderer.on('vscode:leaveFullScreen', () => setFullscreen(false, mainWindow));286287// Proxy Login Dialog288ipcRenderer.on('vscode:openProxyAuthenticationDialog', async (event: unknown, ...argsRaw: unknown[]) => {289const payload = argsRaw[0] as { authInfo: AuthInfo; username?: string; password?: string; replyChannel: string };290const rememberCredentialsKey = 'window.rememberProxyCredentials';291const rememberCredentials = this.storageService.getBoolean(rememberCredentialsKey, StorageScope.APPLICATION);292const result = await this.dialogService.input({293type: 'warning',294message: localize('proxyAuthRequired', "Proxy Authentication Required"),295primaryButton: localize({ key: 'loginButton', comment: ['&& denotes a mnemonic'] }, "&&Log In"),296inputs:297[298{ placeholder: localize('username', "Username"), value: payload.username },299{ placeholder: localize('password', "Password"), type: 'password', value: payload.password }300],301detail: localize('proxyDetail', "The proxy {0} requires a username and password.", `${payload.authInfo.host}:${payload.authInfo.port}`),302checkbox: {303label: localize('rememberCredentials', "Remember my credentials"),304checked: rememberCredentials305}306});307308// Reply back to the channel without result to indicate309// that the login dialog was cancelled310if (!result.confirmed || !result.values) {311ipcRenderer.send(payload.replyChannel);312}313314// Other reply back with the picked credentials315else {316317// Update state based on checkbox318if (result.checkboxChecked) {319this.storageService.store(rememberCredentialsKey, true, StorageScope.APPLICATION, StorageTarget.MACHINE);320} else {321this.storageService.remove(rememberCredentialsKey, StorageScope.APPLICATION);322}323324// Reply back to main side with credentials325const [username, password] = result.values;326ipcRenderer.send(payload.replyChannel, { username, password, remember: !!result.checkboxChecked });327}328});329330// Accessibility support changed event331ipcRenderer.on('vscode:accessibilitySupportChanged', (event: unknown, ...argsRaw: unknown[]) => {332const accessibilitySupportEnabled = argsRaw[0] as boolean;333this.accessibilityService.setAccessibilitySupport(accessibilitySupportEnabled ? AccessibilitySupport.Enabled : AccessibilitySupport.Disabled);334});335336// Allow to update security settings around allowed UNC Host337ipcRenderer.on('vscode:configureAllowedUNCHost', async (event: unknown, ...argsRaw: unknown[]) => {338const host = argsRaw[0] as string;339if (!isWindows) {340return; // only supported on Windows341}342343const allowedUncHosts = new Set<string>();344345const configuredAllowedUncHosts = this.configurationService.getValue<string[] | undefined>('security.allowedUNCHosts',) ?? [];346if (Array.isArray(configuredAllowedUncHosts)) {347for (const configuredAllowedUncHost of configuredAllowedUncHosts) {348if (typeof configuredAllowedUncHost === 'string') {349allowedUncHosts.add(configuredAllowedUncHost);350}351}352}353354if (!allowedUncHosts.has(host)) {355allowedUncHosts.add(host);356357await getWorkbenchContribution<DynamicWorkbenchSecurityConfiguration>(DynamicWorkbenchSecurityConfiguration.ID).ready; // ensure this setting is registered358this.configurationService.updateValue('security.allowedUNCHosts', [...allowedUncHosts.values()], ConfigurationTarget.USER);359}360});361362// Allow to update security settings around protocol handlers363ipcRenderer.on('vscode:disablePromptForProtocolHandling', (event: unknown, ...argsRaw: unknown[]) => {364const kind = argsRaw[0] as 'local' | 'remote';365const setting = kind === 'local' ? 'security.promptForLocalFileProtocolHandling' : 'security.promptForRemoteFileProtocolHandling';366this.configurationService.updateValue(setting, false);367});368369// Window Settings370this._register(this.configurationService.onDidChangeConfiguration(e => {371if (e.affectsConfiguration('window.zoomLevel') || (e.affectsConfiguration('window.zoomPerWindow') && this.configurationService.getValue('window.zoomPerWindow') === false)) {372this.onDidChangeConfiguredWindowZoomLevel();373} else if (e.affectsConfiguration('keyboard.touchbar.enabled') || e.affectsConfiguration('keyboard.touchbar.ignored')) {374this.updateTouchbarMenu();375} else if (e.affectsConfiguration('window.border')) {376this.updateWindowBorder();377}378}));379380this._register(onDidChangeZoomLevel(targetWindowId => this.handleOnDidChangeZoomLevel(targetWindowId)));381382for (const part of this.editorGroupService.parts) {383this.createWindowZoomStatusEntry(part);384}385386this._register(this.editorGroupService.onDidCreateAuxiliaryEditorPart(part => this.createWindowZoomStatusEntry(part)));387388// Listen to visible editor changes (debounced in case a new editor opens immediately after)389this._register(Event.debounce(this.editorService.onDidVisibleEditorsChange, () => undefined, 0, undefined, undefined, undefined, this._store)(() => this.maybeCloseWindow()));390391// Listen to editor closing (if we run with --wait)392const filesToWait = this.nativeEnvironmentService.filesToWait;393if (filesToWait) {394this.trackClosedWaitFiles(filesToWait.waitMarkerFileUri, coalesce(filesToWait.paths.map(path => path.fileUri)));395}396397// macOS OS integration: represented file name398if (isMacintosh) {399for (const part of this.editorGroupService.parts) {400this.handleRepresentedFilename(part);401}402403this._register(this.editorGroupService.onDidCreateAuxiliaryEditorPart(part => this.handleRepresentedFilename(part)));404}405406// Document edited: indicate for dirty working copies407this._register(this.workingCopyService.onDidChangeDirty(workingCopy => {408const gotDirty = workingCopy.isDirty();409if (gotDirty && !(workingCopy.capabilities & WorkingCopyCapabilities.Untitled) && this.filesConfigurationService.hasShortAutoSaveDelay(workingCopy.resource)) {410return; // do not indicate dirty of working copies that are auto saved after short delay411}412413this.updateDocumentEdited(gotDirty ? true : undefined);414}));415416this.updateDocumentEdited(undefined);417418// Detect minimize / maximize419this._register(Event.any(420Event.map(Event.filter(this.nativeHostService.onDidMaximizeWindow, windowId => !!hasWindow(windowId)), windowId => ({ maximized: true, windowId })),421Event.map(Event.filter(this.nativeHostService.onDidUnmaximizeWindow, windowId => !!hasWindow(windowId)), windowId => ({ maximized: false, windowId }))422)(e => this.layoutService.updateWindowMaximizedState(getWindowById(e.windowId)!.window, e.maximized)));423this.layoutService.updateWindowMaximizedState(mainWindow, this.nativeEnvironmentService.window.maximized ?? false);424425// Detect panel position to determine minimum width426this._register(this.layoutService.onDidChangePanelPosition(pos => this.onDidChangePanelPosition(positionFromString(pos))));427this.onDidChangePanelPosition(this.layoutService.getPanelPosition());428429// Border430this._register(this.themeService.onDidColorThemeChange(() => this.updateWindowBorder()));431this._register(this.hostService.onDidChangeActiveWindow(() => this.updateWindowBorder()));432this._register(this.hostService.onDidChangeFocus(() => this.updateWindowBorder()));433434// Lifecycle435this._register(this.lifecycleService.onBeforeShutdown(e => this.onBeforeShutdown(e)));436this._register(this.lifecycleService.onBeforeShutdownError(e => this.onBeforeShutdownError(e)));437this._register(this.lifecycleService.onWillShutdown(e => this.onWillShutdown(e)));438}439440private handleRepresentedFilename(part: IEditorPart): void {441const disposables = new DisposableStore();442Event.once(part.onWillDispose)(() => disposables.dispose());443444this.editorGroupService.getScopedInstantiationService(part).invokeFunction(accessor => {445const editorService = accessor.get(IEditorService);446disposables.add(editorService.onDidActiveEditorChange(() => this.updateRepresentedFilename(editorService, part.windowId)));447});448}449450private updateRepresentedFilename(editorService: IEditorService, targetWindowId: number): void {451const file = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY, filterByScheme: Schemas.file });452453// Represented Filename454this.nativeHostService.setRepresentedFilename(file?.fsPath ?? '', { targetWindowId });455456// Custom title menu (main window only currently)457if (targetWindowId === mainWindow.vscodeWindowId) {458this.provideCustomTitleContextMenu(file?.fsPath);459}460}461462//#region Window Lifecycle463464private onBeforeShutdown({ veto, reason }: BeforeShutdownEvent): void {465if (reason === ShutdownReason.CLOSE) {466const confirmBeforeCloseSetting = this.configurationService.getValue<'always' | 'never' | 'keyboardOnly'>('window.confirmBeforeClose');467468const confirmBeforeClose = confirmBeforeCloseSetting === 'always' || (confirmBeforeCloseSetting === 'keyboardOnly' && ModifierKeyEmitter.getInstance().isModifierPressed);469if (confirmBeforeClose) {470471// When we need to confirm on close or quit, veto the shutdown472// with a long running promise to figure out whether shutdown473// can proceed or not.474475return veto((async () => {476let actualReason: ShutdownReason = reason;477if (reason === ShutdownReason.CLOSE && !isMacintosh) {478const windowCount = await this.nativeHostService.getWindowCount();479if (windowCount === 1) {480actualReason = ShutdownReason.QUIT; // Windows/Linux: closing last window means to QUIT481}482}483484let confirmed = true;485if (confirmBeforeClose) {486confirmed = await this.instantiationService.invokeFunction(accessor => NativeWindow.confirmOnShutdown(accessor, actualReason));487}488489// Progress for long running shutdown490if (confirmed) {491this.progressOnBeforeShutdown(reason);492}493494return !confirmed;495})(), 'veto.confirmBeforeClose');496}497}498499// Progress for long running shutdown500this.progressOnBeforeShutdown(reason);501}502503private progressOnBeforeShutdown(reason: ShutdownReason): void {504this.progressService.withProgress({505location: ProgressLocation.Window, // use window progress to not be too annoying about this operation506delay: 800, // delay so that it only appears when operation takes a long time507title: this.toShutdownLabel(reason, false),508}, () => {509return Event.toPromise(Event.any(510this.lifecycleService.onWillShutdown, // dismiss this dialog when we shutdown511this.lifecycleService.onShutdownVeto, // or when shutdown was vetoed512this.dialogService.onWillShowDialog // or when a dialog asks for input513));514});515}516517private onBeforeShutdownError({ error, reason }: BeforeShutdownErrorEvent): void {518this.dialogService.error(this.toShutdownLabel(reason, true), localize('shutdownErrorDetail', "Error: {0}", toErrorMessage(error)));519}520521private onWillShutdown({ reason, force, joiners }: WillShutdownEvent): void {522523// Delay so that the dialog only appears after timeout524const shutdownDialogScheduler = new RunOnceScheduler(() => {525const pendingJoiners = joiners();526527this.progressService.withProgress({528location: ProgressLocation.Dialog, // use a dialog to prevent the user from making any more interactions now529buttons: [this.toForceShutdownLabel(reason)], // allow to force shutdown anyway530cancellable: false, // do not allow to cancel531sticky: true, // do not allow to dismiss532title: this.toShutdownLabel(reason, false),533detail: pendingJoiners.length > 0 ? localize('willShutdownDetail', "The following operations are still running: \n{0}", pendingJoiners.map(joiner => `- ${joiner.label}`).join('\n')) : undefined534}, () => {535return Event.toPromise(this.lifecycleService.onDidShutdown); // dismiss this dialog when we actually shutdown536}, () => {537force();538});539}, 1200);540shutdownDialogScheduler.schedule();541542// Dispose scheduler when we actually shutdown543Event.once(this.lifecycleService.onDidShutdown)(() => shutdownDialogScheduler.dispose());544}545546private toShutdownLabel(reason: ShutdownReason, isError: boolean): string {547if (isError) {548switch (reason) {549case ShutdownReason.CLOSE:550return localize('shutdownErrorClose', "An unexpected error prevented the window to close");551case ShutdownReason.QUIT:552return localize('shutdownErrorQuit', "An unexpected error prevented the application to quit");553case ShutdownReason.RELOAD:554return localize('shutdownErrorReload', "An unexpected error prevented the window to reload");555case ShutdownReason.LOAD:556return localize('shutdownErrorLoad', "An unexpected error prevented to change the workspace");557}558}559560switch (reason) {561case ShutdownReason.CLOSE:562return localize('shutdownTitleClose', "Closing the window is taking a bit longer...");563case ShutdownReason.QUIT:564return localize('shutdownTitleQuit', "Quitting the application is taking a bit longer...");565case ShutdownReason.RELOAD:566return localize('shutdownTitleReload', "Reloading the window is taking a bit longer...");567case ShutdownReason.LOAD:568return localize('shutdownTitleLoad', "Changing the workspace is taking a bit longer...");569}570}571572private toForceShutdownLabel(reason: ShutdownReason): string {573switch (reason) {574case ShutdownReason.CLOSE:575return localize('shutdownForceClose', "Close Anyway");576case ShutdownReason.QUIT:577return localize('shutdownForceQuit', "Quit Anyway");578case ShutdownReason.RELOAD:579return localize('shutdownForceReload', "Reload Anyway");580case ShutdownReason.LOAD:581return localize('shutdownForceLoad', "Change Anyway");582}583}584585//#endregion586587private updateDocumentEdited(documentEdited: true | undefined): void {588let setDocumentEdited: boolean;589if (typeof documentEdited === 'boolean') {590setDocumentEdited = documentEdited;591} else {592setDocumentEdited = this.workingCopyService.hasDirty;593}594595if ((!this.isDocumentedEdited && setDocumentEdited) || (this.isDocumentedEdited && !setDocumentEdited)) {596this.isDocumentedEdited = setDocumentEdited;597598this.nativeHostService.setDocumentEdited(setDocumentEdited);599}600}601602private getWindowMinimumWidth(panelPosition: Position = this.layoutService.getPanelPosition()): number {603604// if panel is on the side, then return the larger minwidth605const panelOnSide = panelPosition === Position.LEFT || panelPosition === Position.RIGHT;606if (panelOnSide) {607return WindowMinimumSize.WIDTH_WITH_VERTICAL_PANEL;608}609610return WindowMinimumSize.WIDTH;611}612613private onDidChangePanelPosition(pos: Position): void {614const minWidth = this.getWindowMinimumWidth(pos);615616this.nativeHostService.setMinimumSize(minWidth, undefined);617}618619private maybeCloseWindow(): void {620const closeWhenEmpty = this.configurationService.getValue('window.closeWhenEmpty') || this.nativeEnvironmentService.args.wait;621if (!closeWhenEmpty) {622return; // return early if configured to not close when empty623}624625// Close empty editor groups based on setting and environment626for (const editorPart of this.editorGroupService.parts) {627if (editorPart.groups.some(group => !group.isEmpty)) {628continue; // not empty629}630631if (editorPart === this.editorGroupService.mainPart && (632this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY || // only for empty windows633this.environmentService.isExtensionDevelopment || // not when developing an extension634this.editorService.visibleEditors.length > 0 // not when there are still editors open in other windows635)) {636continue;637}638639if (editorPart === this.editorGroupService.mainPart) {640this.nativeHostService.closeWindow();641} else {642editorPart.removeGroup(editorPart.activeGroup);643}644}645}646647private provideCustomTitleContextMenu(filePath: string | undefined): void {648649// Clear old menu650this.customTitleContextMenuDisposable.clear();651652// Only provide a menu when we have a file path and custom titlebar653if (!filePath || hasNativeTitlebar(this.configurationService)) {654return;655}656657// Split up filepath into segments658const segments = filePath.split(posix.sep);659for (let i = segments.length; i > 0; i--) {660const isFile = (i === segments.length);661662let pathOffset = i;663if (!isFile) {664pathOffset++; // for segments which are not the file name we want to open the folder665}666667const path = URI.file(segments.slice(0, pathOffset).join(posix.sep));668669let label: string;670if (!isFile) {671label = this.labelService.getUriBasenameLabel(dirname(path));672} else {673label = this.labelService.getUriBasenameLabel(path);674}675676const commandId = `workbench.action.revealPathInFinder${i}`;677this.customTitleContextMenuDisposable.add(CommandsRegistry.registerCommand(commandId, () => this.nativeHostService.showItemInFolder(path.fsPath)));678this.customTitleContextMenuDisposable.add(MenuRegistry.appendMenuItem(MenuId.TitleBarTitleContext, { command: { id: commandId, title: label || posix.sep }, order: -i, group: '1_file' }));679}680}681682protected create(): void {683684// Handle open calls685this.setupOpenHandlers();686687// Notify some services about lifecycle phases688this.lifecycleService.when(LifecyclePhase.Ready).then(() => this.nativeHostService.notifyReady());689this.lifecycleService.when(LifecyclePhase.Restored).then(() => {690this.sharedProcessService.notifyRestored();691this.utilityProcessWorkerWorkbenchService.notifyRestored();692});693694// Check for situations that are worth warning the user about695this.handleWarnings();696697// Touchbar menu (if enabled)698this.updateTouchbarMenu();699700// Window border701this.updateWindowBorder();702703// Smoke Test Driver704if (this.environmentService.enableSmokeTestDriver) {705registerWindowDriver(this.instantiationService);706}707}708709private async handleWarnings(): Promise<void> {710711// After restored phase is fine for the following ones712await this.lifecycleService.when(LifecyclePhase.Restored);713714// Integrity / Root warning715(async () => {716const isAdmin = await this.nativeHostService.isAdmin();717const { isPure } = await this.integrityService.isPure();718719// Update to title720this.titleService.updateProperties({ isPure, isAdmin });721722// Show warning message (unix only)723if (isAdmin && !isWindows) {724this.notificationService.warn(localize('runningAsRoot', "It is not recommended to run {0} as root user.", this.productService.nameShort));725}726})();727728// Installation Dir Warning729if (this.environmentService.isBuilt && !this.environmentService.extensionDevelopmentLocationURI?.length) {730let installLocationUri: URI;731if (isMacintosh) {732// appRoot = /Applications/Visual Studio Code - Insiders.app/Contents/Resources/app733installLocationUri = dirname(dirname(dirname(URI.file(this.nativeEnvironmentService.appRoot))));734} else {735// appRoot = C:\Users\<name>\AppData\Local\Programs\Microsoft VS Code Insiders\resources\app736// appRoot = /usr/share/code-insiders/resources/app737installLocationUri = dirname(dirname(URI.file(this.nativeEnvironmentService.appRoot)));738}739740for (const folder of this.contextService.getWorkspace().folders) {741if (this.uriIdentityService.extUri.isEqualOrParent(folder.uri, installLocationUri)) {742this.bannerService.show({743id: 'appRootWarning.banner',744message: 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)),745icon: Codicon.warning746});747748break;749}750}751}752753// macOS 11 warning754if (isMacintosh) {755const majorVersion = this.nativeEnvironmentService.os.release.split('.')[0];756const eolReleases = new Map<string, string>([757['20', 'macOS Big Sur'],758]);759760if (eolReleases.has(majorVersion)) {761const message = localize('macoseolmessage', "{0} on {1} will soon stop receiving updates. Consider upgrading your macOS version.", this.productService.nameLong, eolReleases.get(majorVersion));762763this.notificationService.prompt(764Severity.Warning,765message,766[{767label: localize('learnMore', "Learn More"),768run: () => this.openerService.open(URI.parse('https://aka.ms/vscode-faq-old-macOS'))769}],770{771neverShowAgain: { id: 'macoseol', isSecondary: true, scope: NeverShowAgainScope.APPLICATION },772priority: NotificationPriority.URGENT,773sticky: true774}775);776}777}778779// Slow shell environment progress indicator780const shellEnv = process.shellEnv();781this.progressService.withProgress({782title: localize('resolveShellEnvironment', "Resolving shell environment..."),783location: ProgressLocation.Window,784delay: 1600,785buttons: [localize('learnMore', "Learn More")]786}, () => shellEnv, () => this.openerService.open('https://go.microsoft.com/fwlink/?linkid=2149667'));787}788789async resolveExternalUri(uri: URI, options?: OpenOptions): Promise<IResolvedExternalUri | undefined> {790let queryTunnel: RemoteTunnel | string | undefined;791if (options?.allowTunneling) {792const portMappingRequest = extractLocalHostUriMetaDataForPortMapping(uri);793const queryPortMapping = extractQueryLocalHostUriMetaDataForPortMapping(uri);794if (queryPortMapping) {795queryTunnel = await this.openTunnel(queryPortMapping.address, queryPortMapping.port);796if (queryTunnel && (typeof queryTunnel !== 'string')) {797// If the tunnel was mapped to a different port, dispose it, because some services798// validate the port number in the query string.799if (queryTunnel.tunnelRemotePort !== queryPortMapping.port) {800queryTunnel.dispose();801queryTunnel = undefined;802} else {803if (!portMappingRequest) {804const tunnel = queryTunnel;805return {806resolved: uri,807dispose: () => tunnel.dispose()808};809}810}811}812}813814if (portMappingRequest) {815const tunnel = await this.openTunnel(portMappingRequest.address, portMappingRequest.port);816if (tunnel && (typeof tunnel !== 'string')) {817const addressAsUri = URI.parse(tunnel.localAddress).with({ path: uri.path });818const resolved = addressAsUri.scheme.startsWith(uri.scheme) ? addressAsUri : uri.with({ authority: tunnel.localAddress });819return {820resolved,821dispose() {822tunnel.dispose();823if (queryTunnel && (typeof queryTunnel !== 'string')) {824queryTunnel.dispose();825}826}827};828}829}830}831832if (!options?.openExternal) {833const canHandleResource = await this.fileService.canHandleResource(uri);834if (canHandleResource) {835return {836resolved: URI.from({837scheme: this.productService.urlProtocol,838path: 'workspace',839query: uri.toString()840}),841dispose() { }842};843}844}845846return undefined;847}848849private async openTunnel(address: string, port: number): Promise<RemoteTunnel | string | undefined> {850const remoteAuthority = this.environmentService.remoteAuthority;851const addressProvider: IAddressProvider | undefined = remoteAuthority ? {852getAddress: async (): Promise<IAddress> => {853return (await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority)).authority;854}855} : undefined;856857const tunnel = await this.tunnelService.getExistingTunnel(address, port);858if (!tunnel || (typeof tunnel === 'string')) {859return this.tunnelService.openTunnel(addressProvider, address, port);860}861862return tunnel;863}864865private setupOpenHandlers(): void {866867// Handle external open() calls868this.openerService.setDefaultExternalOpener({869openExternal: async (href: string) => {870const success = await this.nativeHostService.openExternal(href, this.configurationService.getValue<string>('workbench.externalBrowser'));871if (!success) {872const fileCandidate = URI.parse(href);873if (fileCandidate.scheme === Schemas.file) {874// if opening failed, and this is a file, we can still try to reveal it875await this.nativeHostService.showItemInFolder(fileCandidate.fsPath);876}877}878879return true;880}881});882883// Register external URI resolver884this.openerService.registerExternalUriResolver({885resolveExternalUri: async (uri: URI, options?: OpenOptions) => {886return this.resolveExternalUri(uri, options);887}888});889}890891//#region Touchbar892893private touchBarMenu: IMenu | undefined;894private readonly touchBarDisposables = this._register(new DisposableStore());895private lastInstalledTouchedBar: ICommandAction[][] | undefined;896897private updateTouchbarMenu(): void {898if (!isMacintosh) {899return; // macOS only900}901902// Dispose old903this.touchBarDisposables.clear();904this.touchBarMenu = undefined;905906// Create new (delayed)907const scheduler: RunOnceScheduler = this.touchBarDisposables.add(new RunOnceScheduler(() => this.doUpdateTouchbarMenu(scheduler), 300));908scheduler.schedule();909}910911private doUpdateTouchbarMenu(scheduler: RunOnceScheduler): void {912if (!this.touchBarMenu) {913const scopedContextKeyService = this.editorService.activeEditorPane?.scopedContextKeyService || this.editorGroupService.activeGroup.scopedContextKeyService;914this.touchBarMenu = this.menuService.createMenu(MenuId.TouchBarContext, scopedContextKeyService);915this.touchBarDisposables.add(this.touchBarMenu);916this.touchBarDisposables.add(this.touchBarMenu.onDidChange(() => scheduler.schedule()));917}918919const disabled = this.configurationService.getValue('keyboard.touchbar.enabled') === false;920const touchbarIgnored = this.configurationService.getValue('keyboard.touchbar.ignored');921const ignoredItems = Array.isArray(touchbarIgnored) ? touchbarIgnored : [];922923// Fill actions into groups respecting order924const actions = getFlatActionBarActions(this.touchBarMenu.getActions());925926// Convert into command action multi array927const items: ICommandAction[][] = [];928let group: ICommandAction[] = [];929if (!disabled) {930for (const action of actions) {931932// Command933if (action instanceof MenuItemAction) {934if (ignoredItems.indexOf(action.item.id) >= 0) {935continue; // ignored936}937938group.push(action.item);939}940941// Separator942else if (action instanceof Separator) {943if (group.length) {944items.push(group);945}946947group = [];948}949}950951if (group.length) {952items.push(group);953}954}955956// Only update if the actions have changed957if (!equals(this.lastInstalledTouchedBar, items)) {958this.lastInstalledTouchedBar = items;959this.nativeHostService.updateTouchBar(items);960}961}962963//#endregion964965//#region Window Border966967private updateWindowBorder(): void {968if (!isWindows) {969return; // windows only970}971972const theme = this.themeService.getColorTheme();973974let activeBorder = theme.getColor(WINDOW_ACTIVE_BORDER)?.toString();975let inactiveBorder = theme.getColor(WINDOW_INACTIVE_BORDER)?.toString();976977const borderSetting = this.configurationService.getValue<string>('window.border');978if (borderSetting === 'off') {979activeBorder = 'off';980inactiveBorder = undefined;981} else if (borderSetting === 'default') {982activeBorder = activeBorder ?? 'default';983} else if (borderSetting === 'system') {984activeBorder = 'default';985inactiveBorder = undefined;986} else {987activeBorder = borderSetting;988inactiveBorder = undefined;989}990991this.nativeHostService.updateWindowAccentColor(activeBorder, inactiveBorder);992}993994//#endregion995996private onAddRemoveFoldersRequest(request: IAddRemoveFoldersRequest): void {997998// Buffer all pending requests999this.pendingFoldersToAdd.push(...request.foldersToAdd.map(folder => URI.revive(folder)));1000this.pendingFoldersToRemove.push(...request.foldersToRemove.map(folder => URI.revive(folder)));10011002// Delay the adding of folders a bit to buffer in case more requests are coming1003if (!this.addRemoveFoldersScheduler.isScheduled()) {1004this.addRemoveFoldersScheduler.schedule();1005}1006}10071008private async doAddRemoveFolders(): Promise<void> {1009const foldersToAdd: IWorkspaceFolderCreationData[] = this.pendingFoldersToAdd.map(folder => ({ uri: folder }));1010const foldersToRemove = this.pendingFoldersToRemove.slice(0);10111012this.pendingFoldersToAdd = [];1013this.pendingFoldersToRemove = [];10141015if (foldersToAdd.length) {1016await this.workspaceEditingService.addFolders(foldersToAdd);1017}10181019if (foldersToRemove.length) {1020await this.workspaceEditingService.removeFolders(foldersToRemove);1021}1022}10231024private async onOpenFiles(request: INativeOpenFileRequest): Promise<void> {1025const diffMode = !!(request.filesToDiff && (request.filesToDiff.length === 2));1026const mergeMode = !!(request.filesToMerge && (request.filesToMerge.length === 4));10271028const inputs = coalesce(await pathsToEditors(mergeMode ? request.filesToMerge : diffMode ? request.filesToDiff : request.filesToOpenOrCreate, this.fileService, this.logService));1029if (inputs.length) {1030const openedEditorPanes = await this.openResources(inputs, diffMode, mergeMode);10311032if (request.filesToWait) {10331034// In wait mode, listen to changes to the editors and wait until the files1035// are closed that the user wants to wait for. When this happens we delete1036// the wait marker file to signal to the outside that editing is done.1037// However, it is possible that opening of the editors failed, as such we1038// check for whether editor panes got opened and otherwise delete the marker1039// right away.10401041if (openedEditorPanes.length) {1042return this.trackClosedWaitFiles(URI.revive(request.filesToWait.waitMarkerFileUri), coalesce(request.filesToWait.paths.map(path => URI.revive(path.fileUri))));1043} else {1044return this.fileService.del(URI.revive(request.filesToWait.waitMarkerFileUri));1045}1046}1047}1048}10491050private async trackClosedWaitFiles(waitMarkerFile: URI, resourcesToWaitFor: URI[]): Promise<void> {10511052// Wait for the resources to be closed in the text editor...1053await this.instantiationService.invokeFunction(accessor => whenEditorClosed(accessor, resourcesToWaitFor));10541055// ...before deleting the wait marker file1056await this.fileService.del(waitMarkerFile);1057}10581059private async openResources(resources: Array<IResourceEditorInput | IUntitledTextResourceEditorInput>, diffMode: boolean, mergeMode: boolean): Promise<readonly IEditorPane[]> {1060const editors: IUntypedEditorInput[] = [];10611062if (mergeMode && isResourceEditorInput(resources[0]) && isResourceEditorInput(resources[1]) && isResourceEditorInput(resources[2]) && isResourceEditorInput(resources[3])) {1063const mergeEditor: IResourceMergeEditorInput = {1064input1: { resource: resources[0].resource },1065input2: { resource: resources[1].resource },1066base: { resource: resources[2].resource },1067result: { resource: resources[3].resource },1068options: { pinned: true }1069};1070editors.push(mergeEditor);1071} else if (diffMode && isResourceEditorInput(resources[0]) && isResourceEditorInput(resources[1])) {1072const diffEditor: IResourceDiffEditorInput = {1073original: { resource: resources[0].resource },1074modified: { resource: resources[1].resource },1075options: { pinned: true }1076};1077editors.push(diffEditor);1078} else {1079editors.push(...resources);1080}10811082return this.editorService.openEditors(editors, undefined, { validateTrust: true });1083}10841085//#region Window Zoom10861087private readonly mapWindowIdToZoomStatusEntry = new Map<number, ZoomStatusEntry>();10881089private configuredWindowZoomLevel: number;10901091private resolveConfiguredWindowZoomLevel(): number {1092const windowZoomLevel = this.configurationService.getValue('window.zoomLevel');10931094return typeof windowZoomLevel === 'number' ? windowZoomLevel : 0;1095}10961097private handleOnDidChangeZoomLevel(targetWindowId: number): void {10981099// Zoom status entry1100this.updateWindowZoomStatusEntry(targetWindowId);11011102// Notify main process about a custom zoom level1103if (targetWindowId === mainWindow.vscodeWindowId) {1104const currentWindowZoomLevel = getZoomLevel(mainWindow);11051106let notifyZoomLevel: number | undefined = undefined;1107if (this.configuredWindowZoomLevel !== currentWindowZoomLevel) {1108notifyZoomLevel = currentWindowZoomLevel;1109}11101111ipcRenderer.invoke('vscode:notifyZoomLevel', notifyZoomLevel);1112}1113}11141115private createWindowZoomStatusEntry(part: IEditorPart): void {1116const disposables = new DisposableStore();1117Event.once(part.onWillDispose)(() => disposables.dispose());11181119const scopedInstantiationService = this.editorGroupService.getScopedInstantiationService(part);1120this.mapWindowIdToZoomStatusEntry.set(part.windowId, disposables.add(scopedInstantiationService.createInstance(ZoomStatusEntry)));1121disposables.add(toDisposable(() => this.mapWindowIdToZoomStatusEntry.delete(part.windowId)));11221123this.updateWindowZoomStatusEntry(part.windowId);1124}11251126private updateWindowZoomStatusEntry(targetWindowId: number): void {1127const targetWindow = getWindowById(targetWindowId);1128const entry = this.mapWindowIdToZoomStatusEntry.get(targetWindowId);1129if (entry && targetWindow) {1130const currentZoomLevel = getZoomLevel(targetWindow.window);11311132let text: string | undefined = undefined;1133if (currentZoomLevel < this.configuredWindowZoomLevel) {1134text = '$(zoom-out)';1135} else if (currentZoomLevel > this.configuredWindowZoomLevel) {1136text = '$(zoom-in)';1137}11381139entry.updateZoomEntry(text ?? false, targetWindowId);1140}1141}11421143private onDidChangeConfiguredWindowZoomLevel(): void {1144this.configuredWindowZoomLevel = this.resolveConfiguredWindowZoomLevel();11451146let applyZoomLevel = false;1147for (const { window } of getWindows()) {1148if (getZoomLevel(window) !== this.configuredWindowZoomLevel) {1149applyZoomLevel = true;1150break;1151}1152}11531154if (applyZoomLevel) {1155applyZoom(this.configuredWindowZoomLevel, ApplyZoomTarget.ALL_WINDOWS);1156}11571158for (const [windowId] of this.mapWindowIdToZoomStatusEntry) {1159this.updateWindowZoomStatusEntry(windowId);1160}1161}11621163//#endregion11641165override dispose(): void {1166super.dispose();11671168for (const [, entry] of this.mapWindowIdToZoomStatusEntry) {1169entry.dispose();1170}1171}1172}11731174class ZoomStatusEntry extends Disposable {11751176private readonly disposable = this._register(new MutableDisposable<DisposableStore>());11771178private zoomLevelLabel: Action | undefined = undefined;11791180constructor(1181@IStatusbarService private readonly statusbarService: IStatusbarService,1182@ICommandService private readonly commandService: ICommandService,1183@IKeybindingService private readonly keybindingService: IKeybindingService1184) {1185super();1186}11871188updateZoomEntry(visibleOrText: false | string, targetWindowId: number): void {1189if (typeof visibleOrText === 'string') {1190if (!this.disposable.value) {1191this.createZoomEntry(visibleOrText);1192}11931194this.updateZoomLevelLabel(targetWindowId);1195} else {1196this.disposable.clear();1197}1198}11991200private createZoomEntry(visibleOrText: string): void {1201const disposables = new DisposableStore();1202this.disposable.value = disposables;12031204const container = $('.zoom-status');12051206const left = $('.zoom-status-left');1207container.appendChild(left);12081209const zoomOutAction: Action = disposables.add(new Action('workbench.action.zoomOut', localize('zoomOut', "Zoom Out"), ThemeIcon.asClassName(Codicon.remove), true, () => this.commandService.executeCommand(zoomOutAction.id)));1210const zoomInAction: Action = disposables.add(new Action('workbench.action.zoomIn', localize('zoomIn', "Zoom In"), ThemeIcon.asClassName(Codicon.plus), true, () => this.commandService.executeCommand(zoomInAction.id)));1211const zoomResetAction: Action = disposables.add(new Action('workbench.action.zoomReset', localize('zoomReset', "Reset"), undefined, true, () => this.commandService.executeCommand(zoomResetAction.id)));1212zoomResetAction.tooltip = this.keybindingService.appendKeybinding(zoomResetAction.label, zoomResetAction.id);1213const zoomSettingsAction: Action = disposables.add(new Action('workbench.action.openSettings', localize('zoomSettings', "Settings"), ThemeIcon.asClassName(Codicon.settingsGear), true, () => this.commandService.executeCommand(zoomSettingsAction.id, 'window.zoom')));1214const zoomLevelLabel = disposables.add(new Action('zoomLabel', undefined, undefined, false));12151216this.zoomLevelLabel = zoomLevelLabel;1217disposables.add(toDisposable(() => this.zoomLevelLabel = undefined));12181219const actionBarLeft = disposables.add(new ActionBar(left, { hoverDelegate: nativeHoverDelegate }));1220actionBarLeft.push(zoomOutAction, { icon: true, label: false, keybinding: this.keybindingService.lookupKeybinding(zoomOutAction.id)?.getLabel() });1221actionBarLeft.push(this.zoomLevelLabel, { icon: false, label: true });1222actionBarLeft.push(zoomInAction, { icon: true, label: false, keybinding: this.keybindingService.lookupKeybinding(zoomInAction.id)?.getLabel() });12231224const right = $('.zoom-status-right');1225container.appendChild(right);12261227const actionBarRight = disposables.add(new ActionBar(right, { hoverDelegate: nativeHoverDelegate }));12281229actionBarRight.push(zoomResetAction, { icon: false, label: true });1230actionBarRight.push(zoomSettingsAction, { icon: true, label: false, keybinding: this.keybindingService.lookupKeybinding(zoomSettingsAction.id)?.getLabel() });12311232const name = localize('status.windowZoom', "Window Zoom");1233disposables.add(this.statusbarService.addEntry({1234name,1235text: visibleOrText,1236tooltip: container,1237ariaLabel: name,1238command: ShowTooltipCommand,1239kind: 'prominent'1240}, 'status.windowZoom', StatusbarAlignment.RIGHT, 102));1241}12421243private updateZoomLevelLabel(targetWindowId: number): void {1244if (this.zoomLevelLabel) {1245const targetWindow = getWindowById(targetWindowId, true).window;1246const zoomFactor = Math.round(getZoomFactor(targetWindow) * 100);1247const zoomLevel = getZoomLevel(targetWindow);12481249this.zoomLevelLabel.label = `${zoomLevel}`;1250this.zoomLevelLabel.tooltip = localize('zoomNumber', "Zoom Level: {0} ({1}%)", zoomLevel, zoomFactor);1251}1252}1253}125412551256