Path: blob/main/src/vs/workbench/browser/actions/windowActions.ts
3296 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 { localize, localize2 } from '../../../nls.js';6import { IWindowOpenable } from '../../../platform/window/common/window.js';7import { IDialogService } from '../../../platform/dialogs/common/dialogs.js';8import { MenuRegistry, MenuId, Action2, registerAction2, IAction2Options } from '../../../platform/actions/common/actions.js';9import { KeyChord, KeyCode, KeyMod } from '../../../base/common/keyCodes.js';10import { IsMainWindowFullscreenContext } from '../../common/contextkeys.js';11import { IsMacNativeContext, IsDevelopmentContext, IsWebContext, IsIOSContext } from '../../../platform/contextkey/common/contextkeys.js';12import { Categories } from '../../../platform/action/common/actionCommonCategories.js';13import { KeybindingsRegistry, KeybindingWeight } from '../../../platform/keybinding/common/keybindingsRegistry.js';14import { IQuickInputButton, IQuickInputService, IQuickPickSeparator, IKeyMods, IQuickPickItem } from '../../../platform/quickinput/common/quickInput.js';15import { IWorkspaceContextService, IWorkspaceIdentifier } from '../../../platform/workspace/common/workspace.js';16import { ILabelService, Verbosity } from '../../../platform/label/common/label.js';17import { IKeybindingService } from '../../../platform/keybinding/common/keybinding.js';18import { IModelService } from '../../../editor/common/services/model.js';19import { ILanguageService } from '../../../editor/common/languages/language.js';20import { IRecent, isRecentFolder, isRecentWorkspace, IWorkspacesService } from '../../../platform/workspaces/common/workspaces.js';21import { URI } from '../../../base/common/uri.js';22import { getIconClasses } from '../../../editor/common/services/getIconClasses.js';23import { FileKind } from '../../../platform/files/common/files.js';24import { splitRecentLabel } from '../../../base/common/labels.js';25import { isMacintosh, isWeb, isWindows } from '../../../base/common/platform.js';26import { ContextKeyExpr } from '../../../platform/contextkey/common/contextkey.js';27import { inQuickPickContext, getQuickNavigateHandler } from '../quickaccess.js';28import { IHostService } from '../../services/host/browser/host.js';29import { ResourceMap } from '../../../base/common/map.js';30import { Codicon } from '../../../base/common/codicons.js';31import { ThemeIcon } from '../../../base/common/themables.js';32import { CommandsRegistry } from '../../../platform/commands/common/commands.js';33import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';34import { ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js';35import { isFolderBackupInfo, isWorkspaceBackupInfo } from '../../../platform/backup/common/backup.js';36import { getActiveElement, getActiveWindow, isHTMLElement } from '../../../base/browser/dom.js';3738export const inRecentFilesPickerContextKey = 'inRecentFilesPicker';3940interface IRecentlyOpenedPick extends IQuickPickItem {41resource: URI;42openable: IWindowOpenable;43remoteAuthority: string | undefined;44}4546abstract class BaseOpenRecentAction extends Action2 {4748private readonly removeFromRecentlyOpened: IQuickInputButton = {49iconClass: ThemeIcon.asClassName(Codicon.removeClose),50tooltip: localize('remove', "Remove from Recently Opened")51};5253private readonly dirtyRecentlyOpenedFolder: IQuickInputButton = {54iconClass: 'dirty-workspace ' + ThemeIcon.asClassName(Codicon.closeDirty),55tooltip: localize('dirtyRecentlyOpenedFolder', "Folder With Unsaved Files"),56alwaysVisible: true57};5859private readonly dirtyRecentlyOpenedWorkspace: IQuickInputButton = {60...this.dirtyRecentlyOpenedFolder,61tooltip: localize('dirtyRecentlyOpenedWorkspace', "Workspace With Unsaved Files"),62};6364constructor(desc: Readonly<IAction2Options>) {65super(desc);66}6768protected abstract isQuickNavigate(): boolean;6970override async run(accessor: ServicesAccessor): Promise<void> {71const workspacesService = accessor.get(IWorkspacesService);72const quickInputService = accessor.get(IQuickInputService);73const contextService = accessor.get(IWorkspaceContextService);74const labelService = accessor.get(ILabelService);75const keybindingService = accessor.get(IKeybindingService);76const modelService = accessor.get(IModelService);77const languageService = accessor.get(ILanguageService);78const hostService = accessor.get(IHostService);79const dialogService = accessor.get(IDialogService);8081const recentlyOpened = await workspacesService.getRecentlyOpened();82const dirtyWorkspacesAndFolders = await workspacesService.getDirtyWorkspaces();8384let hasWorkspaces = false;8586// Identify all folders and workspaces with unsaved files87const dirtyFolders = new ResourceMap<boolean>();88const dirtyWorkspaces = new ResourceMap<IWorkspaceIdentifier>();89for (const dirtyWorkspace of dirtyWorkspacesAndFolders) {90if (isFolderBackupInfo(dirtyWorkspace)) {91dirtyFolders.set(dirtyWorkspace.folderUri, true);92} else {93dirtyWorkspaces.set(dirtyWorkspace.workspace.configPath, dirtyWorkspace.workspace);94hasWorkspaces = true;95}96}9798// Identify all recently opened folders and workspaces99const recentFolders = new ResourceMap<boolean>();100const recentWorkspaces = new ResourceMap<IWorkspaceIdentifier>();101for (const recent of recentlyOpened.workspaces) {102if (isRecentFolder(recent)) {103recentFolders.set(recent.folderUri, true);104} else {105recentWorkspaces.set(recent.workspace.configPath, recent.workspace);106hasWorkspaces = true;107}108}109110// Fill in all known recently opened workspaces111const workspacePicks: IRecentlyOpenedPick[] = [];112for (const recent of recentlyOpened.workspaces) {113const isDirty = isRecentFolder(recent) ? dirtyFolders.has(recent.folderUri) : dirtyWorkspaces.has(recent.workspace.configPath);114115workspacePicks.push(this.toQuickPick(modelService, languageService, labelService, recent, isDirty));116}117118// Fill any backup workspace that is not yet shown at the end119for (const dirtyWorkspaceOrFolder of dirtyWorkspacesAndFolders) {120if (isFolderBackupInfo(dirtyWorkspaceOrFolder) && !recentFolders.has(dirtyWorkspaceOrFolder.folderUri)) {121workspacePicks.push(this.toQuickPick(modelService, languageService, labelService, dirtyWorkspaceOrFolder, true));122} else if (isWorkspaceBackupInfo(dirtyWorkspaceOrFolder) && !recentWorkspaces.has(dirtyWorkspaceOrFolder.workspace.configPath)) {123workspacePicks.push(this.toQuickPick(modelService, languageService, labelService, dirtyWorkspaceOrFolder, true));124}125}126127const filePicks = recentlyOpened.files.map(p => this.toQuickPick(modelService, languageService, labelService, p, false));128129// focus second entry if the first recent workspace is the current workspace130const firstEntry = recentlyOpened.workspaces[0];131const autoFocusSecondEntry: boolean = firstEntry && contextService.isCurrentWorkspace(isRecentWorkspace(firstEntry) ? firstEntry.workspace : firstEntry.folderUri);132133let keyMods: IKeyMods | undefined;134135const workspaceSeparator: IQuickPickSeparator = { type: 'separator', label: hasWorkspaces ? localize('workspacesAndFolders', "folders & workspaces") : localize('folders', "folders") };136const fileSeparator: IQuickPickSeparator = { type: 'separator', label: localize('files', "files") };137const picks = [workspaceSeparator, ...workspacePicks, fileSeparator, ...filePicks];138139const pick = await quickInputService.pick(picks, {140contextKey: inRecentFilesPickerContextKey,141activeItem: [...workspacePicks, ...filePicks][autoFocusSecondEntry ? 1 : 0],142placeHolder: isMacintosh ? localize('openRecentPlaceholderMac', "Select to open (hold Cmd-key to force new window or Option-key for same window)") : localize('openRecentPlaceholder', "Select to open (hold Ctrl-key to force new window or Alt-key for same window)"),143matchOnDescription: true,144onKeyMods: mods => keyMods = mods,145quickNavigate: this.isQuickNavigate() ? { keybindings: keybindingService.lookupKeybindings(this.desc.id) } : undefined,146hideInput: this.isQuickNavigate(),147onDidTriggerItemButton: async context => {148149// Remove150if (context.button === this.removeFromRecentlyOpened) {151await workspacesService.removeRecentlyOpened([context.item.resource]);152context.removeItem();153}154155// Dirty Folder/Workspace156else if (context.button === this.dirtyRecentlyOpenedFolder || context.button === this.dirtyRecentlyOpenedWorkspace) {157const isDirtyWorkspace = context.button === this.dirtyRecentlyOpenedWorkspace;158const { confirmed } = await dialogService.confirm({159title: isDirtyWorkspace ? localize('dirtyWorkspace', "Workspace with Unsaved Files") : localize('dirtyFolder', "Folder with Unsaved Files"),160message: isDirtyWorkspace ? localize('dirtyWorkspaceConfirm', "Do you want to open the workspace to review the unsaved files?") : localize('dirtyFolderConfirm', "Do you want to open the folder to review the unsaved files?"),161detail: isDirtyWorkspace ? localize('dirtyWorkspaceConfirmDetail', "Workspaces with unsaved files cannot be removed until all unsaved files have been saved or reverted.") : localize('dirtyFolderConfirmDetail', "Folders with unsaved files cannot be removed until all unsaved files have been saved or reverted.")162});163164if (confirmed) {165hostService.openWindow(166[context.item.openable], {167remoteAuthority: context.item.remoteAuthority || null // local window if remoteAuthority is not set or can not be deducted from the openable168});169quickInputService.cancel();170}171}172}173});174175if (pick) {176return hostService.openWindow([pick.openable], {177forceNewWindow: keyMods?.ctrlCmd,178forceReuseWindow: keyMods?.alt,179remoteAuthority: pick.remoteAuthority || null // local window if remoteAuthority is not set or can not be deducted from the openable180});181}182}183184private toQuickPick(modelService: IModelService, languageService: ILanguageService, labelService: ILabelService, recent: IRecent, isDirty: boolean): IRecentlyOpenedPick {185let openable: IWindowOpenable | undefined;186let iconClasses: string[];187let fullLabel: string | undefined;188let resource: URI | undefined;189let isWorkspace = false;190191// Folder192if (isRecentFolder(recent)) {193resource = recent.folderUri;194iconClasses = getIconClasses(modelService, languageService, resource, FileKind.FOLDER);195openable = { folderUri: resource };196fullLabel = recent.label || labelService.getWorkspaceLabel(resource, { verbose: Verbosity.LONG });197}198199// Workspace200else if (isRecentWorkspace(recent)) {201resource = recent.workspace.configPath;202iconClasses = getIconClasses(modelService, languageService, resource, FileKind.ROOT_FOLDER);203openable = { workspaceUri: resource };204fullLabel = recent.label || labelService.getWorkspaceLabel(recent.workspace, { verbose: Verbosity.LONG });205isWorkspace = true;206}207208// File209else {210resource = recent.fileUri;211iconClasses = getIconClasses(modelService, languageService, resource, FileKind.FILE);212openable = { fileUri: resource };213fullLabel = recent.label || labelService.getUriLabel(resource, { appendWorkspaceSuffix: true });214}215216const { name, parentPath } = splitRecentLabel(fullLabel);217218return {219iconClasses,220label: name,221ariaLabel: isDirty ? isWorkspace ? localize('recentDirtyWorkspaceAriaLabel', "{0}, workspace with unsaved changes", name) : localize('recentDirtyFolderAriaLabel', "{0}, folder with unsaved changes", name) : name,222description: parentPath,223buttons: isDirty ? [isWorkspace ? this.dirtyRecentlyOpenedWorkspace : this.dirtyRecentlyOpenedFolder] : [this.removeFromRecentlyOpened],224openable,225resource,226remoteAuthority: recent.remoteAuthority227};228}229}230231export class OpenRecentAction extends BaseOpenRecentAction {232233static ID = 'workbench.action.openRecent';234235constructor() {236super({237id: OpenRecentAction.ID,238title: {239...localize2('openRecent', "Open Recent..."),240mnemonicTitle: localize({ key: 'miMore', comment: ['&& denotes a mnemonic'] }, "&&More..."),241},242category: Categories.File,243f1: true,244keybinding: {245weight: KeybindingWeight.WorkbenchContrib,246primary: KeyMod.CtrlCmd | KeyCode.KeyR,247mac: { primary: KeyMod.WinCtrl | KeyCode.KeyR }248},249menu: {250id: MenuId.MenubarRecentMenu,251group: 'y_more',252order: 1253}254});255}256257protected isQuickNavigate(): boolean {258return false;259}260}261262class QuickPickRecentAction extends BaseOpenRecentAction {263264constructor() {265super({266id: 'workbench.action.quickOpenRecent',267title: localize2('quickOpenRecent', 'Quick Open Recent...'),268category: Categories.File,269f1: false // hide quick pickers from command palette to not confuse with the other entry that shows a input field270});271}272273protected isQuickNavigate(): boolean {274return true;275}276}277278class ToggleFullScreenAction extends Action2 {279280constructor() {281super({282id: 'workbench.action.toggleFullScreen',283title: {284...localize2('toggleFullScreen', "Toggle Full Screen"),285mnemonicTitle: localize({ key: 'miToggleFullScreen', comment: ['&& denotes a mnemonic'] }, "&&Full Screen"),286},287category: Categories.View,288f1: true,289keybinding: {290weight: KeybindingWeight.WorkbenchContrib,291primary: KeyCode.F11,292mac: {293primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyF294}295},296precondition: IsIOSContext.toNegated(),297toggled: IsMainWindowFullscreenContext,298menu: [{299id: MenuId.MenubarAppearanceMenu,300group: '1_toggle_view',301order: 1302}]303});304}305306override run(accessor: ServicesAccessor): Promise<void> {307const hostService = accessor.get(IHostService);308309return hostService.toggleFullScreen(getActiveWindow());310}311}312313export class ReloadWindowAction extends Action2 {314315static readonly ID = 'workbench.action.reloadWindow';316317constructor() {318super({319id: ReloadWindowAction.ID,320title: localize2('reloadWindow', 'Reload Window'),321category: Categories.Developer,322f1: true,323keybinding: {324weight: KeybindingWeight.WorkbenchContrib + 50,325when: IsDevelopmentContext,326primary: KeyMod.CtrlCmd | KeyCode.KeyR327}328});329}330331override async run(accessor: ServicesAccessor): Promise<void> {332const hostService = accessor.get(IHostService);333334return hostService.reload();335}336}337338class ShowAboutDialogAction extends Action2 {339340constructor() {341super({342id: 'workbench.action.showAboutDialog',343title: {344...localize2('about', "About"),345mnemonicTitle: localize({ key: 'miAbout', comment: ['&& denotes a mnemonic'] }, "&&About"),346},347category: Categories.Help,348f1: true,349menu: {350id: MenuId.MenubarHelpMenu,351group: 'z_about',352order: 1,353when: IsMacNativeContext.toNegated()354}355});356}357358override run(accessor: ServicesAccessor): Promise<void> {359const dialogService = accessor.get(IDialogService);360361return dialogService.about();362}363}364365class NewWindowAction extends Action2 {366367constructor() {368super({369id: 'workbench.action.newWindow',370title: {371...localize2('newWindow', "New Window"),372mnemonicTitle: localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window"),373},374f1: true,375keybinding: {376weight: KeybindingWeight.WorkbenchContrib,377primary: isWeb ? (isWindows ? KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.Shift | KeyCode.KeyN) : KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.KeyN) : KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyN,378secondary: isWeb ? [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyN] : undefined379},380menu: {381id: MenuId.MenubarFileMenu,382group: '1_new',383order: 3384}385});386}387388override run(accessor: ServicesAccessor): Promise<void> {389const hostService = accessor.get(IHostService);390391return hostService.openWindow({ remoteAuthority: null });392}393}394395class BlurAction extends Action2 {396397constructor() {398super({399id: 'workbench.action.blur',400title: localize2('blur', 'Remove keyboard focus from focused element')401});402}403404run(): void {405const activeElement = getActiveElement();406if (isHTMLElement(activeElement)) {407activeElement.blur();408}409}410}411412// --- Actions Registration413414registerAction2(NewWindowAction);415registerAction2(ToggleFullScreenAction);416registerAction2(QuickPickRecentAction);417registerAction2(OpenRecentAction);418registerAction2(ReloadWindowAction);419registerAction2(ShowAboutDialogAction);420registerAction2(BlurAction);421422// --- Commands/Keybindings Registration423424const recentFilesPickerContext = ContextKeyExpr.and(inQuickPickContext, ContextKeyExpr.has(inRecentFilesPickerContextKey));425426const quickPickNavigateNextInRecentFilesPickerId = 'workbench.action.quickOpenNavigateNextInRecentFilesPicker';427KeybindingsRegistry.registerCommandAndKeybindingRule({428id: quickPickNavigateNextInRecentFilesPickerId,429weight: KeybindingWeight.WorkbenchContrib + 50,430handler: getQuickNavigateHandler(quickPickNavigateNextInRecentFilesPickerId, true),431when: recentFilesPickerContext,432primary: KeyMod.CtrlCmd | KeyCode.KeyR,433mac: { primary: KeyMod.WinCtrl | KeyCode.KeyR }434});435436const quickPickNavigatePreviousInRecentFilesPicker = 'workbench.action.quickOpenNavigatePreviousInRecentFilesPicker';437KeybindingsRegistry.registerCommandAndKeybindingRule({438id: quickPickNavigatePreviousInRecentFilesPicker,439weight: KeybindingWeight.WorkbenchContrib + 50,440handler: getQuickNavigateHandler(quickPickNavigatePreviousInRecentFilesPicker, false),441when: recentFilesPickerContext,442primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyR,443mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KeyR }444});445446CommandsRegistry.registerCommand('workbench.action.toggleConfirmBeforeClose', accessor => {447const configurationService = accessor.get(IConfigurationService);448const setting = configurationService.inspect<'always' | 'keyboardOnly' | 'never'>('window.confirmBeforeClose').userValue;449450return configurationService.updateValue('window.confirmBeforeClose', setting === 'never' ? 'keyboardOnly' : 'never');451});452453// --- Menu Registration454455MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, {456group: 'z_ConfirmClose',457command: {458id: 'workbench.action.toggleConfirmBeforeClose',459title: localize('miConfirmClose', "Confirm Before Close"),460toggled: ContextKeyExpr.notEquals('config.window.confirmBeforeClose', 'never')461},462order: 1,463when: IsWebContext464});465466MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, {467title: localize({ key: 'miOpenRecent', comment: ['&& denotes a mnemonic'] }, "Open &&Recent"),468submenu: MenuId.MenubarRecentMenu,469group: '2_open',470order: 4471});472473474