diff --git a/src/vs/base/common/glob.ts b/src/vs/base/common/glob.ts index 5562380d1aa..c6d01f0376a 100644 --- a/src/vs/base/common/glob.ts +++ b/src/vs/base/common/glob.ts @@ -212,10 +212,15 @@ function parseRegExp(pattern: string): string { const T1 = /^\*\*\/\*\.[\w\.-]+$/; // **/*.something const T2 = /^\*\*\/[\w\.-]+$/; // **/something const T3 = /^{\*\*\/[\*\.]?[\w\.-]+(,\*\*\/[\*\.]?[\w\.-]+)*}$/; // {**/*.something,**/*.else} or {**/package.json,**/project.json} +const T3_2 = /^{\*\*\/[\*\.]?[\w\.-]+(\/\*\*)?(,\*\*\/[\*\.]?[\w\.-]+(\/\*\*)?)*}$/; // Like T3, with optional trailing /** export type ParsedPattern = (path: string, basename?: string) => boolean; export type ParsedExpression = (path: string, basename?: string, siblingsFn?: () => string[]) => string /* the matching pattern */; +export interface IGlobOptions { + trimForExclusions?: boolean; +} + interface ParsedStringPattern { (path: string, basename: string): string /* the matching pattern */; basenames?: string[]; @@ -239,7 +244,7 @@ const NULL = function(): string { return null; }; -function parsePattern(pattern: string): ParsedStringPattern { +function parsePattern(pattern: string, options: IGlobOptions): ParsedStringPattern { if (!pattern) { return NULL; } @@ -248,37 +253,26 @@ function parsePattern(pattern: string): ParsedStringPattern { pattern = pattern.trim(); // Check cache - let parsedPattern = CACHE.get(pattern); + const patternKey = `${pattern}_${!!options.trimForExclusions}`; + let parsedPattern = CACHE.get(patternKey); if (parsedPattern) { return parsedPattern; } // Check for Trivias + let trimmedPattern; if (T1.test(pattern)) { // common pattern: **/*.txt just need endsWith check const base = pattern.substr(4); // '**/*'.length === 4 parsedPattern = function (path, basename) { return path && strings.endsWith(path, base) ? pattern : null; }; } else if (T2.test(pattern)) { // common pattern: **/some.txt just need basename check - const base = pattern.substr(3); // '**/'.length === 3 - const slashBase = `/${base}`; - const backslashBase = `\\${base}`; - parsedPattern = function (path, basename) { - if (!path) { - return null; - } - if (basename) { - return basename === base ? pattern : null; - } - return path === base || strings.endsWith(path, slashBase) || strings.endsWith(path, backslashBase) ? pattern : null; - }; - const basenames = [base]; - parsedPattern.basenames = basenames; - parsedPattern.patterns = [pattern]; - parsedPattern.allBasenames = basenames; - } else if (T3.test(pattern)) { // repetition of common patterns (see above) {**/*.txt,**/*.png} + parsedPattern = trivia2(pattern, pattern); + } else if (options.trimForExclusions && strings.endsWith(pattern, '/**') && T2.test(trimmedPattern = pattern.substr(0, pattern.length - 3))) { // common pattern: **/some/** for exclusions just need basename check + parsedPattern = trivia2(trimmedPattern, pattern); + } else if ((options.trimForExclusions ? T3_2 : T3).test(pattern)) { // repetition of common patterns (see above) {**/*.txt,**/*.png} const parsedPatterns = aggregateBasenameMatches(pattern.slice(1, -1).split(',') - .map(pattern => parsePattern(pattern)) + .map(pattern => parsePattern(pattern, options)) .filter(pattern => pattern !== NULL), pattern); const n = parsedPatterns.length; if (!n) { @@ -307,11 +301,32 @@ function parsePattern(pattern: string): ParsedStringPattern { } // Cache - CACHE.set(pattern, parsedPattern); + CACHE.set(patternKey, parsedPattern); return parsedPattern; } +// common pattern: **/some.txt just need basename check +function trivia2(pattern, originalPattern): ParsedStringPattern { + const base = pattern.substr(3); // '**/'.length === 3 + const slashBase = `/${base}`; + const backslashBase = `\\${base}`; + const parsedPattern: ParsedStringPattern = function (path, basename) { + if (!path) { + return null; + } + if (basename) { + return basename === base ? originalPattern : null; + } + return path === base || strings.endsWith(path, slashBase) || strings.endsWith(path, backslashBase) ? originalPattern : null; + }; + const basenames = [base]; + parsedPattern.basenames = basenames; + parsedPattern.patterns = [originalPattern]; + parsedPattern.allBasenames = basenames; + return parsedPattern; +} + function toRegExp(pattern: string): ParsedStringPattern { try { const regExp = new RegExp(`^${parseRegExp(pattern)}$`); @@ -350,16 +365,16 @@ export function match(arg1: string | IExpression, path: string, siblingsFn?: () * - simple brace expansion ({js,ts} => js or ts) * - character ranges (using [...]) */ -export function parse(pattern: string): ParsedPattern; -export function parse(expression: IExpression): ParsedExpression; -export function parse(arg1: string | IExpression): any { +export function parse(pattern: string, options?: IGlobOptions): ParsedPattern; +export function parse(expression: IExpression, options?: IGlobOptions): ParsedExpression; +export function parse(arg1: string | IExpression, options: IGlobOptions = {}): any { if (!arg1) { return FALSE; } // Glob with String if (typeof arg1 === 'string') { - const parsedPattern = parsePattern(arg1); + const parsedPattern = parsePattern(arg1, options); if (parsedPattern === NULL) { return FALSE; } @@ -373,16 +388,16 @@ export function parse(arg1: string | IExpression): any { } // Glob with Expression - return parsedExpression(arg1); + return parsedExpression(arg1, options); } export function getBasenameTerms(patternOrExpression: ParsedPattern | ParsedExpression): string[] { return (patternOrExpression).allBasenames || []; } -function parsedExpression(expression: IExpression): ParsedExpression { +function parsedExpression(expression: IExpression, options: IGlobOptions): ParsedExpression { const parsedPatterns = aggregateBasenameMatches(Object.getOwnPropertyNames(expression) - .map(pattern => parseExpressionPattern(pattern, expression[pattern])) + .map(pattern => parseExpressionPattern(pattern, expression[pattern], options)) .filter(pattern => pattern !== NULL)); const n = parsedPatterns.length; @@ -454,12 +469,12 @@ function parsedExpression(expression: IExpression): ParsedExpression { return resultExpression; } -function parseExpressionPattern(pattern: string, value: any): (ParsedStringPattern | ParsedExpressionPattern) { +function parseExpressionPattern(pattern: string, value: any, options: IGlobOptions): (ParsedStringPattern | ParsedExpressionPattern) { if (value === false) { return NULL; // pattern is disabled } - const parsedPattern = parsePattern(pattern); + const parsedPattern = parsePattern(pattern, options); if (parsedPattern === NULL) { return NULL; } diff --git a/src/vs/base/test/common/glob.test.ts b/src/vs/base/test/common/glob.test.ts index 242fec0dcfe..e71b4ab1548 100644 --- a/src/vs/base/test/common/glob.test.ts +++ b/src/vs/base/test/common/glob.test.ts @@ -712,4 +712,46 @@ suite('Glob', () => { '**/bar': true })), ['bar']); }); + + test('expression/pattern optimization for basenames', function () { + assert.deepStrictEqual(glob.getBasenameTerms(glob.parse('**/foo/**')), []); + assert.deepStrictEqual(glob.getBasenameTerms(glob.parse('**/foo/**', { trimForExclusions: true })), ['foo']); + + testOptimizationForBasenames('**/*.foo/**', [], [['baz/bar.foo/bar/baz', true]]); + testOptimizationForBasenames('**/foo/**', ['foo'], [['bar/foo', true], ['bar/foo/baz', false]]); + testOptimizationForBasenames('{**/baz/**,**/foo/**}', ['baz', 'foo'], [['bar/baz', true], ['bar/foo', true]]); + + testOptimizationForBasenames({ + '**/foo/**': true, + '{**/bar/**,**/baz/**}': true, + '**/bulb/**': false + }, ['foo', 'bar', 'baz'], [ + ['bar/foo', '**/foo/**'], + ['foo/bar', '{**/bar/**,**/baz/**}'], + ['bar/nope', null] + ]); + + const siblingsFn = () => ['baz', 'baz.zip', 'nope']; + testOptimizationForBasenames({ + '**/foo/**': { when: '$(basename).zip' }, + '**/bar/**': true + }, ['bar'], [ + ['bar/foo', null], + ['bar/foo/baz', null], + ['bar/foo/nope', null], + ['foo/bar', '**/bar/**'], + ], [ + null, + siblingsFn, + siblingsFn + ]); + }); + + function testOptimizationForBasenames(pattern: string|glob.IExpression, basenameTerms: string[], matches: [string, string|boolean][], siblingsFns: (() => string[])[] = []) { + const parsed = glob.parse(pattern, { trimForExclusions: true }); + assert.deepStrictEqual(glob.getBasenameTerms(parsed), basenameTerms); + matches.forEach(([text, result], i) => { + assert.strictEqual(parsed(text, null, siblingsFns[i]), result); + }); + } }); \ No newline at end of file diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index f46e6d16eb9..e692c6dea23 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -67,7 +67,7 @@ export class FileWalker { constructor(config: IRawSearch) { this.config = config; this.filePattern = config.filePattern; - this.excludePattern = glob.parse(config.excludePattern); + this.excludePattern = glob.parse(config.excludePattern, { trimForExclusions: true }); this.includePattern = config.includePattern && glob.parse(config.includePattern); this.maxResults = config.maxResults || null; this.maxFilesize = config.maxFilesize || null;