UNPKG

@theia/core

Version:

Theia is a cloud & desktop IDE framework implemented in TypeScript.

1,120 lines (1,032 loc) • 122 kB
// ***************************************************************************** // Copyright (C) 2017 TypeFox and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** /* eslint-disable max-len, @typescript-eslint/indent */ import debounce = require('lodash.debounce'); import { injectable, inject, optional } from 'inversify'; import { MAIN_MENU_BAR, MANAGE_MENU, MenuContribution, MenuModelRegistry, ACCOUNTS_MENU, CompoundMenuNode, CommandMenu, Group, Submenu } from '../common/menu'; import { CommonMenus } from './common-menus'; export { CommonMenus }; import { KeybindingContribution, KeybindingRegistry } from './keybinding'; import { FrontendApplication } from './frontend-application'; import { FrontendApplicationContribution, OnWillStopAction } from './frontend-application-contribution'; import { CommandContribution, CommandRegistry, Command } from '../common/command'; import { CommonCommands } from './common-commands'; export { CommonCommands }; import { UriAwareCommandHandler } from '../common/uri-command-handler'; import { SelectionService } from '../common/selection-service'; import { MessageService } from '../common/message-service'; import { OpenerService, open } from '../browser/opener-service'; import { ApplicationShell } from './shell/application-shell'; import { SHELL_TABBAR_CONTEXT_CLOSE, SHELL_TABBAR_CONTEXT_COPY, SHELL_TABBAR_CONTEXT_PIN, SHELL_TABBAR_CONTEXT_SPLIT } from './shell/tab-bars'; import { AboutDialog } from './about-dialog'; import * as browser from './browser'; import URI from '../common/uri'; import { ContextKey, ContextKeyService } from './context-key-service'; import { OS, isOSX, isWindows, EOL } from '../common/os'; import { ResourceContextKey } from './resource-context-key'; import { UriSelection } from '../common/selection'; import { StorageService } from './storage-service'; import { Navigatable, NavigatableWidget } from './navigatable'; import { QuickViewService } from './quick-input/quick-view-service'; import { environment } from '@theia/application-package/lib/environment'; import { IconTheme, IconThemeService } from './icon-theme-service'; import { ColorContribution } from './color-application-contribution'; import { ColorRegistry } from './color-registry'; import { Color } from '../common/color'; import { CoreConfiguration, CorePreferences } from '../common/core-preferences'; import { ThemeService } from './theming'; import { ClipboardService } from './clipboard-service'; import { EncodingRegistry } from './encoding-registry'; import { UTF8 } from '../common/encodings'; import { EnvVariablesServer } from '../common/env-variables'; import { AuthenticationService } from './authentication-service'; import { FormatType, Saveable, SaveOptions } from './saveable'; import { QuickInputService, QuickPickItem, QuickPickItemOrSeparator, QuickPickSeparator } from './quick-input'; import { AsyncLocalizationProvider } from '../common/i18n/localization'; import { nls } from '../common/nls'; import { CurrentWidgetCommandAdapter } from './shell/current-widget-command-adapter'; import { ConfirmDialog, confirmExit, ConfirmSaveDialog, Dialog } from './dialogs'; import { WindowService } from './window/window-service'; import { FrontendApplicationConfigProvider } from './frontend-application-config-provider'; import { DecorationStyle } from './decoration-style'; import { codicon, isPinned, Title, togglePinned, Widget } from './widgets'; import { SaveableService } from './saveable-service'; import { UserWorkingDirectoryProvider } from './user-working-directory-provider'; import { PreferenceChangeEvent, PreferenceScope, PreferenceService, UNTITLED_SCHEME, UntitledResourceResolver } from '../common'; import { LanguageQuickPickService } from './i18n/language-quick-pick-service'; import { SidebarMenu } from './shell/sidebar-menu-widget'; import { UndoRedoHandlerService } from './undo-redo-handler'; import { timeout } from '../common/promise-util'; export const supportCut = environment.electron.is() || document.queryCommandSupported('cut'); export const supportCopy = environment.electron.is() || document.queryCommandSupported('copy'); // Chrome incorrectly returns true for document.queryCommandSupported('paste') // when the paste feature is available but the calling script has insufficient // privileges to actually perform the action export const supportPaste = environment.electron.is() || (!browser.isChrome && document.queryCommandSupported('paste')); export const RECENT_COMMANDS_STORAGE_KEY = 'commands'; export const CLASSNAME_OS_MAC = 'mac'; export const CLASSNAME_OS_WINDOWS = 'windows'; export const CLASSNAME_OS_LINUX = 'linux'; @injectable() export class CommonFrontendContribution implements FrontendApplicationContribution, MenuContribution, CommandContribution, KeybindingContribution, ColorContribution { protected commonDecorationsStyleSheet: CSSStyleSheet = DecorationStyle.createStyleSheet('coreCommonDecorationsStyle'); constructor( @inject(ApplicationShell) protected readonly shell: ApplicationShell, @inject(SelectionService) protected readonly selectionService: SelectionService, @inject(MessageService) protected readonly messageService: MessageService, @inject(OpenerService) protected readonly openerService: OpenerService, @inject(AboutDialog) protected readonly aboutDialog: AboutDialog, @inject(AsyncLocalizationProvider) protected readonly localizationProvider: AsyncLocalizationProvider, @inject(SaveableService) protected readonly saveResourceService: SaveableService, ) { } @inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService; @inject(ResourceContextKey) protected readonly resourceContextKey: ResourceContextKey; @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; @inject(MenuModelRegistry) protected readonly menuRegistry: MenuModelRegistry; @inject(StorageService) protected readonly storageService: StorageService; @inject(QuickInputService) @optional() protected readonly quickInputService: QuickInputService; @inject(IconThemeService) protected readonly iconThemes: IconThemeService; @inject(ThemeService) protected readonly themeService: ThemeService; @inject(CorePreferences) protected readonly preferences: CorePreferences; @inject(PreferenceService) protected readonly preferenceService: PreferenceService; @inject(ClipboardService) protected readonly clipboardService: ClipboardService; @inject(EncodingRegistry) protected readonly encodingRegistry: EncodingRegistry; @inject(EnvVariablesServer) protected readonly environments: EnvVariablesServer; @inject(AuthenticationService) protected readonly authenticationService: AuthenticationService; @inject(WindowService) protected readonly windowService: WindowService; @inject(UserWorkingDirectoryProvider) protected readonly workingDirProvider: UserWorkingDirectoryProvider; @inject(LanguageQuickPickService) protected readonly languageQuickPickService: LanguageQuickPickService; @inject(UntitledResourceResolver) protected readonly untitledResourceResolver: UntitledResourceResolver; @inject(UndoRedoHandlerService) protected readonly undoRedoHandlerService: UndoRedoHandlerService; protected pinnedKey: ContextKey<boolean>; protected inputFocus: ContextKey<boolean>; async configure(app: FrontendApplication): Promise<void> { // FIXME: This request blocks valuable startup time (~200ms). const configDirUri = await this.environments.getConfigDirUri(); // Global settings this.encodingRegistry.registerOverride({ encoding: UTF8, parent: new URI(configDirUri) }); this.contextKeyService.createKey<boolean>('isLinux', OS.type() === OS.Type.Linux); this.contextKeyService.createKey<boolean>('isMac', OS.type() === OS.Type.OSX); this.contextKeyService.createKey<boolean>('isWindows', OS.type() === OS.Type.Windows); this.contextKeyService.createKey<boolean>('isWeb', !this.isElectron()); this.inputFocus = this.contextKeyService.createKey<boolean>('inputFocus', false); this.updateInputFocus(); browser.onDomEvent(document, 'focusin', () => this.updateInputFocus()); this.pinnedKey = this.contextKeyService.createKey<boolean>('activeEditorIsPinned', false); this.updatePinnedKey(); this.shell.onDidChangeActiveWidget(() => this.updatePinnedKey()); this.initResourceContextKeys(); this.registerCtrlWHandling(); this.setOsClass(); this.updateStyles(); this.preferences.ready.then(() => this.setSashProperties()); this.preferences.onPreferenceChanged(e => this.handlePreferenceChange(e, app)); app.shell.initialized.then(() => { app.shell.leftPanelHandler.addBottomMenu({ id: 'settings-menu', iconClass: codicon('settings-gear'), title: nls.localizeByDefault(CommonCommands.MANAGE_CATEGORY), menuPath: MANAGE_MENU, order: 0, }); const accountsMenu: SidebarMenu = { id: 'accounts-menu', iconClass: codicon('account'), title: nls.localizeByDefault('Accounts'), menuPath: ACCOUNTS_MENU, order: 1, onDidBadgeChange: this.authenticationService.onDidUpdateSignInCount }; this.authenticationService.onDidRegisterAuthenticationProvider(() => { app.shell.leftPanelHandler.addBottomMenu(accountsMenu); }); this.authenticationService.onDidUnregisterAuthenticationProvider(() => { if (this.authenticationService.getProviderIds().length === 0) { app.shell.leftPanelHandler.removeBottomMenu(accountsMenu.id); } }); }); } protected setOsClass(): void { if (isOSX) { document.body.classList.add(CLASSNAME_OS_MAC); } else if (isWindows) { document.body.classList.add(CLASSNAME_OS_WINDOWS); } else { document.body.classList.add(CLASSNAME_OS_LINUX); } } protected updateStyles(): void { document.body.classList.remove('theia-editor-highlightModifiedTabs'); if (this.preferences['workbench.editor.highlightModifiedTabs']) { document.body.classList.add('theia-editor-highlightModifiedTabs'); } } protected updateInputFocus(): void { const activeElement = document.activeElement; if (activeElement) { const isInput = activeElement.tagName?.toLowerCase() === 'input' || activeElement.tagName?.toLowerCase() === 'textarea'; this.inputFocus.set(isInput); } } protected updatePinnedKey(): void { const activeTab = this.shell.findTabBar(); const pinningTarget = activeTab && this.shell.findTitle(activeTab); const value = pinningTarget && isPinned(pinningTarget); this.pinnedKey.set(value); } protected handlePreferenceChange(e: PreferenceChangeEvent<CoreConfiguration>, app: FrontendApplication): void { switch (e.preferenceName) { case 'workbench.editor.highlightModifiedTabs': { this.updateStyles(); break; } case 'window.menuBarVisibility': { const { newValue } = e; const mainMenuId = 'main-menu'; if (newValue === 'compact') { this.shell.leftPanelHandler.addTopMenu({ id: mainMenuId, iconClass: `theia-compact-menu ${codicon('menu')}`, title: nls.localizeByDefault('Application Menu'), menuPath: MAIN_MENU_BAR, order: 0, }); } else { app.shell.leftPanelHandler.removeTopMenu(mainMenuId); } break; } case 'workbench.sash.hoverDelay': case 'workbench.sash.size': { this.setSashProperties(); break; } } } protected setSashProperties(): void { const sashRule = `:root { --theia-sash-hoverDelay: ${this.preferences['workbench.sash.hoverDelay']}ms; --theia-sash-width: ${this.preferences['workbench.sash.size']}px; }`; DecorationStyle.deleteStyleRule(':root', this.commonDecorationsStyleSheet); this.commonDecorationsStyleSheet.insertRule(sashRule); } onStart(): void { this.storageService.getData<{ recent: Command[] }>(RECENT_COMMANDS_STORAGE_KEY, { recent: [] }) .then(tasks => this.commandRegistry.recent = tasks.recent); } onStop(): void { const recent = this.commandRegistry.recent; this.storageService.setData<{ recent: Command[] }>(RECENT_COMMANDS_STORAGE_KEY, { recent }); window.localStorage.setItem(IconThemeService.STORAGE_KEY, this.iconThemes.current); window.localStorage.setItem(ThemeService.STORAGE_KEY, this.themeService.getCurrentTheme().id); } protected initResourceContextKeys(): void { const updateContextKeys = () => { const selection = this.selectionService.selection; const resourceUri = Navigatable.is(selection) && selection.getResourceUri() || UriSelection.getUri(selection); this.resourceContextKey.set(resourceUri); }; updateContextKeys(); this.selectionService.onSelectionChanged(updateContextKeys); } registerMenus(registry: MenuModelRegistry): void { registry.registerSubmenu(CommonMenus.FILE, nls.localizeByDefault('File')); registry.registerSubmenu(CommonMenus.EDIT, nls.localizeByDefault('Edit')); registry.registerSubmenu(CommonMenus.VIEW, nls.localizeByDefault('View')); registry.registerSubmenu(CommonMenus.HELP, nls.localizeByDefault('Help')); // For plugins contributing create new file commands/menu-actions registry.registerSubmenu(CommonMenus.FILE_NEW_CONTRIBUTIONS, nls.localizeByDefault('New File...')); registry.registerMenuAction(CommonMenus.FILE_SAVE, { commandId: CommonCommands.SAVE.id }); registry.registerMenuAction(CommonMenus.FILE_SAVE, { commandId: CommonCommands.SAVE_ALL.id }); registry.registerMenuAction(CommonMenus.FILE_AUTOSAVE, { commandId: CommonCommands.AUTO_SAVE.id }); registry.registerSubmenu(CommonMenus.FILE_SETTINGS_SUBMENU, nls.localizeByDefault(CommonCommands.PREFERENCES_CATEGORY)); registry.registerMenuAction(CommonMenus.EDIT_UNDO, { commandId: CommonCommands.UNDO.id, order: '0' }); registry.registerMenuAction(CommonMenus.EDIT_UNDO, { commandId: CommonCommands.REDO.id, order: '1' }); registry.registerMenuAction(CommonMenus.EDIT_FIND, { commandId: CommonCommands.FIND.id, order: '0' }); registry.registerMenuAction(CommonMenus.EDIT_FIND, { commandId: CommonCommands.REPLACE.id, order: '1' }); registry.registerMenuAction(CommonMenus.EDIT_CLIPBOARD, { commandId: CommonCommands.CUT.id, order: '0' }); registry.registerMenuAction(CommonMenus.EDIT_CLIPBOARD, { commandId: CommonCommands.COPY.id, order: '1' }); registry.registerMenuAction(CommonMenus.EDIT_CLIPBOARD, { commandId: CommonCommands.PASTE.id, order: '2' }); registry.registerMenuAction(CommonMenus.EDIT_CLIPBOARD, { commandId: CommonCommands.COPY_PATH.id, order: '3' }); registry.registerMenuAction(CommonMenus.VIEW_APPEARANCE_SUBMENU_BAR, { commandId: CommonCommands.TOGGLE_BOTTOM_PANEL.id, order: '1' }); registry.registerMenuAction(CommonMenus.VIEW_APPEARANCE_SUBMENU_BAR, { commandId: CommonCommands.TOGGLE_STATUS_BAR.id, order: '2', label: nls.localizeByDefault('Toggle Status Bar Visibility') }); registry.registerMenuAction(CommonMenus.VIEW_APPEARANCE_SUBMENU_BAR, { commandId: CommonCommands.COLLAPSE_ALL_PANELS.id, order: '3' }); registry.registerMenuAction(SHELL_TABBAR_CONTEXT_CLOSE, { commandId: CommonCommands.CLOSE_TAB.id, label: nls.localizeByDefault('Close'), order: '0' }); registry.registerMenuAction(SHELL_TABBAR_CONTEXT_CLOSE, { commandId: CommonCommands.CLOSE_OTHER_TABS.id, label: nls.localizeByDefault('Close Others'), order: '1' }); registry.registerMenuAction(SHELL_TABBAR_CONTEXT_CLOSE, { commandId: CommonCommands.CLOSE_RIGHT_TABS.id, label: nls.localizeByDefault('Close to the Right'), order: '2' }); registry.registerMenuAction(SHELL_TABBAR_CONTEXT_CLOSE, { commandId: CommonCommands.CLOSE_SAVED_TABS.id, label: nls.localizeByDefault('Close Saved'), order: '3', }); registry.registerMenuAction(SHELL_TABBAR_CONTEXT_CLOSE, { commandId: CommonCommands.CLOSE_ALL_TABS.id, label: nls.localizeByDefault('Close All'), order: '4' }); registry.registerMenuAction(SHELL_TABBAR_CONTEXT_SPLIT, { commandId: CommonCommands.COLLAPSE_PANEL.id, label: CommonCommands.COLLAPSE_PANEL.label, order: '5' }); registry.registerMenuAction(SHELL_TABBAR_CONTEXT_SPLIT, { commandId: CommonCommands.TOGGLE_MAXIMIZED.id, label: CommonCommands.TOGGLE_MAXIMIZED.label, order: '6' }); registry.registerMenuAction(CommonMenus.VIEW_APPEARANCE_SUBMENU_SCREEN, { commandId: CommonCommands.TOGGLE_MAXIMIZED.id, label: CommonCommands.TOGGLE_MAXIMIZED.label, order: '6' }); registry.registerMenuAction(SHELL_TABBAR_CONTEXT_COPY, { commandId: CommonCommands.COPY_PATH.id, label: CommonCommands.COPY_PATH.label, order: '1', }); registry.registerMenuAction(CommonMenus.VIEW_APPEARANCE_SUBMENU_BAR, { commandId: CommonCommands.SHOW_MENU_BAR.id, label: nls.localizeByDefault('Toggle Menu Bar'), order: '0' }); registry.registerMenuAction(SHELL_TABBAR_CONTEXT_PIN, { commandId: CommonCommands.PIN_TAB.id, label: nls.localizeByDefault('Pin'), order: '7' }); registry.registerMenuAction(SHELL_TABBAR_CONTEXT_PIN, { commandId: CommonCommands.UNPIN_TAB.id, label: nls.localizeByDefault('Unpin'), order: '8' }); registry.registerMenuAction(CommonMenus.HELP, { commandId: CommonCommands.ABOUT_COMMAND.id, label: CommonCommands.ABOUT_COMMAND.label, order: '9' }); registry.registerMenuAction(CommonMenus.VIEW_PRIMARY, { commandId: CommonCommands.OPEN_VIEW.id }); registry.registerMenuAction(CommonMenus.FILE_SETTINGS_SUBMENU_THEME, { commandId: CommonCommands.SELECT_COLOR_THEME.id }); registry.registerMenuAction(CommonMenus.FILE_SETTINGS_SUBMENU_THEME, { commandId: CommonCommands.SELECT_ICON_THEME.id }); registry.registerSubmenu(CommonMenus.MANAGE_SETTINGS_THEMES, nls.localizeByDefault('Themes'), { sortString: 'a50' }); registry.registerMenuAction(CommonMenus.MANAGE_SETTINGS_THEMES, { commandId: CommonCommands.SELECT_COLOR_THEME.id, order: '0' }); registry.registerMenuAction(CommonMenus.MANAGE_SETTINGS_THEMES, { commandId: CommonCommands.SELECT_ICON_THEME.id, order: '1' }); registry.registerSubmenu(CommonMenus.VIEW_APPEARANCE_SUBMENU, nls.localizeByDefault('Appearance')); registry.registerMenuAction(CommonMenus.FILE_NEW_TEXT, { commandId: CommonCommands.NEW_UNTITLED_TEXT_FILE.id, label: nls.localizeByDefault('New Text File'), order: 'a' }); registry.registerMenuAction(CommonMenus.FILE_NEW_TEXT, { commandId: CommonCommands.PICK_NEW_FILE.id, label: nls.localizeByDefault('New File...'), order: 'a1' }); } registerCommands(commandRegistry: CommandRegistry): void { commandRegistry.registerCommand(CommonCommands.OPEN, UriAwareCommandHandler.MultiSelect(this.selectionService, { execute: uris => uris.map(uri => open(this.openerService, uri)), })); commandRegistry.registerCommand(CommonCommands.CUT, { execute: () => { if (supportCut) { document.execCommand('cut'); } else { this.messageService.warn(nls.localize('theia/core/cutWarn', "Please use the browser's cut command or shortcut.")); } } }); commandRegistry.registerCommand(CommonCommands.COPY, { execute: () => { if (supportCopy) { document.execCommand('copy'); } else { this.messageService.warn(nls.localize('theia/core/copyWarn', "Please use the browser's copy command or shortcut.")); } } }); commandRegistry.registerCommand(CommonCommands.PASTE, { execute: () => { if (supportPaste) { document.execCommand('paste'); } else { this.messageService.warn(nls.localize('theia/core/pasteWarn', "Please use the browser's paste command or shortcut.")); } } }); commandRegistry.registerCommand(CommonCommands.COPY_PATH, UriAwareCommandHandler.MultiSelect(this.selectionService, { isVisible: uris => Array.isArray(uris) && uris.some(uri => uri instanceof URI), isEnabled: uris => Array.isArray(uris) && uris.some(uri => uri instanceof URI), execute: async uris => { if (uris.length) { const lineDelimiter = EOL; const text = uris.map(resource => resource.path.fsPath()).join(lineDelimiter); await this.clipboardService.writeText(text); } else { await this.messageService.info(nls.localize('theia/core/copyInfo', 'Open a file first to copy its path')); } } })); commandRegistry.registerCommand(CommonCommands.UNDO, { execute: () => { this.undoRedoHandlerService.undo(); } }); commandRegistry.registerCommand(CommonCommands.REDO, { execute: () => { this.undoRedoHandlerService.redo(); } }); commandRegistry.registerCommand(CommonCommands.SELECT_ALL, { execute: () => document.execCommand('selectAll') }); commandRegistry.registerCommand(CommonCommands.FIND, { execute: () => { /* no-op */ } }); commandRegistry.registerCommand(CommonCommands.REPLACE, { execute: () => { /* no-op */ } }); commandRegistry.registerCommand(CommonCommands.NEXT_TAB, { isEnabled: () => this.shell.currentTabBar !== undefined, execute: () => this.shell.activateNextTab() }); commandRegistry.registerCommand(CommonCommands.PREVIOUS_TAB, { isEnabled: () => this.shell.currentTabBar !== undefined, execute: () => this.shell.activatePreviousTab() }); commandRegistry.registerCommand(CommonCommands.NEXT_TAB_IN_GROUP, { isEnabled: () => this.shell.nextTabIndexInTabBar() !== -1, execute: () => this.shell.activateNextTabInTabBar() }); commandRegistry.registerCommand(CommonCommands.PREVIOUS_TAB_IN_GROUP, { isEnabled: () => this.shell.previousTabIndexInTabBar() !== -1, execute: () => this.shell.activatePreviousTabInTabBar() }); commandRegistry.registerCommand(CommonCommands.NEXT_TAB_GROUP, { isEnabled: () => this.shell.nextTabBar() !== undefined, execute: () => this.shell.activateNextTabBar() }); commandRegistry.registerCommand(CommonCommands.PREVIOUS_TAB_GROUP, { isEnabled: () => this.shell.previousTabBar() !== undefined, execute: () => this.shell.activatePreviousTabBar() }); commandRegistry.registerCommand(CommonCommands.CLOSE_TAB, new CurrentWidgetCommandAdapter(this.shell, { isEnabled: title => Boolean(title?.closable), execute: (title, tabBar) => tabBar && this.shell.closeTabs(tabBar, candidate => candidate === title), })); commandRegistry.registerCommand(CommonCommands.CLOSE_OTHER_TABS, new CurrentWidgetCommandAdapter(this.shell, { isEnabled: (title, tabbar) => Boolean(tabbar?.titles.some(candidate => candidate !== title && candidate.closable)), execute: (title, tabbar) => tabbar && this.shell.closeTabs(tabbar, candidate => candidate !== title && candidate.closable), })); commandRegistry.registerCommand(CommonCommands.CLOSE_SAVED_TABS, new CurrentWidgetCommandAdapter(this.shell, { isEnabled: (_title, tabbar) => Boolean(tabbar?.titles.some(candidate => candidate.closable && !Saveable.isDirty(candidate.owner))), execute: (_title, tabbar) => tabbar && this.shell.closeTabs(tabbar, candidate => candidate.closable && !Saveable.isDirty(candidate.owner)), })); commandRegistry.registerCommand(CommonCommands.CLOSE_RIGHT_TABS, new CurrentWidgetCommandAdapter(this.shell, { isEnabled: (title, tabbar) => { let targetSeen = false; return Boolean(tabbar?.titles.some(candidate => { if (targetSeen && candidate.closable) { return true; }; if (candidate === title) { targetSeen = true; }; })); }, isVisible: (_title, tabbar) => { const area = (tabbar && this.shell.getAreaFor(tabbar)) ?? this.shell.currentTabArea; return area !== undefined && area !== 'left' && area !== 'right'; }, execute: (title, tabbar) => { if (tabbar) { let targetSeen = false; this.shell.closeTabs(tabbar, candidate => { if (targetSeen && candidate.closable) { return true; }; if (candidate === title) { targetSeen = true; }; return false; }); } } })); commandRegistry.registerCommand(CommonCommands.CLOSE_ALL_TABS, new CurrentWidgetCommandAdapter(this.shell, { isEnabled: (_title, tabbar) => Boolean(tabbar?.titles.some(title => title.closable)), execute: (_title, tabbar) => tabbar && this.shell.closeTabs(tabbar, candidate => candidate.closable), })); commandRegistry.registerCommand(CommonCommands.CLOSE_MAIN_TAB, { isEnabled: () => { const currentWidget = this.shell.getCurrentWidget('main'); return currentWidget !== undefined && currentWidget.title.closable; }, execute: () => this.shell.getCurrentWidget('main')!.close() }); commandRegistry.registerCommand(CommonCommands.CLOSE_OTHER_MAIN_TABS, { isEnabled: () => { const currentWidget = this.shell.getCurrentWidget('main'); return currentWidget !== undefined && this.shell.mainAreaTabBars.some(tb => tb.titles.some(title => title.owner !== currentWidget && title.closable)); }, execute: () => { const currentWidget = this.shell.getCurrentWidget('main'); this.shell.closeTabs('main', title => title.owner !== currentWidget && title.closable); } }); commandRegistry.registerCommand(CommonCommands.CLOSE_ALL_MAIN_TABS, { isEnabled: () => this.shell.mainAreaTabBars.some(tb => tb.titles.some(title => title.closable)), execute: () => this.shell.closeTabs('main', title => title.closable) }); commandRegistry.registerCommand(CommonCommands.COLLAPSE_PANEL, new CurrentWidgetCommandAdapter(this.shell, { isEnabled: (_title, tabbar) => { if (tabbar) { const area = this.shell.getAreaFor(tabbar); return ApplicationShell.isSideArea(area) && this.shell.isExpanded(area); } return false; }, isVisible: (_title, tabbar) => Boolean(tabbar && ApplicationShell.isSideArea(this.shell.getAreaFor(tabbar))), execute: (_title, tabbar) => tabbar && this.shell.collapsePanel(this.shell.getAreaFor(tabbar)!) })); commandRegistry.registerCommand(CommonCommands.COLLAPSE_ALL_PANELS, { execute: () => { this.shell.collapsePanel('left'); this.shell.collapsePanel('right'); this.shell.collapsePanel('bottom'); } }); commandRegistry.registerCommand(CommonCommands.TOGGLE_BOTTOM_PANEL, { isEnabled: () => this.shell.getWidgets('bottom').length > 0, execute: () => { if (this.shell.isExpanded('bottom')) { this.shell.collapsePanel('bottom'); } else { this.shell.expandPanel('bottom'); } } }); commandRegistry.registerCommand(CommonCommands.TOGGLE_LEFT_PANEL, { isEnabled: () => this.shell.getWidgets('left').length > 0, execute: () => { if (this.shell.isExpanded('left')) { this.shell.collapsePanel('left'); } else { this.shell.expandPanel('left'); } } }); commandRegistry.registerCommand(CommonCommands.TOGGLE_RIGHT_PANEL, { isEnabled: () => this.shell.getWidgets('right').length > 0, execute: () => { if (this.shell.isExpanded('right')) { this.shell.collapsePanel('right'); } else { this.shell.expandPanel('right'); } } }); commandRegistry.registerCommand(CommonCommands.TOGGLE_STATUS_BAR, { execute: () => this.preferenceService.updateValue('workbench.statusBar.visible', !this.preferences['workbench.statusBar.visible']) }); commandRegistry.registerCommand(CommonCommands.TOGGLE_MAXIMIZED, new CurrentWidgetCommandAdapter(this.shell, { isEnabled: title => Boolean(title?.owner && this.shell.canToggleMaximized(title?.owner)), isVisible: title => Boolean(title?.owner && this.shell.canToggleMaximized(title?.owner)), execute: title => title?.owner && this.shell.toggleMaximized(title?.owner), })); commandRegistry.registerCommand(CommonCommands.SHOW_MENU_BAR, { isEnabled: () => !isOSX, isVisible: () => !isOSX, execute: () => { const menuBarVisibility = 'window.menuBarVisibility'; const visibility = this.preferences[menuBarVisibility]; if (visibility !== 'compact') { this.preferenceService.updateValue(menuBarVisibility, 'compact'); } else { this.preferenceService.updateValue(menuBarVisibility, 'classic'); } } }); commandRegistry.registerCommand(CommonCommands.SAVE, { execute: () => this.save({ formatType: FormatType.ON }) }); commandRegistry.registerCommand(CommonCommands.SAVE_AS, { isEnabled: () => this.saveResourceService.canSaveAs(this.shell.currentWidget), execute: () => { const { currentWidget } = this.shell; // No clue what could have happened between `isEnabled` and `execute` // when fetching currentWidget, so better to double-check: if (this.saveResourceService.canSaveAs(currentWidget)) { this.saveResourceService.saveAs(currentWidget); } else { this.messageService.error(nls.localize('theia/workspace/failSaveAs', 'Cannot run "{0}" for the current widget.', CommonCommands.SAVE_AS.label!)); } }, }); commandRegistry.registerCommand(CommonCommands.SAVE_WITHOUT_FORMATTING, { execute: () => this.save({ formatType: FormatType.OFF }) }); commandRegistry.registerCommand(CommonCommands.SAVE_ALL, { execute: () => this.shell.saveAll({ formatType: FormatType.DIRTY }) }); commandRegistry.registerCommand(CommonCommands.ABOUT_COMMAND, { execute: () => this.openAbout() }); commandRegistry.registerCommand(CommonCommands.OPEN_VIEW, { execute: () => this.quickInputService?.open(QuickViewService.PREFIX) }); commandRegistry.registerCommand(CommonCommands.SELECT_COLOR_THEME, { execute: () => this.selectColorTheme() }); commandRegistry.registerCommand(CommonCommands.SELECT_ICON_THEME, { execute: () => this.selectIconTheme() }); commandRegistry.registerCommand(CommonCommands.PIN_TAB, new CurrentWidgetCommandAdapter(this.shell, { isEnabled: title => Boolean(title && !isPinned(title)), execute: title => this.togglePinned(title), })); commandRegistry.registerCommand(CommonCommands.UNPIN_TAB, new CurrentWidgetCommandAdapter(this.shell, { isEnabled: title => Boolean(title && isPinned(title)), execute: title => this.togglePinned(title), })); commandRegistry.registerCommand(CommonCommands.CONFIGURE_DISPLAY_LANGUAGE, { execute: () => this.configureDisplayLanguage() }); commandRegistry.registerCommand(CommonCommands.TOGGLE_BREADCRUMBS, { execute: () => this.toggleBreadcrumbs(), isToggled: () => this.isBreadcrumbsEnabled(), }); commandRegistry.registerCommand(CommonCommands.NEW_UNTITLED_TEXT_FILE, { execute: async () => { const untitledUri = this.untitledResourceResolver.createUntitledURI('', await this.workingDirProvider.getUserWorkingDir()); this.untitledResourceResolver.resolve(untitledUri); const editor = await open(this.openerService, untitledUri); // Wait for all of the listeners of the `onDidOpen` event to be notified await timeout(50); // Afterwards, we can return from the command with the newly created editor // If we don't wait, we return from the command before the plugin API has been notified of the new editor return editor; } }); commandRegistry.registerCommand(CommonCommands.PICK_NEW_FILE, { execute: async () => this.showNewFilePicker() }); for (const [index, ordinal] of this.getOrdinalNumbers().entries()) { commandRegistry.registerCommand({ id: `workbench.action.focus${ordinal}EditorGroup`, label: index === 0 ? nls.localizeByDefault('Focus First Editor Group') : '', category: nls.localize(CommonCommands.VIEW_CATEGORY_KEY, CommonCommands.VIEW_CATEGORY) }, { isEnabled: () => this.shell.mainAreaTabBars.length > index, execute: () => { const widget = this.shell.mainAreaTabBars[index]?.currentTitle?.owner; if (widget) { this.shell.activateWidget(widget.id); } } }); } } protected getOrdinalNumbers(): readonly string[] { return ['First', 'Second', 'Third', 'Fourth', 'Fifth', 'Sixth', 'Seventh', 'Eighth', 'Ninth']; } protected isElectron(): boolean { return environment.electron.is(); } protected togglePinned(title?: Title<Widget>): void { if (title) { togglePinned(title); this.updatePinnedKey(); } } registerKeybindings(registry: KeybindingRegistry): void { if (supportCut) { registry.registerKeybinding({ command: CommonCommands.CUT.id, keybinding: 'ctrlcmd+x' }); } if (supportCopy) { registry.registerKeybinding({ command: CommonCommands.COPY.id, keybinding: 'ctrlcmd+c' }); } if (supportPaste) { registry.registerKeybinding({ command: CommonCommands.PASTE.id, keybinding: 'ctrlcmd+v' }); } registry.registerKeybinding({ command: CommonCommands.COPY_PATH.id, keybinding: isWindows ? 'shift+alt+c' : 'ctrlcmd+alt+c', when: '!editorFocus' }); registry.registerKeybindings( // Edition { command: CommonCommands.UNDO.id, keybinding: 'ctrlcmd+z' }, { command: CommonCommands.REDO.id, keybinding: isOSX ? 'ctrlcmd+shift+z' : 'ctrlcmd+y' }, { command: CommonCommands.SELECT_ALL.id, keybinding: 'ctrlcmd+a' }, { command: CommonCommands.FIND.id, keybinding: 'ctrlcmd+f' }, { command: CommonCommands.REPLACE.id, keybinding: 'ctrlcmd+alt+f' }, // Tabs { command: CommonCommands.NEXT_TAB.id, keybinding: 'ctrl+tab' }, { command: CommonCommands.NEXT_TAB.id, keybinding: 'ctrlcmd+alt+d' }, { command: CommonCommands.PREVIOUS_TAB.id, keybinding: 'ctrl+shift+tab' }, { command: CommonCommands.PREVIOUS_TAB.id, keybinding: 'ctrlcmd+alt+a' }, { command: CommonCommands.CLOSE_MAIN_TAB.id, keybinding: this.isElectron() ? (isWindows ? 'ctrl+f4' : 'ctrlcmd+w') : 'alt+w' }, { command: CommonCommands.CLOSE_OTHER_MAIN_TABS.id, keybinding: 'ctrlcmd+alt+t' }, { command: CommonCommands.CLOSE_ALL_MAIN_TABS.id, keybinding: this.isElectron() ? 'ctrlCmd+k ctrlCmd+w' : 'alt+shift+w' }, // Panels { command: CommonCommands.COLLAPSE_PANEL.id, keybinding: 'alt+c' }, { command: CommonCommands.TOGGLE_BOTTOM_PANEL.id, keybinding: 'ctrlcmd+j', }, { command: CommonCommands.COLLAPSE_ALL_PANELS.id, keybinding: 'alt+shift+c', }, { command: CommonCommands.TOGGLE_MAXIMIZED.id, keybinding: 'alt+m', }, // Saving { command: CommonCommands.SAVE.id, keybinding: 'ctrlcmd+s' }, { command: CommonCommands.SAVE_WITHOUT_FORMATTING.id, keybinding: 'ctrlcmd+k s' }, { command: CommonCommands.SAVE_ALL.id, keybinding: 'ctrlcmd+alt+s' }, // Theming { command: CommonCommands.SELECT_COLOR_THEME.id, keybinding: 'ctrlcmd+k ctrlcmd+t' }, { command: CommonCommands.PIN_TAB.id, keybinding: 'ctrlcmd+k shift+enter', when: '!activeEditorIsPinned' }, { command: CommonCommands.UNPIN_TAB.id, keybinding: 'ctrlcmd+k shift+enter', when: 'activeEditorIsPinned' }, { command: CommonCommands.NEW_UNTITLED_TEXT_FILE.id, keybinding: this.isElectron() ? 'ctrlcmd+n' : 'alt+n', }, { command: CommonCommands.PICK_NEW_FILE.id, keybinding: 'ctrlcmd+alt+n' } ); for (const [index, ordinal] of this.getOrdinalNumbers().entries()) { registry.registerKeybinding({ command: `workbench.action.focus${ordinal}EditorGroup`, keybinding: `ctrlcmd+${(index + 1) % 10}`, }); } } protected async save(options?: SaveOptions): Promise<void> { const widget = this.shell.currentWidget; this.saveResourceService.save(widget, options); } protected async openAbout(): Promise<void> { this.aboutDialog.open(false); } protected shouldPreventClose = false; /** * registers event listener which make sure that * window doesn't get closed if CMD/CTRL W is pressed. * Too many users have that in their muscle memory. * Chrome doesn't let us rebind or prevent default the keybinding, so this * at least doesn't close the window immediately. */ protected registerCtrlWHandling(): void { function isCtrlCmd(event: KeyboardEvent): boolean { return (isOSX && event.metaKey) || (!isOSX && event.ctrlKey); } window.document.addEventListener('keydown', event => { this.shouldPreventClose = isCtrlCmd(event) && event.code === 'KeyW'; }); window.document.addEventListener('keyup', () => { this.shouldPreventClose = false; }); } onWillStop(): OnWillStopAction | undefined { if (this.shouldPreventClose || this.shell.canSaveAll()) { return { reason: 'Dirty editors present', action: async () => { const captionsToSave = this.unsavedTabsCaptions(); const untitledCaptionsToSave = this.unsavedUntitledTabsCaptions(); const shouldExit = await this.confirmExitWithOrWithoutSaving(captionsToSave, async () => { await this.saveDirty(untitledCaptionsToSave); await this.shell.saveAll(); }); const allSavedOrDoNotSave = ( shouldExit === true && untitledCaptionsToSave.length === 0 // Should save and cancel if any captions failed to save ) || shouldExit === false; // Do not save this.shouldPreventClose = !allSavedOrDoNotSave; return allSavedOrDoNotSave; } }; } } // Asks the user to confirm whether they want to exit with or without saving the changes private async confirmExitWithOrWithoutSaving(captionsToSave: string[], performSave: () => Promise<void>): Promise<boolean | undefined> { const div: HTMLElement = document.createElement('div'); div.innerText = nls.localizeByDefault("Your changes will be lost if you don't save them."); let result; if (captionsToSave.length > 0) { const span = document.createElement('span'); span.appendChild(document.createElement('br')); captionsToSave.forEach(cap => { const b = document.createElement('b'); b.innerText = cap; span.appendChild(b); span.appendChild(document.createElement('br')); }); span.appendChild(document.createElement('br')); div.appendChild(span); result = await new ConfirmSaveDialog({ title: nls.localizeByDefault('Do you want to save the changes to the following {0} files?', captionsToSave.length), msg: div, dontSave: nls.localizeByDefault("Don't Save"), save: nls.localizeByDefault('Save All'), cancel: Dialog.CANCEL }).open(); if (result) { await performSave(); } } else { // fallback if not passed with an empty caption-list. result = confirmExit(); } if (result !== undefined) { return result === true; } else { return undefined; }; } protected unsavedTabsCaptions(): string[] { return this.shell.widgets .filter(widget => this.saveResourceService.canSave(widget)) .map(widget => widget.title.label); } protected unsavedUntitledTabsCaptions(): Widget[] { return this.shell.widgets.filter(widget => NavigatableWidget.getUri(widget)?.scheme === UNTITLED_SCHEME && this.saveResourceService.canSaveAs(widget) ); } protected async configureDisplayLanguage(): Promise<void> { const languageInfo = await this.languageQuickPickService.pickDisplayLanguage(); if (languageInfo && !nls.isSelectedLocale(languageInfo.languageId) && await this.confirmRestart( languageInfo.localizedLanguageName ?? languageInfo.languageName ?? languageInfo.languageId )) { nls.setLocale(languageInfo.languageId); this.windowService.setSafeToShutDown(); this.windowService.reload(); } } /** * saves any dirty widget in toSave * side effect - will pop all widgets from toSave that was saved * @param toSave */ protected async saveDirty(toSave: Widget[]): Promise<void> { for (const widget of toSave) { const saveable = Saveable.get(widget); if (saveable?.dirty) { await this.saveResourceService.save(widget); if (!this.saveResourceService.canSave(widget)) { toSave.pop(); } } } } protected toggleBreadcrumbs(): void { const value: boolean | undefined = this.preferenceService.get('breadcrumbs.enabled'); this.preferenceService.set('breadcrumbs.enabled', !value, PreferenceScope.User); } protected isBreadcrumbsEnabled(): boolean { return !!this.preferenceService.get('breadcrumbs.enabled'); } protected async confirmRestart(languageName: string): Promise<boolean> { const appName = FrontendApplicationConfigProvider.get().applicationName; const shouldRestart = await new ConfirmDialog({ title: nls.localizeByDefault('Restart {0} to switch to {1}?', appName, languageName), msg: nls.localizeByDefault('To change the display language to {0}, {1} needs to restart.', languageName, appName), ok: nls.localizeByDefault('Restart'), cancel: Dialog.CANCEL, }).open(); return shouldRestart === true; } protected selectIconTheme(): void { let resetTo: IconTheme | undefined = this.iconThemes.getCurrent(); const setTheme = (id: string, persist: boolean) => { const theme = this.iconThemes.getDefinition(id); if (theme) { this.iconThemes.setCurrent(theme as IconTheme, persist); } }; const previewTheme = debounce(setTheme, 200); let items: Array<QuickPickItem & { id: string }> = []; for (const iconTheme of this.iconThemes.definitions) { items.push({ id: iconTheme.id, label: iconTheme.label, description: iconTheme.description, }); } items = items.sort((a, b) => { if (a.id === 'none') { return -1; } return a.label!.localeCompare(b.label!); }); this.quickInputService?.showQuickPick(items, { placeholder: nls.localizeByDefault('Select File Icon Theme'), activeItem: items.find(item => item.id === resetTo?.id), onDidChangeSelection: (_, selectedItems) => { resetTo = undefined; setT