diff --git a/src/vs/platform/search/common/search.ts b/src/vs/platform/search/common/search.ts index 1aa0ca13b5eb2..959bbfdbfd615 100644 --- a/src/vs/platform/search/common/search.ts +++ b/src/vs/platform/search/common/search.ts @@ -32,11 +32,9 @@ export interface IFolderQueryOptions { fileEncoding?: string; } -export interface IQueryOptions { +export interface ICommonQueryOptions { extraFileResources?: uri[]; - filePattern?: string; - excludePattern?: IExpression; - includePattern?: IExpression; + filePattern?: string; // file search only fileEncoding?: string; maxResults?: number; sortByScore?: boolean; @@ -44,11 +42,19 @@ export interface IQueryOptions { useRipgrep?: boolean; disregardIgnoreFiles?: boolean; disregardExcludeSettings?: boolean; - searchPaths?: string[]; } -export interface ISearchQuery extends IQueryOptions { +export interface IQueryOptions extends ICommonQueryOptions { + excludePattern?: string; + includePattern?: string; +} + +export interface ISearchQuery extends ICommonQueryOptions { type: QueryType; + + excludePattern?: IExpression; + includePattern?: IExpression; + searchPaths?: string[]; contentPattern?: IPatternInfo; folderQueries?: IFolderQueryOptions[]; } diff --git a/src/vs/workbench/parts/search/browser/patternInputWidget.ts b/src/vs/workbench/parts/search/browser/patternInputWidget.ts index ab4ec71e94bba..5cbdbbb413558 100644 --- a/src/vs/workbench/parts/search/browser/patternInputWidget.ts +++ b/src/vs/workbench/parts/search/browser/patternInputWidget.ts @@ -5,15 +5,11 @@ import nls = require('vs/nls'); import * as dom from 'vs/base/browser/dom'; -import strings = require('vs/base/common/strings'); -import paths = require('vs/base/common/paths'); -import collections = require('vs/base/common/collections'); import { $ } from 'vs/base/browser/builder'; import { Widget } from 'vs/base/browser/ui/widget'; -import { IExpression, splitGlobAware } from 'vs/base/common/glob'; import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; -import { MessageType, InputBox, IInputValidator } from 'vs/base/browser/ui/inputbox/inputBox'; +import { InputBox, IInputValidator } from 'vs/base/browser/ui/inputbox/inputBox'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import CommonEvent, { Emitter } from 'vs/base/common/event'; @@ -39,8 +35,6 @@ export class PatternInputWidget extends Widget { private placeholder: string; private ariaLabel: string; - private pattern: Checkbox; - private domNode: HTMLElement; private inputNode: HTMLInputElement; protected inputBox: InputBox; @@ -55,7 +49,6 @@ export class PatternInputWidget extends Widget { this.placeholder = options.placeholder || ''; this.ariaLabel = options.ariaLabel || nls.localize('defaultLabel', "input"); - this.pattern = null; this.domNode = null; this.inputNode = null; this.inputBox = null; @@ -67,7 +60,6 @@ export class PatternInputWidget extends Widget { public dispose(): void { super.dispose(); - this.pattern.dispose(); if (this.inputFocusTracker) { this.inputFocusTracker.dispose(); } @@ -103,58 +95,6 @@ export class PatternInputWidget extends Widget { } } - public getGlob(): { expression?: IExpression, searchPaths?: string[] } { - const pattern = this.getValue(); - const isGlobPattern = this.isGlobPattern(); - - if (!pattern) { - return {}; - } - - let exprSegments: string[]; - let searchPaths: string[]; - if (isGlobPattern) { - const segments = splitGlobAware(pattern, ',') - .map(s => s.trim()) - .filter(s => !!s.length); - - const groups = this.groupByPathsAndExprSegments(segments); - searchPaths = groups.searchPaths; - exprSegments = groups.exprSegments; - } else { - const segments = pattern.split(',') - .map(s => s.trim()) - .filter(s => !!s.length); - - const groups = this.groupByPathsAndExprSegments(segments); - searchPaths = groups.searchPaths; - exprSegments = groups.exprSegments - .map(p => { - if (p[0] === '.') { - p = '*' + p; // convert ".js" to "*.js" - } - - return strings.format('{{0}/**,**/{1}}', p, p); // convert foo to {foo/**,**/foo} to cover files and folders - }); - } - - const expression = exprSegments.reduce((glob, cur) => { glob[cur] = true; return glob; }, Object.create(null)); - return { expression, searchPaths }; - } - - private groupByPathsAndExprSegments(segments: string[]) { - const isSearchPath = (segment: string) => { - // A segment is a search path if it is an absolute path or starts with ./ - return paths.isAbsolute(segment) || strings.startsWith(segment, './'); - }; - - const groups = collections.groupBy(segments, - segment => isSearchPath(segment) ? 'searchPaths' : 'exprSegments'); - groups.searchPaths = groups.searchPaths || []; - groups.exprSegments = groups.exprSegments || []; - - return groups; - } public select(): void { this.inputBox.select(); @@ -168,20 +108,12 @@ export class PatternInputWidget extends Widget { return this.inputBox.hasFocus(); } - public isGlobPattern(): boolean { - return this.pattern.checked; - } - - public setIsGlobPattern(value: boolean): void { - this.pattern.checked = value; - } - private setInputWidth(): void { - this.inputBox.width = this.width - this.getSubcontrolsWidth(); + this.inputBox.width = this.width; } protected getSubcontrolsWidth(): number { - return this.pattern.width(); + return 0; } private render(): void { @@ -201,35 +133,6 @@ export class PatternInputWidget extends Widget { this.inputFocusTracker = dom.trackFocus(this.inputBox.inputElement); this.onkeyup(this.inputBox.inputElement, (keyboardEvent) => this.onInputKeyUp(keyboardEvent)); - this.pattern = new Checkbox({ - actionClassName: 'pattern', - title: nls.localize('patternDescription', "Use Glob Patterns"), - isChecked: false, - onChange: (viaKeyboard) => { - this.onOptionChange(null); - if (!viaKeyboard) { - this.inputBox.focus(); - } - - if (this.isGlobPattern()) { - this.showGlobHelp(); - } else { - this.inputBox.hideMessage(); - } - } - }); - this._register(attachCheckboxStyler(this.pattern, this.themeService)); - - $(this.pattern.domNode).on('mouseover', () => { - if (this.isGlobPattern()) { - this.showGlobHelp(); - } - }); - - $(this.pattern.domNode).on(['mouseleave', 'mouseout'], () => { - this.inputBox.hideMessage(); - }); - let controls = document.createElement('div'); controls.className = 'controls'; this.renderSubcontrols(controls); @@ -239,17 +142,6 @@ export class PatternInputWidget extends Widget { } protected renderSubcontrols(controlsDiv: HTMLDivElement): void { - controlsDiv.appendChild(this.pattern.domNode); - } - - private showGlobHelp(): void { - this.inputBox.showMessage({ - type: MessageType.INFO, - formatContent: true, - content: nls.localize('patternHelpInclude', - "The pattern to match. e.g. **\\*\\*/*.js** to match all JavaScript files or **myFolder/\\*\\*** to match that folder with all children.\n\n**Reference**:\n**\\*** matches 0 or more characters\n**?** matches 1 character\n**\\*\\*** matches zero or more directories\n**[a-z]** matches a range of characters\n**{a,b}** matches any of the patterns)" - ) - }, true); } private onInputKeyUp(keyboardEvent: IKeyboardEvent) { diff --git a/src/vs/workbench/parts/search/browser/searchViewlet.ts b/src/vs/workbench/parts/search/browser/searchViewlet.ts index b24862f5f20e0..a0d0869cc0629 100644 --- a/src/vs/workbench/parts/search/browser/searchViewlet.ts +++ b/src/vs/workbench/parts/search/browser/searchViewlet.ts @@ -93,7 +93,7 @@ export class SearchViewlet extends Viewlet { private searchWidget: SearchWidget; private size: Dimension; private queryDetails: HTMLElement; - private inputPatternExclusions: ExcludePatternInputWidget; + private inputPatternExcludes: ExcludePatternInputWidget; private inputPatternGlobalExclusions: InputBox; private inputPatternGlobalExclusionsContainer: Builder; private inputPatternIncludes: PatternInputWidget; @@ -174,8 +174,6 @@ export class SearchViewlet extends Viewlet { const filePatterns = this.viewletSettings['query.filePatterns'] || ''; const patternExclusions = this.viewletSettings['query.folderExclusions'] || ''; - const exclusionsUsePattern = this.viewletSettings['query.exclusionsUsePattern']; - const includesUsePattern = this.viewletSettings['query.includesUsePattern']; const patternIncludes = this.viewletSettings['query.folderIncludes'] || ''; const queryDetailsExpanded = this.viewletSettings['query.queryDetailsExpanded'] || ''; const useIgnoreFiles = typeof this.viewletSettings['query.useIgnoreFiles'] === 'boolean' ? @@ -206,7 +204,6 @@ export class SearchViewlet extends Viewlet { ariaLabel: nls.localize('label.includes', 'Search Include Patterns') }); - this.inputPatternIncludes.setIsGlobPattern(includesUsePattern); this.inputPatternIncludes.setValue(patternIncludes); this.inputPatternIncludes @@ -223,22 +220,21 @@ export class SearchViewlet extends Viewlet { let title = nls.localize('searchScope.excludes', "files to exclude"); builder.element('h4', { text: title }); - this.inputPatternExclusions = new ExcludePatternInputWidget(builder.getContainer(), this.contextViewService, this.themeService, this.telemetryService, { + this.inputPatternExcludes = new ExcludePatternInputWidget(builder.getContainer(), this.contextViewService, this.themeService, this.telemetryService, { ariaLabel: nls.localize('label.excludes', 'Search Exclude Patterns') }); - this.inputPatternExclusions.setIsGlobPattern(exclusionsUsePattern); - this.inputPatternExclusions.setValue(patternExclusions); - this.inputPatternExclusions.setUseIgnoreFiles(useIgnoreFiles); - this.inputPatternExclusions.setUseExcludeSettings(useExcludeSettings); + this.inputPatternExcludes.setValue(patternExclusions); + this.inputPatternExcludes.setUseIgnoreFiles(useIgnoreFiles); + this.inputPatternExcludes.setUseExcludeSettings(useExcludeSettings); - this.inputPatternExclusions + this.inputPatternExcludes .on(FindInput.OPTION_CHANGE, (e) => { this.onQueryChanged(false); }); - this.inputPatternExclusions.onSubmit(() => this.onQueryChanged(true, true)); - this.trackInputBox(this.inputPatternExclusions.inputFocusTracker); + this.inputPatternExcludes.onSubmit(() => this.onQueryChanged(true, true)); + this.trackInputBox(this.inputPatternExcludes.inputFocusTracker); }); // add hint if we have global exclusion @@ -332,7 +328,7 @@ export class SearchViewlet extends Viewlet { this.inputBoxFocussed.set(this.searchWidget.searchInputHasFocus() || this.searchWidget.replaceInputHasFocus() || this.inputPatternIncludes.inputHasFocus() - || this.inputPatternExclusions.inputHasFocus()); + || this.inputPatternExcludes.inputHasFocus()); })); } @@ -691,12 +687,12 @@ export class SearchViewlet extends Viewlet { } if (this.inputPatternIncludes.inputHasFocus()) { - this.inputPatternExclusions.focus(); - this.inputPatternExclusions.select(); + this.inputPatternExcludes.focus(); + this.inputPatternExcludes.select(); return; } - if (this.inputPatternExclusions.inputHasFocus()) { + if (this.inputPatternExcludes.inputHasFocus()) { this.selectTreeIfNotSelected(); return; } @@ -725,7 +721,7 @@ export class SearchViewlet extends Viewlet { return; } - if (this.inputPatternExclusions.inputHasFocus()) { + if (this.inputPatternExcludes.inputHasFocus()) { this.inputPatternIncludes.focus(); this.inputPatternIncludes.select(); return; @@ -752,7 +748,7 @@ export class SearchViewlet extends Viewlet { this.searchWidget.setWidth(this.size.width - 25 /* container margin */); - this.inputPatternExclusions.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 */); this.inputPatternGlobalExclusions.width = this.size.width - 28 /* container margin */ - 24 /* actions */; @@ -874,8 +870,8 @@ export class SearchViewlet extends Viewlet { dom.addClass(this.queryDetails, cls); if (moveFocus) { if (reverse) { - this.inputPatternExclusions.focus(); - this.inputPatternExclusions.select(); + this.inputPatternExcludes.focus(); + this.inputPatternExcludes.select(); } else { this.inputPatternIncludes.focus(); this.inputPatternIncludes.select(); @@ -930,7 +926,6 @@ export class SearchViewlet extends Viewlet { this.toggleQueryDetails(true, true); } - this.inputPatternIncludes.setIsGlobPattern(false); this.inputPatternIncludes.setValue(folderPath); this.searchWidget.focus(false); } @@ -940,10 +935,10 @@ export class SearchViewlet extends Viewlet { const isWholeWords = this.searchWidget.searchInput.getWholeWords(); const isCaseSensitive = this.searchWidget.searchInput.getCaseSensitive(); const contentPattern = this.searchWidget.searchInput.getValue(); - const excludePatternText = this.inputPatternExclusions.getValue().trim(); + const excludePatternText = this.inputPatternExcludes.getValue().trim(); const includePatternText = this.inputPatternIncludes.getValue().trim(); - const useIgnoreFiles = this.inputPatternExclusions.useIgnoreFiles(); - const useExcludeSettings = this.inputPatternExclusions.useExcludeSettings(); + const useIgnoreFiles = this.inputPatternExcludes.useIgnoreFiles(); + const useExcludeSettings = this.inputPatternExcludes.useExcludeSettings(); if (!rerunQuery) { return; @@ -975,17 +970,16 @@ export class SearchViewlet extends Viewlet { wordSeparators: this.configurationService.getConfiguration().editor.wordSeparators }; - const { expression: excludePattern } = this.inputPatternExclusions.getGlob(); - const { expression: includePattern, searchPaths } = this.inputPatternIncludes.getGlob(); + const excludePattern = this.inputPatternExcludes.getValue(); + const includePattern = this.inputPatternIncludes.getValue(); const options: IQueryOptions = { extraFileResources: getOutOfWorkspaceEditorResources(this.editorGroupService, this.contextService), - excludePattern, - includePattern, maxResults: SearchViewlet.MAX_TEXT_RESULTS, disregardIgnoreFiles: !useIgnoreFiles, disregardExcludeSettings: !useExcludeSettings, - searchPaths + excludePattern, + includePattern }; const folderResources = this.contextService.hasWorkspace() ? this.contextService.getWorkspace().roots : []; @@ -1105,7 +1099,7 @@ export class SearchViewlet extends Viewlet { }).on(dom.EventType.CLICK, (e: MouseEvent) => { dom.EventHelper.stop(e, false); - this.inputPatternExclusions.setValue(''); + this.inputPatternExcludes.setValue(''); this.inputPatternIncludes.setValue(''); this.onQueryChanged(true); @@ -1386,11 +1380,9 @@ export class SearchViewlet extends Viewlet { const isWholeWords = this.searchWidget.searchInput.getWholeWords(); const isCaseSensitive = this.searchWidget.searchInput.getCaseSensitive(); const contentPattern = this.searchWidget.searchInput.getValue(); - const patternExcludes = this.inputPatternExclusions.getValue().trim(); - const exclusionsUsePattern = this.inputPatternExclusions.isGlobPattern(); + const patternExcludes = this.inputPatternExcludes.getValue().trim(); const patternIncludes = this.inputPatternIncludes.getValue().trim(); - const includesUsePattern = this.inputPatternIncludes.isGlobPattern(); - const useIgnoreFiles = this.inputPatternExclusions.useIgnoreFiles(); + const useIgnoreFiles = this.inputPatternExcludes.useIgnoreFiles(); // store memento this.viewletSettings['query.contentPattern'] = contentPattern; @@ -1398,9 +1390,7 @@ export class SearchViewlet extends Viewlet { this.viewletSettings['query.wholeWords'] = isWholeWords; this.viewletSettings['query.caseSensitive'] = isCaseSensitive; this.viewletSettings['query.folderExclusions'] = patternExcludes; - this.viewletSettings['query.exclusionsUsePattern'] = exclusionsUsePattern; this.viewletSettings['query.folderIncludes'] = patternIncludes; - this.viewletSettings['query.includesUsePattern'] = includesUsePattern; this.viewletSettings['query.useIgnoreFiles'] = useIgnoreFiles; this.viewletSettings['query.contentPattern'] = contentPattern; @@ -1418,7 +1408,7 @@ export class SearchViewlet extends Viewlet { this.searchWidget.dispose(); this.inputPatternIncludes.dispose(); - this.inputPatternExclusions.dispose(); + this.inputPatternExcludes.dispose(); this.viewModel.dispose(); diff --git a/src/vs/workbench/parts/search/common/searchQuery.ts b/src/vs/workbench/parts/search/common/searchQuery.ts index b011e526b2193..364cbab9ec71d 100644 --- a/src/vs/workbench/parts/search/common/searchQuery.ts +++ b/src/vs/workbench/parts/search/common/searchQuery.ts @@ -5,9 +5,10 @@ 'use strict'; // import nls = require('vs/nls'); -import { IExpression } from 'vs/base/common/glob'; -import * as objects from 'vs/base/common/objects'; +import * as collections from 'vs/base/common/collections'; +import * as glob from 'vs/base/common/glob'; import * as paths from 'vs/base/common/paths'; +import * as strings from 'vs/base/common/strings'; import uri from 'vs/base/common/uri'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IPatternInfo, IQueryOptions, IFolderQueryOptions, ISearchQuery, QueryType, ISearchConfiguration, getExcludes } from 'vs/platform/search/common/search'; @@ -43,18 +44,16 @@ export class QueryBuilder { return folderConfig.search.useRipgrep; }); - const { searchPaths, additionalIncludePatterns } = this.getSearchPaths(options); - const includePattern = objects.clone(options.includePattern); - for (const additionalInclude of additionalIncludePatterns) { - includePattern[additionalInclude] = true; - } + const { searchPaths, includePattern } = this.getSearchPaths(options.includePattern); + + const excludePattern = patternListToIExpression(splitGlobPattern(options.excludePattern)); return { type, folderQueries, extraFileResources: options.extraFileResources, filePattern: options.filePattern, - excludePattern: options.excludePattern, + excludePattern, includePattern, maxResults: options.maxResults, sortByScore: options.sortByScore, @@ -67,35 +66,67 @@ export class QueryBuilder { }; } - private getExcludesForFolder(folderConfig: ISearchConfiguration, options: IQueryOptions): IExpression | null { - const settingsExcludePattern = getExcludes(folderConfig); + /** + * Take the includePattern as seen in the search viewlet, and split into components that look like searchPaths, and + * glob patterns. Glob patterns are expanded from 'foo/bar' to '{foo/bar/**, **\/foo/bar} + */ + private getSearchPaths(pattern: string): { searchPaths: string[]; includePattern: glob.IExpression } { + const isSearchPath = (segment: string) => { + // A segment is a search path if it is an absolute path or starts with ./ + return paths.isAbsolute(segment) || strings.startsWith(segment, './'); + }; - if (options.disregardExcludeSettings) { - return null; - } else if (options.excludePattern) { - return objects.mixin(options.excludePattern, settingsExcludePattern, false /* no overwrite */); - } else { - return settingsExcludePattern; - } + const segments = splitGlobPattern(pattern); + const groups = collections.groupBy(segments, + segment => isSearchPath(segment) ? 'searchPaths' : 'exprSegments'); + groups.searchPaths = groups.searchPaths || []; + groups.exprSegments = groups.exprSegments || []; + + const searchPaths = groups.searchPaths; + const exprSegments = groups.exprSegments + .map(p => { + if (p[0] === '.') { + p = '*' + p; // convert ".js" to "*.js" + } + + return strings.format('{{0}/**,**/{1}}', p, p); // convert foo to {foo/**,**/foo} to cover files and folders + }); + + const { absoluteSearchPaths, additionalIncludePatterns } = this.splitSearchPaths(searchPaths); + const expression = patternListToIExpression(exprSegments.concat(additionalIncludePatterns)); + + return { + searchPaths: absoluteSearchPaths, + includePattern: expression + }; + } + + private getExcludesForFolder(folderConfig: ISearchConfiguration, options: IQueryOptions): glob.IExpression | null { + return options.disregardExcludeSettings ? + null : + getExcludes(folderConfig); } - private getSearchPaths(options: IQueryOptions): { searchPaths: string[], additionalIncludePatterns: string[] } { - if (!this.workspaceContextService.hasWorkspace() || !options.searchPaths) { + /** + * Split search paths (./ or absolute paths in the includePatterns) into absolute paths and globs applied to those paths + */ + private splitSearchPaths(searchPaths: string[]): { absoluteSearchPaths: string[], additionalIncludePatterns: string[] } { + if (!this.workspaceContextService.hasWorkspace() || !searchPaths.length) { // No workspace => ignore search paths return { - searchPaths: [], + absoluteSearchPaths: [], additionalIncludePatterns: [] }; } - const searchPaths: string[] = []; + const absoluteSearchPaths: string[] = []; const additionalIncludePatterns: string[] = []; - for (const searchPath of options.searchPaths) { + for (const searchPath of searchPaths) { // 1 open folder => just resolve the search paths to absolute paths const { pathPortion, globPortion } = splitGlobFromPath(searchPath); const absolutePathPortions = this.expandAbsoluteSearchPaths(pathPortion); - searchPaths.push(...absolutePathPortions); + absoluteSearchPaths.push(...absolutePathPortions); if (globPortion) { additionalIncludePatterns.push(...absolutePathPortions.map(abs => paths.join(abs, globPortion))); @@ -103,7 +134,7 @@ export class QueryBuilder { } return { - searchPaths, + absoluteSearchPaths, additionalIncludePatterns }; } @@ -154,3 +185,13 @@ function splitGlobFromPath(searchPath: string): { pathPortion: string, globPorti pathPortion: searchPath }; } + +function patternListToIExpression(patterns: string[]): glob.IExpression { + return patterns.reduce((glob, cur) => { glob[cur] = true; return glob; }, Object.create(null)); +} + +function splitGlobPattern(pattern: string): string[] { + return glob.splitGlobAware(pattern, ',') + .map(s => s.trim()) + .filter(s => !!s.length); +}