From 7fdf1e3b50b57df7c0ab54cf4012ae1880aed01c Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 13 Mar 2018 16:37:03 -0700 Subject: [PATCH] Fix #45702 - merge include/exclude inputs --- .../parts/search/browser/searchActions.ts | 42 ------- .../parts/search/browser/searchView.ts | 106 +++++------------- .../parts/search/common/queryBuilder.ts | 22 ++++ .../electron-browser/search.contribution.ts | 5 +- .../search/test/common/queryBuilder.test.ts | 45 ++++++++ 5 files changed, 97 insertions(+), 123 deletions(-) diff --git a/src/vs/workbench/parts/search/browser/searchActions.ts b/src/vs/workbench/parts/search/browser/searchActions.ts index a64c73b739ca4..76608dee1a930 100644 --- a/src/vs/workbench/parts/search/browser/searchActions.ts +++ b/src/vs/workbench/parts/search/browser/searchActions.ts @@ -121,48 +121,6 @@ export class ShowPreviousSearchIncludeAction extends Action { } } -export class ShowNextSearchExcludeAction extends Action { - - public static readonly ID = 'search.history.showNextExcludePattern'; - public static readonly LABEL = nls.localize('nextSearchExcludePattern', "Show Next Search Exclude Pattern"); - - constructor(id: string, label: string, - @IViewletService private viewletService: IViewletService, - @IPanelService private panelService: IPanelService, - @IContextKeyService private contextKeyService: IContextKeyService - ) { - super(id, label); - this.enabled = this.contextKeyService.contextMatchesRules(Constants.SearchViewVisibleKey); - } - - public run(): TPromise { - const searchView = getSearchView(this.viewletService, this.panelService); - searchView.searchExcludePattern.showNextTerm(); - return TPromise.as(null); - } -} - -export class ShowPreviousSearchExcludeAction extends Action { - - public static readonly ID = 'search.history.showPreviousExcludePattern'; - public static readonly LABEL = nls.localize('previousSearchExcludePattern', "Show Previous Search Exclude Pattern"); - - constructor(id: string, label: string, - @IViewletService private viewletService: IViewletService, - @IContextKeyService private contextKeyService: IContextKeyService, - @IPanelService private panelService: IPanelService - ) { - super(id, label); - this.enabled = this.contextKeyService.contextMatchesRules(Constants.SearchViewVisibleKey); - } - - public run(): TPromise { - const searchView = getSearchView(this.viewletService, this.panelService); - searchView.searchExcludePattern.showPreviousTerm(); - return TPromise.as(null); - } -} - export class ShowNextSearchTermAction extends Action { public static readonly ID = 'search.history.showNext'; diff --git a/src/vs/workbench/parts/search/browser/searchView.ts b/src/vs/workbench/parts/search/browser/searchView.ts index 6d3c32171f522..fde99b17e98c6 100644 --- a/src/vs/workbench/parts/search/browser/searchView.ts +++ b/src/vs/workbench/parts/search/browser/searchView.ts @@ -76,7 +76,6 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { private viewletVisible: IContextKey; private inputBoxFocused: IContextKey; private inputPatternIncludesFocused: IContextKey; - private inputPatternExclusionsFocused: IContextKey; private firstMatchFocused: IContextKey; private fileMatchOrMatchFocused: IContextKey; private fileMatchFocused: IContextKey; @@ -94,8 +93,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { private searchWidget: SearchWidget; private size: Dimension; private queryDetails: HTMLElement; - private inputPatternExcludes: ExcludePatternInputWidget; - private inputPatternIncludes: PatternInputWidget; + private inputPatternIncludes: ExcludePatternInputWidget; private results: Builder; private currentSelectedFileMatch: FileMatch; @@ -132,7 +130,6 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.viewletVisible = Constants.SearchViewVisibleKey.bindTo(contextKeyService); this.inputBoxFocused = Constants.InputBoxFocusedKey.bindTo(this.contextKeyService); this.inputPatternIncludesFocused = Constants.PatternIncludesFocusedKey.bindTo(this.contextKeyService); - this.inputPatternExclusionsFocused = Constants.PatternExcludesFocusedKey.bindTo(this.contextKeyService); this.firstMatchFocused = Constants.FirstMatchFocusKey.bindTo(contextKeyService); this.fileMatchOrMatchFocused = Constants.FileMatchOrMatchFocusKey.bindTo(contextKeyService); this.fileMatchFocused = Constants.FileFocusKey.bindTo(contextKeyService); @@ -178,7 +175,6 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { const filePatterns = this.viewletSettings['query.filePatterns'] || ''; const patternExclusions = this.viewletSettings['query.folderExclusions'] || ''; - const patternExclusionsHistory = this.viewletSettings['query.folderExclusionsHistory'] || []; const patternIncludes = this.viewletSettings['query.folderIncludes'] || ''; const patternIncludesHistory = this.viewletSettings['query.folderIncludesHistory'] || []; const queryDetailsExpanded = this.viewletSettings['query.queryDetailsExpanded'] || ''; @@ -201,15 +197,27 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { //folder includes list builder.div({ 'class': 'file-types' }, (builder) => { - let title = nls.localize('searchScope.includes', "files to include"); + let title = nls.localize('searchIncludeExclude.label', "files to include/exclude"); builder.element('h4', { text: title }); - this.inputPatternIncludes = new PatternInputWidget(builder.getContainer(), this.contextViewService, this.themeService, { - ariaLabel: nls.localize('label.includes', 'Search Include Patterns') + this.inputPatternIncludes = new ExcludePatternInputWidget(builder.getContainer(), this.contextViewService, this.themeService, { + ariaLabel: nls.localize('searchIncludeExclude.ariaLabel', 'Search Include/Exclude Patterns'), + placeholder: nls.localize('searchIncludeExclude.placeholder', "Examples: src, !*.ts, test/**/*.log") }); - this.inputPatternIncludes.setValue(patternIncludes); + + let mergedIncludeExcludes = patternIncludes; + if (patternExclusions) { + if (mergedIncludeExcludes) { + mergedIncludeExcludes += ', '; + } + + mergedIncludeExcludes += patternExclusions; + } + + this.inputPatternIncludes.setValue(mergedIncludeExcludes); this.inputPatternIncludes.setHistory(patternIncludesHistory); + this.inputPatternIncludes.setUseExcludesAndIgnoreFiles(useExcludesAndIgnoreFiles); this.inputPatternIncludes .on(FindInput.OPTION_CHANGE, (e) => { @@ -220,30 +228,6 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.inputPatternIncludes.onCancel(() => this.viewModel.cancelSearch()); // Cancel search without focusing the search widget this.trackInputBox(this.inputPatternIncludes.inputFocusTracker, this.inputPatternIncludesFocused); }); - - //pattern exclusion list - builder.div({ 'class': 'file-types' }, (builder) => { - let title = nls.localize('searchScope.excludes', "files to exclude"); - builder.element('h4', { text: title }); - - this.inputPatternExcludes = new ExcludePatternInputWidget(builder.getContainer(), this.contextViewService, this.themeService, { - ariaLabel: nls.localize('label.excludes', 'Search Exclude Patterns') - }); - - this.inputPatternExcludes.setValue(patternExclusions); - this.inputPatternExcludes.setUseExcludesAndIgnoreFiles(useExcludesAndIgnoreFiles); - this.inputPatternExcludes.setHistory(patternExclusionsHistory); - - this.inputPatternExcludes - .on(FindInput.OPTION_CHANGE, (e) => { - this.onQueryChanged(false); - }); - - this.inputPatternExcludes.onSubmit(() => this.onQueryChanged(true, true)); - this.inputPatternExcludes.onSubmit(() => this.onQueryChanged(true, true)); - this.inputPatternExcludes.onCancel(() => this.viewModel.cancelSearch()); // Cancel search without focusing the search widget - this.trackInputBox(this.inputPatternExcludes.inputFocusTracker, this.inputPatternExclusionsFocused); - }); }).getHTMLElement(); this.messages = builder.div({ 'class': 'messages' }).hide().clone(); @@ -276,10 +260,6 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { return this.inputPatternIncludes; } - public get searchExcludePattern(): PatternInputWidget { - return this.inputPatternExcludes; - } - private updateActions(): void { for (const action of this.actions) { action.update(); @@ -336,8 +316,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.toUnbind.push(inputFocusTracker.onDidBlur(() => { this.inputBoxFocused.set(this.searchWidget.searchInputHasFocus() || this.searchWidget.replaceInputHasFocus() - || this.inputPatternIncludes.inputHasFocus() - || this.inputPatternExcludes.inputHasFocus()); + || this.inputPatternIncludes.inputHasFocus()); if (contextKey) { contextKey.set(false); } @@ -714,12 +693,6 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { } if (this.inputPatternIncludes.inputHasFocus()) { - this.inputPatternExcludes.focus(); - this.inputPatternExcludes.select(); - return; - } - - if (this.inputPatternExcludes.inputHasFocus()) { this.selectTreeIfNotSelected(); return; } @@ -748,12 +721,6 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { return; } - if (this.inputPatternExcludes.inputHasFocus()) { - this.inputPatternIncludes.focus(); - this.inputPatternIncludes.select(); - return; - } - if (this.tree.isDOMFocused()) { this.moveFocusFromResults(); return; @@ -762,7 +729,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { private moveFocusFromResults(): void { if (this.showsFileTypes()) { - this.toggleQueryDetails(true, true, false, true); + this.toggleQueryDetails(true, true, false); } else { this.searchWidget.focus(true, true); } @@ -775,7 +742,6 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.searchWidget.setWidth(this.size.width - 28 /* container margin */); - this.inputPatternExcludes.setWidth(this.size.width - 28 /* container margin */); this.inputPatternIncludes.setWidth(this.size.width - 28 /* container margin */); const messagesSize = this.messages.isHidden() ? 0 : dom.getTotalHeight(this.messages.getHTMLElement()); @@ -896,7 +862,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.onQueryChanged(true, true); } - public toggleQueryDetails(moveFocus?: boolean, show?: boolean, skipLayout?: boolean, reverse?: boolean): void { + public toggleQueryDetails(moveFocus?: boolean, show?: boolean, skipLayout?: boolean): void { let cls = 'more'; show = typeof show === 'undefined' ? !dom.hasClass(this.queryDetails, cls) : Boolean(show); this.viewletSettings['query.queryDetailsExpanded'] = show; @@ -905,13 +871,8 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { if (show) { dom.addClass(this.queryDetails, cls); if (moveFocus) { - if (reverse) { - this.inputPatternExcludes.focus(); - this.inputPatternExcludes.select(); - } else { - this.inputPatternIncludes.focus(); - this.inputPatternIncludes.select(); - } + this.inputPatternIncludes.focus(); + this.inputPatternIncludes.select(); } } else { dom.removeClass(this.queryDetails, cls); @@ -979,9 +940,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { const isWholeWords = this.searchWidget.searchInput.getWholeWords(); const isCaseSensitive = this.searchWidget.searchInput.getCaseSensitive(); const contentPattern = this.searchWidget.searchInput.getValue(); - const excludePatternText = this.inputPatternExcludes.getValue().trim(); - const includePatternText = this.inputPatternIncludes.getValue().trim(); - const useExcludesAndIgnoreFiles = this.inputPatternExcludes.useExcludesAndIgnoreFiles(); + const useExcludesAndIgnoreFiles = this.inputPatternIncludes.useExcludesAndIgnoreFiles(); if (!rerunQuery) { return; @@ -1014,8 +973,8 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { isSmartCase: this.configurationService.getValue().search.smartCase }; - const excludePattern = this.inputPatternExcludes.getValue(); - const includePattern = this.inputPatternIncludes.getValue(); + const includeExcludePattern = this.inputPatternIncludes.getValue().trim(); + const { includePattern, excludePattern } = this.queryBuilder.parseIncludeExcludePattern(includeExcludePattern); const options: IQueryOptions = { extraFileResources: getOutOfWorkspaceEditorResources(this.editorGroupService, this.contextService), @@ -1041,7 +1000,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { } this.validateQuery(query).then(() => { - this.onQueryTriggered(query, excludePatternText, includePatternText); + this.onQueryTriggered(query, excludePattern, includePattern); if (!preserveFocus) { this.searchWidget.focus(false); // focus back to input field @@ -1072,7 +1031,6 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { } private onQueryTriggered(query: ISearchQuery, excludePatternText: string, includePatternText: string): void { - this.inputPatternExcludes.onSearchSubmit(); this.inputPatternIncludes.onSearchSubmit(); this.viewModel.cancelSearch(); @@ -1133,8 +1091,8 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { } if (!hasResults) { - let hasExcludes = !!excludePatternText; - let hasIncludes = !!includePatternText; + let hasExcludes = !!query.excludePattern; + let hasIncludes = !!query.includePattern; let message: string; if (!completed) { @@ -1174,7 +1132,6 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { }).on(dom.EventType.CLICK, (e: MouseEvent) => { dom.EventHelper.stop(e, false); - this.inputPatternExcludes.setValue(''); this.inputPatternIncludes.setValue(''); this.onQueryChanged(true); @@ -1463,11 +1420,9 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { const isWholeWords = this.searchWidget.searchInput.getWholeWords(); const isCaseSensitive = this.searchWidget.searchInput.getCaseSensitive(); const contentPattern = this.searchWidget.searchInput.getValue(); - const patternExcludes = this.inputPatternExcludes.getValue().trim(); const patternIncludes = this.inputPatternIncludes.getValue().trim(); - const useExcludesAndIgnoreFiles = this.inputPatternExcludes.useExcludesAndIgnoreFiles(); + const useExcludesAndIgnoreFiles = this.inputPatternIncludes.useExcludesAndIgnoreFiles(); const searchHistory = this.searchWidget.getHistory(); - const patternExcludesHistory = this.inputPatternExcludes.getHistory(); const patternIncludesHistory = this.inputPatternIncludes.getHistory(); // store memento @@ -1476,9 +1431,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.viewletSettings['query.regex'] = isRegex; this.viewletSettings['query.wholeWords'] = isWholeWords; this.viewletSettings['query.caseSensitive'] = isCaseSensitive; - this.viewletSettings['query.folderExclusions'] = patternExcludes; this.viewletSettings['query.folderIncludes'] = patternIncludes; - this.viewletSettings['query.folderExclusionsHistory'] = patternExcludesHistory; this.viewletSettings['query.folderIncludesHistory'] = patternIncludesHistory; this.viewletSettings['query.useExcludesAndIgnoreFiles'] = useExcludesAndIgnoreFiles; @@ -1494,7 +1447,6 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { this.searchWidget.dispose(); this.inputPatternIncludes.dispose(); - this.inputPatternExcludes.dispose(); this.viewModel.dispose(); diff --git a/src/vs/workbench/parts/search/common/queryBuilder.ts b/src/vs/workbench/parts/search/common/queryBuilder.ts index c619c0be30548..deef62eeff5f4 100644 --- a/src/vs/workbench/parts/search/common/queryBuilder.ts +++ b/src/vs/workbench/parts/search/common/queryBuilder.ts @@ -182,6 +182,28 @@ export class QueryBuilder { return Object.keys(excludeExpression).length ? excludeExpression : undefined; } + /** + * A helper that splits positive and negative patterns from a string that combines both. + */ + public parseIncludeExcludePattern(pattern: string): { includePattern?: string, excludePattern?: string } { + const grouped = collections.groupBy( + splitGlobPattern(pattern), + s => strings.startsWith(s, '!') ? 'excludePattern' : 'includePattern'); + + const result = {}; + if (grouped.includePattern) { + result['includePattern'] = grouped.includePattern.join(', '); + } + + if (grouped.excludePattern) { + result['excludePattern'] = grouped.excludePattern + .map(s => strings.ltrim(s, '!')) + .join(', '); + } + + return result; + } + private mergeExcludesFromFolderQueries(folderQueries: IFolderQuery[]): glob.IExpression | undefined { const mergedExcludes = folderQueries.reduce((merged: glob.IExpression, fq: IFolderQuery) => { if (fq.excludePattern) { diff --git a/src/vs/workbench/parts/search/electron-browser/search.contribution.ts b/src/vs/workbench/parts/search/electron-browser/search.contribution.ts index f79a3cf2747cb..8bbf528a62223 100644 --- a/src/vs/workbench/parts/search/electron-browser/search.contribution.ts +++ b/src/vs/workbench/parts/search/electron-browser/search.contribution.ts @@ -53,7 +53,7 @@ import { getMultiSelectedResources } from 'vs/workbench/parts/files/browser/file import { Schemas } from 'vs/base/common/network'; import { PanelRegistry, Extensions as PanelExtensions, PanelDescriptor } from 'vs/workbench/browser/panel'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { openSearchView, getSearchView, ReplaceAllInFolderAction, ReplaceAllAction, CloseReplaceAction, FocusNextInputAction, FocusPreviousInputAction, FocusNextSearchResultAction, FocusPreviousSearchResultAction, ReplaceInFilesAction, FindInFilesAction, FocusActiveEditorCommand, toggleCaseSensitiveCommand, ShowNextSearchTermAction, ShowPreviousSearchTermAction, toggleRegexCommand, ShowNextSearchExcludeAction, ShowPreviousSearchIncludeAction, ShowNextSearchIncludeAction, ShowPreviousSearchExcludeAction, CollapseDeepestExpandedLevelAction, toggleWholeWordCommand, RemoveAction, ReplaceAction } from 'vs/workbench/parts/search/browser/searchActions'; +import { openSearchView, getSearchView, ReplaceAllInFolderAction, ReplaceAllAction, CloseReplaceAction, FocusNextInputAction, FocusPreviousInputAction, FocusNextSearchResultAction, FocusPreviousSearchResultAction, ReplaceInFilesAction, FindInFilesAction, FocusActiveEditorCommand, toggleCaseSensitiveCommand, ShowNextSearchTermAction, ShowPreviousSearchTermAction, toggleRegexCommand, ShowPreviousSearchIncludeAction, ShowNextSearchIncludeAction, CollapseDeepestExpandedLevelAction, toggleWholeWordCommand, RemoveAction, ReplaceAction } from 'vs/workbench/parts/search/browser/searchActions'; import { VIEW_ID } from 'vs/platform/search/common/search'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; @@ -353,9 +353,6 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(ShowPreviousSearchTerm registry.registerWorkbenchAction(new SyncActionDescriptor(ShowNextSearchIncludeAction, ShowNextSearchIncludeAction.ID, ShowNextSearchIncludeAction.LABEL, ShowNextFindTermKeybinding, ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.PatternIncludesFocusedKey)), 'Search: Show Next Search Include Pattern', category); registry.registerWorkbenchAction(new SyncActionDescriptor(ShowPreviousSearchIncludeAction, ShowPreviousSearchIncludeAction.ID, ShowPreviousSearchIncludeAction.LABEL, ShowPreviousFindTermKeybinding, ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.PatternIncludesFocusedKey)), 'Search: Show Previous Search Include Pattern', category); -registry.registerWorkbenchAction(new SyncActionDescriptor(ShowNextSearchExcludeAction, ShowNextSearchExcludeAction.ID, ShowNextSearchExcludeAction.LABEL, ShowNextFindTermKeybinding, ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.PatternExcludesFocusedKey)), 'Search: Show Next Search Exclude Pattern', category); -registry.registerWorkbenchAction(new SyncActionDescriptor(ShowPreviousSearchExcludeAction, ShowPreviousSearchExcludeAction.ID, ShowPreviousSearchExcludeAction.LABEL, ShowPreviousFindTermKeybinding, ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.PatternExcludesFocusedKey)), 'Search: Show Previous Search Exclude Pattern', category); - registry.registerWorkbenchAction(new SyncActionDescriptor(CollapseDeepestExpandedLevelAction, CollapseDeepestExpandedLevelAction.ID, CollapseDeepestExpandedLevelAction.LABEL), 'Search: Collapse All', category); registry.registerWorkbenchAction(new SyncActionDescriptor(ShowAllSymbolsAction, ShowAllSymbolsAction.ID, ShowAllSymbolsAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_T }), 'Go to Symbol in Workspace...'); diff --git a/src/vs/workbench/parts/search/test/common/queryBuilder.test.ts b/src/vs/workbench/parts/search/test/common/queryBuilder.test.ts index bb9e17f68f05b..4ae1b1a4fafcc 100644 --- a/src/vs/workbench/parts/search/test/common/queryBuilder.test.ts +++ b/src/vs/workbench/parts/search/test/common/queryBuilder.test.ts @@ -697,6 +697,51 @@ suite('QueryBuilder', () => { assert(query.sortByScore); }); }); + + suite('parseIncludeExcludePattern', () => { + test('nothing', () => { + assert.deepEqual( + queryBuilder.parseIncludeExcludePattern(''), + {}); + }); + + test('includes', () => { + assert.deepEqual( + queryBuilder.parseIncludeExcludePattern('src'), + { + includePattern: 'src' + }); + + assert.deepEqual( + queryBuilder.parseIncludeExcludePattern('src, test'), + { + includePattern: 'src, test' + }); + }); + + test('excludes', () => { + assert.deepEqual( + queryBuilder.parseIncludeExcludePattern('!src'), + { + excludePattern: 'src' + }); + + assert.deepEqual( + queryBuilder.parseIncludeExcludePattern('!src, !test'), + { + excludePattern: 'src, test' + }); + }); + + test('includes and excludes', () => { + assert.deepEqual( + queryBuilder.parseIncludeExcludePattern('!src, test, !foo, bar'), + { + includePattern: 'test, bar', + excludePattern: 'src, foo' + }); + }); + }); }); function assertEqualQueries(actual: ISearchQuery, expected: ISearchQuery): void {