From 9318bcc5183dbbc49cea8843859cbdeb59b94eaf Mon Sep 17 00:00:00 2001 From: Andrea Mah <31675041+andreamah@users.noreply.github.com> Date: Wed, 16 Nov 2022 14:37:44 -0800 Subject: [PATCH] Search Contributions Cleanup (#165538) Major cleanup on search contribs/actions, namely: - converting all `Action` and related registrations to `Action2` - Converting the `Actionbar` that appears on hover of a search result to `MenuWorkbenchToolBar` - Overall organization - Converting any `ICommandHandler` to functions if they don't need to be `ICommandHandler` - Adding titles to all search commands --- src/vs/platform/actions/common/actions.ts | 1 + .../search/browser/search.contribution.ts | 958 +----------------- .../contrib/search/browser/searchActions.ts | 778 -------------- .../search/browser/searchActionsBase.ts | 62 ++ .../search/browser/searchActionsCopy.ts | 256 +++++ .../search/browser/searchActionsFind.ts | 384 +++++++ .../search/browser/searchActionsNav.ts | 533 ++++++++++ .../browser/searchActionsRemoveReplace.ts | 408 ++++++++ .../search/browser/searchActionsSymbol.ts | 48 + .../search/browser/searchActionsTopBar.ts | 348 +++++++ .../search/browser/searchResultsView.ts | 113 ++- .../contrib/search/browser/searchView.ts | 7 +- .../contrib/search/browser/searchWidget.ts | 2 +- .../contrib/search/common/constants.ts | 5 + .../search/test/browser/searchActions.test.ts | 2 +- .../browser/searchEditor.contribution.ts | 2 +- .../browser/searchEditorActions.ts | 2 +- .../terminal/browser/terminalActions.ts | 4 +- 18 files changed, 2135 insertions(+), 1778 deletions(-) delete mode 100644 src/vs/workbench/contrib/search/browser/searchActions.ts create mode 100644 src/vs/workbench/contrib/search/browser/searchActionsBase.ts create mode 100644 src/vs/workbench/contrib/search/browser/searchActionsCopy.ts create mode 100644 src/vs/workbench/contrib/search/browser/searchActionsFind.ts create mode 100644 src/vs/workbench/contrib/search/browser/searchActionsNav.ts create mode 100644 src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts create mode 100644 src/vs/workbench/contrib/search/browser/searchActionsSymbol.ts create mode 100644 src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 53460c60090..a39a46fd6c5 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -103,6 +103,7 @@ export class MenuId { static readonly SCMSourceControl = new MenuId('SCMSourceControl'); static readonly SCMTitle = new MenuId('SCMTitle'); static readonly SearchContext = new MenuId('SearchContext'); + static readonly SearchActionMenu = new MenuId('SearchActionContext'); static readonly StatusBarWindowIndicatorMenu = new MenuId('StatusBarWindowIndicatorMenu'); static readonly StatusBarRemoteIndicatorMenu = new MenuId('StatusBarRemoteIndicatorMenu'); static readonly StickyScrollContext = new MenuId('StickyScrollContext'); diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index f0f6746a40f..b6f74172133 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -3,58 +3,44 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { onUnexpectedError } from 'vs/base/common/errors'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import * as platform from 'vs/base/common/platform'; -import { dirname } from 'vs/base/common/resources'; -import { assertIsDefined, assertType } from 'vs/base/common/types'; -import { URI } from 'vs/base/common/uri'; -import { ToggleCaseSensitiveKeybinding, TogglePreserveCaseKeybinding, ToggleRegexKeybinding, ToggleWholeWordKeybinding } from 'vs/editor/contrib/find/browser/findModel'; import { AbstractGotoLineQuickAccessProvider } from 'vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess'; import * as nls from 'vs/nls'; -import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; -import { ICommandAction } from 'vs/platform/action/common/action'; -import { CommandsRegistry, ICommandHandler, ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IFileService } from 'vs/platform/files/common/files'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IListService, WorkbenchListFocusContextKey, WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; import { Extensions as QuickAccessExtensions, IQuickAccessRegistry } from 'vs/platform/quickinput/common/quickAccess'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { defaultQuickAccessContextKeyValue } from 'vs/workbench/browser/quickaccess'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { Extensions as ViewExtensions, IViewContainersRegistry, IViewDescriptor, IViewDescriptorService, IViewsRegistry, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; +import { Extensions as ViewExtensions, IViewContainersRegistry, IViewDescriptor, IViewDescriptorService, IViewsRegistry, ViewContainerLocation } from 'vs/workbench/common/views'; import { GotoSymbolQuickAccessProvider } from 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess'; -import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/explorerViewlet'; -import { getMultiSelectedResources, IExplorerService } from 'vs/workbench/contrib/files/browser/files'; -import { ExplorerFolderContext, ExplorerRootContext, FilesExplorerFocusCondition, VIEWLET_ID as VIEWLET_ID_FILES } from 'vs/workbench/contrib/files/common/files'; import { AnythingQuickAccessProvider } from 'vs/workbench/contrib/search/browser/anythingQuickAccess'; import { registerContributions as replaceContributions } from 'vs/workbench/contrib/search/browser/replaceContributions'; -import { cancelSearch, clearHistoryCommand, clearSearchResults, CloseReplaceAction, collapseDeepestExpandedLevel, copyAllCommand, copyMatchCommand, copyPathCommand, expandAll, FindInFilesCommand, findOrReplaceInFiles, FocusNextInputAction, focusNextSearchResult, FocusPreviousInputAction, focusPreviousSearchResult, focusSearchListCommand, getMultiSelectedSearchResources, getSearchView, openSearchView, refreshSearch, RemoveAction, ReplaceAction, ReplaceAllAction, ReplaceAllInFolderAction, toggleCaseSensitiveCommand, togglePreserveCaseCommand, toggleRegexCommand, toggleWholeWordCommand } from 'vs/workbench/contrib/search/browser/searchActions'; -import { searchClearIcon, searchCollapseAllIcon, searchExpandAllIcon, searchRefreshIcon, searchStopIcon, searchShowAsTree, searchViewIcon, searchShowAsList } from 'vs/workbench/contrib/search/browser/searchIcons'; +import { searchViewIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; import { SearchView } from 'vs/workbench/contrib/search/browser/searchView'; import { registerContributions as searchWidgetContributions } from 'vs/workbench/contrib/search/browser/searchWidget'; import { SymbolsQuickAccessProvider } from 'vs/workbench/contrib/search/browser/symbolsQuickAccess'; -import * as Constants from 'vs/workbench/contrib/search/common/constants'; -import { resolveResourcesForSearchIncludes } from 'vs/workbench/services/search/common/queryBuilder'; -import { getWorkspaceSymbols, IWorkspaceSymbol, SearchStateKey, SearchUIState } from 'vs/workbench/contrib/search/common/search'; import { ISearchHistoryService, SearchHistoryService } from 'vs/workbench/contrib/search/common/searchHistoryService'; -import { FileMatch, FileMatchOrMatch, FolderMatch, FolderMatchWithResource, ISearchWorkbenchService, Match, RenderableMatch, SearchWorkbenchService } from 'vs/workbench/contrib/search/common/searchModel'; -import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants'; -import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ISearchWorkbenchService, SearchWorkbenchService } from 'vs/workbench/contrib/search/common/searchModel'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; -import { ISearchConfiguration, SearchSortOrder, SEARCH_EXCLUDE_CONFIG, VIEWLET_ID, ViewMode, VIEW_ID } from 'vs/workbench/services/search/common/search'; +import { SearchSortOrder, SEARCH_EXCLUDE_CONFIG, VIEWLET_ID, ViewMode, VIEW_ID } from 'vs/workbench/services/search/common/search'; import { Extensions, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { assertType } from 'vs/base/common/types'; +import { getWorkspaceSymbols, IWorkspaceSymbol } from 'vs/workbench/contrib/search/common/search'; +import * as Constants from 'vs/workbench/contrib/search/common/constants'; + +import 'vs/workbench/contrib/search/browser/searchActionsCopy'; +import 'vs/workbench/contrib/search/browser/searchActionsFind'; +import 'vs/workbench/contrib/search/browser/searchActionsNav'; +import 'vs/workbench/contrib/search/browser/searchActionsRemoveReplace'; +import 'vs/workbench/contrib/search/browser/searchActionsSymbol'; +import 'vs/workbench/contrib/search/browser/searchActionsTopBar'; registerSingleton(ISearchWorkbenchService, SearchWorkbenchService, InstantiationType.Delayed); registerSingleton(ISearchHistoryService, SearchHistoryService, InstantiationType.Delayed); @@ -62,710 +48,6 @@ registerSingleton(ISearchHistoryService, SearchHistoryService, InstantiationType replaceContributions(); searchWidgetContributions(); -const category = { value: nls.localize('search', "Search"), original: 'Search' }; - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.ToggleQueryDetailsActionId, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.or(Constants.SearchViewFocusedKey, SearchEditorConstants.InSearchEditor), - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyJ, - handler: accessor => { - const contextService = accessor.get(IContextKeyService).getContext(document.activeElement); - if (contextService.getValue(SearchEditorConstants.InSearchEditor.serialize())) { - (accessor.get(IEditorService).activeEditorPane as SearchEditor).toggleQueryDetails(); - } else if (contextService.getValue(Constants.SearchViewFocusedKey.serialize())) { - const searchView = getSearchView(accessor.get(IViewsService)); - assertIsDefined(searchView).toggleQueryDetails(); - } - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.FocusSearchFromResults, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FirstMatchFocusKey), - primary: KeyMod.CtrlCmd | KeyCode.UpArrow, - handler: (accessor, args: any) => { - const searchView = getSearchView(accessor.get(IViewsService)); - searchView?.focusPreviousInputBox(); - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.OpenMatch, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey), - primary: KeyCode.Enter, - mac: { - primary: KeyCode.Enter, - secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow] - }, - handler: (accessor) => { - const searchView = getSearchView(accessor.get(IViewsService)); - if (searchView) { - const tree: WorkbenchCompressibleObjectTree = searchView.getControl(); - const viewer = searchView.getControl(); - const focus = tree.getFocus()[0]; - - if (focus instanceof FolderMatch) { - viewer.toggleCollapsed(focus); - } else { - searchView.open(tree.getFocus()[0], false, false, true); - } - } - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.OpenMatchToSide, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey), - primary: KeyMod.CtrlCmd | KeyCode.Enter, - mac: { - primary: KeyMod.WinCtrl | KeyCode.Enter - }, - handler: (accessor, args: any) => { - const searchView = getSearchView(accessor.get(IViewsService)); - if (searchView) { - const tree: WorkbenchCompressibleObjectTree = searchView.getControl(); - searchView.open(tree.getFocus()[0], false, true, true); - } - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.RemoveActionId, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey), - primary: KeyCode.Delete, - mac: { - primary: KeyMod.CtrlCmd | KeyCode.Backspace, - }, - handler: (accessor, args: any) => { - const searchView = getSearchView(accessor.get(IViewsService)); - if (searchView) { - const tree: WorkbenchCompressibleObjectTree = searchView.getControl(); - accessor.get(IInstantiationService).createInstance(RemoveAction, tree, tree.getFocus()[0]!).run(); - } - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.ReplaceActionId, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.MatchFocusKey), - primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.Digit1, - handler: (accessor, args: any) => { - const searchView = getSearchView(accessor.get(IViewsService)); - if (searchView) { - const tree: WorkbenchCompressibleObjectTree = searchView.getControl(); - accessor.get(IInstantiationService).createInstance(ReplaceAction, tree, tree.getFocus()[0] as Match, searchView).run(); - } - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.ReplaceAllInFileActionId, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.FileFocusKey), - primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.Digit1, - secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter], - handler: (accessor, args: any) => { - const searchView = getSearchView(accessor.get(IViewsService)); - if (searchView) { - const tree: WorkbenchCompressibleObjectTree = searchView.getControl(); - accessor.get(IInstantiationService).createInstance(ReplaceAllAction, searchView, tree.getFocus()[0] as FileMatch).run(); - } - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.ReplaceAllInFolderActionId, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.FolderFocusKey), - primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.Digit1, - secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter], - handler: (accessor, args: any) => { - const searchView = getSearchView(accessor.get(IViewsService)); - if (searchView) { - const tree: WorkbenchCompressibleObjectTree = searchView.getControl(); - accessor.get(IInstantiationService).createInstance(ReplaceAllInFolderAction, tree, tree.getFocus()[0] as FolderMatch).run(); - } - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.CloseReplaceWidgetActionId, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceInputBoxFocusedKey), - primary: KeyCode.Escape, - handler: (accessor, args: any) => { - accessor.get(IInstantiationService).createInstance(CloseReplaceAction, Constants.CloseReplaceWidgetActionId, '').run(); - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: FocusNextInputAction.ID, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.or( - ContextKeyExpr.and(SearchEditorConstants.InSearchEditor, Constants.InputBoxFocusedKey), - ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.InputBoxFocusedKey)), - primary: KeyMod.CtrlCmd | KeyCode.DownArrow, - handler: (accessor, args: any) => { - accessor.get(IInstantiationService).createInstance(FocusNextInputAction, FocusNextInputAction.ID, '').run(); - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: FocusPreviousInputAction.ID, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.or( - ContextKeyExpr.and(SearchEditorConstants.InSearchEditor, Constants.InputBoxFocusedKey), - ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.InputBoxFocusedKey, Constants.SearchInputBoxFocusedKey.toNegated())), - primary: KeyMod.CtrlCmd | KeyCode.UpArrow, - handler: (accessor, args: any) => { - accessor.get(IInstantiationService).createInstance(FocusPreviousInputAction, FocusPreviousInputAction.ID, '').run(); - } -}); - -const restrictSearchToFolderFromSearch: ICommandHandler = async (accessor, folderMatch?: FolderMatchWithResource) => { - return searchWithFolderCommand(accessor, false, true, undefined, folderMatch); -}; -const excludeFolderFromSearch: ICommandHandler = async (accessor, folderMatch?: FolderMatchWithResource) => { - return searchWithFolderCommand(accessor, false, false, undefined, folderMatch); -}; -const RESTRICT_SEARCH_TO_FOLDER_ID = 'search.restrictSearchToFolder'; -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: RESTRICT_SEARCH_TO_FOLDER_ID, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ResourceFolderFocusKey), - primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KeyF, - handler: restrictSearchToFolderFromSearch -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.ExcludeFolderFromSearchId, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ResourceFolderFocusKey), - handler: excludeFolderFromSearch -}); - -MenuRegistry.appendMenuItem(MenuId.SearchContext, { - command: { - id: Constants.ReplaceActionId, - title: ReplaceAction.LABEL - }, - when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.MatchFocusKey), - group: 'search', - order: 1 -}); - -MenuRegistry.appendMenuItem(MenuId.SearchContext, { - command: { - id: Constants.ReplaceAllInFolderActionId, - title: ReplaceAllInFolderAction.LABEL - }, - when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FolderFocusKey), - group: 'search', - order: 1 -}); - -MenuRegistry.appendMenuItem(MenuId.SearchContext, { - command: { - id: Constants.ReplaceAllInFileActionId, - title: ReplaceAllAction.LABEL - }, - when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FileFocusKey), - group: 'search', - order: 1 -}); - -MenuRegistry.appendMenuItem(MenuId.SearchContext, { - command: { - id: Constants.RemoveActionId, - title: RemoveAction.LABEL - }, - when: Constants.FileMatchOrMatchFocusKey, - group: 'search', - order: 2 -}); - -MenuRegistry.appendMenuItem(MenuId.SearchContext, { - group: 'search', - order: 3, - command: { - id: RESTRICT_SEARCH_TO_FOLDER_ID, - title: nls.localize('restrictResultsToFolder', "Restrict Search to Folder") - }, - when: ContextKeyExpr.and(Constants.ResourceFolderFocusKey) -}); - -MenuRegistry.appendMenuItem(MenuId.SearchContext, { - group: 'search', - order: 4, - command: { - id: Constants.ExcludeFolderFromSearchId, - title: nls.localize('excludeFolderFromSearch', "Exclude Folder from Search") - }, - when: ContextKeyExpr.and(Constants.ResourceFolderFocusKey) -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.CopyMatchCommandId, - weight: KeybindingWeight.WorkbenchContrib, - when: Constants.FileMatchOrMatchFocusKey, - primary: KeyMod.CtrlCmd | KeyCode.KeyC, - handler: copyMatchCommand -}); - -MenuRegistry.appendMenuItem(MenuId.SearchContext, { - command: { - id: Constants.CopyMatchCommandId, - title: nls.localize('copyMatchLabel', "Copy") - }, - when: Constants.FileMatchOrMatchFocusKey, - group: 'search_2', - order: 1 -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.CopyPathCommandId, - weight: KeybindingWeight.WorkbenchContrib, - when: Constants.FileMatchOrFolderMatchWithResourceFocusKey, - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyC, - win: { - primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KeyC - }, - handler: copyPathCommand -}); - -MenuRegistry.appendMenuItem(MenuId.SearchContext, { - command: { - id: Constants.CopyPathCommandId, - title: nls.localize('copyPathLabel', "Copy Path") - }, - when: Constants.FileMatchOrFolderMatchWithResourceFocusKey, - group: 'search_2', - order: 2 -}); - -MenuRegistry.appendMenuItem(MenuId.SearchContext, { - command: { - id: Constants.CopyAllCommandId, - title: nls.localize('copyAllLabel', "Copy All") - }, - when: Constants.HasSearchResults, - group: 'search_2', - order: 3 -}); - -CommandsRegistry.registerCommand({ - id: Constants.CopyAllCommandId, - handler: copyAllCommand -}); - -CommandsRegistry.registerCommand({ - id: Constants.ClearSearchHistoryCommandId, - handler: clearHistoryCommand -}); - -CommandsRegistry.registerCommand({ - id: Constants.RevealInSideBarForSearchResults, - handler: (accessor, args: any) => { - const paneCompositeService = accessor.get(IPaneCompositePartService); - const explorerService = accessor.get(IExplorerService); - const contextService = accessor.get(IWorkspaceContextService); - - const searchView = getSearchView(accessor.get(IViewsService)); - if (!searchView) { - return; - } - - let fileMatch: FileMatch; - if (!(args instanceof FileMatch)) { - args = searchView.getControl().getFocus()[0]; - } - if (args instanceof FileMatch) { - fileMatch = args; - } else { - return; - } - - paneCompositeService.openPaneComposite(VIEWLET_ID_FILES, ViewContainerLocation.Sidebar, false).then((viewlet) => { - if (!viewlet) { - return; - } - - const explorerViewContainer = viewlet.getViewPaneContainer() as ExplorerViewPaneContainer; - const uri = fileMatch.resource; - if (uri && contextService.isInsideWorkspace(uri)) { - const explorerView = explorerViewContainer.getExplorerView(); - explorerView.setExpanded(true); - explorerService.select(uri, true).then(() => explorerView.focus(), onUnexpectedError); - } - }); - } -}); - -registerAction2(class CancelSearchAction extends Action2 { - constructor() { - super({ - id: Constants.CancelSearchActionId, - title: { - value: nls.localize('CancelSearchAction.label', "Cancel Search"), - original: 'Cancel Search' - }, - icon: searchStopIcon, - category, - f1: true, - precondition: SearchStateKey.isEqualTo(SearchUIState.Idle).negate(), - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, WorkbenchListFocusContextKey), - primary: KeyCode.Escape, - }, - menu: [{ - id: MenuId.ViewTitle, - group: 'navigation', - order: 0, - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), SearchStateKey.isEqualTo(SearchUIState.SlowSearch)), - }] - }); - } - run(accessor: ServicesAccessor, ...args: any[]) { - return cancelSearch(accessor); - } -}); - -registerAction2(class RefreshAction extends Action2 { - constructor() { - super({ - id: Constants.RefreshSearchResultsActionId, - title: { - value: nls.localize('RefreshAction.label', "Refresh"), - original: 'Refresh' - }, - icon: searchRefreshIcon, - precondition: Constants.ViewHasSearchPatternKey, - category, - f1: true, - menu: [{ - id: MenuId.ViewTitle, - group: 'navigation', - order: 0, - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), SearchStateKey.isEqualTo(SearchUIState.SlowSearch).negate()), - }] - }); - } - run(accessor: ServicesAccessor, ...args: any[]) { - return refreshSearch(accessor); - } -}); - -registerAction2(class CollapseDeepestExpandedLevelAction extends Action2 { - constructor() { - super({ - id: Constants.CollapseSearchResultsActionId, - title: { - value: nls.localize('CollapseDeepestExpandedLevelAction.label', "Collapse All"), - original: 'Collapse All' - }, - category, - icon: searchCollapseAllIcon, - f1: true, - precondition: ContextKeyExpr.and(Constants.HasSearchResults, Constants.ViewHasSomeCollapsibleKey), - menu: [{ - id: MenuId.ViewTitle, - group: 'navigation', - order: 3, - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), ContextKeyExpr.or(Constants.HasSearchResults.negate(), Constants.ViewHasSomeCollapsibleKey)), - }] - }); - } - run(accessor: ServicesAccessor, ...args: any[]) { - return collapseDeepestExpandedLevel(accessor); - } -}); - -registerAction2(class ExpandAllAction extends Action2 { - constructor() { - super({ - id: Constants.ExpandSearchResultsActionId, - title: { - value: nls.localize('ExpandAllAction.label', "Expand All"), - original: 'Expand All' - }, - category, - icon: searchExpandAllIcon, - f1: true, - precondition: ContextKeyExpr.and(Constants.HasSearchResults, Constants.ViewHasSomeCollapsibleKey.toNegated()), - menu: [{ - id: MenuId.ViewTitle, - group: 'navigation', - order: 3, - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), Constants.HasSearchResults, Constants.ViewHasSomeCollapsibleKey.toNegated()), - }] - }); - } - run(accessor: ServicesAccessor, ...args: any[]) { - return expandAll(accessor); - } -}); - -registerAction2(class ClearSearchResultsAction extends Action2 { - constructor() { - super({ - id: Constants.ClearSearchResultsActionId, - title: { - value: nls.localize('ClearSearchResultsAction.label', "Clear Search Results"), - original: 'Clear Search Results' - }, - category, - icon: searchClearIcon, - f1: true, - precondition: ContextKeyExpr.or(Constants.HasSearchResults, Constants.ViewHasSearchPatternKey, Constants.ViewHasReplacePatternKey, Constants.ViewHasFilePatternKey), - menu: [{ - id: MenuId.ViewTitle, - group: 'navigation', - order: 1, - when: ContextKeyExpr.equals('view', VIEW_ID), - }] - }); - } - run(accessor: ServicesAccessor, ...args: any[]) { - return clearSearchResults(accessor); - } -}); - -registerAction2(class ViewAsTreeAction extends Action2 { - constructor() { - super({ - id: Constants.ViewAsTreeActionId, - title: { - value: nls.localize('ViewAsTreeAction.label', "View as Tree"), - original: 'View as Tree' - }, - category, - icon: searchShowAsList, - f1: true, - precondition: ContextKeyExpr.and(Constants.HasSearchResults, Constants.InTreeViewKey.toNegated()), - menu: [{ - id: MenuId.ViewTitle, - group: 'navigation', - order: 2, - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), Constants.InTreeViewKey.toNegated()), - }] - }); - } - run(accessor: ServicesAccessor, ...args: any[]) { - const searchView = getSearchView(accessor.get(IViewsService)); - if (searchView) { - searchView.setTreeView(true); - } - } -}); - - -registerAction2(class ViewAsListAction extends Action2 { - constructor() { - super({ - id: Constants.ViewAsListActionId, - title: { - value: nls.localize('ViewAsListAction.label', "View as List"), - original: 'View as List' - }, - category, - icon: searchShowAsTree, - f1: true, - precondition: ContextKeyExpr.and(Constants.HasSearchResults, Constants.InTreeViewKey), - menu: [{ - id: MenuId.ViewTitle, - group: 'navigation', - order: 2, - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), Constants.InTreeViewKey), - }] - }); - } - run(accessor: ServicesAccessor, ...args: any[]) { - const searchView = getSearchView(accessor.get(IViewsService)); - if (searchView) { - searchView.setTreeView(false); - } - } -}); - -const RevealInSideBarForSearchResultsCommand: ICommandAction = { - id: Constants.RevealInSideBarForSearchResults, - title: nls.localize('revealInSideBar', "Reveal in Explorer View") -}; - -MenuRegistry.appendMenuItem(MenuId.SearchContext, { - command: RevealInSideBarForSearchResultsCommand, - when: ContextKeyExpr.and(Constants.FileFocusKey, Constants.HasSearchResults), - group: 'search_3', - order: 1 -}); - -const ClearSearchHistoryCommand: ICommandAction = { - id: Constants.ClearSearchHistoryCommandId, - title: { value: nls.localize('clearSearchHistoryLabel', "Clear Search History"), original: 'Clear Search History' }, - category -}; -MenuRegistry.addCommand(ClearSearchHistoryCommand); - -CommandsRegistry.registerCommand({ - id: Constants.FocusSearchListCommandID, - handler: focusSearchListCommand -}); - -const FocusSearchListCommand: ICommandAction = { - id: Constants.FocusSearchListCommandID, - title: { value: nls.localize('focusSearchListCommandLabel', "Focus List"), original: 'Focus List' }, - category -}; -MenuRegistry.addCommand(FocusSearchListCommand); - -const searchInFolderFromExplorer: ICommandHandler = async (accessor, resource?: URI) => { - return searchWithFolderCommand(accessor, true, true, resource); -}; - -const searchWithFolderCommand: ICommandHandler = async (accessor, isFromExplorer: boolean, isIncludes: boolean, resource?: URI, folderMatch?: FolderMatchWithResource) => { - const listService = accessor.get(IListService); - const fileService = accessor.get(IFileService); - const viewsService = accessor.get(IViewsService); - const contextService = accessor.get(IWorkspaceContextService); - const commandService = accessor.get(ICommandService); - const searchConfig = accessor.get(IConfigurationService).getValue().search; - const mode = searchConfig.mode; - - let resources: URI[]; - - if (isFromExplorer) { - resources = getMultiSelectedResources(resource, listService, accessor.get(IEditorService), accessor.get(IExplorerService)); - } else { - const searchView = getSearchView(accessor.get(IViewsService)); - if (!searchView) { - return; - } - resources = getMultiSelectedSearchResources(searchView.getControl(), folderMatch, searchConfig); - } - - const resolvedResources = fileService.resolveAll(resources.map(resource => ({ resource }))).then(results => { - const folders: URI[] = []; - results.forEach(result => { - if (result.success && result.stat) { - folders.push(result.stat.isDirectory ? result.stat.resource : dirname(result.stat.resource)); - } - }); - return resolveResourcesForSearchIncludes(folders, contextService); - }); - - if (mode === 'view') { - const searchView = await openSearchView(viewsService, true); - if (resources && resources.length && searchView) { - if (isIncludes) { - searchView.searchInFolders(await resolvedResources); - } else { - searchView.searchOutsideOfFolders(await resolvedResources); - } - } - return undefined; - } else { - if (isIncludes) { - return commandService.executeCommand(SearchEditorConstants.OpenEditorCommandId, { - filesToInclude: (await resolvedResources).join(', '), - showIncludesExcludes: true, - location: mode === 'newEditor' ? 'new' : 'reuse', - }); - } - else { - return commandService.executeCommand(SearchEditorConstants.OpenEditorCommandId, { - filesToExclude: (await resolvedResources).join(', '), - showIncludesExcludes: true, - location: mode === 'newEditor' ? 'new' : 'reuse', - }); - } - } -}; - -const FIND_IN_FOLDER_EXPLORER_ID = 'filesExplorer.findInFolder'; -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: FIND_IN_FOLDER_EXPLORER_ID, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerFolderContext), - primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KeyF, - handler: searchInFolderFromExplorer -}); - -const FIND_IN_WORKSPACE_ID = 'filesExplorer.findInWorkspace'; -CommandsRegistry.registerCommand({ - id: FIND_IN_WORKSPACE_ID, - handler: async (accessor) => { - const searchConfig = accessor.get(IConfigurationService).getValue().search; - const mode = searchConfig.mode; - - if (mode === 'view') { - const searchView = await openSearchView(accessor.get(IViewsService), true); - searchView?.searchInFolders(); - } - else { - return accessor.get(ICommandService).executeCommand(SearchEditorConstants.OpenEditorCommandId, { - location: mode === 'newEditor' ? 'new' : 'reuse', - filesToInclude: '', - }); - } - } -}); - -MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { - group: '4_search', - order: 10, - command: { - id: FIND_IN_FOLDER_EXPLORER_ID, - title: nls.localize('findInFolder', "Find in Folder...") - }, - when: ContextKeyExpr.and(ExplorerFolderContext) -}); - -MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { - group: '4_search', - order: 10, - command: { - id: FIND_IN_WORKSPACE_ID, - title: nls.localize('findInWorkspace', "Find in Workspace...") - }, - when: ContextKeyExpr.and(ExplorerRootContext, ExplorerFolderContext.toNegated()) -}); - -class ShowAllSymbolsAction extends Action2 { - - static readonly ID = 'workbench.action.showAllSymbols'; - static readonly LABEL = nls.localize('showTriggerActions', "Go to Symbol in Workspace..."); - static readonly ALL_SYMBOLS_PREFIX = '#'; - - constructor( - ) { - super({ - id: Constants.ShowAllSymbolsActionId, - title: { - value: nls.localize('showTriggerActions', "Go to Symbol in Workspace..."), - original: 'Go to Symbol in Workspace...' - }, - f1: true, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyCode.KeyT - } - }); - } - - override async run(accessor: ServicesAccessor): Promise { - accessor.get(IQuickInputService).quickAccess.show(ShowAllSymbolsAction.ALL_SYMBOLS_PREFIX); - } -} - -registerAction2(ShowAllSymbolsAction); - const SEARCH_MODE_CONFIG = 'search.mode'; const viewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ @@ -799,7 +81,6 @@ const viewDescriptor: IViewDescriptor = { // Register search default location to sidebar Registry.as(ViewExtensions.ViewsRegistry).registerViews([viewDescriptor], viewContainer); - // Migrate search location setting to new model class RegisterSearchViewContribution implements IWorkbenchContribution { constructor( @@ -816,200 +97,6 @@ class RegisterSearchViewContribution implements IWorkbenchContribution { } Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(RegisterSearchViewContribution, LifecyclePhase.Starting); -// Find in Files by default is the same as View: Show Search, but can be configured to open a search editor instead with the `search.mode` binding -KeybindingsRegistry.registerCommandAndKeybindingRule({ - description: { - description: nls.localize('findInFiles.description', "Open a workspace search"), - args: [ - { - name: nls.localize('findInFiles.args', "A set of options for the search"), - schema: { - type: 'object', - properties: { - query: { 'type': 'string' }, - replace: { 'type': 'string' }, - preserveCase: { 'type': 'boolean' }, - triggerSearch: { 'type': 'boolean' }, - filesToInclude: { 'type': 'string' }, - filesToExclude: { 'type': 'string' }, - isRegex: { 'type': 'boolean' }, - isCaseSensitive: { 'type': 'boolean' }, - matchWholeWord: { 'type': 'boolean' }, - useExcludeSettingsAndIgnoreFiles: { 'type': 'boolean' }, - onlyOpenEditors: { 'type': 'boolean' }, - } - } - }, - ] - }, - id: Constants.FindInFilesActionId, - weight: KeybindingWeight.WorkbenchContrib, - when: null, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyF, - handler: FindInFilesCommand -}); -MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: Constants.FindInFilesActionId, title: { value: nls.localize('findInFiles', "Find in Files"), original: 'Find in Files' }, category } }); -MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { - group: '4_find_global', - command: { - id: Constants.FindInFilesActionId, - title: nls.localize({ key: 'miFindInFiles', comment: ['&& denotes a mnemonic'] }, "Find &&in Files") - }, - order: 1 -}); - -registerAction2(class FocusNextSearchResultAction extends Action2 { - constructor() { - super({ - id: Constants.FocusNextSearchResultActionId, - title: { - value: nls.localize('FocusNextSearchResult.label', 'Focus Next Search Result'), - original: 'Focus Next Search Result' - }, - keybinding: [{ - primary: KeyCode.F4, - weight: KeybindingWeight.WorkbenchContrib, - }], - category: category.value, - - precondition: ContextKeyExpr.or(Constants.HasSearchResults, SearchEditorConstants.InSearchEditor), - }); - } - - override async run(accessor: ServicesAccessor): Promise { - return focusNextSearchResult(accessor); - } -}); - -registerAction2(class FocusPreviousSearchResultAction extends Action2 { - constructor() { - super({ - id: Constants.FocusPreviousSearchResultActionId, - title: { - value: nls.localize('FocusPreviousSearchResult.label', 'Search: Focus Previous Search Result'), - original: 'Search: Focus Previous Search Result' - }, - keybinding: [{ - primary: KeyMod.Shift | KeyCode.F4, - weight: KeybindingWeight.WorkbenchContrib, - }], - category: category.value, - - precondition: ContextKeyExpr.or(Constants.HasSearchResults, SearchEditorConstants.InSearchEditor), - }); - } - - override async run(accessor: ServicesAccessor): Promise { - return focusPreviousSearchResult(accessor); - } -}); - -registerAction2(class ReplaceInFilesAction extends Action2 { - constructor() { - super({ - id: Constants.ReplaceInFilesActionId, - title: { - value: nls.localize('replaceInFiles', 'Search: Replace in Files'), - original: 'Search: Replace in Files' - }, - keybinding: [{ - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyH, - weight: KeybindingWeight.WorkbenchContrib, - }], - category: category.value, - }); - } - - override async run(accessor: ServicesAccessor): Promise { - return findOrReplaceInFiles(accessor, true); - } -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { - group: '4_find_global', - command: { - id: Constants.ReplaceInFilesActionId, - title: nls.localize({ key: 'miReplaceInFiles', comment: ['&& denotes a mnemonic'] }, "Replace &&in Files") - }, - order: 2 -}); - -if (platform.isMacintosh) { - // Register this with a more restrictive `when` on mac to avoid conflict with "copy path" - KeybindingsRegistry.registerCommandAndKeybindingRule(Object.assign({ - id: Constants.ToggleCaseSensitiveCommandId, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewFocusedKey, Constants.FileMatchOrFolderMatchFocusKey.toNegated()), - handler: toggleCaseSensitiveCommand - }, ToggleCaseSensitiveKeybinding)); -} else { - KeybindingsRegistry.registerCommandAndKeybindingRule(Object.assign({ - id: Constants.ToggleCaseSensitiveCommandId, - weight: KeybindingWeight.WorkbenchContrib, - when: Constants.SearchViewFocusedKey, - handler: toggleCaseSensitiveCommand - }, ToggleCaseSensitiveKeybinding)); -} - -KeybindingsRegistry.registerCommandAndKeybindingRule(Object.assign({ - id: Constants.ToggleWholeWordCommandId, - weight: KeybindingWeight.WorkbenchContrib, - when: Constants.SearchViewFocusedKey, - handler: toggleWholeWordCommand -}, ToggleWholeWordKeybinding)); - -KeybindingsRegistry.registerCommandAndKeybindingRule(Object.assign({ - id: Constants.ToggleRegexCommandId, - weight: KeybindingWeight.WorkbenchContrib, - when: Constants.SearchViewFocusedKey, - handler: toggleRegexCommand -}, ToggleRegexKeybinding)); - -KeybindingsRegistry.registerCommandAndKeybindingRule(Object.assign({ - id: Constants.TogglePreserveCaseId, - weight: KeybindingWeight.WorkbenchContrib, - when: Constants.SearchViewFocusedKey, - handler: togglePreserveCaseCommand -}, TogglePreserveCaseKeybinding)); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.AddCursorsAtSearchResults, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey), - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL, - handler: (accessor, args: any) => { - const searchView = getSearchView(accessor.get(IViewsService)); - if (searchView) { - const tree: WorkbenchCompressibleObjectTree = searchView.getControl(); - searchView.openEditorWithMultiCursor(tree.getFocus()[0]); - } - } -}); -// --- Toggle Search On Type - -registerAction2(class ToggleSearchOnTypeAction extends Action2 { - private static readonly searchOnTypeKey = 'search.searchOnType'; - - constructor( - ) { - super({ - id: Constants.ToggleSearchOnTypeActionId, - title: { - value: nls.localize('toggleTabs', 'Toggle Search on Type'), - original: 'Toggle Search on Type' - }, - category: category.value, - }); - - } - - override async run(accessor: ServicesAccessor): Promise { - const configurationService = accessor.get(IConfigurationService); - const searchOnType = configurationService.getValue(ToggleSearchOnTypeAction.searchOnTypeKey); - return configurationService.updateValue(ToggleSearchOnTypeAction.searchOnTypeKey, !searchOnType); - } -}); - // Register Quick Access Handler const quickAccessRegistry = Registry.as(QuickAccessExtensions.Quickaccess); @@ -1026,7 +113,7 @@ quickAccessRegistry.registerQuickAccessProvider({ prefix: SymbolsQuickAccessProvider.PREFIX, placeholder: nls.localize('symbolsQuickAccessPlaceholder', "Type the name of a symbol to open."), contextKey: 'inWorkspaceSymbolsPicker', - helpEntries: [{ description: nls.localize('symbolsQuickAccess', "Go to Symbol in Workspace"), commandId: ShowAllSymbolsAction.ID }] + helpEntries: [{ description: nls.localize('symbolsQuickAccess', "Go to Symbol in Workspace"), commandId: Constants.ShowAllSymbolsActionId }] }); // Configuration @@ -1272,14 +359,3 @@ CommandsRegistry.registerCommand('_executeWorkspaceSymbolProvider', async functi const result = await getWorkspaceSymbols(query); return result.map(item => item.symbol); }); - -// Go to menu - -MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { - group: '3_global_nav', - command: { - id: Constants.ShowAllSymbolsActionId, - title: nls.localize({ key: 'miGotoSymbolInWorkspace', comment: ['&& denotes a mnemonic'] }, "Go to Symbol in &&Workspace...") - }, - order: 2 -}); diff --git a/src/vs/workbench/contrib/search/browser/searchActions.ts b/src/vs/workbench/contrib/search/browser/searchActions.ts deleted file mode 100644 index 87ccd82a90a..00000000000 --- a/src/vs/workbench/contrib/search/browser/searchActions.ts +++ /dev/null @@ -1,778 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as DOM from 'vs/base/browser/dom'; -import { ITreeNavigator } from 'vs/base/browser/ui/tree/tree'; -import { Action, IAction } from 'vs/base/common/actions'; -import { createKeybinding, ResolvedKeybinding } from 'vs/base/common/keybindings'; -import { isWindows, OS } from 'vs/base/common/platform'; -import * as nls from 'vs/nls'; -import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { ICommandHandler, ICommandService } from 'vs/platform/commands/common/commands'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { getSelectionKeyboardEvent, WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { IViewsService } from 'vs/workbench/common/views'; -import { searchRemoveIcon, searchReplaceAllIcon, searchReplaceIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; -import { SearchView } from 'vs/workbench/contrib/search/browser/searchView'; -import * as Constants from 'vs/workbench/contrib/search/common/constants'; -import { IReplaceService } from 'vs/workbench/contrib/search/common/replace'; -import { ISearchHistoryService } from 'vs/workbench/contrib/search/common/searchHistoryService'; -import { arrayContainsElementOrParent, FileMatch, FolderMatch, FolderMatchNoRoot, FolderMatchWithResource, FolderMatchWorkspaceRoot, Match, RenderableMatch, searchComparer, searchMatchComparer, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; -import { OpenEditorCommandId } from 'vs/workbench/contrib/searchEditor/browser/constants'; -import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; -import { OpenSearchEditorArgs } from 'vs/workbench/contrib/searchEditor/browser/searchEditor.contribution'; -import { SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ISearchConfiguration, ISearchConfigurationProperties, VIEW_ID } from 'vs/workbench/services/search/common/search'; -import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { URI } from 'vs/base/common/uri'; - -export function isSearchViewFocused(viewsService: IViewsService): boolean { - const searchView = getSearchView(viewsService); - const activeElement = document.activeElement; - return !!(searchView && activeElement && DOM.isAncestor(activeElement, searchView.getContainer())); -} - -export function appendKeyBindingLabel(label: string, inputKeyBinding: number | ResolvedKeybinding | undefined, keyBindingService2: IKeybindingService): string { - if (typeof inputKeyBinding === 'number') { - const keybinding = createKeybinding(inputKeyBinding, OS); - if (keybinding) { - const resolvedKeybindings = keyBindingService2.resolveKeybinding(keybinding); - return doAppendKeyBindingLabel(label, resolvedKeybindings.length > 0 ? resolvedKeybindings[0] : undefined); - } - return doAppendKeyBindingLabel(label, undefined); - } else { - return doAppendKeyBindingLabel(label, inputKeyBinding); - } -} - -export function openSearchView(viewsService: IViewsService, focus?: boolean): Promise { - return viewsService.openView(VIEW_ID, focus).then(view => (view as SearchView ?? undefined)); -} - -export function getSearchView(viewsService: IViewsService): SearchView | undefined { - return viewsService.getActiveViewWithId(VIEW_ID) as SearchView ?? undefined; -} - -function doAppendKeyBindingLabel(label: string, keyBinding: ResolvedKeybinding | undefined): string { - return keyBinding ? label + ' (' + keyBinding.getLabel() + ')' : label; -} - -export const toggleCaseSensitiveCommand = (accessor: ServicesAccessor) => { - const searchView = getSearchView(accessor.get(IViewsService)); - searchView?.toggleCaseSensitive(); -}; - -export const toggleWholeWordCommand = (accessor: ServicesAccessor) => { - const searchView = getSearchView(accessor.get(IViewsService)); - searchView?.toggleWholeWords(); -}; - -export const toggleRegexCommand = (accessor: ServicesAccessor) => { - const searchView = getSearchView(accessor.get(IViewsService)); - searchView?.toggleRegex(); -}; - -export const togglePreserveCaseCommand = (accessor: ServicesAccessor) => { - const searchView = getSearchView(accessor.get(IViewsService)); - searchView?.togglePreserveCase(); -}; - -export class FocusNextInputAction extends Action { - - static readonly ID = 'search.focus.nextInputBox'; - - constructor(id: string, label: string, - @IViewsService private readonly viewsService: IViewsService, - @IEditorService private readonly editorService: IEditorService, - ) { - super(id, label); - } - - override async run(): Promise { - const input = this.editorService.activeEditor; - if (input instanceof SearchEditorInput) { - // cast as we cannot import SearchEditor as a value b/c cyclic dependency. - (this.editorService.activeEditorPane as SearchEditor).focusNextInput(); - } - - const searchView = getSearchView(this.viewsService); - searchView?.focusNextInputBox(); - } -} - -export class FocusPreviousInputAction extends Action { - - static readonly ID = 'search.focus.previousInputBox'; - - constructor(id: string, label: string, - @IViewsService private readonly viewsService: IViewsService, - @IEditorService private readonly editorService: IEditorService, - ) { - super(id, label); - } - - override async run(): Promise { - const input = this.editorService.activeEditor; - if (input instanceof SearchEditorInput) { - // cast as we cannot import SearchEditor as a value b/c cyclic dependency. - (this.editorService.activeEditorPane as SearchEditor).focusPrevInput(); - } - - const searchView = getSearchView(this.viewsService); - searchView?.focusPreviousInputBox(); - } -} - -export interface IFindInFilesArgs { - query?: string; - replace?: string; - preserveCase?: boolean; - triggerSearch?: boolean; - filesToInclude?: string; - filesToExclude?: string; - isRegex?: boolean; - isCaseSensitive?: boolean; - matchWholeWord?: boolean; - useExcludeSettingsAndIgnoreFiles?: boolean; - onlyOpenEditors?: boolean; -} - -export const FindInFilesCommand: ICommandHandler = (accessor, args: IFindInFilesArgs = {}) => { - const searchConfig = accessor.get(IConfigurationService).getValue().search; - const mode = searchConfig.mode; - if (mode === 'view') { - const viewsService = accessor.get(IViewsService); - openSearchView(viewsService, false).then(openedView => { - if (openedView) { - const searchAndReplaceWidget = openedView.searchAndReplaceWidget; - searchAndReplaceWidget.toggleReplace(typeof args.replace === 'string'); - let updatedText = false; - if (typeof args.query === 'string') { - openedView.setSearchParameters(args); - } else { - updatedText = openedView.updateTextFromFindWidgetOrSelection({ allowUnselectedWord: typeof args.replace !== 'string' }); - } - openedView.searchAndReplaceWidget.focus(undefined, updatedText, updatedText); - } - }); - } else { - const convertArgs = (args: IFindInFilesArgs): OpenSearchEditorArgs => ({ - location: mode === 'newEditor' ? 'new' : 'reuse', - query: args.query, - filesToInclude: args.filesToInclude, - filesToExclude: args.filesToExclude, - matchWholeWord: args.matchWholeWord, - isCaseSensitive: args.isCaseSensitive, - isRegexp: args.isRegex, - useExcludeSettingsAndIgnoreFiles: args.useExcludeSettingsAndIgnoreFiles, - onlyOpenEditors: args.onlyOpenEditors, - showIncludesExcludes: !!(args.filesToExclude || args.filesToExclude || !args.useExcludeSettingsAndIgnoreFiles), - }); - accessor.get(ICommandService).executeCommand(OpenEditorCommandId, convertArgs(args)); - } -}; - -export class CloseReplaceAction extends Action { - - constructor(id: string, label: string, - @IViewsService private readonly viewsService: IViewsService - ) { - super(id, label); - } - - override run(): Promise { - const searchView = getSearchView(this.viewsService); - if (searchView) { - searchView.searchAndReplaceWidget.toggleReplace(false); - searchView.searchAndReplaceWidget.focus(); - } - return Promise.resolve(null); - } -} - -export function expandAll(accessor: ServicesAccessor) { - const viewsService = accessor.get(IViewsService); - const searchView = getSearchView(viewsService); - if (searchView) { - const viewer = searchView.getControl(); - viewer.expandAll(); - } -} - -export function clearSearchResults(accessor: ServicesAccessor) { - const viewsService = accessor.get(IViewsService); - const searchView = getSearchView(viewsService); - searchView?.clearSearchResults(); -} - -export function cancelSearch(accessor: ServicesAccessor) { - const viewsService = accessor.get(IViewsService); - const searchView = getSearchView(viewsService); - searchView?.cancelSearch(); -} - -export function refreshSearch(accessor: ServicesAccessor) { - const viewsService = accessor.get(IViewsService); - const searchView = getSearchView(viewsService); - searchView?.triggerQueryChange({ preserveFocus: false }); -} - -export function collapseDeepestExpandedLevel(accessor: ServicesAccessor) { - - const viewsService = accessor.get(IViewsService); - const searchView = getSearchView(viewsService); - if (searchView) { - const viewer = searchView.getControl(); - - /** - * one level to collapse so collapse everything. If FolderMatch, check if there are visible grandchildren, - * i.e. if Matches are returned by the navigator, and if so, collapse to them, otherwise collapse all levels. - */ - const navigator = viewer.navigate(); - let node = navigator.first(); - let canCollapseFileMatchLevel = false; - let canCollapseFirstLevel = false; - - if (node instanceof FolderMatchWorkspaceRoot) { - while (node = navigator.next()) { - if (node instanceof Match) { - canCollapseFileMatchLevel = true; - break; - } - if (searchView.isTreeLayoutViewVisible && !canCollapseFirstLevel) { - let nodeToTest = node; - - if (node instanceof FolderMatch) { - nodeToTest = node.compressionStartParent ?? node; - } - - const immediateParent = nodeToTest.parent(); - - if (!(immediateParent instanceof FolderMatchWorkspaceRoot || immediateParent instanceof FolderMatchNoRoot || immediateParent instanceof SearchResult)) { - canCollapseFirstLevel = true; - } - } - } - } - - if (canCollapseFileMatchLevel) { - node = navigator.first(); - do { - if (node instanceof FileMatch) { - viewer.collapse(node); - } - } while (node = navigator.next()); - } else if (canCollapseFirstLevel) { - node = navigator.first(); - if (node) { - do { - - let nodeToTest = node; - - if (node instanceof FolderMatch) { - nodeToTest = node.compressionStartParent ?? node; - } - const immediateParent = nodeToTest.parent(); - - if (immediateParent instanceof FolderMatchWorkspaceRoot || immediateParent instanceof FolderMatchNoRoot) { - if (viewer.hasElement(node)) { - viewer.collapse(node, true); - } else { - viewer.collapseAll(); - } - } - } while (node = navigator.next()); - } - } else { - viewer.collapseAll(); - } - - const firstFocusParent = viewer.getFocus()[0]?.parent(); - - if (firstFocusParent && (firstFocusParent instanceof FolderMatch || firstFocusParent instanceof FileMatch) && - viewer.hasElement(firstFocusParent) && viewer.isCollapsed(firstFocusParent)) { - viewer.domFocus(); - viewer.focusFirst(); - viewer.setSelection(viewer.getFocus()); - } - } -} - -export async function focusNextSearchResult(accessor: ServicesAccessor): Promise { - const editorService = accessor.get(IEditorService); - const input = editorService.activeEditor; - if (input instanceof SearchEditorInput) { - // cast as we cannot import SearchEditor as a value b/c cyclic dependency. - return (editorService.activeEditorPane as SearchEditor).focusNextResult(); - } - - return openSearchView(accessor.get(IViewsService)).then(searchView => { - searchView?.selectNextMatch(); - }); -} - -export async function focusPreviousSearchResult(accessor: ServicesAccessor): Promise { - const editorService = accessor.get(IEditorService); - const input = editorService.activeEditor; - if (input instanceof SearchEditorInput) { - // cast as we cannot import SearchEditor as a value b/c cyclic dependency. - return (editorService.activeEditorPane as SearchEditor).focusPreviousResult(); - } - - return openSearchView(accessor.get(IViewsService)).then(searchView => { - searchView?.selectPreviousMatch(); - }); -} - -export async function findOrReplaceInFiles(accessor: ServicesAccessor, expandSearchReplaceWidget: boolean): Promise { - return openSearchView(accessor.get(IViewsService), false).then(openedView => { - if (openedView) { - const searchAndReplaceWidget = openedView.searchAndReplaceWidget; - searchAndReplaceWidget.toggleReplace(expandSearchReplaceWidget); - - const updatedText = openedView.updateTextFromFindWidgetOrSelection({ allowUnselectedWord: !expandSearchReplaceWidget }); - openedView.searchAndReplaceWidget.focus(undefined, updatedText, updatedText); - } - }); -} - - -class ReplaceActionRunner { - constructor( - private viewer: WorkbenchCompressibleObjectTree, - private viewlet: SearchView | undefined, - // Services - @IReplaceService private readonly replaceService: IReplaceService, - @IEditorService private readonly editorService: IEditorService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @IViewsService private readonly viewsService: IViewsService - ) { } - - async performReplace(element: RenderableMatch): Promise { - // since multiple elements can be selected, we need to check the type of the FolderMatch/FileMatch/Match before we perform the replace. - const opInfo = getElementsToOperateOnInfo(this.viewer, element, this.configurationService.getValue('search')); - const elementsToReplace = opInfo.elements; - let focusElement = this.viewer.getFocus()[0]; - - if (!focusElement || (focusElement && !arrayContainsElementOrParent(focusElement, elementsToReplace)) || (focusElement instanceof SearchResult)) { - focusElement = element; - } - - if (elementsToReplace.length === 0) { - return; - } - let nextFocusElement; - if (focusElement) { - nextFocusElement = getElementToFocusAfterRemoved(this.viewer, focusElement, elementsToReplace); - } - - const searchResult = getSearchView(this.viewsService)?.searchResult; - - if (searchResult) { - searchResult.batchReplace(elementsToReplace); - } - - if (focusElement) { - if (!nextFocusElement) { - nextFocusElement = getLastNodeFromSameType(this.viewer, focusElement); - } - - if (nextFocusElement) { - this.viewer.reveal(nextFocusElement); - this.viewer.setFocus([nextFocusElement], getSelectionKeyboardEvent()); - this.viewer.setSelection([nextFocusElement], getSelectionKeyboardEvent()); - - if (nextFocusElement instanceof Match) { - const useReplacePreview = this.configurationService.getValue().search.useReplacePreview; - if (!useReplacePreview || this.hasToOpenFile(nextFocusElement)) { - this.viewlet?.open(nextFocusElement, true); - } else { - this.replaceService.openReplacePreview(nextFocusElement, true); - } - } else if (nextFocusElement instanceof FileMatch) { - this.viewlet?.open(nextFocusElement, true); - } - } - - } - - this.viewer.domFocus(); - } - - private hasToOpenFile(currBottomElem: RenderableMatch): boolean { - if (!(currBottomElem instanceof Match)) { - return false; - } - const activeEditor = this.editorService.activeEditor; - const file = activeEditor?.resource; - if (file) { - return this.uriIdentityService.extUri.isEqual(file, currBottomElem.parent().resource); - } - return false; - } -} - -export class RemoveAction implements IAction { - - static readonly LABEL = nls.localize('RemoveAction.label', "Dismiss"); - - readonly id = Constants.RemoveActionId; - public class = ThemeIcon.asClassName(searchRemoveIcon); - public label: string; - public tooltip = ''; - public enabled = true; - - constructor( - private readonly viewer: WorkbenchCompressibleObjectTree, - private readonly element: RenderableMatch, - @IKeybindingService keyBindingService: IKeybindingService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IViewsService private readonly viewsService: IViewsService, - ) { - this.label = appendKeyBindingLabel(RemoveAction.LABEL, keyBindingService.lookupKeybinding(Constants.RemoveActionId), keyBindingService); - } - - run(): void { - const opInfo = getElementsToOperateOnInfo(this.viewer, this.element, this.configurationService.getValue('search')); - const elementsToRemove = opInfo.elements; - let focusElement = this.viewer.getFocus()[0]; - - if (elementsToRemove.length === 0) { - return; - } - - if (!focusElement || (focusElement instanceof SearchResult)) { - focusElement = this.element; - } - - let nextFocusElement; - if (opInfo.mustReselect && focusElement) { - nextFocusElement = getElementToFocusAfterRemoved(this.viewer, focusElement, elementsToRemove); - } - - const searchResult = getSearchView(this.viewsService)?.searchResult; - - if (searchResult) { - searchResult.batchRemove(elementsToRemove); - } - - if (opInfo.mustReselect && focusElement) { - if (!nextFocusElement) { - nextFocusElement = getLastNodeFromSameType(this.viewer, focusElement); - } - - if (nextFocusElement && !arrayContainsElementOrParent(nextFocusElement, elementsToRemove)) { - this.viewer.reveal(nextFocusElement); - this.viewer.setFocus([nextFocusElement], getSelectionKeyboardEvent()); - this.viewer.setSelection([nextFocusElement], getSelectionKeyboardEvent()); - } - } - - this.viewer.domFocus(); - return; - } -} - -export class ReplaceAction extends Action { - - static readonly LABEL = nls.localize('match.replace.label', "Replace"); - - static runQ = Promise.resolve(); - private replaceRunner: ReplaceActionRunner; - - constructor(viewer: WorkbenchCompressibleObjectTree, private element: Match, viewlet: SearchView, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IKeybindingService keyBindingService: IKeybindingService, - ) { - super(Constants.ReplaceActionId, appendKeyBindingLabel(ReplaceAction.LABEL, keyBindingService.lookupKeybinding(Constants.ReplaceActionId), keyBindingService), ThemeIcon.asClassName(searchReplaceIcon)); - this.replaceRunner = this.instantiationService.createInstance(ReplaceActionRunner, viewer, viewlet); - } - - override async run(): Promise { - return this.replaceRunner.performReplace(this.element); - } -} - -export class ReplaceAllAction extends Action { - - static readonly LABEL = nls.localize('file.replaceAll.label', "Replace All"); - private replaceRunner: ReplaceActionRunner; - constructor( - viewlet: SearchView, - private fileMatch: FileMatch, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IKeybindingService keyBindingService: IKeybindingService, - ) { - super(Constants.ReplaceAllInFileActionId, appendKeyBindingLabel(ReplaceAllAction.LABEL, keyBindingService.lookupKeybinding(Constants.ReplaceAllInFileActionId), keyBindingService), ThemeIcon.asClassName(searchReplaceAllIcon)); - this.replaceRunner = this.instantiationService.createInstance(ReplaceActionRunner, viewlet.getControl(), viewlet); - } - - override async run(): Promise { - return this.replaceRunner.performReplace(this.fileMatch); - } -} - -export class ReplaceAllInFolderAction extends Action { - - static readonly LABEL = nls.localize('file.replaceAll.label', "Replace All"); - private replaceRunner: ReplaceActionRunner; - - constructor(viewer: WorkbenchCompressibleObjectTree, private folderMatch: FolderMatch, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IKeybindingService keyBindingService: IKeybindingService - ) { - super(Constants.ReplaceAllInFolderActionId, appendKeyBindingLabel(ReplaceAllInFolderAction.LABEL, keyBindingService.lookupKeybinding(Constants.ReplaceAllInFolderActionId), keyBindingService), ThemeIcon.asClassName(searchReplaceAllIcon)); - this.replaceRunner = this.instantiationService.createInstance(ReplaceActionRunner, viewer, undefined); - } - - override run(): Promise { - return this.replaceRunner.performReplace(this.folderMatch); - } -} - -export const copyPathCommand: ICommandHandler = async (accessor, fileMatch: FileMatch | FolderMatchWithResource | undefined) => { - if (!fileMatch) { - const selection = getSelectedRow(accessor); - if (!(selection instanceof FileMatch || selection instanceof FolderMatchWithResource)) { - return; - } - - fileMatch = selection; - } - - const clipboardService = accessor.get(IClipboardService); - const labelService = accessor.get(ILabelService); - - const text = labelService.getUriLabel(fileMatch.resource, { noPrefix: true }); - await clipboardService.writeText(text); -}; - -function matchToString(match: Match, indent = 0): string { - const getFirstLinePrefix = () => `${match.range().startLineNumber},${match.range().startColumn}`; - const getOtherLinePrefix = (i: number) => match.range().startLineNumber + i + ''; - - const fullMatchLines = match.fullPreviewLines(); - const largestPrefixSize = fullMatchLines.reduce((largest, _, i) => { - const thisSize = i === 0 ? - getFirstLinePrefix().length : - getOtherLinePrefix(i).length; - - return Math.max(thisSize, largest); - }, 0); - - const formattedLines = fullMatchLines - .map((line, i) => { - const prefix = i === 0 ? - getFirstLinePrefix() : - getOtherLinePrefix(i); - - const paddingStr = ' '.repeat(largestPrefixSize - prefix.length); - const indentStr = ' '.repeat(indent); - return `${indentStr}${prefix}: ${paddingStr}${line}`; - }); - - return formattedLines.join('\n'); -} -function fileFolderMatchToString(match: FileMatch | FolderMatch | FolderMatchWithResource, labelService: ILabelService): { text: string; count: number } { - if (match instanceof FileMatch) { - return fileMatchToString(match, labelService); - } else { - return folderMatchToString(match, labelService); - } -} -const lineDelimiter = isWindows ? '\r\n' : '\n'; -function fileMatchToString(fileMatch: FileMatch, labelService: ILabelService): { text: string; count: number } { - const matchTextRows = fileMatch.matches() - .sort(searchMatchComparer) - .map(match => matchToString(match, 2)); - const uriString = labelService.getUriLabel(fileMatch.resource, { noPrefix: true }); - return { - text: `${uriString}${lineDelimiter}${matchTextRows.join(lineDelimiter)}`, - count: matchTextRows.length - }; -} - -function folderMatchToString(folderMatch: FolderMatchWithResource | FolderMatch, labelService: ILabelService): { text: string; count: number } { - const results: string[] = []; - let numMatches = 0; - - const matches = folderMatch.matches().sort(searchMatchComparer); - - matches.forEach(match => { - const result = fileFolderMatchToString(match, labelService); - numMatches += result.count; - results.push(result.text); - }); - - return { - text: results.join(lineDelimiter + lineDelimiter), - count: numMatches - }; -} - -export const copyMatchCommand: ICommandHandler = async (accessor, match: RenderableMatch | undefined) => { - if (!match) { - const selection = getSelectedRow(accessor); - if (!selection) { - return; - } - - match = selection; - } - - const clipboardService = accessor.get(IClipboardService); - const labelService = accessor.get(ILabelService); - - let text: string | undefined; - if (match instanceof Match) { - text = matchToString(match); - } else if (match instanceof FileMatch) { - text = fileMatchToString(match, labelService).text; - } else if (match instanceof FolderMatch) { - text = folderMatchToString(match, labelService).text; - } - - if (text) { - await clipboardService.writeText(text); - } -}; - -function allFolderMatchesToString(folderMatches: Array, labelService: ILabelService): string { - const folderResults: string[] = []; - folderMatches = folderMatches.sort(searchMatchComparer); - for (let i = 0; i < folderMatches.length; i++) { - const folderResult = folderMatchToString(folderMatches[i], labelService); - if (folderResult.count) { - folderResults.push(folderResult.text); - } - } - - return folderResults.join(lineDelimiter + lineDelimiter); -} - -function getSelectedRow(accessor: ServicesAccessor): RenderableMatch | undefined | null { - const viewsService = accessor.get(IViewsService); - const searchView = getSearchView(viewsService); - return searchView?.getControl().getSelection()[0]; -} - -export const copyAllCommand: ICommandHandler = async (accessor) => { - const viewsService = accessor.get(IViewsService); - const clipboardService = accessor.get(IClipboardService); - const labelService = accessor.get(ILabelService); - - const searchView = getSearchView(viewsService); - if (searchView) { - const root = searchView.searchResult; - - const text = allFolderMatchesToString(root.folderMatches(), labelService); - await clipboardService.writeText(text); - } -}; - -export const clearHistoryCommand: ICommandHandler = accessor => { - const searchHistoryService = accessor.get(ISearchHistoryService); - searchHistoryService.clearHistory(); -}; - -export const focusSearchListCommand: ICommandHandler = accessor => { - const viewsService = accessor.get(IViewsService); - openSearchView(viewsService).then(searchView => { - searchView?.moveFocusToResults(); - }); -}; - -export function getMultiSelectedSearchResources(viewer: WorkbenchCompressibleObjectTree, currElement: RenderableMatch | undefined, sortConfig: ISearchConfigurationProperties): URI[] { - return getElementsToOperateOnInfo(viewer, currElement, sortConfig).elements - .map((renderableMatch) => ((renderableMatch instanceof Match) ? null : renderableMatch.resource)) - .filter((renderableMatch): renderableMatch is URI => (renderableMatch !== null)); -} - -function getElementsToOperateOnInfo(viewer: WorkbenchCompressibleObjectTree, currElement: RenderableMatch | undefined, sortConfig: ISearchConfigurationProperties): { elements: RenderableMatch[]; mustReselect: boolean } { - let elements: RenderableMatch[] = viewer.getSelection().filter((x): x is RenderableMatch => x !== null).sort((a, b) => searchComparer(a, b, sortConfig.sortOrder)); - - const mustReselect = !currElement || elements.includes(currElement); // this indicates whether we need to re-focus/re-select on a remove. - - // if selection doesn't include multiple elements, just return current focus element. - if (currElement && !(elements.length > 1 && elements.includes(currElement))) { - elements = [currElement]; - } - - return { elements, mustReselect }; -} - - -function compareLevels(elem1: RenderableMatch, elem2: RenderableMatch) { - if (elem1 instanceof Match) { - if (elem2 instanceof Match) { - return 0; - } else { - return -1; - } - - } else if (elem1 instanceof FileMatch) { - if (elem2 instanceof Match) { - return 1; - } else if (elem2 instanceof FileMatch) { - return 0; - } else { - return -1; - } - - } else { - // FolderMatch - if (elem2 instanceof FolderMatch) { - return 0; - } else { - return 1; - } - } -} - -/** - * Returns element to focus after removing the given element - */ -export function getElementToFocusAfterRemoved(viewer: WorkbenchCompressibleObjectTree, element: RenderableMatch, elementsToRemove: RenderableMatch[]): RenderableMatch | undefined { - const navigator: ITreeNavigator = viewer.navigate(element); - if (element instanceof FolderMatch) { - while (!!navigator.next() && (!(navigator.current() instanceof FolderMatch) || arrayContainsElementOrParent(navigator.current(), elementsToRemove))) { } - } else if (element instanceof FileMatch) { - while (!!navigator.next() && (!(navigator.current() instanceof FileMatch) || arrayContainsElementOrParent(navigator.current(), elementsToRemove))) { - viewer.expand(navigator.current()); - } - } else { - while (navigator.next() && (!(navigator.current() instanceof Match) || arrayContainsElementOrParent(navigator.current(), elementsToRemove))) { - viewer.expand(navigator.current()); - } - } - return navigator.current(); -} - -/*** - * Finds the last element in the tree with the same type as `element` - */ -export function getLastNodeFromSameType(viewer: WorkbenchCompressibleObjectTree, element: RenderableMatch): RenderableMatch | undefined { - let lastElem: RenderableMatch | null = viewer.lastVisibleElement ?? null; - - while (lastElem) { - const compareVal = compareLevels(element, lastElem); - if (compareVal === -1) { - viewer.expand(lastElem); - lastElem = viewer.lastVisibleElement; - } else if (compareVal === 1) { - lastElem = viewer.getParentElement(lastElem); - } else { - return lastElem; - } - } - - return undefined; -} diff --git a/src/vs/workbench/contrib/search/browser/searchActionsBase.ts b/src/vs/workbench/contrib/search/browser/searchActionsBase.ts new file mode 100644 index 00000000000..932b714e3ce --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/searchActionsBase.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { createKeybinding, ResolvedKeybinding } from 'vs/base/common/keybindings'; +import { OS } from 'vs/base/common/platform'; +import * as nls from 'vs/nls'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; +import { IViewsService } from 'vs/workbench/common/views'; +import { SearchView } from 'vs/workbench/contrib/search/browser/searchView'; +import { RenderableMatch, searchComparer } from 'vs/workbench/contrib/search/common/searchModel'; +import { ISearchConfigurationProperties, VIEW_ID } from 'vs/workbench/services/search/common/search'; + +export const category = { value: nls.localize('search', "Search"), original: 'Search' }; + +export function isSearchViewFocused(viewsService: IViewsService): boolean { + const searchView = getSearchView(viewsService); + const activeElement = document.activeElement; + return !!(searchView && activeElement && DOM.isAncestor(activeElement, searchView.getContainer())); +} + +export function appendKeyBindingLabel(label: string, inputKeyBinding: number | ResolvedKeybinding | undefined, keyBindingService2: IKeybindingService): string { + if (typeof inputKeyBinding === 'number') { + const keybinding = createKeybinding(inputKeyBinding, OS); + if (keybinding) { + const resolvedKeybindings = keyBindingService2.resolveKeybinding(keybinding); + return doAppendKeyBindingLabel(label, resolvedKeybindings.length > 0 ? resolvedKeybindings[0] : undefined); + } + return doAppendKeyBindingLabel(label, undefined); + } else { + return doAppendKeyBindingLabel(label, inputKeyBinding); + } +} + +export function getSearchView(viewsService: IViewsService): SearchView | undefined { + return viewsService.getActiveViewWithId(VIEW_ID) as SearchView; +} + +export function getElementsToOperateOnInfo(viewer: WorkbenchCompressibleObjectTree, currElement: RenderableMatch | undefined, sortConfig: ISearchConfigurationProperties): { elements: RenderableMatch[]; mustReselect: boolean } { + let elements: RenderableMatch[] = viewer.getSelection().filter((x): x is RenderableMatch => x !== null).sort((a, b) => searchComparer(a, b, sortConfig.sortOrder)); + + const mustReselect = !currElement || elements.includes(currElement); // this indicates whether we need to re-focus/re-select on a remove. + + // if selection doesn't include multiple elements, just return current focus element. + if (currElement && !(elements.length > 1 && elements.includes(currElement))) { + elements = [currElement]; + } + + return { elements, mustReselect }; +} + +export function openSearchView(viewsService: IViewsService, focus?: boolean): Promise { + return viewsService.openView(VIEW_ID, focus).then(view => (view as SearchView ?? undefined)); +} + +function doAppendKeyBindingLabel(label: string, keyBinding: ResolvedKeybinding | undefined): string { + return keyBinding ? label + ' (' + keyBinding.getLabel() + ')' : label; +} + diff --git a/src/vs/workbench/contrib/search/browser/searchActionsCopy.ts b/src/vs/workbench/contrib/search/browser/searchActionsCopy.ts new file mode 100644 index 00000000000..58db3b759d5 --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/searchActionsCopy.ts @@ -0,0 +1,256 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IViewsService } from 'vs/workbench/common/views'; +import * as Constants from 'vs/workbench/contrib/search/common/constants'; +import { FileMatch, FolderMatch, FolderMatchWithResource, Match, RenderableMatch, searchMatchComparer } from 'vs/workbench/contrib/search/common/searchModel'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { category, getSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase'; +import { isWindows } from 'vs/base/common/platform'; + +//#region Actions +registerAction2(class CopyMatchCommandAction extends Action2 { + + constructor( + ) { + super({ + id: Constants.CopyMatchCommandId, + title: { + value: nls.localize('copyMatchLabel', "Copy"), + original: 'Copy' + }, + category: category.value, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: Constants.FileMatchOrMatchFocusKey, + primary: KeyMod.CtrlCmd | KeyCode.KeyC, + }, + menu: [{ + id: MenuId.SearchContext, + when: Constants.FileMatchOrMatchFocusKey, + group: 'search_2', + order: 1 + }] + }); + + } + + override async run(accessor: ServicesAccessor, match: RenderableMatch | undefined): Promise { + await copyMatchCommand(accessor, match); + } +}); + +registerAction2(class CopyPathCommandAction extends Action2 { + + constructor( + ) { + super({ + id: Constants.CopyPathCommandId, + title: { + value: nls.localize('copyPathLabel', "Copy Path"), + original: 'Copy Path' + }, + category: category.value, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: Constants.FileMatchOrFolderMatchWithResourceFocusKey, + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyC, + win: { + primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KeyC + }, + }, + menu: [{ + id: MenuId.SearchContext, + when: Constants.FileMatchOrFolderMatchWithResourceFocusKey, + group: 'search_2', + order: 2 + }] + }); + + } + + override async run(accessor: ServicesAccessor, fileMatch: FileMatch | FolderMatchWithResource | undefined): Promise { + await copyPathCommand(accessor, fileMatch); + } +}); + +registerAction2(class CopyAllCommandAction extends Action2 { + + constructor( + ) { + super({ + id: Constants.CopyAllCommandId, + title: { + value: nls.localize('copyAllLabel', "Copy All"), + original: 'Copy All' + }, + category: category.value, + menu: [{ + id: MenuId.SearchContext, + when: Constants.HasSearchResults, + group: 'search_2', + order: 3 + }] + }); + + } + + override async run(accessor: ServicesAccessor): Promise { + await copyAllCommand(accessor); + } +}); + +//#endregion + +//#region Helpers +export const lineDelimiter = isWindows ? '\r\n' : '\n'; + +async function copyPathCommand(accessor: ServicesAccessor, fileMatch: FileMatch | FolderMatchWithResource | undefined) { + if (!fileMatch) { + const selection = getSelectedRow(accessor); + if (!(selection instanceof FileMatch || selection instanceof FolderMatchWithResource)) { + return; + } + + fileMatch = selection; + } + + const clipboardService = accessor.get(IClipboardService); + const labelService = accessor.get(ILabelService); + + const text = labelService.getUriLabel(fileMatch.resource, { noPrefix: true }); + await clipboardService.writeText(text); +} + +async function copyMatchCommand(accessor: ServicesAccessor, match: RenderableMatch | undefined) { + if (!match) { + const selection = getSelectedRow(accessor); + if (!selection) { + return; + } + + match = selection; + } + + const clipboardService = accessor.get(IClipboardService); + const labelService = accessor.get(ILabelService); + + let text: string | undefined; + if (match instanceof Match) { + text = matchToString(match); + } else if (match instanceof FileMatch) { + text = fileMatchToString(match, labelService).text; + } else if (match instanceof FolderMatch) { + text = folderMatchToString(match, labelService).text; + } + + if (text) { + await clipboardService.writeText(text); + } +} + +async function copyAllCommand(accessor: ServicesAccessor) { + const viewsService = accessor.get(IViewsService); + const clipboardService = accessor.get(IClipboardService); + const labelService = accessor.get(ILabelService); + + const searchView = getSearchView(viewsService); + if (searchView) { + const root = searchView.searchResult; + + const text = allFolderMatchesToString(root.folderMatches(), labelService); + await clipboardService.writeText(text); + } +} + +function matchToString(match: Match, indent = 0): string { + const getFirstLinePrefix = () => `${match.range().startLineNumber},${match.range().startColumn}`; + const getOtherLinePrefix = (i: number) => match.range().startLineNumber + i + ''; + + const fullMatchLines = match.fullPreviewLines(); + const largestPrefixSize = fullMatchLines.reduce((largest, _, i) => { + const thisSize = i === 0 ? + getFirstLinePrefix().length : + getOtherLinePrefix(i).length; + + return Math.max(thisSize, largest); + }, 0); + + const formattedLines = fullMatchLines + .map((line, i) => { + const prefix = i === 0 ? + getFirstLinePrefix() : + getOtherLinePrefix(i); + + const paddingStr = ' '.repeat(largestPrefixSize - prefix.length); + const indentStr = ' '.repeat(indent); + return `${indentStr}${prefix}: ${paddingStr}${line}`; + }); + + return formattedLines.join('\n'); +} + +function fileFolderMatchToString(match: FileMatch | FolderMatch | FolderMatchWithResource, labelService: ILabelService): { text: string; count: number } { + if (match instanceof FileMatch) { + return fileMatchToString(match, labelService); + } else { + return folderMatchToString(match, labelService); + } +} + +function fileMatchToString(fileMatch: FileMatch, labelService: ILabelService): { text: string; count: number } { + const matchTextRows = fileMatch.matches() + .sort(searchMatchComparer) + .map(match => matchToString(match, 2)); + const uriString = labelService.getUriLabel(fileMatch.resource, { noPrefix: true }); + return { + text: `${uriString}${lineDelimiter}${matchTextRows.join(lineDelimiter)}`, + count: matchTextRows.length + }; +} + +function folderMatchToString(folderMatch: FolderMatchWithResource | FolderMatch, labelService: ILabelService): { text: string; count: number } { + const results: string[] = []; + let numMatches = 0; + + const matches = folderMatch.matches().sort(searchMatchComparer); + + matches.forEach(match => { + const result = fileFolderMatchToString(match, labelService); + numMatches += result.count; + results.push(result.text); + }); + + return { + text: results.join(lineDelimiter + lineDelimiter), + count: numMatches + }; +} + +function allFolderMatchesToString(folderMatches: Array, labelService: ILabelService): string { + const folderResults: string[] = []; + folderMatches = folderMatches.sort(searchMatchComparer); + for (let i = 0; i < folderMatches.length; i++) { + const folderResult = folderMatchToString(folderMatches[i], labelService); + if (folderResult.count) { + folderResults.push(folderResult.text); + } + } + + return folderResults.join(lineDelimiter + lineDelimiter); +} + +function getSelectedRow(accessor: ServicesAccessor): RenderableMatch | undefined | null { + const viewsService = accessor.get(IViewsService); + const searchView = getSearchView(viewsService); + return searchView?.getControl().getSelection()[0]; +} + +//#endregion diff --git a/src/vs/workbench/contrib/search/browser/searchActionsFind.ts b/src/vs/workbench/contrib/search/browser/searchActionsFind.ts new file mode 100644 index 00000000000..2d8cf8d9929 --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/searchActionsFind.ts @@ -0,0 +1,384 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { dirname } from 'vs/base/common/resources'; +import * as nls from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IListService, WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; +import { IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; +import * as Constants from 'vs/workbench/contrib/search/common/constants'; +import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants'; +import { FileMatch, FolderMatchWithResource, Match, RenderableMatch } from 'vs/workbench/contrib/search/common/searchModel'; +import { OpenSearchEditorArgs } from 'vs/workbench/contrib/searchEditor/browser/searchEditor.contribution'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ISearchConfiguration, ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; +import { URI } from 'vs/base/common/uri'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { resolveResourcesForSearchIncludes } from 'vs/workbench/services/search/common/queryBuilder'; +import { getMultiSelectedResources, IExplorerService } from 'vs/workbench/contrib/files/browser/files'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { ExplorerFolderContext, ExplorerRootContext, FilesExplorerFocusCondition, VIEWLET_ID as VIEWLET_ID_FILES } from 'vs/workbench/contrib/files/common/files'; +import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/explorerViewlet'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { category, getElementsToOperateOnInfo, getSearchView, openSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase'; + + +//#region Interfaces +export interface IFindInFilesArgs { + query?: string; + replace?: string; + preserveCase?: boolean; + triggerSearch?: boolean; + filesToInclude?: string; + filesToExclude?: string; + isRegex?: boolean; + isCaseSensitive?: boolean; + matchWholeWord?: boolean; + useExcludeSettingsAndIgnoreFiles?: boolean; + onlyOpenEditors?: boolean; +} +//#endregion + +registerAction2(class RestrictSearchToFolderAction extends Action2 { + constructor() { + super({ + id: Constants.RestrictSearchToFolderId, + title: { + value: nls.localize('restrictResultsToFolder', "Restrict Search to Folder"), + original: 'Restrict Search to Folder' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ResourceFolderFocusKey), + primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KeyF, + }, + menu: [ + { + id: MenuId.SearchContext, + group: 'search', + order: 3, + when: ContextKeyExpr.and(Constants.ResourceFolderFocusKey) + } + ] + }); + } + async run(accessor: ServicesAccessor, folderMatch?: FolderMatchWithResource) { + await searchWithFolderCommand(accessor, false, true, undefined, folderMatch); + } +}); + +registerAction2(class ExcludeFolderFromSearchAction extends Action2 { + constructor() { + super({ + id: Constants.ExcludeFolderFromSearchId, + title: { + value: nls.localize('excludeFolderFromSearch', "Exclude Folder from Search"), + original: 'Exclude Folder from Search' + }, + category, + menu: [ + { + id: MenuId.SearchContext, + group: 'search', + order: 4, + when: ContextKeyExpr.and(Constants.ResourceFolderFocusKey) + } + ] + }); + } + async run(accessor: ServicesAccessor, folderMatch?: FolderMatchWithResource) { + await searchWithFolderCommand(accessor, false, false, undefined, folderMatch); + } +}); + +registerAction2(class RevealInSideBarForSearchResultsAction extends Action2 { + + constructor( + ) { + super({ + id: Constants.RevealInSideBarForSearchResults, + title: { + value: nls.localize('revealInSideBar', "Reveal in Explorer View"), + original: 'Reveal in Explorer View' + }, + category: category, + menu: [{ + id: MenuId.SearchContext, + when: ContextKeyExpr.and(Constants.FileFocusKey, Constants.HasSearchResults), + group: 'search_3', + order: 1 + }] + }); + + } + + override async run(accessor: ServicesAccessor, args: any): Promise { + const paneCompositeService = accessor.get(IPaneCompositePartService); + const explorerService = accessor.get(IExplorerService); + const contextService = accessor.get(IWorkspaceContextService); + + const searchView = getSearchView(accessor.get(IViewsService)); + if (!searchView) { + return; + } + + let fileMatch: FileMatch; + if (!(args instanceof FileMatch)) { + args = searchView.getControl().getFocus()[0]; + } + if (args instanceof FileMatch) { + fileMatch = args; + } else { + return; + } + + paneCompositeService.openPaneComposite(VIEWLET_ID_FILES, ViewContainerLocation.Sidebar, false).then((viewlet) => { + if (!viewlet) { + return; + } + + const explorerViewContainer = viewlet.getViewPaneContainer() as ExplorerViewPaneContainer; + const uri = fileMatch.resource; + if (uri && contextService.isInsideWorkspace(uri)) { + const explorerView = explorerViewContainer.getExplorerView(); + explorerView.setExpanded(true); + explorerService.select(uri, true).then(() => explorerView.focus(), onUnexpectedError); + } + }); + } +}); + +// Find in Files by default is the same as View: Show Search, but can be configured to open a search editor instead with the `search.mode` binding +registerAction2(class FindInFilesAction extends Action2 { + + constructor( + ) { + super({ + id: Constants.FindInFilesActionId, + title: { + value: nls.localize('findInFiles', "Find in Files"), + mnemonicTitle: nls.localize({ key: 'miFindInFiles', comment: ['&& denotes a mnemonic'] }, "Find &&in Files"), + original: 'Find in Files' + }, + description: { + description: nls.localize('findInFiles.description', "Open a workspace search"), + args: [ + { + name: nls.localize('findInFiles.args', "A set of options for the search"), + schema: { + type: 'object', + properties: { + query: { 'type': 'string' }, + replace: { 'type': 'string' }, + preserveCase: { 'type': 'boolean' }, + triggerSearch: { 'type': 'boolean' }, + filesToInclude: { 'type': 'string' }, + filesToExclude: { 'type': 'string' }, + isRegex: { 'type': 'boolean' }, + isCaseSensitive: { 'type': 'boolean' }, + matchWholeWord: { 'type': 'boolean' }, + useExcludeSettingsAndIgnoreFiles: { 'type': 'boolean' }, + onlyOpenEditors: { 'type': 'boolean' }, + } + } + }, + ] + }, + category: category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyF, + }, + menu: [{ + id: MenuId.MenubarEditMenu, + group: '4_find_global', + order: 1, + }], + f1: true + }); + + } + + override async run(accessor: ServicesAccessor, args: IFindInFilesArgs = {}): Promise { + findInFilesCommand(accessor, args); + } +}); + +registerAction2(class FindInFolderAction extends Action2 { + // from explorer + constructor() { + super({ + id: Constants.FindInFolderId, + title: { + value: nls.localize('findInFolder', "Find in Folder..."), + original: 'Find in Folder...' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerFolderContext), + primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KeyF, + }, + menu: [ + { + id: MenuId.ExplorerContext, + group: '4_search', + order: 10, + when: ContextKeyExpr.and(ExplorerFolderContext) + } + ] + }); + } + async run(accessor: ServicesAccessor, resource?: URI) { + await searchWithFolderCommand(accessor, true, true, resource); + } +}); + +registerAction2(class FindInWorkspaceAction extends Action2 { + // from explorer + constructor() { + super({ + id: Constants.FindInWorkspaceId, + title: { + value: nls.localize('findInWorkspace', "Find in Workspace..."), + original: 'Find in Workspace...' + }, + category, + menu: [ + { + id: MenuId.ExplorerContext, + group: '4_search', + order: 10, + when: ContextKeyExpr.and(ExplorerRootContext, ExplorerFolderContext.toNegated()) + + } + ] + }); + } + async run(accessor: ServicesAccessor) { + const searchConfig = accessor.get(IConfigurationService).getValue().search; + const mode = searchConfig.mode; + + if (mode === 'view') { + const searchView = await openSearchView(accessor.get(IViewsService), true); + searchView?.searchInFolders(); + } + else { + return accessor.get(ICommandService).executeCommand(SearchEditorConstants.OpenEditorCommandId, { + location: mode === 'newEditor' ? 'new' : 'reuse', + filesToInclude: '', + }); + } + } +}); + +//#region Helpers +async function searchWithFolderCommand(accessor: ServicesAccessor, isFromExplorer: boolean, isIncludes: boolean, resource?: URI, folderMatch?: FolderMatchWithResource) { + const listService = accessor.get(IListService); + const fileService = accessor.get(IFileService); + const viewsService = accessor.get(IViewsService); + const contextService = accessor.get(IWorkspaceContextService); + const commandService = accessor.get(ICommandService); + const searchConfig = accessor.get(IConfigurationService).getValue().search; + const mode = searchConfig.mode; + + let resources: URI[]; + + if (isFromExplorer) { + resources = getMultiSelectedResources(resource, listService, accessor.get(IEditorService), accessor.get(IExplorerService)); + } else { + const searchView = getSearchView(accessor.get(IViewsService)); + if (!searchView) { + return; + } + resources = getMultiSelectedSearchResources(searchView.getControl(), folderMatch, searchConfig); + } + + const resolvedResources = fileService.resolveAll(resources.map(resource => ({ resource }))).then(results => { + const folders: URI[] = []; + results.forEach(result => { + if (result.success && result.stat) { + folders.push(result.stat.isDirectory ? result.stat.resource : dirname(result.stat.resource)); + } + }); + return resolveResourcesForSearchIncludes(folders, contextService); + }); + + if (mode === 'view') { + const searchView = await openSearchView(viewsService, true); + if (resources && resources.length && searchView) { + if (isIncludes) { + searchView.searchInFolders(await resolvedResources); + } else { + searchView.searchOutsideOfFolders(await resolvedResources); + } + } + return undefined; + } else { + if (isIncludes) { + return commandService.executeCommand(SearchEditorConstants.OpenEditorCommandId, { + filesToInclude: (await resolvedResources).join(', '), + showIncludesExcludes: true, + location: mode === 'newEditor' ? 'new' : 'reuse', + }); + } + else { + return commandService.executeCommand(SearchEditorConstants.OpenEditorCommandId, { + filesToExclude: (await resolvedResources).join(', '), + showIncludesExcludes: true, + location: mode === 'newEditor' ? 'new' : 'reuse', + }); + } + } +} + +function getMultiSelectedSearchResources(viewer: WorkbenchCompressibleObjectTree, currElement: RenderableMatch | undefined, sortConfig: ISearchConfigurationProperties): URI[] { + return getElementsToOperateOnInfo(viewer, currElement, sortConfig).elements + .map((renderableMatch) => ((renderableMatch instanceof Match) ? null : renderableMatch.resource)) + .filter((renderableMatch): renderableMatch is URI => (renderableMatch !== null)); +} + +export function findInFilesCommand(accessor: ServicesAccessor, args: IFindInFilesArgs = {}) { + const searchConfig = accessor.get(IConfigurationService).getValue().search; + const mode = searchConfig.mode; + if (mode === 'view') { + const viewsService = accessor.get(IViewsService); + openSearchView(viewsService, false).then(openedView => { + if (openedView) { + const searchAndReplaceWidget = openedView.searchAndReplaceWidget; + searchAndReplaceWidget.toggleReplace(typeof args.replace === 'string'); + let updatedText = false; + if (typeof args.query === 'string') { + openedView.setSearchParameters(args); + } else { + updatedText = openedView.updateTextFromFindWidgetOrSelection({ allowUnselectedWord: typeof args.replace !== 'string' }); + } + openedView.searchAndReplaceWidget.focus(undefined, updatedText, updatedText); + } + }); + } else { + const convertArgs = (args: IFindInFilesArgs): OpenSearchEditorArgs => ({ + location: mode === 'newEditor' ? 'new' : 'reuse', + query: args.query, + filesToInclude: args.filesToInclude, + filesToExclude: args.filesToExclude, + matchWholeWord: args.matchWholeWord, + isCaseSensitive: args.isCaseSensitive, + isRegexp: args.isRegex, + useExcludeSettingsAndIgnoreFiles: args.useExcludeSettingsAndIgnoreFiles, + onlyOpenEditors: args.onlyOpenEditors, + showIncludesExcludes: !!(args.filesToExclude || args.filesToExclude || !args.useExcludeSettingsAndIgnoreFiles), + }); + accessor.get(ICommandService).executeCommand(SearchEditorConstants.OpenEditorCommandId, convertArgs(args)); + } +} +//#endregion diff --git a/src/vs/workbench/contrib/search/browser/searchActionsNav.ts b/src/vs/workbench/contrib/search/browser/searchActionsNav.ts new file mode 100644 index 00000000000..bc0f99679f8 --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/searchActionsNav.ts @@ -0,0 +1,533 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { isMacintosh } from 'vs/base/common/platform'; +import * as nls from 'vs/nls'; +import { ICommandHandler } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; +import { IViewsService } from 'vs/workbench/common/views'; +import * as Constants from 'vs/workbench/contrib/search/common/constants'; +import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants'; +import { FileMatchOrMatch, FolderMatch, RenderableMatch } from 'vs/workbench/contrib/search/common/searchModel'; +import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; +import { SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { assertIsDefined } from 'vs/base/common/types'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { ToggleCaseSensitiveKeybinding, TogglePreserveCaseKeybinding, ToggleRegexKeybinding, ToggleWholeWordKeybinding } from 'vs/editor/contrib/find/browser/findModel'; +import { category, getSearchView, openSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase'; + +//#region Actions: Changing Search Input Options +registerAction2(class ToggleQueryDetailsAction extends Action2 { + constructor() { + super({ + id: Constants.ToggleQueryDetailsActionId, + title: { + value: nls.localize('ToggleQueryDetailsAction.label', "Toggle Query Details"), + original: 'Toggle Query Details' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.or(Constants.SearchViewFocusedKey, SearchEditorConstants.InSearchEditor), + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyJ, + }, + }); + } + run(accessor: ServicesAccessor) { + const contextService = accessor.get(IContextKeyService).getContext(document.activeElement); + if (contextService.getValue(SearchEditorConstants.InSearchEditor.serialize())) { + (accessor.get(IEditorService).activeEditorPane as SearchEditor).toggleQueryDetails(); + } else if (contextService.getValue(Constants.SearchViewFocusedKey.serialize())) { + const searchView = getSearchView(accessor.get(IViewsService)); + assertIsDefined(searchView).toggleQueryDetails(); + } + } +}); + +registerAction2(class CloseReplaceAction extends Action2 { + constructor() { + super({ + id: Constants.CloseReplaceWidgetActionId, + title: { + value: nls.localize('CloseReplaceWidget.label', "Close Replace Widget"), + original: 'Close Replace Widget' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceInputBoxFocusedKey), + primary: KeyCode.Escape, + }, + }); + } + run(accessor: ServicesAccessor) { + + const searchView = getSearchView(accessor.get(IViewsService)); + if (searchView) { + searchView.searchAndReplaceWidget.toggleReplace(false); + searchView.searchAndReplaceWidget.focus(); + } + return Promise.resolve(null); + } +}); + +registerAction2(class ToggleCaseSensitiveCommandAction extends Action2 { + + constructor( + ) { + + super({ + id: Constants.ToggleCaseSensitiveCommandId, + title: { + value: nls.localize('ToggleCaseSensitiveCommandId.label', "Toggle Case Sensitive"), + original: 'Toggle Case Sensitive' + }, + category: category, + keybinding: Object.assign({ + weight: KeybindingWeight.WorkbenchContrib, + when: isMacintosh ? ContextKeyExpr.and(Constants.SearchViewFocusedKey, Constants.FileMatchOrFolderMatchFocusKey.toNegated()) : Constants.SearchViewFocusedKey, + }, ToggleCaseSensitiveKeybinding) + + }); + + } + + override async run(accessor: ServicesAccessor): Promise { + toggleCaseSensitiveCommand(accessor); + } +}); + +registerAction2(class ToggleWholeWordCommandAction extends Action2 { + constructor() { + super({ + id: Constants.ToggleWholeWordCommandId, + title: { + value: nls.localize('ToggleWholeWordCommandId.label', 'Toggle Whole Word'), + original: 'Toggle Whole Word' + }, + keybinding: Object.assign({ + weight: KeybindingWeight.WorkbenchContrib, + when: Constants.SearchViewFocusedKey, + }, ToggleWholeWordKeybinding), + category: category.value, + }); + } + + override async run(accessor: ServicesAccessor): Promise { + return toggleWholeWordCommand(accessor); + } +}); + +registerAction2(class ToggleRegexCommandAction extends Action2 { + constructor() { + super({ + id: Constants.ToggleRegexCommandId, + title: { + value: nls.localize('ToggleRegexCommandId.label', 'Toggle Regex'), + original: 'Toggle Regex' + }, + keybinding: Object.assign({ + weight: KeybindingWeight.WorkbenchContrib, + when: Constants.SearchViewFocusedKey, + }, ToggleRegexKeybinding), + category: category.value, + }); + } + + override async run(accessor: ServicesAccessor): Promise { + return toggleRegexCommand(accessor); + } +}); + +registerAction2(class TogglePreserveCaseAction extends Action2 { + constructor() { + super({ + id: Constants.TogglePreserveCaseId, + title: { + value: nls.localize('TogglePreserveCaseId.label', 'Toggle Preserve Case'), + original: 'Toggle Preserve Case' + }, + keybinding: Object.assign({ + weight: KeybindingWeight.WorkbenchContrib, + when: Constants.SearchViewFocusedKey, + }, TogglePreserveCaseKeybinding), + category: category.value, + }); + } + + override async run(accessor: ServicesAccessor): Promise { + return togglePreserveCaseCommand(accessor); + } +}); + +//#endregion +//#region Actions: Opening Matches +registerAction2(class OpenMatchAction extends Action2 { + constructor() { + super({ + id: Constants.OpenMatch, + title: { + value: nls.localize('OpenMatch.label', "Open Match"), + original: 'Open Match' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey), + primary: KeyCode.Enter, + mac: { + primary: KeyCode.Enter, + secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow] + }, + }, + }); + } + run(accessor: ServicesAccessor) { + const searchView = getSearchView(accessor.get(IViewsService)); + if (searchView) { + const tree: WorkbenchCompressibleObjectTree = searchView.getControl(); + const viewer = searchView.getControl(); + const focus = tree.getFocus()[0]; + + if (focus instanceof FolderMatch) { + viewer.toggleCollapsed(focus); + } else { + searchView.open(tree.getFocus()[0], false, false, true); + } + } + } +}); + +registerAction2(class OpenMatchToSideAction extends Action2 { + constructor() { + super({ + id: Constants.OpenMatchToSide, + title: { + value: nls.localize('OpenMatchToSide.label', "Open Match To Side"), + original: 'Open Match To Side' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey), + primary: KeyMod.CtrlCmd | KeyCode.Enter, + mac: { + primary: KeyMod.WinCtrl | KeyCode.Enter + }, + }, + }); + } + run(accessor: ServicesAccessor) { + const searchView = getSearchView(accessor.get(IViewsService)); + if (searchView) { + const tree: WorkbenchCompressibleObjectTree = searchView.getControl(); + searchView.open(tree.getFocus()[0], false, true, true); + } + } +}); + +registerAction2(class AddCursorsAtSearchResultsAction extends Action2 { + constructor() { + super({ + id: Constants.AddCursorsAtSearchResults, + title: { + value: nls.localize('AddCursorsAtSearchResults.label', 'Add Cursors at Search Results'), + original: 'Add Cursors at Search Results' + }, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey), + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL, + }, + category: category.value, + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const searchView = getSearchView(accessor.get(IViewsService)); + if (searchView) { + const tree: WorkbenchCompressibleObjectTree = searchView.getControl(); + searchView.openEditorWithMultiCursor(tree.getFocus()[0]); + } + } +}); + +//#endregion +//#region Actions: Toggling Focus +registerAction2(class FocusNextInputAction extends Action2 { + constructor() { + super({ + id: Constants.FocusNextInputActionId, + title: { + value: nls.localize('FocusNextInputAction.label', "Focus Next Input"), + original: 'Focus Next Input' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.or( + ContextKeyExpr.and(SearchEditorConstants.InSearchEditor, Constants.InputBoxFocusedKey), + ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.InputBoxFocusedKey)), + primary: KeyMod.CtrlCmd | KeyCode.DownArrow, + }, + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const input = editorService.activeEditor; + if (input instanceof SearchEditorInput) { + // cast as we cannot import SearchEditor as a value b/c cyclic dependency. + (editorService.activeEditorPane as SearchEditor).focusNextInput(); + } + + const searchView = getSearchView(accessor.get(IViewsService)); + searchView?.focusNextInputBox(); + } +}); + +registerAction2(class FocusPreviousInputAction extends Action2 { + constructor() { + super({ + id: Constants.FocusPreviousInputActionId, + title: { + value: nls.localize('FocusPreviousInputAction.label', "Focus Previous Input"), + original: 'Focus Previous Input' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.or( + ContextKeyExpr.and(SearchEditorConstants.InSearchEditor, Constants.InputBoxFocusedKey), + ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.InputBoxFocusedKey, Constants.SearchInputBoxFocusedKey.toNegated())), + primary: KeyMod.CtrlCmd | KeyCode.UpArrow, + }, + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const input = editorService.activeEditor; + if (input instanceof SearchEditorInput) { + // cast as we cannot import SearchEditor as a value b/c cyclic dependency. + (editorService.activeEditorPane as SearchEditor).focusPrevInput(); + } + + const searchView = getSearchView(accessor.get(IViewsService)); + searchView?.focusPreviousInputBox(); + } +}); + +registerAction2(class FocusSearchFromResultsAction extends Action2 { + constructor() { + super({ + id: Constants.FocusSearchFromResults, + title: { + value: nls.localize('FocusSearchFromResults.label', "Focus Search From Results"), + original: 'Focus Search From Results' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FirstMatchFocusKey), + primary: KeyMod.CtrlCmd | KeyCode.UpArrow, + }, + }); + } + run(accessor: ServicesAccessor) { + const searchView = getSearchView(accessor.get(IViewsService)); + searchView?.focusPreviousInputBox(); + } +}); + +registerAction2(class ToggleSearchOnTypeAction extends Action2 { + private static readonly searchOnTypeKey = 'search.searchOnType'; + + constructor( + ) { + super({ + id: Constants.ToggleSearchOnTypeActionId, + title: { + value: nls.localize('toggleTabs', 'Toggle Search on Type'), + original: 'Toggle Search on Type' + }, + category: category.value, + }); + + } + + override async run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + const searchOnType = configurationService.getValue(ToggleSearchOnTypeAction.searchOnTypeKey); + return configurationService.updateValue(ToggleSearchOnTypeAction.searchOnTypeKey, !searchOnType); + } +}); + +registerAction2(class FocusSearchListCommandAction extends Action2 { + + constructor( + ) { + super({ + id: Constants.FocusSearchListCommandID, + title: { + value: nls.localize('focusSearchListCommandLabel', "Focus List"), + original: 'Focus List' + }, + category: category, + f1: true + }); + } + + override async run(accessor: ServicesAccessor): Promise { + focusSearchListCommand(accessor); + } +}); + +registerAction2(class FocusNextSearchResultAction extends Action2 { + constructor() { + super({ + id: Constants.FocusNextSearchResultActionId, + title: { + value: nls.localize('FocusNextSearchResult.label', 'Focus Next Search Result'), + original: 'Focus Next Search Result' + }, + keybinding: [{ + primary: KeyCode.F4, + weight: KeybindingWeight.WorkbenchContrib, + }], + category: category.value, + + precondition: ContextKeyExpr.or(Constants.HasSearchResults, SearchEditorConstants.InSearchEditor), + }); + } + + override async run(accessor: ServicesAccessor): Promise { + return await focusNextSearchResult(accessor); + } +}); + +registerAction2(class FocusPreviousSearchResultAction extends Action2 { + constructor() { + super({ + id: Constants.FocusPreviousSearchResultActionId, + title: { + value: nls.localize('FocusPreviousSearchResult.label', 'Search: Focus Previous Search Result'), + original: 'Search: Focus Previous Search Result' + }, + keybinding: [{ + primary: KeyMod.Shift | KeyCode.F4, + weight: KeybindingWeight.WorkbenchContrib, + }], + category: category.value, + + precondition: ContextKeyExpr.or(Constants.HasSearchResults, SearchEditorConstants.InSearchEditor), + }); + } + + override async run(accessor: ServicesAccessor): Promise { + return await focusPreviousSearchResult(accessor); + } +}); + +registerAction2(class ReplaceInFilesAction extends Action2 { + constructor() { + super({ + id: Constants.ReplaceInFilesActionId, + title: { + value: nls.localize('replaceInFiles', 'Search: Replace in Files'), + original: 'Search: Replace in Files' + }, + keybinding: [{ + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyH, + weight: KeybindingWeight.WorkbenchContrib, + }], + category: category.value, + menu: [{ + id: MenuId.MenubarEditMenu, + group: '4_find_global', + order: 2 + }], + }); + } + + override async run(accessor: ServicesAccessor): Promise { + return await findOrReplaceInFiles(accessor, true); + } +}); + +//#endregion + +//#region Helpers +function toggleCaseSensitiveCommand(accessor: ServicesAccessor) { + const searchView = getSearchView(accessor.get(IViewsService)); + searchView?.toggleCaseSensitive(); +} + +function toggleWholeWordCommand(accessor: ServicesAccessor) { + const searchView = getSearchView(accessor.get(IViewsService)); + searchView?.toggleWholeWords(); +} + +function toggleRegexCommand(accessor: ServicesAccessor) { + const searchView = getSearchView(accessor.get(IViewsService)); + searchView?.toggleRegex(); +} + +function togglePreserveCaseCommand(accessor: ServicesAccessor) { + const searchView = getSearchView(accessor.get(IViewsService)); + searchView?.togglePreserveCase(); +} + +const focusSearchListCommand: ICommandHandler = accessor => { + const viewsService = accessor.get(IViewsService); + openSearchView(viewsService).then(searchView => { + searchView?.moveFocusToResults(); + }); +}; + +async function focusNextSearchResult(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const input = editorService.activeEditor; + if (input instanceof SearchEditorInput) { + // cast as we cannot import SearchEditor as a value b/c cyclic dependency. + return (editorService.activeEditorPane as SearchEditor).focusNextResult(); + } + + return openSearchView(accessor.get(IViewsService)).then(searchView => { + searchView?.selectNextMatch(); + }); +} + +async function focusPreviousSearchResult(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const input = editorService.activeEditor; + if (input instanceof SearchEditorInput) { + // cast as we cannot import SearchEditor as a value b/c cyclic dependency. + return (editorService.activeEditorPane as SearchEditor).focusPreviousResult(); + } + + return openSearchView(accessor.get(IViewsService)).then(searchView => { + searchView?.selectPreviousMatch(); + }); +} + +async function findOrReplaceInFiles(accessor: ServicesAccessor, expandSearchReplaceWidget: boolean): Promise { + return openSearchView(accessor.get(IViewsService), false).then(openedView => { + if (openedView) { + const searchAndReplaceWidget = openedView.searchAndReplaceWidget; + searchAndReplaceWidget.toggleReplace(expandSearchReplaceWidget); + + const updatedText = openedView.updateTextFromFindWidgetOrSelection({ allowUnselectedWord: !expandSearchReplaceWidget }); + openedView.searchAndReplaceWidget.focus(undefined, updatedText, updatedText); + } + }); +} +//#endregion diff --git a/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts b/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts new file mode 100644 index 00000000000..857f868b810 --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts @@ -0,0 +1,408 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITreeNavigator } from 'vs/base/browser/ui/tree/tree'; +import * as nls from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { getSelectionKeyboardEvent, WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; +import { IViewsService } from 'vs/workbench/common/views'; +import { searchRemoveIcon, searchReplaceIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; +import { SearchView } from 'vs/workbench/contrib/search/browser/searchView'; +import * as Constants from 'vs/workbench/contrib/search/common/constants'; +import { IReplaceService } from 'vs/workbench/contrib/search/common/replace'; +import { arrayContainsElementOrParent, FileMatch, FolderMatch, Match, RenderableMatch, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ISearchConfiguration, ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { category, getElementsToOperateOnInfo, getSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase'; + + +//#region Interfaces +export interface ISearchActionContext { + readonly viewer: WorkbenchCompressibleObjectTree; + readonly element: RenderableMatch; +} + + +export interface IFindInFilesArgs { + query?: string; + replace?: string; + preserveCase?: boolean; + triggerSearch?: boolean; + filesToInclude?: string; + filesToExclude?: string; + isRegex?: boolean; + isCaseSensitive?: boolean; + matchWholeWord?: boolean; + useExcludeSettingsAndIgnoreFiles?: boolean; + onlyOpenEditors?: boolean; +} + +//#endregion + +//#region Actions +registerAction2(class RemoveAction extends Action2 { + + constructor( + ) { + super({ + id: Constants.RemoveActionId, + title: { + value: nls.localize('RemoveAction.label', "Dismiss"), + original: 'Dismiss' + }, + category, + icon: searchRemoveIcon, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey), + primary: KeyCode.Delete, + mac: { + primary: KeyMod.CtrlCmd | KeyCode.Backspace, + }, + }, + menu: [ + { + id: MenuId.SearchContext, + group: 'search', + order: 2, + }, + { + id: MenuId.SearchActionMenu, + group: 'inline', + order: 2, + }, + ] + }); + } + + run(accessor: ServicesAccessor, context: ISearchActionContext | undefined): void { + const viewsService = accessor.get(IViewsService); + const configurationService = accessor.get(IConfigurationService); + const searchView = getSearchView(viewsService); + + if (!searchView) { + return; + } + + let element = context?.element; + let viewer = context?.viewer; + if (!viewer) { + viewer = searchView.getControl(); + } + if (!element) { + element = viewer.getFocus()[0] ?? undefined; + } + + const opInfo = getElementsToOperateOnInfo(viewer, element, configurationService.getValue('search')); + const elementsToRemove = opInfo.elements; + let focusElement = viewer.getFocus()[0] ?? undefined; + + if (elementsToRemove.length === 0) { + return; + } + + if (!focusElement || (focusElement instanceof SearchResult)) { + focusElement = element; + } + + let nextFocusElement; + if (opInfo.mustReselect && focusElement) { + nextFocusElement = getElementToFocusAfterRemoved(viewer, focusElement, elementsToRemove); + } + + const searchResult = searchView.searchResult; + + if (searchResult) { + searchResult.batchRemove(elementsToRemove); + } + + if (opInfo.mustReselect && focusElement) { + if (!nextFocusElement) { + nextFocusElement = getLastNodeFromSameType(viewer, focusElement); + } + + if (nextFocusElement && !arrayContainsElementOrParent(nextFocusElement, elementsToRemove)) { + viewer.reveal(nextFocusElement); + viewer.setFocus([nextFocusElement], getSelectionKeyboardEvent()); + viewer.setSelection([nextFocusElement], getSelectionKeyboardEvent()); + } + } + + viewer.domFocus(); + return; + } +}); + +registerAction2(class ReplaceAction extends Action2 { + constructor( + ) { + super({ + id: Constants.ReplaceActionId, + title: { + value: nls.localize('match.replace.label', "Replace"), + original: 'Replace' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.MatchFocusKey), + primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.Digit1, + }, + icon: searchReplaceIcon, + menu: [ + { + id: MenuId.SearchContext, + when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.MatchFocusKey), + group: 'search', + order: 1 + }, + { + id: MenuId.SearchActionMenu, + when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.MatchFocusKey), + group: 'inline', + order: 1 + } + ] + }); + } + + override async run(accessor: ServicesAccessor, context: ISearchActionContext | undefined): Promise { + return performReplace(accessor, context); + } +}); + +registerAction2(class ReplaceAllAction extends Action2 { + + constructor( + ) { + super({ + id: Constants.ReplaceAllInFileActionId, + title: { + value: nls.localize('file.replaceAll.label', "Replace All"), + original: 'Replace All' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.FileFocusKey), + primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.Digit1, + secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter], + }, + icon: searchReplaceIcon, + menu: [ + { + id: MenuId.SearchContext, + when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FileFocusKey), + group: 'search', + order: 1 + }, + { + id: MenuId.SearchActionMenu, + when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FileFocusKey), + group: 'inline', + order: 1 + } + ] + }); + } + + override async run(accessor: ServicesAccessor, context: ISearchActionContext | undefined): Promise { + return performReplace(accessor, context); + } +}); + +registerAction2(class ReplaceAllInFolderAction extends Action2 { + constructor( + ) { + super({ + id: Constants.ReplaceAllInFolderActionId, + title: { + value: nls.localize('file.replaceAll.label', "Replace All"), + original: 'Replace All' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.FolderFocusKey), + primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.Digit1, + secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter], + }, + icon: searchReplaceIcon, + menu: [ + { + id: MenuId.SearchContext, + when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FolderFocusKey), + group: 'search', + order: 1 + }, + { + id: MenuId.SearchActionMenu, + when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FolderFocusKey), + group: 'inline', + order: 1 + } + ] + }); + } + + override async run(accessor: ServicesAccessor, context: ISearchActionContext | undefined): Promise { + return performReplace(accessor, context); + } +}); + +//#endregion + +//#region Helpers + +function performReplace(accessor: ServicesAccessor, + context: ISearchActionContext | undefined): void { + const configurationService = accessor.get(IConfigurationService); + const viewsService = accessor.get(IViewsService); + + const viewlet: SearchView | undefined = getSearchView(viewsService); + const viewer: WorkbenchCompressibleObjectTree | undefined = context?.viewer ?? viewlet?.getControl(); + + if (!viewer) { + return; + } + const element: RenderableMatch | null = context?.element ?? viewer.getFocus()[0]; + + // since multiple elements can be selected, we need to check the type of the FolderMatch/FileMatch/Match before we perform the replace. + const opInfo = getElementsToOperateOnInfo(viewer, element ?? undefined, configurationService.getValue('search')); + const elementsToReplace = opInfo.elements; + let focusElement = viewer.getFocus()[0]; + + if (!focusElement || (focusElement && !arrayContainsElementOrParent(focusElement, elementsToReplace)) || (focusElement instanceof SearchResult)) { + focusElement = element; + } + + if (elementsToReplace.length === 0) { + return; + } + let nextFocusElement; + if (focusElement) { + nextFocusElement = getElementToFocusAfterRemoved(viewer, focusElement, elementsToReplace); + } + + const searchResult = viewlet?.searchResult; + + if (searchResult) { + searchResult.batchReplace(elementsToReplace); + } + + if (focusElement) { + if (!nextFocusElement) { + nextFocusElement = getLastNodeFromSameType(viewer, focusElement); + } + + if (nextFocusElement) { + viewer.reveal(nextFocusElement); + viewer.setFocus([nextFocusElement], getSelectionKeyboardEvent()); + viewer.setSelection([nextFocusElement], getSelectionKeyboardEvent()); + + if (nextFocusElement instanceof Match) { + const useReplacePreview = configurationService.getValue().search.useReplacePreview; + if (!useReplacePreview || hasToOpenFile(accessor, nextFocusElement)) { + viewlet?.open(nextFocusElement, true); + } else { + accessor.get(IReplaceService).openReplacePreview(nextFocusElement, true); + } + } else if (nextFocusElement instanceof FileMatch) { + viewlet?.open(nextFocusElement, true); + } + } + + } + + viewer.domFocus(); +} + +function hasToOpenFile(accessor: ServicesAccessor, currBottomElem: RenderableMatch): boolean { + if (!(currBottomElem instanceof Match)) { + return false; + } + const activeEditor = accessor.get(IEditorService).activeEditor; + const file = activeEditor?.resource; + if (file) { + return accessor.get(IUriIdentityService).extUri.isEqual(file, currBottomElem.parent().resource); + } + return false; +} + +function compareLevels(elem1: RenderableMatch, elem2: RenderableMatch) { + if (elem1 instanceof Match) { + if (elem2 instanceof Match) { + return 0; + } else { + return -1; + } + + } else if (elem1 instanceof FileMatch) { + if (elem2 instanceof Match) { + return 1; + } else if (elem2 instanceof FileMatch) { + return 0; + } else { + return -1; + } + + } else { + // FolderMatch + if (elem2 instanceof FolderMatch) { + return 0; + } else { + return 1; + } + } +} + +/** + * Returns element to focus after removing the given element + */ +export function getElementToFocusAfterRemoved(viewer: WorkbenchCompressibleObjectTree, element: RenderableMatch, elementsToRemove: RenderableMatch[]): RenderableMatch | undefined { + const navigator: ITreeNavigator = viewer.navigate(element); + if (element instanceof FolderMatch) { + while (!!navigator.next() && (!(navigator.current() instanceof FolderMatch) || arrayContainsElementOrParent(navigator.current(), elementsToRemove))) { } + } else if (element instanceof FileMatch) { + while (!!navigator.next() && (!(navigator.current() instanceof FileMatch) || arrayContainsElementOrParent(navigator.current(), elementsToRemove))) { + viewer.expand(navigator.current()); + } + } else { + while (navigator.next() && (!(navigator.current() instanceof Match) || arrayContainsElementOrParent(navigator.current(), elementsToRemove))) { + viewer.expand(navigator.current()); + } + } + return navigator.current(); +} + +/*** + * Finds the last element in the tree with the same type as `element` + */ +export function getLastNodeFromSameType(viewer: WorkbenchCompressibleObjectTree, element: RenderableMatch): RenderableMatch | undefined { + let lastElem: RenderableMatch | null = viewer.lastVisibleElement ?? null; + + while (lastElem) { + const compareVal = compareLevels(element, lastElem); + if (compareVal === -1) { + viewer.expand(lastElem); + lastElem = viewer.lastVisibleElement; + } else if (compareVal === 1) { + lastElem = viewer.getParentElement(lastElem); + } else { + return lastElem; + } + } + + return undefined; +} + +//#endregion + diff --git a/src/vs/workbench/contrib/search/browser/searchActionsSymbol.ts b/src/vs/workbench/contrib/search/browser/searchActionsSymbol.ts new file mode 100644 index 00000000000..e6dec6fab8a --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/searchActionsSymbol.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import * as Constants from 'vs/workbench/contrib/search/common/constants'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; + +//#region Actions +registerAction2(class ShowAllSymbolsAction extends Action2 { + + static readonly ID = 'workbench.action.showAllSymbols'; + static readonly LABEL = nls.localize('showTriggerActions', "Go to Symbol in Workspace..."); + static readonly ALL_SYMBOLS_PREFIX = '#'; + + constructor( + ) { + super({ + id: Constants.ShowAllSymbolsActionId, + title: { + value: nls.localize('showTriggerActions', "Go to Symbol in Workspace..."), + original: 'Go to Symbol in Workspace...', + mnemonicTitle: nls.localize({ key: 'miGotoSymbolInWorkspace', comment: ['&& denotes a mnemonic'] }, "Go to Symbol in &&Workspace...") + }, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KeyT + }, + menu: { + id: MenuId.MenubarGoMenu, + group: '3_global_nav', + order: 2 + } + }); + } + + override async run(accessor: ServicesAccessor): Promise { + accessor.get(IQuickInputService).quickAccess.show(ShowAllSymbolsAction.ALL_SYMBOLS_PREFIX); + } +}); + +//#endregion diff --git a/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts b/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts new file mode 100644 index 00000000000..9c9d8c92057 --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts @@ -0,0 +1,348 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { ICommandHandler } from 'vs/platform/commands/common/commands'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; +import { IViewsService } from 'vs/workbench/common/views'; +import { searchClearIcon, searchCollapseAllIcon, searchExpandAllIcon, searchRefreshIcon, searchShowAsList, searchShowAsTree, searchStopIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; +import * as Constants from 'vs/workbench/contrib/search/common/constants'; +import { ISearchHistoryService } from 'vs/workbench/contrib/search/common/searchHistoryService'; +import { FileMatch, FolderMatch, FolderMatchNoRoot, FolderMatchWorkspaceRoot, Match, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; +import { VIEW_ID } from 'vs/workbench/services/search/common/search'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { SearchStateKey, SearchUIState } from 'vs/workbench/contrib/search/common/search'; +import { category, getSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase'; + +//#region Actions +registerAction2(class ClearSearchHistoryCommandAction extends Action2 { + + constructor( + ) { + super({ + id: Constants.ClearSearchHistoryCommandId, + title: { + value: nls.localize('clearSearchHistoryLabel', "Clear Search History"), + original: 'Clear Search History' + }, + category: category, + f1: true + }); + + } + + override async run(accessor: ServicesAccessor): Promise { + clearHistoryCommand(accessor); + } +}); + +registerAction2(class CancelSearchAction extends Action2 { + constructor() { + super({ + id: Constants.CancelSearchActionId, + title: { + value: nls.localize('CancelSearchAction.label', "Cancel Search"), + original: 'Cancel Search' + }, + icon: searchStopIcon, + category, + f1: true, + precondition: SearchStateKey.isEqualTo(SearchUIState.Idle).negate(), + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, WorkbenchListFocusContextKey), + primary: KeyCode.Escape, + }, + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 0, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), SearchStateKey.isEqualTo(SearchUIState.SlowSearch)), + }] + }); + } + run(accessor: ServicesAccessor) { + return cancelSearch(accessor); + } +}); + +registerAction2(class RefreshAction extends Action2 { + constructor() { + super({ + id: Constants.RefreshSearchResultsActionId, + title: { + value: nls.localize('RefreshAction.label', "Refresh"), + original: 'Refresh' + }, + icon: searchRefreshIcon, + precondition: Constants.ViewHasSearchPatternKey, + category, + f1: true, + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 0, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), SearchStateKey.isEqualTo(SearchUIState.SlowSearch).negate()), + }] + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + return refreshSearch(accessor); + } +}); + +registerAction2(class CollapseDeepestExpandedLevelAction extends Action2 { + constructor() { + super({ + id: Constants.CollapseSearchResultsActionId, + title: { + value: nls.localize('CollapseDeepestExpandedLevelAction.label', "Collapse All"), + original: 'Collapse All' + }, + category, + icon: searchCollapseAllIcon, + f1: true, + precondition: ContextKeyExpr.and(Constants.HasSearchResults, Constants.ViewHasSomeCollapsibleKey), + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 3, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), ContextKeyExpr.or(Constants.HasSearchResults.negate(), Constants.ViewHasSomeCollapsibleKey)), + }] + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + return collapseDeepestExpandedLevel(accessor); + } +}); + +registerAction2(class ExpandAllAction extends Action2 { + constructor() { + super({ + id: Constants.ExpandSearchResultsActionId, + title: { + value: nls.localize('ExpandAllAction.label', "Expand All"), + original: 'Expand All' + }, + category, + icon: searchExpandAllIcon, + f1: true, + precondition: ContextKeyExpr.and(Constants.HasSearchResults, Constants.ViewHasSomeCollapsibleKey.toNegated()), + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 3, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), Constants.HasSearchResults, Constants.ViewHasSomeCollapsibleKey.toNegated()), + }] + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + return expandAll(accessor); + } +}); + +registerAction2(class ClearSearchResultsAction extends Action2 { + constructor() { + super({ + id: Constants.ClearSearchResultsActionId, + title: { + value: nls.localize('ClearSearchResultsAction.label', "Clear Search Results"), + original: 'Clear Search Results' + }, + category, + icon: searchClearIcon, + f1: true, + precondition: ContextKeyExpr.or(Constants.HasSearchResults, Constants.ViewHasSearchPatternKey, Constants.ViewHasReplacePatternKey, Constants.ViewHasFilePatternKey), + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 1, + when: ContextKeyExpr.equals('view', VIEW_ID), + }] + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + return clearSearchResults(accessor); + } +}); + + +registerAction2(class ViewAsTreeAction extends Action2 { + constructor() { + super({ + id: Constants.ViewAsTreeActionId, + title: { + value: nls.localize('ViewAsTreeAction.label', "View as Tree"), + original: 'View as Tree' + }, + category, + icon: searchShowAsList, + f1: true, + precondition: ContextKeyExpr.and(Constants.HasSearchResults, Constants.InTreeViewKey.toNegated()), + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 2, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), Constants.InTreeViewKey.toNegated()), + }] + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + const searchView = getSearchView(accessor.get(IViewsService)); + if (searchView) { + searchView.setTreeView(true); + } + } +}); + +registerAction2(class ViewAsListAction extends Action2 { + constructor() { + super({ + id: Constants.ViewAsListActionId, + title: { + value: nls.localize('ViewAsListAction.label', "View as List"), + original: 'View as List' + }, + category, + icon: searchShowAsTree, + f1: true, + precondition: ContextKeyExpr.and(Constants.HasSearchResults, Constants.InTreeViewKey), + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 2, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), Constants.InTreeViewKey), + }] + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + const searchView = getSearchView(accessor.get(IViewsService)); + if (searchView) { + searchView.setTreeView(false); + } + } +}); + +//#endregion + +//#region Helpers +const clearHistoryCommand: ICommandHandler = accessor => { + const searchHistoryService = accessor.get(ISearchHistoryService); + searchHistoryService.clearHistory(); +}; + +function expandAll(accessor: ServicesAccessor) { + const viewsService = accessor.get(IViewsService); + const searchView = getSearchView(viewsService); + if (searchView) { + const viewer = searchView.getControl(); + viewer.expandAll(); + } +} + +function clearSearchResults(accessor: ServicesAccessor) { + const viewsService = accessor.get(IViewsService); + const searchView = getSearchView(viewsService); + searchView?.clearSearchResults(); +} + +function cancelSearch(accessor: ServicesAccessor) { + const viewsService = accessor.get(IViewsService); + const searchView = getSearchView(viewsService); + searchView?.cancelSearch(); +} + +function refreshSearch(accessor: ServicesAccessor) { + const viewsService = accessor.get(IViewsService); + const searchView = getSearchView(viewsService); + searchView?.triggerQueryChange({ preserveFocus: false }); +} + +function collapseDeepestExpandedLevel(accessor: ServicesAccessor) { + + const viewsService = accessor.get(IViewsService); + const searchView = getSearchView(viewsService); + if (searchView) { + const viewer = searchView.getControl(); + + /** + * one level to collapse so collapse everything. If FolderMatch, check if there are visible grandchildren, + * i.e. if Matches are returned by the navigator, and if so, collapse to them, otherwise collapse all levels. + */ + const navigator = viewer.navigate(); + let node = navigator.first(); + let canCollapseFileMatchLevel = false; + let canCollapseFirstLevel = false; + + if (node instanceof FolderMatchWorkspaceRoot) { + while (node = navigator.next()) { + if (node instanceof Match) { + canCollapseFileMatchLevel = true; + break; + } + if (searchView.isTreeLayoutViewVisible && !canCollapseFirstLevel) { + let nodeToTest = node; + + if (node instanceof FolderMatch) { + nodeToTest = node.compressionStartParent ?? node; + } + + const immediateParent = nodeToTest.parent(); + + if (!(immediateParent instanceof FolderMatchWorkspaceRoot || immediateParent instanceof FolderMatchNoRoot || immediateParent instanceof SearchResult)) { + canCollapseFirstLevel = true; + } + } + } + } + + if (canCollapseFileMatchLevel) { + node = navigator.first(); + do { + if (node instanceof FileMatch) { + viewer.collapse(node); + } + } while (node = navigator.next()); + } else if (canCollapseFirstLevel) { + node = navigator.first(); + if (node) { + do { + + let nodeToTest = node; + + if (node instanceof FolderMatch) { + nodeToTest = node.compressionStartParent ?? node; + } + const immediateParent = nodeToTest.parent(); + + if (immediateParent instanceof FolderMatchWorkspaceRoot || immediateParent instanceof FolderMatchNoRoot) { + if (viewer.hasElement(node)) { + viewer.collapse(node, true); + } else { + viewer.collapseAll(); + } + } + } while (node = navigator.next()); + } + } else { + viewer.collapseAll(); + } + + const firstFocusParent = viewer.getFocus()[0]?.parent(); + + if (firstFocusParent && (firstFocusParent instanceof FolderMatch || firstFocusParent instanceof FileMatch) && + viewer.hasElement(firstFocusParent) && viewer.isCollapsed(firstFocusParent)) { + viewer.domFocus(); + viewer.focusFirst(); + viewer.setSelection(viewer.getFocus()); + } + } +} + +//#endregion diff --git a/src/vs/workbench/contrib/search/browser/searchResultsView.ts b/src/vs/workbench/contrib/search/browser/searchResultsView.ts index 265e89bc965..d7a75dc5a3a 100644 --- a/src/vs/workbench/contrib/search/browser/searchResultsView.ts +++ b/src/vs/workbench/contrib/search/browser/searchResultsView.ts @@ -4,35 +4,38 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { ITreeNode } from 'vs/base/browser/ui/tree/tree'; -import { IAction } from 'vs/base/common/actions'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import * as paths from 'vs/base/common/path'; import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { FileKind } from 'vs/platform/files/common/files'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; -import { RemoveAction, ReplaceAction, ReplaceAllAction, ReplaceAllInFolderAction } from 'vs/workbench/contrib/search/browser/searchActions'; import { SearchView } from 'vs/workbench/contrib/search/browser/searchView'; import { FileMatch, Match, RenderableMatch, SearchModel, FolderMatch, FolderMatchNoRoot, FolderMatchWorkspaceRoot } from 'vs/workbench/contrib/search/common/searchModel'; import { isEqual } from 'vs/base/common/resources'; import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { HiddenItemStrategy, MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; +import { ISearchActionContext } from 'vs/workbench/contrib/search/browser/searchActionsRemoveReplace'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { FileFocusKey, FolderFocusKey, MatchFocusKey } from 'vs/workbench/contrib/search/common/constants'; interface IFolderMatchTemplate { label: IResourceLabel; badge: CountBadge; - actions: ActionBar; + actions: MenuWorkbenchToolBar; disposables: DisposableStore; disposableActions: DisposableStore; } @@ -41,7 +44,7 @@ interface IFileMatchTemplate { el: HTMLElement; label: IResourceLabel; badge: CountBadge; - actions: ActionBar; + actions: MenuWorkbenchToolBar; disposables: DisposableStore; } @@ -52,7 +55,8 @@ interface IMatchTemplate { replace: HTMLElement; after: HTMLElement; lineNumber: HTMLElement; - actions: ActionBar; + actions: MenuWorkbenchToolBar; + disposables: DisposableStore; } export class SearchDelegate implements IListVirtualDelegate { @@ -82,13 +86,13 @@ export class FolderMatchRenderer extends Disposable implements ICompressibleTree readonly templateId = FolderMatchRenderer.TEMPLATE_ID; constructor( - private searchModel: SearchModel, private searchView: SearchView, private labels: ResourceLabels, - @IInstantiationService private readonly instantiationService: IInstantiationService, @IThemeService private readonly themeService: IThemeService, @IWorkspaceContextService protected contextService: IWorkspaceContextService, @ILabelService private readonly labelService: ILabelService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { super(); } @@ -109,8 +113,6 @@ export class FolderMatchRenderer extends Disposable implements ICompressibleTree templateData.label.setLabel(nls.localize('searchFolderMatch.other.label', "Other files")); } - templateData.actions.clear(); - templateData.actions.context = folder; this.renderFolderDetails(folder, templateData); } @@ -123,12 +125,22 @@ export class FolderMatchRenderer extends Disposable implements ICompressibleTree const badge = new CountBadge(DOM.append(folderMatchElement, DOM.$('.badge'))); disposables.add(attachBadgeStyler(badge, this.themeService)); const actionBarContainer = DOM.append(folderMatchElement, DOM.$('.actionBarContainer')); - const actions = new ActionBar(actionBarContainer, { animated: false }); - disposables.add(actions); const disposableElements = new DisposableStore(); disposables.add(disposableElements); + const contextKeyService = this.contextKeyService.createOverlay([[FolderFocusKey.key, true]]); + const instantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService])); + const actions = disposables.add(instantiationService.createInstance(MenuWorkbenchToolBar, actionBarContainer, MenuId.SearchActionMenu, { + menuOptions: { + shouldForwardArgs: true + }, + hiddenItemStrategy: HiddenItemStrategy.Ignore, + toolbarOptions: { + primaryGroup: g => /^inline/.test(g), + }, + })); + return { label, badge, @@ -151,7 +163,6 @@ export class FolderMatchRenderer extends Disposable implements ICompressibleTree } else { templateData.label.setLabel(nls.localize('searchFolderMatch.other.label', "Other files")); } - templateData.actions.clear(); this.renderFolderDetails(folderMatch, templateData); } @@ -172,16 +183,7 @@ export class FolderMatchRenderer extends Disposable implements ICompressibleTree templateData.badge.setCount(count); templateData.badge.setTitleFormat(count > 1 ? nls.localize('searchFileMatches', "{0} files found", count) : nls.localize('searchFileMatch', "{0} file found", count)); - const actions: IAction[] = []; - if (this.searchModel.isReplaceActive() && count > 0) { - const replaceAction = this.instantiationService.createInstance(ReplaceAllInFolderAction, this.searchView.getControl(), folder); - actions.push(replaceAction); - templateData.disposableActions.add(replaceAction); - } - const removeAction = this.instantiationService.createInstance(RemoveAction, this.searchView.getControl(), folder); - actions.push(removeAction); - - templateData.actions.push(actions, { icon: true, label: false }); + templateData.actions.context = { viewer: this.searchView.getControl(), element: folder }; } } @@ -191,13 +193,13 @@ export class FileMatchRenderer extends Disposable implements ICompressibleTreeRe readonly templateId = FileMatchRenderer.TEMPLATE_ID; constructor( - private searchModel: SearchModel, private searchView: SearchView, private labels: ResourceLabels, - @IInstantiationService private readonly instantiationService: IInstantiationService, @IThemeService private readonly themeService: IThemeService, @IWorkspaceContextService protected contextService: IWorkspaceContextService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { super(); } @@ -214,15 +216,25 @@ export class FileMatchRenderer extends Disposable implements ICompressibleTreeRe const badge = new CountBadge(DOM.append(fileMatchElement, DOM.$('.badge'))); disposables.add(attachBadgeStyler(badge, this.themeService)); const actionBarContainer = DOM.append(fileMatchElement, DOM.$('.actionBarContainer')); - const actions = new ActionBar(actionBarContainer, { animated: false }); - disposables.add(actions); + + const contextKeyService = this.contextKeyService.createOverlay([[FileFocusKey.key, true]]); + const instantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService])); + const actions = disposables.add(instantiationService.createInstance(MenuWorkbenchToolBar, actionBarContainer, MenuId.SearchActionMenu, { + menuOptions: { + shouldForwardArgs: true + }, + hiddenItemStrategy: HiddenItemStrategy.Ignore, + toolbarOptions: { + primaryGroup: g => /^inline/.test(g), + }, + })); return { el: fileMatchElement, label, badge, actions, - disposables + disposables, }; } @@ -236,14 +248,7 @@ export class FileMatchRenderer extends Disposable implements ICompressibleTreeRe templateData.badge.setCount(count); templateData.badge.setTitleFormat(count > 1 ? nls.localize('searchMatches', "{0} matches found", count) : nls.localize('searchMatch', "{0} match found", count)); - templateData.actions.clear(); - - const actions: IAction[] = []; - if (this.searchModel.isReplaceActive() && count > 0) { - actions.push(this.instantiationService.createInstance(ReplaceAllAction, this.searchView, fileMatch)); - } - actions.push(this.instantiationService.createInstance(RemoveAction, this.searchView.getControl(), fileMatch)); - templateData.actions.push(actions, { icon: true, label: false }); + templateData.actions.context = { viewer: this.searchView.getControl(), element: fileMatch }; } disposeElement(element: ITreeNode, index: number, templateData: IFileMatchTemplate): void { @@ -262,9 +267,10 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender constructor( private searchModel: SearchModel, private searchView: SearchView, - @IInstantiationService private readonly instantiationService: IInstantiationService, @IWorkspaceContextService protected contextService: IWorkspaceContextService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { super(); } @@ -282,7 +288,20 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender const after = DOM.append(parent, DOM.$('span')); const lineNumber = DOM.append(container, DOM.$('span.matchLineNum')); const actionBarContainer = DOM.append(container, DOM.$('span.actionBarContainer')); - const actions = new ActionBar(actionBarContainer, { animated: false }); + + const disposables = new DisposableStore(); + + const contextKeyService = this.contextKeyService.createOverlay([[MatchFocusKey.key, true]]); + const instantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService])); + const actions = disposables.add(instantiationService.createInstance(MenuWorkbenchToolBar, actionBarContainer, MenuId.SearchActionMenu, { + menuOptions: { + shouldForwardArgs: true + }, + hiddenItemStrategy: HiddenItemStrategy.Ignore, + toolbarOptions: { + primaryGroup: g => /^inline/.test(g), + }, + })); return { parent, @@ -291,7 +310,8 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender replace, after, lineNumber, - actions + actions, + disposables, }; } @@ -317,19 +337,12 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender templateData.lineNumber.textContent = lineNumberStr + extraLinesStr; templateData.lineNumber.setAttribute('title', this.getMatchTitle(match, showLineNumbers)); - templateData.actions.clear(); - if (this.searchModel.isReplaceActive()) { - templateData.actions.push([this.instantiationService.createInstance(ReplaceAction, this.searchView.getControl(), match, this.searchView), this.instantiationService.createInstance(RemoveAction, this.searchView.getControl(), match)], { icon: true, label: false }); - } else { - templateData.actions.push([this.instantiationService.createInstance(RemoveAction, this.searchView.getControl(), match)], { icon: true, label: false }); - } - } + templateData.actions.context = { viewer: this.searchView.getControl(), element: match }; - disposeElement(element: ITreeNode, index: number, templateData: IMatchTemplate): void { } disposeTemplate(templateData: IMatchTemplate): void { - templateData.actions.dispose(); + templateData.disposables.dispose(); } private getMatchTitle(match: Match, showLineNumbers: boolean): string { diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 30454a8e988..48b8b31194e 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -62,7 +62,8 @@ import { IViewDescriptorService } from 'vs/workbench/common/views'; import { NotebookFindContrib } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget'; import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; import { ExcludePatternInputWidget, IncludePatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget'; -import { appendKeyBindingLabel, IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActions'; +import { appendKeyBindingLabel } from 'vs/workbench/contrib/search/browser/searchActionsBase'; +import { IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActionsFind'; import { searchDetailsIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; import { renderSearchMessage } from 'vs/workbench/contrib/search/browser/searchMessage'; import { FileMatchRenderer, FolderMatchRenderer, MatchRenderer, SearchAccessibilityProvider, SearchDelegate } from 'vs/workbench/contrib/search/browser/searchResultsView'; @@ -750,8 +751,8 @@ export class SearchView extends ViewPane { this.resultsElement, delegate, [ - this._register(this.instantiationService.createInstance(FolderMatchRenderer, this.viewModel, this, this.treeLabels)), - this._register(this.instantiationService.createInstance(FileMatchRenderer, this.viewModel, this, this.treeLabels)), + this._register(this.instantiationService.createInstance(FolderMatchRenderer, this, this.treeLabels)), + this._register(this.instantiationService.createInstance(FileMatchRenderer, this, this.treeLabels)), this._register(this.instantiationService.createInstance(MatchRenderer, this.viewModel, this)), ], { diff --git a/src/vs/workbench/contrib/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index f80bf8d2e57..578c1d2f74a 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -27,7 +27,7 @@ import { ISearchConfigurationProperties } from 'vs/workbench/services/search/com import { attachFindReplaceInputBoxStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ContextScopedFindInput, ContextScopedReplaceInput } from 'vs/platform/history/browser/contextScopedHistoryWidget'; -import { appendKeyBindingLabel, isSearchViewFocused, getSearchView } from 'vs/workbench/contrib/search/browser/searchActions'; +import { appendKeyBindingLabel, isSearchViewFocused, getSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase'; import * as Constants from 'vs/workbench/contrib/search/common/constants'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { isMacintosh } from 'vs/base/common/platform'; diff --git a/src/vs/workbench/contrib/search/common/constants.ts b/src/vs/workbench/contrib/search/common/constants.ts index 9e5df9fd94e..f0700ce6a99 100644 --- a/src/vs/workbench/contrib/search/common/constants.ts +++ b/src/vs/workbench/contrib/search/common/constants.ts @@ -42,6 +42,11 @@ export const ViewAsTreeActionId = 'search.action.viewAsTree'; export const ViewAsListActionId = 'search.action.viewAsList'; export const ToggleQueryDetailsActionId = 'workbench.action.search.toggleQueryDetails'; export const ExcludeFolderFromSearchId = 'search.action.excludeFromSearch'; +export const FocusNextInputActionId = 'search.focus.nextInputBox'; +export const FocusPreviousInputActionId = 'search.focus.previousInputBox'; +export const RestrictSearchToFolderId = 'search.action.restrictSearchToFolder'; +export const FindInFolderId = 'filesExplorer.findInFolder'; +export const FindInWorkspaceId = 'filesExplorer.findInWorkspace'; export const SearchViewVisibleKey = new RawContextKey('searchViewletVisible', true); export const SearchViewFocusedKey = new RawContextKey('searchViewletFocus', false); diff --git a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts index f8103e8652c..783ee335c4c 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts @@ -15,7 +15,7 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; import { IFileMatch } from 'vs/workbench/services/search/common/search'; -import { getElementToFocusAfterRemoved, getLastNodeFromSameType } from 'vs/workbench/contrib/search/browser/searchActions'; +import { getElementToFocusAfterRemoved, getLastNodeFromSameType } from 'vs/workbench/contrib/search/browser/searchActionsRemoveReplace'; import { FileMatch, FileMatchOrMatch, FolderMatch, Match, SearchModel } from 'vs/workbench/contrib/search/common/searchModel'; import { MockObjectTree } from 'vs/workbench/contrib/search/test/browser/mockSearchTree'; import { IThemeService } from 'vs/platform/theme/common/themeService'; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index b395623dc08..5540e023a3d 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts @@ -23,7 +23,7 @@ import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchCo import { IEditorSerializer, IEditorFactoryRegistry, EditorExtensions, DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor'; import { ActiveEditorContext } from 'vs/workbench/common/contextkeys'; import { IViewsService } from 'vs/workbench/common/views'; -import { getSearchView } from 'vs/workbench/contrib/search/browser/searchActions'; +import { getSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase'; import { searchNewEditorIcon, searchRefreshIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; import * as SearchConstants from 'vs/workbench/contrib/search/common/constants'; import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants'; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts index e1d2e65956d..b5a7b7c9980 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts @@ -16,7 +16,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { EditorsOrder } from 'vs/workbench/common/editor'; import { IViewsService } from 'vs/workbench/common/views'; -import { getSearchView } from 'vs/workbench/contrib/search/browser/searchActions'; +import { getSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase'; import { SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; import { OpenSearchEditorArgs } from 'vs/workbench/contrib/searchEditor/browser/searchEditor.contribution'; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index e3b2e292c52..877d7af2671 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -33,7 +33,7 @@ import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspac import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; import { CLOSE_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; import { ResourceContextKey } from 'vs/workbench/common/contextkeys'; -import { FindInFilesCommand, IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActions'; +import { findInFilesCommand, IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActionsFind'; import { Direction, ICreateTerminalOptions, IInternalXtermTerminal, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalInstanceService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalQuickAccessProvider } from 'vs/workbench/contrib/terminal/browser/terminalQuickAccess'; import { IRemoteTerminalAttachTarget, ITerminalConfigHelper, ITerminalProfileService, TerminalCommandId, TERMINAL_ACTION_CATEGORY } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -1610,7 +1610,7 @@ export function registerTerminalActions() { } run(accessor: ServicesAccessor) { const query = accessor.get(ITerminalService).activeInstance?.selection; - FindInFilesCommand(accessor, { query } as IFindInFilesArgs); + findInFilesCommand(accessor, { query } as IFindInFilesArgs); } }); registerAction2(class extends Action2 {