diff --git a/packages/core/src/browser/shell/application-shell.ts b/packages/core/src/browser/shell/application-shell.ts index eb5b08dc63781..39f4455bd9e83 100644 --- a/packages/core/src/browser/shell/application-shell.ts +++ b/packages/core/src/browser/shell/application-shell.ts @@ -1400,6 +1400,29 @@ export class ApplicationShell extends Widget { } } + saveTabs(tabBarOrArea: TabBar | ApplicationShell.Area, + filter?: (title: Title, index: number) => boolean): void { + if (tabBarOrArea === 'main') { + this.mainAreaTabBars.forEach(tb => this.saveTabs(tb, filter)); + } else if (tabBarOrArea === 'bottom') { + this.bottomAreaTabBars.forEach(tb => this.saveTabs(tb, filter)); + } else if (typeof tabBarOrArea === 'string') { + const tabBar = this.getTabBarFor(tabBarOrArea); + if (tabBar) { + this.saveTabs(tabBar, filter); + } + } else if (tabBarOrArea) { + const titles = toArray(tabBarOrArea.titles); + for (let i = 0; i < titles.length; i++) { + if (filter === undefined || filter(titles[i], i)) { + const widget = titles[i].owner; + const saveable = Saveable.get(widget); + saveable?.save(); + } + } + } + } + async closeWidget(id: string, options?: ApplicationShell.CloseOptions): Promise { // TODO handle save for composite widgets, i.e. the preference widget has 2 editors const stack = this.toTrackedStack(id); @@ -1811,6 +1834,12 @@ export namespace ApplicationShell { return area === 'left' || area === 'right' || area === 'bottom'; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + export function isValidArea(area?: any): area is ApplicationShell.Area { + const areas = ['main', 'top', 'left', 'right', 'bottom']; + return (area !== undefined && typeof area === 'string' && areas.includes(area)); + } + /** * General options for the application shell. These are passed on construction and can be modified * through dependency injection (`ApplicationShellOptions` symbol). diff --git a/packages/navigator/src/browser/navigator-contribution.ts b/packages/navigator/src/browser/navigator-contribution.ts index feb2e9528f73c..c8f4e86f3d15f 100644 --- a/packages/navigator/src/browser/navigator-contribution.ts +++ b/packages/navigator/src/browser/navigator-contribution.ts @@ -30,7 +30,9 @@ import { SHELL_TABBAR_CONTEXT_MENU, Widget, NavigatableWidget, - Saveable, + ApplicationShell, + TabBar, + Title } from '@theia/core/lib/browser'; import { FileDownloadCommands } from '@theia/filesystem/lib/browser/download/file-download-command-contribution'; import { @@ -377,22 +379,20 @@ export class FileNavigatorContribution extends AbstractViewContribution this.withOpenEditorsWidget(widget, () => !!this.editorWidgets.length), isVisible: widget => this.withOpenEditorsWidget(widget, () => !!this.editorWidgets.length) }); - registry.registerCommand(OpenEditorsCommands.CLOSE_ALL_IN_GROUP, { - execute: async (id): Promise => { - const openEditorsWidget = await this.widgetManager.getOrCreateWidget(OpenEditorsWidget.ID); - const widgets = openEditorsWidget.getEditorWidgetsByGroup(id); - widgets?.forEach(widget => widget.close()); + + const filterEditorWidgets = (title: Title) => { + const { owner } = title; + return NavigatableWidget.is(owner); + }; + registry.registerCommand(OpenEditorsCommands.CLOSE_ALL_EDITORS_IN_GROUP_FROM_ICON, { + execute: (tabBarOrArea: ApplicationShell.Area | TabBar): void => { + this.shell.closeTabs(tabBarOrArea, filterEditorWidgets); }, isVisible: () => false }); - registry.registerCommand(OpenEditorsCommands.SAVE_ALL_IN_GROUP, { - execute: async (id): Promise => { - const openEditorsWidget = await this.widgetManager.getOrCreateWidget(OpenEditorsWidget.ID); - const widgets = openEditorsWidget.getEditorWidgetsByGroup(id); - widgets?.forEach(widget => { - const saveable = Saveable.get(widget); - saveable?.save(); - }); + registry.registerCommand(OpenEditorsCommands.SAVE_ALL_IN_GROUP_FROM_ICON, { + execute: (tabBarOrArea: ApplicationShell.Area | TabBar) => { + this.shell.saveTabs(tabBarOrArea, filterEditorWidgets); }, isVisible: () => false }); diff --git a/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-commands.ts b/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-commands.ts index 7f4e43c9b1c42..504cceca2fd44 100644 --- a/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-commands.ts +++ b/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-commands.ts @@ -18,30 +18,30 @@ import { Command } from '@theia/core/lib/common'; export namespace OpenEditorsCommands { export const CLOSE_ALL_TABS_FROM_TOOLBAR: Command = { - id: 'navigator.close.all.editors', + id: 'navigator.close.all.editors.toolbar', category: 'File', label: 'Close All Editors', iconClass: 'codicon codicon-close-all' }; export const SAVE_ALL_TABS_FROM_TOOLBAR: Command = { - id: 'navigator.save.all.editors', + id: 'navigator.save.all.editors.toolbar', category: 'File', label: 'Save All Editors', iconClass: 'codicon codicon-save-all' }; - export const SAVE_ALL_IN_GROUP: Command = { - id: 'navigator.save.all.in.ground', - category: 'File', - label: 'Save All in Group', - iconClass: 'codicon codicon-save-all' + export const CLOSE_ALL_EDITORS_IN_GROUP_FROM_ICON: Command = { + id: 'navigator.close.all.in.area.icon', + category: 'View', + label: 'Close Group', + iconClass: 'codicon codicon-close-all' }; - export const CLOSE_ALL_IN_GROUP: Command = { - id: 'navigator.close.all.in.group', + export const SAVE_ALL_IN_GROUP_FROM_ICON: Command = { + id: 'navigator.save.all.in.area.icon', category: 'File', - label: 'Close Group', - iconClass: 'codicon codicon-close-all' + label: 'Save All in Group', + iconClass: 'codicon codicon-save-all' }; } diff --git a/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-tree-model.ts b/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-tree-model.ts index 5b5591021dea9..ab6663ed94e47 100644 --- a/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-tree-model.ts +++ b/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-tree-model.ts @@ -25,7 +25,8 @@ import { SelectableTreeNode, TreeNode, Widget, - ExpandableTreeNode + ExpandableTreeNode, + TabBar } from '@theia/core/lib/browser'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import debounce = require('@theia/core/shared/lodash.debounce'); @@ -51,7 +52,7 @@ export class OpenEditorsModel extends FileTreeModel { protected toDisposeOnPreviewWidgetReplaced = new DisposableCollection(); // Returns the collection of editors belonging to a tabbar group in the main area - protected _editorWidgetsByGroup = new Map(); + protected _editorWidgetsByGroup = new Map }>(); // Returns the collection of editors belonging to an area grouping (main, left, right bottom) protected _editorWidgetsByArea = new Map(); @@ -65,12 +66,8 @@ export class OpenEditorsModel extends FileTreeModel { return editorWidgets; } - getEditorWidgetsByGroup(id: ApplicationShell.Area | number): NavigatableWidget[] | undefined { - const idAsNum = Number(id); - if (!isNaN(idAsNum)) { - return this._editorWidgetsByGroup.get(idAsNum); - } - return this._editorWidgetsByArea.get(id as ApplicationShell.Area); + getTabBarForGroup(id: number): TabBar | undefined { + return this._editorWidgetsByGroup.get(id)?.tabbar; } @postConstruct() @@ -163,7 +160,7 @@ export class OpenEditorsModel extends FileTreeModel { widgets.push(owner); } }); - this._editorWidgetsByGroup.set(groupNumber, widgets); + this._editorWidgetsByGroup.set(groupNumber, { widgets, tabbar }); }); } return widgetGroupMap; diff --git a/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-widget.tsx b/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-widget.tsx index e4c26e75b0296..1e4e957ff06b1 100644 --- a/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-widget.tsx +++ b/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-widget.tsx @@ -23,12 +23,14 @@ import { NavigatableWidget, NodeProps, Saveable, + TabBar, TreeDecoratorService, TreeModel, TreeNode, TreeProps, TreeWidget, TREE_NODE_CONTENT_CLASS, + Widget, } from '@theia/core/lib/browser'; import { OpenEditorNode, OpenEditorsModel } from './navigator-open-editors-tree-model'; import { createFileTreeContainer, FileTreeModel, FileTreeWidget } from '@theia/filesystem/lib/browser'; @@ -96,10 +98,6 @@ export class OpenEditorsWidget extends FileTreeWidget { return this.model.editorWidgets; } - getEditorWidgetsByGroup(id: ApplicationShell.Area | number): NavigatableWidget[] | undefined { - return this.model.getEditorWidgetsByGroup(id); - } - // eslint-disable-next-line no-null/no-null protected activeTreeNodePrefixElement: string | undefined | null; @@ -146,18 +144,18 @@ export class OpenEditorsWidget extends FileTreeWidget { return ( @@ -170,9 +168,27 @@ export class OpenEditorsWidget extends FileTreeWidget { const groupName = e.currentTarget.getAttribute('data-id'); const command = e.currentTarget.id; if (groupName && command) { - const group = groupName.split(':').pop(); - return this.commandService.executeCommand(command, group); + const groupFromTarget: string | number | undefined = groupName.split(':').pop(); + const areaOrTabBar = this.sanitizeInputFromClickHandler(groupFromTarget); + if (areaOrTabBar) { + return this.commandService.executeCommand(command, areaOrTabBar); + } + } + } + + protected sanitizeInputFromClickHandler(groupFromTarget?: string): ApplicationShell.Area | TabBar | undefined { + let areaOrTabBar: ApplicationShell.Area | TabBar | undefined; + if (groupFromTarget) { + if (ApplicationShell.isValidArea(groupFromTarget)) { + areaOrTabBar = groupFromTarget; + } else { + const groupAsNum = parseInt(groupFromTarget); + if (!isNaN(groupAsNum)) { + areaOrTabBar = this.model.getTabBarForGroup(groupAsNum); + } + } } + return areaOrTabBar; } protected renderPrefixIcon(node: OpenEditorNode): React.ReactNode { diff --git a/packages/navigator/src/browser/open-editors-widget/open-editors.css b/packages/navigator/src/browser/open-editors-widget/open-editors.css index fc8631e521d8c..ad93d8e260d73 100644 --- a/packages/navigator/src/browser/open-editors-widget/open-editors.css +++ b/packages/navigator/src/browser/open-editors-widget/open-editors.css @@ -23,50 +23,50 @@ font-size: var(--theia-ui-font-size0); } -.open-editors-node-row .open-editors-prefix-icon-container { +.theia-open-editors-widget .open-editors-node-row .open-editors-prefix-icon-container { min-width: var(--theia-open-editors-icon-width); } -.open-editors-node-row .open-editors-prefix-icon.dirty, -.open-editors-node-row.dirty:hover .open-editors-prefix-icon.dirty { +.theia-open-editors-widget .open-editors-node-row .open-editors-prefix-icon.dirty, +.theia-open-editors-widget .open-editors-node-row.dirty:hover .open-editors-prefix-icon.dirty { display: none; } -.open-editors-node-row.dirty .open-editors-prefix-icon.dirty { +.theia-open-editors-widget .open-editors-node-row.dirty .open-editors-prefix-icon.dirty { display: block; } -.open-editors-node-row .open-editors-prefix-icon.close { +.theia-open-editors-widget .open-editors-node-row .open-editors-prefix-icon.close { display: none; } -.open-editors-node-row:hover .open-editors-prefix-icon.close { +.theia-open-editors-widget .open-editors-node-row:hover .open-editors-prefix-icon.close { display: block; } -.open-editors-node-row.group-node, -.open-editors-node-row.area-node { +.theia-open-editors-widget .open-editors-node-row.group-node, +.theia-open-editors-widget .open-editors-node-row.area-node { font-weight: 700; text-transform: uppercase; font-size: var(--theia-ui-font-size0); } -.open-editors-node-row.area-node { +.theia-open-editors-widget .open-editors-node-row.area-node { font-style: italic; } -.open-editors-inline-actions-container { +.theia-open-editors-widget .open-editors-inline-actions-container { display: flex; justify-content: flex-end; margin-left: 3px; min-height: 16px; } -.open-editors-inline-action a { +.theia-open-editors-widget .open-editors-inline-action a { color: var(--theia-icon-foreground); } -.open-editors-inline-action { +.theia-open-editors-widget .open-editors-inline-action { padding: 0px 3px; font-size: var(--theia-ui-font-size1); margin: 0 2px; @@ -75,10 +75,10 @@ align-items: center; } -.open-editors-node-row .open-editors-inline-actions-container { +.theia-open-editors-widget .open-editors-node-row .open-editors-inline-actions-container { visibility: hidden; } -.open-editors-node-row:hover .open-editors-inline-actions-container { +.theia-open-editors-widget .open-editors-node-row:hover .open-editors-inline-actions-container { visibility: visible; } diff --git a/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts b/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts index 58bfdacc39629..0828b39b10db9 100755 --- a/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts +++ b/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts @@ -22,7 +22,10 @@ import { open, OpenerService, QuickInputService, - Saveable + Saveable, + TabBar, + Title, + Widget } from '@theia/core/lib/browser'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; import { ApplicationShellMouseTracker } from '@theia/core/lib/browser/shell/application-shell-mouse-tracker'; @@ -287,25 +290,41 @@ export class PluginVscodeCommandsContribution implements CommandContribution { } } }); - commands.registerCommand({ id: 'workbench.action.closeEditorsInGroup' }, { - execute: (uri?: monaco.Uri) => { - let editor = this.editorManager.currentEditor || this.shell.currentWidget; - if (uri) { - const uriString = uri.toString(); - editor = this.editorManager.all.find(e => { - const resourceUri = e.getResourceUri(); - return (resourceUri && resourceUri.toString()) === uriString; - }); - } - if (editor) { - const tabBar = this.shell.getTabBarFor(editor); - if (tabBar) { - this.shell.closeTabs(tabBar, - ({ owner }) => this.codeEditorWidgetUtil.is(owner) - ); - } + + const performActionOnGroup = ( + cb: ( + tabBarOrArea: TabBar | ApplicationShell.Area, + filter?: ((title: Title, index: number) => boolean) | undefined + ) => void, + uri?: monaco.Uri + ): void => { + let editor = this.editorManager.currentEditor || this.shell.currentWidget; + if (uri) { + const uriString = uri.toString(); + editor = this.editorManager.all.find(e => { + const resourceUri = e.getResourceUri(); + return (resourceUri && resourceUri.toString()) === uriString; + }); + } + if (editor) { + const tabBar = this.shell.getTabBarFor(editor); + if (tabBar) { + cb(tabBar, ({ owner }) => this.codeEditorWidgetUtil.is(owner)); } } + }; + + commands.registerCommand({ + id: 'workbench.action.closeEditorsInGroup', + label: 'Close All Editors in Group' + }, { + execute: (uri?: monaco.Uri) => performActionOnGroup(this.shell.closeTabs, uri) + }); + commands.registerCommand({ + id: 'workbench.files.saveAllInGroup', + label: 'Save All in Group' + }, { + execute: (uri?: monaco.Uri) => performActionOnGroup(this.shell.saveTabs, uri) }); commands.registerCommand({ id: 'workbench.action.closeEditorsInOtherGroups' }, { execute: () => {