From e58e858f9568ec57d25f7d79a9c0a7c82ffb9cf9 Mon Sep 17 00:00:00 2001 From: Vincent Fugnitto Date: Tue, 20 Aug 2019 11:39:48 -0400 Subject: [PATCH] Implement 'more' toolbar item for the explorer Fixes #5951 - implemented `More Actions...` toolbar item for the explorer which is used to host commands specific to the workspace root whenever there is a single-root workspace currently opened. The initial implementation adds only a subset of available commands which make sense for the workspace root. The following commands have been added: 1. new file 2. new folder 3. open in terminal 4. find in folder Signed-off-by: Vincent Fugnitto --- .../src/browser/navigator-contribution.ts | 47 +++++++- ...arch-in-workspace-frontend-contribution.ts | 45 +++++++- packages/terminal/package.json | 1 + .../browser/terminal-frontend-contribution.ts | 81 +++++++++++--- .../src/browser/workspace-commands.ts | 102 +++++++++++------- 5 files changed, 215 insertions(+), 61 deletions(-) diff --git a/packages/navigator/src/browser/navigator-contribution.ts b/packages/navigator/src/browser/navigator-contribution.ts index d4dcc631dc6d7..e68a5b15345a2 100644 --- a/packages/navigator/src/browser/navigator-contribution.ts +++ b/packages/navigator/src/browser/navigator-contribution.ts @@ -21,7 +21,7 @@ import { OpenerService, FrontendApplicationContribution, FrontendApplication, CompositeTreeNode } from '@theia/core/lib/browser'; import { FileDownloadCommands } from '@theia/filesystem/lib/browser/download/file-download-command-contribution'; -import { CommandRegistry, MenuModelRegistry, MenuPath, isOSX, Command, DisposableCollection } from '@theia/core/lib/common'; +import { CommandRegistry, MenuModelRegistry, MenuPath, isOSX, Command, DisposableCollection, Mutable } from '@theia/core/lib/common'; import { SHELL_TABBAR_CONTEXT_MENU } from '@theia/core/lib/browser'; import { WorkspaceCommands, WorkspaceService, WorkspacePreferences } from '@theia/workspace/lib/browser'; import { FILE_NAVIGATOR_ID, FileNavigatorWidget, EXPLORER_VIEW_CONTAINER_ID } from './navigator-widget'; @@ -30,7 +30,7 @@ import { NavigatorKeybindingContexts } from './navigator-keybinding-context'; import { FileNavigatorFilter } from './navigator-filter'; import { WorkspaceNode } from './navigator-tree'; import { NavigatorContextKeyService } from './navigator-context-key-service'; -import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { TabBarToolbarContribution, TabBarToolbarRegistry, TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { FileSystemCommands } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution'; import { NavigatorDiff, NavigatorDiffCommands } from './navigator-diff'; import { UriSelection } from '@theia/core/lib/common/selection'; @@ -61,6 +61,14 @@ export namespace FileNavigatorCommands { }; } +/** + * Navigator `More Actions...` toolbar item groups. + */ +export namespace NavigatorMoreToolbarGroups { + export const NEW_OPEN = '1_navigator_new_open'; + export const SEARCH = '2_navigator_search'; +} + export const NAVIGATOR_CONTEXT_MENU: MenuPath = ['navigator-context-menu']; /** @@ -95,6 +103,9 @@ export namespace NavigatorContextMenu { @injectable() export class FileNavigatorContribution extends AbstractViewContribution implements FrontendApplicationContribution, TabBarToolbarContribution { + @inject(CommandRegistry) + protected readonly commandRegistry: CommandRegistry; + @inject(NavigatorContextKeyService) protected readonly contextKeyService: NavigatorContextKeyService; @@ -340,6 +351,38 @@ export class FileNavigatorContribution extends AbstractViewContribution) => { + const commandId = item.command; + const id = 'navigator.tabbar.toolbar.' + commandId; + const command = this.commandRegistry.getCommand(commandId); + this.commandRegistry.registerCommand({ id, iconClass: command && command.iconClass }, { + execute: (w, ...args) => w instanceof FileNavigatorWidget + && this.commandRegistry.executeCommand(commandId, ...args), + isEnabled: (w, ...args) => w instanceof FileNavigatorWidget + && !!this.workspaceService.workspace && !this.workspaceService.isMultiRootWorkspaceOpened + && this.commandRegistry.isEnabled(commandId, ...args), + isVisible: (w, ...args) => w instanceof FileNavigatorWidget + && !!this.workspaceService.workspace && !this.workspaceService.isMultiRootWorkspaceOpened + && this.commandRegistry.isVisible(commandId, ...args), + }); + item.command = id; + toolbarRegistry.registerItem(item); + }; + navigatorRegisterItem({ + id: WorkspaceCommands.NEW_FILE_ROOT.id, + command: WorkspaceCommands.NEW_FILE_ROOT.id, + tooltip: 'New File', + group: NavigatorMoreToolbarGroups.NEW_OPEN, + }); + navigatorRegisterItem({ + id: WorkspaceCommands.NEW_FOLDER_ROOT.id, + command: WorkspaceCommands.NEW_FOLDER_ROOT.id, + tooltip: 'New Folder', + group: NavigatorMoreToolbarGroups.NEW_OPEN, + }); } /** diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts index 742c1b784ba69..55f66743991c0 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts +++ b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts @@ -17,17 +17,18 @@ import { AbstractViewContribution, KeybindingRegistry, LabelProvider, CommonMenus, FrontendApplication, FrontendApplicationContribution } from '@theia/core/lib/browser'; import { SearchInWorkspaceWidget } from './search-in-workspace-widget'; import { injectable, inject, postConstruct } from 'inversify'; -import { CommandRegistry, MenuModelRegistry, SelectionService, Command } from '@theia/core'; +import { CommandRegistry, MenuModelRegistry, SelectionService, Command, Mutable } from '@theia/core'; import { Widget } from '@theia/core/lib/browser/widgets'; -import { NavigatorContextMenu } from '@theia/navigator/lib/browser/navigator-contribution'; +import { NavigatorContextMenu, NavigatorMoreToolbarGroups } from '@theia/navigator/lib/browser/navigator-contribution'; import { UriCommandHandler, UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler'; import URI from '@theia/core/lib/common/uri'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { FileSystem } from '@theia/filesystem/lib/common'; import { SearchInWorkspaceContextKeyService } from './search-in-workspace-context-key-service'; -import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { TabBarToolbarContribution, TabBarToolbarRegistry, TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; import { Range } from 'vscode-languageserver-types'; +import { FileNavigatorWidget } from '@theia/navigator/lib/browser/navigator-widget'; export namespace SearchInWorkspaceCommands { const SEARCH_CATEGORY = 'Search'; @@ -45,6 +46,9 @@ export namespace SearchInWorkspaceCommands { category: SEARCH_CATEGORY, label: 'Find in Folder' }; + export const FIND_IN_FOLDER_ROOT: Command = { + id: 'search-in-workspace.in-folder.root', + }; export const REFRESH_RESULTS: Command = { id: 'search-in-workspace.refresh', category: SEARCH_CATEGORY, @@ -68,6 +72,7 @@ export namespace SearchInWorkspaceCommands { @injectable() export class SearchInWorkspaceFrontendContribution extends AbstractViewContribution implements FrontendApplicationContribution, TabBarToolbarContribution { + @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; @inject(SelectionService) protected readonly selectionService: SelectionService; @inject(LabelProvider) protected readonly labelProvider: LabelProvider; @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; @@ -110,7 +115,13 @@ export class SearchInWorkspaceFrontendContribution extends AbstractViewContribut widget.updateSearchTerm(this.getSearchTerm()); } }); - + commands.registerCommand(SearchInWorkspaceCommands.FIND_IN_FOLDER_ROOT, { + execute: async () => { + const widget = await this.openView({ activate: true }); + widget.clear(); + widget.updateSearchTerm(this.getSearchTerm()); + } + }); commands.registerCommand(SearchInWorkspaceCommands.FIND_IN_FOLDER, this.newMultiUriAwareCommandHandler({ execute: async uris => { const resources: string[] = []; @@ -222,6 +233,32 @@ export class SearchInWorkspaceFrontendContribution extends AbstractViewContribut priority: 2, onDidChange }); + // Register 'more actions' toolbar item (consists of multiple commands for the workspace root). + // Commands are available when there is a single-root workspace currently opened. + const navigatorRegisterItem = (item: Mutable) => { + const commandId = item.command; + const id = 'navigator.tabbar.toolbar.' + commandId; + const command = this.commandRegistry.getCommand(commandId); + this.commandRegistry.registerCommand({ id, iconClass: command && command.iconClass }, { + execute: (w, ...args) => w instanceof FileNavigatorWidget + && this.commandRegistry.executeCommand(commandId, ...args), + isEnabled: (w, ...args) => w instanceof FileNavigatorWidget + && !!this.workspaceService.workspace && !this.workspaceService.isMultiRootWorkspaceOpened + && !!this.commandRegistry.isEnabled(commandId, ...args), + isVisible: (w, ...args) => w instanceof FileNavigatorWidget + && !!this.workspaceService.workspace && !this.workspaceService.isMultiRootWorkspaceOpened + && this.commandRegistry.isVisible(commandId, ...args), + }); + item.command = id; + toolbarRegistry.registerItem(item); + }; + navigatorRegisterItem({ + id: SearchInWorkspaceCommands.FIND_IN_FOLDER_ROOT.id, + command: SearchInWorkspaceCommands.FIND_IN_FOLDER_ROOT.id, + tooltip: 'Find in Folder', + group: NavigatorMoreToolbarGroups.SEARCH, + }); + } protected newUriAwareCommandHandler(handler: UriCommandHandler): UriAwareCommandHandler { diff --git a/packages/terminal/package.json b/packages/terminal/package.json index c87c085997451..90fd6c4431936 100644 --- a/packages/terminal/package.json +++ b/packages/terminal/package.json @@ -6,6 +6,7 @@ "@theia/core": "^0.9.0", "@theia/editor": "^0.9.0", "@theia/filesystem": "^0.9.0", + "@theia/navigator": "^0.9.0", "@theia/process": "^0.9.0", "@theia/workspace": "^0.9.0", "xterm": "3.13.0" diff --git a/packages/terminal/src/browser/terminal-frontend-contribution.ts b/packages/terminal/src/browser/terminal-frontend-contribution.ts index 5b496b0d76c12..384dba62bdbfc 100644 --- a/packages/terminal/src/browser/terminal-frontend-contribution.ts +++ b/packages/terminal/src/browser/terminal-frontend-contribution.ts @@ -23,14 +23,14 @@ import { MenuModelRegistry, isOSX, SelectionService, - Emitter, Event + Emitter, Event, Mutable } from '@theia/core/lib/common'; import { QuickPickService } from '@theia/core/lib/common/quick-pick-service'; import { ApplicationShell, KeybindingContribution, KeyCode, Key, KeybindingRegistry, Widget, LabelProvider, WidgetOpenerOptions } from '@theia/core/lib/browser'; -import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { TabBarToolbarContribution, TabBarToolbarRegistry, TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { WidgetManager } from '@theia/core/lib/browser'; import { TERMINAL_WIDGET_FACTORY_ID, TerminalWidgetFactoryOptions } from './terminal-widget-impl'; import { TerminalKeybindingContexts } from './terminal-keybinding-contexts'; @@ -42,6 +42,8 @@ import URI from '@theia/core/lib/common/uri'; import { MAIN_MENU_BAR } from '@theia/core'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; +import { FileNavigatorWidget } from '@theia/navigator/lib/browser/navigator-widget'; +import { NavigatorMoreToolbarGroups, NAVIGATOR_CONTEXT_MENU } from '@theia/navigator/lib/browser/navigator-contribution'; export namespace TerminalMenus { export const TERMINAL = [...MAIN_MENU_BAR, '7_terminal']; @@ -49,7 +51,7 @@ export namespace TerminalMenus { export const TERMINAL_TASKS = [...TERMINAL, '2_terminal']; export const TERMINAL_TASKS_INFO = [...TERMINAL_TASKS, '3_terminal']; export const TERMINAL_TASKS_CONFIG = [...TERMINAL_TASKS, '4_terminal']; - export const TERMINAL_NAVIGATOR_CONTEXT_MENU = ['navigator-context-menu', 'navigation']; + export const TERMINAL_NAVIGATOR_CONTEXT_MENU = [...NAVIGATOR_CONTEXT_MENU, 'navigation']; } export namespace TerminalCommands { @@ -74,6 +76,9 @@ export namespace TerminalCommands { category: TERMINAL_CATEGORY, label: 'Open in Terminal' }; + export const TERMINAL_CONTEXT_ROOT: Command = { + id: 'terminal:context:root', + }; export const SPLIT: Command = { id: 'terminal:split', category: TERMINAL_CATEGORY, @@ -99,6 +104,9 @@ export class TerminalFrontendContribution implements TerminalService, CommandCon @inject(SelectionService) protected readonly selectionService: SelectionService ) { } + @inject(CommandRegistry) + protected readonly commandRegistry: CommandRegistry; + @inject(LabelProvider) protected readonly labelProvider: LabelProvider; @@ -180,22 +188,16 @@ export class TerminalFrontendContribution implements TerminalService, CommandCon }); commands.registerCommand(TerminalCommands.TERMINAL_CONTEXT, new UriAwareCommandHandler(this.selectionService, { - execute: async uri => { - // Determine folder path of URI - const stat = await this.fileSystem.getFileStat(uri.toString()); - if (!stat) { - return; + execute: async uri => this.openTerminalAtUri(uri) + })); + commands.registerCommand(TerminalCommands.TERMINAL_CONTEXT_ROOT, { + execute: async () => { + if (this.workspaceService.workspace) { + const uri = new URI(this.workspaceService.workspace.uri); + this.openTerminalAtUri(uri); } - - // Use folder if a file was selected - const cwd = (stat.isDirectory) ? uri.toString() : uri.parent.toString(); - - // Open terminal - const termWidget = await this.newTerminal({ cwd }); - termWidget.start(); - this.activateTerminal(termWidget); } - })); + }); } registerMenus(menus: MenuModelRegistry): void { @@ -222,6 +224,31 @@ export class TerminalFrontendContribution implements TerminalService, CommandCon text: '$(columns)', tooltip: TerminalCommands.SPLIT.label }); + // Register 'more actions' toolbar item (consists of multiple commands for the workspace root). + // Commands are available when there is a single-root workspace currently opened. + const navigatorRegisterItem = (item: Mutable) => { + const commandId = item.command; + const id = 'navigator.tabbar.toolbar.' + commandId; + const command = this.commandRegistry.getCommand(commandId); + this.commandRegistry.registerCommand({ id, iconClass: command && command.iconClass }, { + execute: (w, ...args) => w instanceof FileNavigatorWidget + && this.commandRegistry.executeCommand(commandId, ...args), + isEnabled: (w, ...args) => w instanceof FileNavigatorWidget + && !!this.workspaceService.workspace && !this.workspaceService.isMultiRootWorkspaceOpened + && this.commandRegistry.isEnabled(commandId, ...args), + isVisible: (w, ...args) => w instanceof FileNavigatorWidget + && !!this.workspaceService.workspace && !this.workspaceService.isMultiRootWorkspaceOpened + && this.commandRegistry.isVisible(commandId, ...args), + }); + item.command = id; + toolbar.registerItem(item); + }; + navigatorRegisterItem({ + id: TerminalCommands.TERMINAL_CONTEXT_ROOT.id, + command: TerminalCommands.TERMINAL_CONTEXT_ROOT.id, + tooltip: 'Open in Terminal', + group: NavigatorMoreToolbarGroups.NEW_OPEN, + }); } registerKeybindings(keybindings: KeybindingRegistry): void { @@ -356,6 +383,26 @@ export class TerminalFrontendContribution implements TerminalService, CommandCon } } + /** + * Open a terminal at the provided uri. + * @param uri {URI} the given uri. + */ + protected async openTerminalAtUri(uri: URI): Promise { + // Determine folder path of URI + const stat = await this.fileSystem.getFileStat(uri.toString()); + if (!stat) { + return; + } + + // Use folder if a file was selected + const cwd = (stat.isDirectory) ? uri.toString() : uri.parent.toString(); + + // Open terminal + const termWidget = await this.newTerminal({ cwd }); + termWidget.start(); + this.activateTerminal(termWidget); + } + protected async selectTerminalCwd(): Promise { const roots = this.workspaceService.tryGetRoots(); return this.quickPick.show(roots.map( diff --git a/packages/workspace/src/browser/workspace-commands.ts b/packages/workspace/src/browser/workspace-commands.ts index 6397b97807c62..3ec7c24deb73e 100644 --- a/packages/workspace/src/browser/workspace-commands.ts +++ b/packages/workspace/src/browser/workspace-commands.ts @@ -83,11 +83,17 @@ export namespace WorkspaceCommands { category: FILE_CATEGORY, label: 'New File' }; + export const NEW_FILE_ROOT: Command = { + id: 'file.newFile.root', + }; export const NEW_FOLDER: Command = { id: 'file.newFolder', category: FILE_CATEGORY, label: 'New Folder' }; + export const NEW_FOLDER_ROOT: Command = { + id: 'file.newFolder.root', + }; export const FILE_OPEN_WITH = (opener: OpenHandler): Command => ({ id: `file.openWith.${opener.id}` }); @@ -195,47 +201,17 @@ export class WorkspaceCommandContribution implements CommandContribution { } }); registry.registerCommand(WorkspaceCommands.NEW_FILE, this.newWorkspaceRootUriAwareCommandHandler({ - execute: uri => this.getDirectory(uri).then(parent => { - if (parent) { - const parentUri = new URI(parent.uri); - const { fileName, fileExtension } = this.getDefaultFileConfig(); - const vacantChildUri = FileSystemUtils.generateUniqueResourceURI(parentUri, parent, fileName, fileExtension); - - const dialog = new SingleTextInputDialog({ - title: 'New File', - initialValue: vacantChildUri.path.base, - validate: name => this.validateFileName(name, parent, true) - }); - - dialog.open().then(name => { - if (name) { - const fileUri = parentUri.resolve(name); - this.fileSystem.createFile(fileUri.toString()).then(() => { - open(this.openerService, fileUri); - }); - } - }); - } - }) + execute: uri => this.getDirectory(uri).then(parent => this.createNewFile(parent)) })); + registry.registerCommand(WorkspaceCommands.NEW_FILE_ROOT, { + execute: () => this.createNewFile(this.workspaceService.workspace), + }); registry.registerCommand(WorkspaceCommands.NEW_FOLDER, this.newWorkspaceRootUriAwareCommandHandler({ - execute: uri => this.getDirectory(uri).then(parent => { - if (parent) { - const parentUri = new URI(parent.uri); - const vacantChildUri = FileSystemUtils.generateUniqueResourceURI(parentUri, parent, 'Untitled'); - const dialog = new SingleTextInputDialog({ - title: 'New Folder', - initialValue: vacantChildUri.path.base, - validate: name => this.validateFileName(name, parent, true) - }); - dialog.open().then(name => { - if (name) { - this.fileSystem.createFolder(parentUri.resolve(name).toString()); - } - }); - } - }) + execute: uri => this.getDirectory(uri).then(parent => this.createNewFolder(parent)) })); + registry.registerCommand(WorkspaceCommands.NEW_FOLDER_ROOT, { + execute: () => this.createNewFolder(this.workspaceService.workspace), + }); registry.registerCommand(WorkspaceCommands.FILE_RENAME, this.newMultiUriAwareCommandHandler({ isEnabled: uris => uris.some(uri => !this.isWorkspaceRoot(uri)) && uris.length === 1, isVisible: uris => uris.some(uri => !this.isWorkspaceRoot(uri)) && uris.length === 1, @@ -323,6 +299,56 @@ export class WorkspaceCommandContribution implements CommandContribution { return new WorkspaceRootUriAwareCommandHandler(this.workspaceService, this.selectionService, handler); } + /** + * Create a new file given a file stat with the default name and extension params. + * @param stat {FileStat} the given file stat. + */ + protected createNewFile(stat: FileStat | undefined): void { + if (!stat) { + return; + } + const parentUri = new URI(stat.uri); + const { fileName, fileExtension } = this.getDefaultFileConfig(); + const vacantChildUri = FileSystemUtils.generateUniqueResourceURI(parentUri, stat, fileName, fileExtension); + + const dialog = new SingleTextInputDialog({ + title: 'New File', + initialValue: vacantChildUri.path.base, + validate: name => this.validateFileName(name, stat, true) + }); + + dialog.open().then(name => { + if (name) { + const fileUri = parentUri.resolve(name); + this.fileSystem.createFile(fileUri.toString()).then(() => { + open(this.openerService, fileUri); + }); + } + }); + } + + /** + * Create a new folder given a file stat. + * @param stat {FileStat} the given file stat. + */ + protected createNewFolder(stat: FileStat | undefined): void { + if (!stat) { + return; + } + const parentUri = new URI(stat.uri); + const vacantChildUri = FileSystemUtils.generateUniqueResourceURI(parentUri, stat, 'Untitled'); + const dialog = new SingleTextInputDialog({ + title: 'New Folder', + initialValue: vacantChildUri.path.base, + validate: name => this.validateFileName(name, stat, true) + }); + dialog.open().then(name => { + if (name) { + this.fileSystem.createFolder(parentUri.resolve(name).toString()); + } + }); + } + /** * Returns an error message if the file name is invalid. Otherwise, an empty string. *