diff --git a/src/vs/base/common/glob.ts b/src/vs/base/common/glob.ts index 8ea9941c5c1..61c28a4a77e 100644 --- a/src/vs/base/common/glob.ts +++ b/src/vs/base/common/glob.ts @@ -214,6 +214,7 @@ 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 /** const T4 = /^\*\*((\/[\w\.-]+)+)\/?$/; // **/something/else +const T5 = /^([\w\.-]+(\/[\w\.-]+)*)\/?$/; // something/else export type ParsedPattern = (path: string, basename?: string) => boolean; export type ParsedExpression = (path: string, basename?: string, siblingsFn?: () => string[]) => string /* the matching pattern */; @@ -227,14 +228,14 @@ interface ParsedStringPattern { basenames?: string[]; patterns?: string[]; allBasenames?: string[]; - allPathEnds?: string[]; + allPaths?: string[]; } type SiblingsPattern = { siblings: string[], name: string }; interface ParsedExpressionPattern { (path: string, basename: string, siblingsPatternFn: () => SiblingsPattern): string /* the matching pattern */; requiresSiblings?: boolean; allBasenames?: string[]; - allPathEnds?: string[]; + allPaths?: string[]; } const CACHE = new BoundedLinkedMap(10000); // bounded to 10000 elements @@ -274,7 +275,9 @@ function parsePattern(pattern: string, options: IGlobOptions): ParsedStringPatte } else if ((options.trimForExclusions ? T3_2 : T3).test(pattern)) { // repetition of common patterns (see above) {**/*.txt,**/*.png} parsedPattern = trivia3(pattern, options); } else if (match = T4.exec(trimForExclusions(pattern, options))) { // common pattern: **/something/else just need endsWith check - parsedPattern = trivia4(match[1], pattern); + parsedPattern = trivia4and5(match[1].substr(1), pattern, true); + } else if (match = T5.exec(trimForExclusions(pattern, options))) { // common pattern: something/else just need equals check + parsedPattern = trivia4and5(match[1], pattern, false); } // Otherwise convert to pattern @@ -336,21 +339,23 @@ function trivia3 (pattern: string, options: IGlobOptions): ParsedStringPattern { if (withBasenames) { parsedPattern.allBasenames = (withBasenames).allBasenames; } - const allPathEnds = parsedPatterns.reduce((all, current) => current.allPathEnds ? all.concat(current.allPathEnds) : all, []); - if (allPathEnds.length) { - parsedPattern.allPathEnds = allPathEnds; + const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, []); + if (allPaths.length) { + parsedPattern.allPaths = allPaths; } return parsedPattern; } -// common pattern: **/something/else just need endsWith check -function trivia4(pathEnd: string, pattern: string): ParsedStringPattern { - const nativePathEnd = pathEnd.replace(paths.sep, paths.nativeSep); - const nativePath = nativePathEnd.substr(1); - const parsedPattern: ParsedStringPattern = function (path, basename) { +// common patterns: **/something/else just need endsWith check, something/else just needs and equals check +function trivia4and5(path: string, pattern: string, matchPathEnds: boolean): ParsedStringPattern { + const nativePath = path.replace(paths.sep, paths.nativeSep); + const nativePathEnd = paths.nativeSep + nativePath; + const parsedPattern: ParsedStringPattern = matchPathEnds ? function (path, basename) { return path && (path === nativePath || strings.endsWith(path, nativePathEnd)) ? pattern : null; + } : function (path, basename) { + return path && path === nativePath ? pattern : null; }; - parsedPattern.allPathEnds = [pathEnd]; + parsedPattern.allPaths = [(matchPathEnds ? '*/' : './') + path]; return parsedPattern; } @@ -411,8 +416,8 @@ export function parse(arg1: string | IExpression, options: IGlobOptions = {}): a if (parsedPattern.allBasenames) { (resultPattern).allBasenames = parsedPattern.allBasenames; } - if (parsedPattern.allPathEnds) { - (resultPattern).allPathEnds = parsedPattern.allPathEnds; + if (parsedPattern.allPaths) { + (resultPattern).allPaths = parsedPattern.allPaths; } return resultPattern; } @@ -425,8 +430,8 @@ export function getBasenameTerms(patternOrExpression: ParsedPattern | ParsedExpr return (patternOrExpression).allBasenames || []; } -export function getPathEndTerms(patternOrExpression: ParsedPattern | ParsedExpression): string[] { - return (patternOrExpression).allPathEnds || []; +export function getPathTerms(patternOrExpression: ParsedPattern | ParsedExpression): string[] { + return (patternOrExpression).allPaths || []; } function parsedExpression(expression: IExpression, options: IGlobOptions): ParsedExpression { @@ -461,9 +466,9 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse resultExpression.allBasenames = (withBasenames).allBasenames; } - const allPathEnds = parsedPatterns.reduce((all, current) => current.allPathEnds ? all.concat(current.allPathEnds) : all, []); - if (allPathEnds.length) { - resultExpression.allPathEnds = allPathEnds; + const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, []); + if (allPaths.length) { + resultExpression.allPaths = allPaths; } return resultExpression; @@ -505,9 +510,9 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse resultExpression.allBasenames = (withBasenames).allBasenames; } - const allPathEnds = parsedPatterns.reduce((all, current) => current.allPathEnds ? all.concat(current.allPathEnds) : all, []); - if (allPathEnds.length) { - resultExpression.allPathEnds = allPathEnds; + const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, []); + if (allPaths.length) { + resultExpression.allPaths = allPaths; } return resultExpression; diff --git a/src/vs/base/test/common/glob.test.ts b/src/vs/base/test/common/glob.test.ts index a085b05005a..c587290e236 100644 --- a/src/vs/base/test/common/glob.test.ts +++ b/src/vs/base/test/common/glob.test.ts @@ -5,6 +5,7 @@ 'use strict'; import * as assert from 'assert'; +import * as path from 'path'; import glob = require('vs/base/common/glob'); suite('Glob', () => { @@ -460,12 +461,10 @@ suite('Glob', () => { assert(glob.match(p, 'foo.f')); }); - test('backslash agnostic', function () { + test('full path', function () { var p = 'testing/this/foo.txt'; - assert(glob.match(p, 'testing/this/foo.txt')); - assert(glob.match(p, 'testing\\this\\foo.txt')); - assert(glob.match(p, 'testing/this\\foo.txt')); + assert(glob.match(p, 'testing/this/foo.txt'.replace('/', path.sep))); }); test('prefix agnostic', function () { @@ -772,7 +771,7 @@ suite('Glob', () => { assert.strictEqual(glob.parse('{**/foo/,**/abc/}', { trimForExclusions: true })('bar/abc', 'abc'), true); }); - test('expression/pattern path end', function () { + test('expression/pattern path', function () { assert.strictEqual(glob.parse('**/foo/bar')('foo/baz', 'baz'), false); assert.strictEqual(glob.parse('**/foo/bar')('foo/bar', 'bar'), true); assert.strictEqual(glob.parse('**/foo/bar')('bar/foo/bar', 'bar'), true); @@ -780,16 +779,23 @@ suite('Glob', () => { assert.strictEqual(glob.parse('**/foo/bar/**')('bar/foo/bar/baz', 'baz'), true); assert.strictEqual(glob.parse('**/foo/bar/**', { trimForExclusions: true })('bar/foo/bar', 'bar'), true); assert.strictEqual(glob.parse('**/foo/bar/**', { trimForExclusions: true })('bar/foo/bar/baz', 'baz'), false); + + assert.strictEqual(glob.parse('foo/bar')('foo/baz', 'baz'), false); + assert.strictEqual(glob.parse('foo/bar')('foo/bar', 'bar'), true); + assert.strictEqual(glob.parse('foo/bar')('bar/foo/bar', 'bar'), false); + assert.strictEqual(glob.parse('foo/bar/**')('foo/bar/baz', 'baz'), true); + assert.strictEqual(glob.parse('foo/bar/**', { trimForExclusions: true })('foo/bar', 'bar'), true); + assert.strictEqual(glob.parse('foo/bar/**', { trimForExclusions: true })('foo/bar/baz', 'baz'), false); }); - test('expression/pattern path ends', function () { - assert.deepStrictEqual(glob.getPathEndTerms(glob.parse('**/*.foo')), []); - assert.deepStrictEqual(glob.getPathEndTerms(glob.parse('**/foo')), []); - assert.deepStrictEqual(glob.getPathEndTerms(glob.parse('**/foo/bar')), ['/foo/bar']); - assert.deepStrictEqual(glob.getPathEndTerms(glob.parse('**/foo/bar/')), ['/foo/bar']); + test('expression/pattern paths', function () { + assert.deepStrictEqual(glob.getPathTerms(glob.parse('**/*.foo')), []); + assert.deepStrictEqual(glob.getPathTerms(glob.parse('**/foo')), []); + assert.deepStrictEqual(glob.getPathTerms(glob.parse('**/foo/bar')), ['*/foo/bar']); + assert.deepStrictEqual(glob.getPathTerms(glob.parse('**/foo/bar/')), ['*/foo/bar']); // Not supported - // assert.deepStrictEqual(glob.getNativePathEnds(glob.parse('{**/baz/bar,**/foo/bar,**/bar}')), ['/baz/bar', '/foo/bar']); - // assert.deepStrictEqual(glob.getNativePathEnds(glob.parse('{**/baz/bar/,**/foo/bar/,**/bar/}')), ['/baz/bar', '/foo/bar']); + // assert.deepStrictEqual(glob.getPathTerms(glob.parse('{**/baz/bar,**/foo/bar,**/bar}')), ['*/baz/bar', '*/foo/bar']); + // assert.deepStrictEqual(glob.getPathTerms(glob.parse('{**/baz/bar/,**/foo/bar/,**/bar/}')), ['*/baz/bar', '*/foo/bar']); const parsed = glob.parse({ '**/foo/bar': true, @@ -801,30 +807,30 @@ suite('Glob', () => { '**/bulb2': true, '**/bulb/foo': false }); - assert.deepStrictEqual(glob.getPathEndTerms(parsed), ['/foo/bar', '/foo2/bar2']); + assert.deepStrictEqual(glob.getPathTerms(parsed), ['*/foo/bar', '*/foo2/bar2']); assert.deepStrictEqual(glob.getBasenameTerms(parsed), ['bulb', 'bulb2']); - assert.deepStrictEqual(glob.getPathEndTerms(glob.parse({ + assert.deepStrictEqual(glob.getPathTerms(glob.parse({ '**/foo/bar': { when: '$(basename).zip' }, '**/bar/foo': true, '**/bar2/foo2': true - })), ['/bar/foo', '/bar2/foo2']); + })), ['*/bar/foo', '*/bar2/foo2']); }); - test('expression/pattern optimization for path ends', function () { - assert.deepStrictEqual(glob.getPathEndTerms(glob.parse('**/foo/bar/**')), []); - assert.deepStrictEqual(glob.getPathEndTerms(glob.parse('**/foo/bar/**', { trimForExclusions: true })), ['/foo/bar']); + test('expression/pattern optimization for paths', function () { + assert.deepStrictEqual(glob.getPathTerms(glob.parse('**/foo/bar/**')), []); + assert.deepStrictEqual(glob.getPathTerms(glob.parse('**/foo/bar/**', { trimForExclusions: true })), ['*/foo/bar']); - testOptimizationForPathEnds('**/*.foo/bar/**', [], [['baz/bar.foo/bar/baz', true]]); - testOptimizationForPathEnds('**/foo/bar/**', ['/foo/bar'], [['bar/foo/bar', true], ['bar/foo/bar/baz', false]]); + testOptimizationForPaths('**/*.foo/bar/**', [], [['baz/bar.foo/bar/baz', true]]); + testOptimizationForPaths('**/foo/bar/**', ['*/foo/bar'], [['bar/foo/bar', true], ['bar/foo/bar/baz', false]]); // Not supported - // testOptimizationForPathEnds('{**/baz/bar/**,**/foo/bar/**}', ['/baz/bar', '/foo/bar'], [['bar/baz/bar', true], ['bar/foo/bar', true]]); + // testOptimizationForPaths('{**/baz/bar/**,**/foo/bar/**}', ['*/baz/bar', '*/foo/bar'], [['bar/baz/bar', true], ['bar/foo/bar', true]]); - testOptimizationForPathEnds({ + testOptimizationForPaths({ '**/foo/bar/**': true, // Not supported // '{**/bar/bar/**,**/baz/bar/**}': true, '**/bulb/bar/**': false - }, ['/foo/bar'], [ + }, ['*/foo/bar'], [ ['bar/foo/bar', '**/foo/bar/**'], // Not supported // ['foo/bar/bar', '{**/bar/bar/**,**/baz/bar/**}'], @@ -832,10 +838,10 @@ suite('Glob', () => { ]); const siblingsFn = () => ['baz', 'baz.zip', 'nope']; - testOptimizationForPathEnds({ + testOptimizationForPaths({ '**/foo/123/**': { when: '$(basename).zip' }, '**/bar/123/**': true - }, ['/bar/123'], [ + }, ['*/bar/123'], [ ['bar/foo/123', null], ['bar/foo/123/baz', null], ['bar/foo/123/nope', null], @@ -847,9 +853,9 @@ suite('Glob', () => { ]); }); - function testOptimizationForPathEnds(pattern: string|glob.IExpression, pathEndTerms: string[], matches: [string, string|boolean][], siblingsFns: (() => string[])[] = []) { + function testOptimizationForPaths(pattern: string|glob.IExpression, pathTerms: string[], matches: [string, string|boolean][], siblingsFns: (() => string[])[] = []) { const parsed = glob.parse(pattern, { trimForExclusions: true }); - assert.deepStrictEqual(glob.getPathEndTerms(parsed), pathEndTerms); + assert.deepStrictEqual(glob.getPathTerms(parsed), pathTerms); matches.forEach(([text, result], i) => { assert.strictEqual(parsed(text, null, siblingsFns[i]), result); }); diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index 600c1643577..6e9472386d1 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -256,16 +256,16 @@ export class FileWalker { */ public spawnFindCmd(rootFolder: string, excludePattern: glob.ParsedExpression) { const basenames = glob.getBasenameTerms(excludePattern); - const pathEnds = glob.getPathEndTerms(excludePattern); + const paths = glob.getPathTerms(excludePattern); let args = ['-L', '.']; - if (basenames.length || pathEnds.length) { + if (basenames.length || paths.length) { args.push('-not', '(', '('); for (const basename of basenames) { - args.push('-name', FileWalker.escapeGlobSpecials(basename)); + args.push('-name', basename); args.push('-o'); } - for (const pathEnd of pathEnds) { - args.push('-path', '*' + FileWalker.escapeGlobSpecials(pathEnd)); + for (const path of paths) { + args.push('-path', path); args.push('-o'); } args.pop(); @@ -275,12 +275,6 @@ export class FileWalker { return childProcess.spawn('find', args, { cwd: rootFolder }); } - private static GLOB_SPECIALS = /[*?\[\]\\]/g; - private static ESCAPE_CHAR = '\\$&'; - private static escapeGlobSpecials(string) { - return string.replace(this.GLOB_SPECIALS, this.ESCAPE_CHAR); - } - /** * Public for testing. */ diff --git a/src/vs/workbench/services/search/test/node/search.test.ts b/src/vs/workbench/services/search/test/node/search.test.ts index d649a585c9a..f3a60d1c78d 100644 --- a/src/vs/workbench/services/search/test/node/search.test.ts +++ b/src/vs/workbench/services/search/test/node/search.test.ts @@ -478,7 +478,7 @@ suite('Search', () => { }); }); - test('Find: exclude folder path', function (done: () => void) { + test('Find: exclude folder path suffix', function (done: () => void) { if (platform.isWindows) { done(); return; @@ -504,7 +504,7 @@ suite('Search', () => { }); }); - test('Find: exclude subfolder path', function (done: () => void) { + test('Find: exclude subfolder path suffix', function (done: () => void) { if (platform.isWindows) { done(); return; @@ -530,6 +530,32 @@ suite('Search', () => { }); }); + test('Find: exclude folder path', function (done: () => void) { + if (platform.isWindows) { + done(); + return; + } + + const walker = new FileWalker({ rootFolders: rootfolders() }); + const file0 = './examples/company.js'; + const file1 = './examples/subfolder/subfile.txt'; + + const cmd1 = walker.spawnFindCmd(rootfolders()[0], glob.parse({ 'examples/something': true })); + walker.readStdout(cmd1, 'utf8', (err1, stdout1) => { + assert.equal(err1, null); + assert.notStrictEqual(stdout1.split('\n').indexOf(file0), -1, stdout1); + assert.notStrictEqual(stdout1.split('\n').indexOf(file1), -1, stdout1); + + const cmd2 = walker.spawnFindCmd(rootfolders()[0], glob.parse({ 'examples/subfolder': true })); + walker.readStdout(cmd2, 'utf8', (err2, stdout2) => { + assert.equal(err2, null); + assert.notStrictEqual(stdout1.split('\n').indexOf(file0), -1, stdout1); + assert.strictEqual(stdout2.split('\n').indexOf(file1), -1, stdout2); + done(); + }); + }); + }); + test('Find: exclude combination of paths', function (done: () => void) { if (platform.isWindows) { done();