diff --git a/src/typings/chokidar.d.ts b/src/typings/chokidar.d.ts index 2aeacfc8db3..411080c2cc7 100644 --- a/src/typings/chokidar.d.ts +++ b/src/typings/chokidar.d.ts @@ -8,7 +8,7 @@ declare module 'vscode-chokidar' { /** * takes paths to be watched recursively and options */ - export function watch(paths: string, options: IOptions): FSWatcher; + export function watch(paths: string | string[], options: IOptions): FSWatcher; export interface IOptions { diff --git a/src/vs/base/test/node/glob.test.ts b/src/vs/base/test/node/glob.test.ts index 6538ee24941..5d2d82599e2 100644 --- a/src/vs/base/test/node/glob.test.ts +++ b/src/vs/base/test/node/glob.test.ts @@ -63,350 +63,369 @@ suite('Glob', () => { // console.profileEnd(); // }); + function assertGlobMatch(pattern: string | glob.IRelativePattern, input: string) { + assert(glob.match(pattern, input), `${pattern} should match ${input}`); + } + + function assertNoGlobMatch(pattern: string | glob.IRelativePattern, input: string) { + assert(!glob.match(pattern, input), `${pattern} should not match ${input}`); + } + test('simple', function () { let p = 'node_modules'; - assert(glob.match(p, 'node_modules')); - assert(!glob.match(p, 'node_module')); - assert(!glob.match(p, '/node_modules')); - assert(!glob.match(p, 'test/node_modules')); + assertGlobMatch(p, 'node_modules'); + assertNoGlobMatch(p, 'node_module'); + assertNoGlobMatch(p, '/node_modules'); + assertNoGlobMatch(p, 'test/node_modules'); p = 'test.txt'; - assert(glob.match(p, 'test.txt')); - assert(!glob.match(p, 'test?txt')); - assert(!glob.match(p, '/text.txt')); - assert(!glob.match(p, 'test/test.txt')); + assertGlobMatch(p, 'test.txt'); + assertNoGlobMatch(p, 'test?txt'); + assertNoGlobMatch(p, '/text.txt'); + assertNoGlobMatch(p, 'test/test.txt'); p = 'test(.txt'; - assert(glob.match(p, 'test(.txt')); - assert(!glob.match(p, 'test?txt')); + assertGlobMatch(p, 'test(.txt'); + assertNoGlobMatch(p, 'test?txt'); p = 'qunit'; - assert(glob.match(p, 'qunit')); - assert(!glob.match(p, 'qunit.css')); - assert(!glob.match(p, 'test/qunit')); + assertGlobMatch(p, 'qunit'); + assertNoGlobMatch(p, 'qunit.css'); + assertNoGlobMatch(p, 'test/qunit'); // Absolute p = '/DNXConsoleApp/**/*.cs'; - assert(glob.match(p, '/DNXConsoleApp/Program.cs')); - assert(glob.match(p, '/DNXConsoleApp/foo/Program.cs')); + assertGlobMatch(p, '/DNXConsoleApp/Program.cs'); + assertGlobMatch(p, '/DNXConsoleApp/foo/Program.cs'); p = 'C:/DNXConsoleApp/**/*.cs'; - assert(glob.match(p, 'C:\\DNXConsoleApp\\Program.cs')); - assert(glob.match(p, 'C:\\DNXConsoleApp\\foo\\Program.cs')); + assertGlobMatch(p, 'C:\\DNXConsoleApp\\Program.cs'); + assertGlobMatch(p, 'C:\\DNXConsoleApp\\foo\\Program.cs'); }); test('dot hidden', function () { let p = '.*'; - assert(glob.match(p, '.git')); - assert(glob.match(p, '.hidden.txt')); - assert(!glob.match(p, 'git')); - assert(!glob.match(p, 'hidden.txt')); - assert(!glob.match(p, 'path/.git')); - assert(!glob.match(p, 'path/.hidden.txt')); + assertGlobMatch(p, '.git'); + assertGlobMatch(p, '.hidden.txt'); + assertNoGlobMatch(p, 'git'); + assertNoGlobMatch(p, 'hidden.txt'); + assertNoGlobMatch(p, 'path/.git'); + assertNoGlobMatch(p, 'path/.hidden.txt'); p = '**/.*'; - assert(glob.match(p, '.git')); - assert(glob.match(p, '.hidden.txt')); - assert(!glob.match(p, 'git')); - assert(!glob.match(p, 'hidden.txt')); - assert(glob.match(p, 'path/.git')); - assert(glob.match(p, 'path/.hidden.txt')); - assert(!glob.match(p, 'path/git')); - assert(!glob.match(p, 'pat.h/hidden.txt')); + assertGlobMatch(p, '.git'); + assertGlobMatch(p, '.hidden.txt'); + assertNoGlobMatch(p, 'git'); + assertNoGlobMatch(p, 'hidden.txt'); + assertGlobMatch(p, 'path/.git'); + assertGlobMatch(p, 'path/.hidden.txt'); + assertNoGlobMatch(p, 'path/git'); + assertNoGlobMatch(p, 'pat.h/hidden.txt'); p = '._*'; - assert(glob.match(p, '._git')); - assert(glob.match(p, '._hidden.txt')); - assert(!glob.match(p, 'git')); - assert(!glob.match(p, 'hidden.txt')); - assert(!glob.match(p, 'path/._git')); - assert(!glob.match(p, 'path/._hidden.txt')); + assertGlobMatch(p, '._git'); + assertGlobMatch(p, '._hidden.txt'); + assertNoGlobMatch(p, 'git'); + assertNoGlobMatch(p, 'hidden.txt'); + assertNoGlobMatch(p, 'path/._git'); + assertNoGlobMatch(p, 'path/._hidden.txt'); p = '**/._*'; - assert(glob.match(p, '._git')); - assert(glob.match(p, '._hidden.txt')); - assert(!glob.match(p, 'git')); - assert(!glob.match(p, 'hidden._txt')); - assert(glob.match(p, 'path/._git')); - assert(glob.match(p, 'path/._hidden.txt')); - assert(!glob.match(p, 'path/git')); - assert(!glob.match(p, 'pat.h/hidden._txt')); + assertGlobMatch(p, '._git'); + assertGlobMatch(p, '._hidden.txt'); + assertNoGlobMatch(p, 'git'); + assertNoGlobMatch(p, 'hidden._txt'); + assertGlobMatch(p, 'path/._git'); + assertGlobMatch(p, 'path/._hidden.txt'); + assertNoGlobMatch(p, 'path/git'); + assertNoGlobMatch(p, 'pat.h/hidden._txt'); }); test('file pattern', function () { let p = '*.js'; - assert(glob.match(p, 'foo.js')); - assert(!glob.match(p, 'folder/foo.js')); - assert(!glob.match(p, '/node_modules/foo.js')); - assert(!glob.match(p, 'foo.jss')); - assert(!glob.match(p, 'some.js/test')); + assertGlobMatch(p, 'foo.js'); + assertNoGlobMatch(p, 'folder/foo.js'); + assertNoGlobMatch(p, '/node_modules/foo.js'); + assertNoGlobMatch(p, 'foo.jss'); + assertNoGlobMatch(p, 'some.js/test'); p = 'html.*'; - assert(glob.match(p, 'html.js')); - assert(glob.match(p, 'html.txt')); - assert(!glob.match(p, 'htm.txt')); + assertGlobMatch(p, 'html.js'); + assertGlobMatch(p, 'html.txt'); + assertNoGlobMatch(p, 'htm.txt'); p = '*.*'; - assert(glob.match(p, 'html.js')); - assert(glob.match(p, 'html.txt')); - assert(glob.match(p, 'htm.txt')); - assert(!glob.match(p, 'folder/foo.js')); - assert(!glob.match(p, '/node_modules/foo.js')); + assertGlobMatch(p, 'html.js'); + assertGlobMatch(p, 'html.txt'); + assertGlobMatch(p, 'htm.txt'); + assertNoGlobMatch(p, 'folder/foo.js'); + assertNoGlobMatch(p, '/node_modules/foo.js'); p = 'node_modules/test/*.js'; - assert(glob.match(p, 'node_modules/test/foo.js')); - assert(!glob.match(p, 'folder/foo.js')); - assert(!glob.match(p, '/node_module/test/foo.js')); - assert(!glob.match(p, 'foo.jss')); - assert(!glob.match(p, 'some.js/test')); + assertGlobMatch(p, 'node_modules/test/foo.js'); + assertNoGlobMatch(p, 'folder/foo.js'); + assertNoGlobMatch(p, '/node_module/test/foo.js'); + assertNoGlobMatch(p, 'foo.jss'); + assertNoGlobMatch(p, 'some.js/test'); }); test('star', function () { let p = 'node*modules'; - assert(glob.match(p, 'node_modules')); - assert(glob.match(p, 'node_super_modules')); - assert(!glob.match(p, 'node_module')); - assert(!glob.match(p, '/node_modules')); - assert(!glob.match(p, 'test/node_modules')); + assertGlobMatch(p, 'node_modules'); + assertGlobMatch(p, 'node_super_modules'); + assertNoGlobMatch(p, 'node_module'); + assertNoGlobMatch(p, '/node_modules'); + assertNoGlobMatch(p, 'test/node_modules'); p = '*'; - assert(glob.match(p, 'html.js')); - assert(glob.match(p, 'html.txt')); - assert(glob.match(p, 'htm.txt')); - assert(!glob.match(p, 'folder/foo.js')); - assert(!glob.match(p, '/node_modules/foo.js')); + assertGlobMatch(p, 'html.js'); + assertGlobMatch(p, 'html.txt'); + assertGlobMatch(p, 'htm.txt'); + assertNoGlobMatch(p, 'folder/foo.js'); + assertNoGlobMatch(p, '/node_modules/foo.js'); + }); + + test('file / folder match', function () { + let p = '**/node_modules/**'; + + assertGlobMatch(p, 'node_modules'); + assertGlobMatch(p, 'node_modules/'); + assertGlobMatch(p, 'a/node_modules'); + assertGlobMatch(p, 'a/node_modules/'); + assertGlobMatch(p, 'node_modules/foo'); + assertGlobMatch(p, 'foo/node_modules/foo/bar'); }); test('questionmark', function () { let p = 'node?modules'; - assert(glob.match(p, 'node_modules')); - assert(!glob.match(p, 'node_super_modules')); - assert(!glob.match(p, 'node_module')); - assert(!glob.match(p, '/node_modules')); - assert(!glob.match(p, 'test/node_modules')); + assertGlobMatch(p, 'node_modules'); + assertNoGlobMatch(p, 'node_super_modules'); + assertNoGlobMatch(p, 'node_module'); + assertNoGlobMatch(p, '/node_modules'); + assertNoGlobMatch(p, 'test/node_modules'); p = '?'; - assert(glob.match(p, 'h')); - assert(!glob.match(p, 'html.txt')); - assert(!glob.match(p, 'htm.txt')); - assert(!glob.match(p, 'folder/foo.js')); - assert(!glob.match(p, '/node_modules/foo.js')); + assertGlobMatch(p, 'h'); + assertNoGlobMatch(p, 'html.txt'); + assertNoGlobMatch(p, 'htm.txt'); + assertNoGlobMatch(p, 'folder/foo.js'); + assertNoGlobMatch(p, '/node_modules/foo.js'); }); test('globstar', function () { let p = '**/*.js'; - assert(glob.match(p, 'foo.js')); - assert(glob.match(p, 'folder/foo.js')); - assert(glob.match(p, '/node_modules/foo.js')); - assert(!glob.match(p, 'foo.jss')); - assert(!glob.match(p, 'some.js/test')); - assert(!glob.match(p, '/some.js/test')); - assert(!glob.match(p, '\\some.js\\test')); + assertGlobMatch(p, 'foo.js'); + assertGlobMatch(p, 'folder/foo.js'); + assertGlobMatch(p, '/node_modules/foo.js'); + assertNoGlobMatch(p, 'foo.jss'); + assertNoGlobMatch(p, 'some.js/test'); + assertNoGlobMatch(p, '/some.js/test'); + assertNoGlobMatch(p, '\\some.js\\test'); p = '**/project.json'; - assert(glob.match(p, 'project.json')); - assert(glob.match(p, '/project.json')); - assert(glob.match(p, 'some/folder/project.json')); - assert(!glob.match(p, 'some/folder/file_project.json')); - assert(!glob.match(p, 'some/folder/fileproject.json')); - // assert(!glob.match(p, '/rrproject.json')); TODO@ben this still fails if T1-3 are disabled - assert(!glob.match(p, 'some/rrproject.json')); - // assert(!glob.match(p, 'rrproject.json')); - // assert(!glob.match(p, '\\rrproject.json')); - assert(!glob.match(p, 'some\\rrproject.json')); + assertGlobMatch(p, 'project.json'); + assertGlobMatch(p, '/project.json'); + assertGlobMatch(p, 'some/folder/project.json'); + assertNoGlobMatch(p, 'some/folder/file_project.json'); + assertNoGlobMatch(p, 'some/folder/fileproject.json'); + // assertNoGlobMatch(p, '/rrproject.json'); TODO@ben this still fails if T1-3 are disabled + assertNoGlobMatch(p, 'some/rrproject.json'); + // assertNoGlobMatch(p, 'rrproject.json'); + // assertNoGlobMatch(p, '\\rrproject.json'); + assertNoGlobMatch(p, 'some\\rrproject.json'); p = 'test/**'; - assert(glob.match(p, 'test')); - assert(glob.match(p, 'test/foo.js')); - assert(glob.match(p, 'test/other/foo.js')); - assert(!glob.match(p, 'est/other/foo.js')); + assertGlobMatch(p, 'test'); + assertGlobMatch(p, 'test/foo.js'); + assertGlobMatch(p, 'test/other/foo.js'); + assertNoGlobMatch(p, 'est/other/foo.js'); p = '**'; - assert(glob.match(p, 'foo.js')); - assert(glob.match(p, 'folder/foo.js')); - assert(glob.match(p, '/node_modules/foo.js')); - assert(glob.match(p, 'foo.jss')); - assert(glob.match(p, 'some.js/test')); + assertGlobMatch(p, 'foo.js'); + assertGlobMatch(p, 'folder/foo.js'); + assertGlobMatch(p, '/node_modules/foo.js'); + assertGlobMatch(p, 'foo.jss'); + assertGlobMatch(p, 'some.js/test'); p = 'test/**/*.js'; - assert(glob.match(p, 'test/foo.js')); - assert(glob.match(p, 'test/other/foo.js')); - assert(glob.match(p, 'test/other/more/foo.js')); - assert(!glob.match(p, 'test/foo.ts')); - assert(!glob.match(p, 'test/other/foo.ts')); - assert(!glob.match(p, 'test/other/more/foo.ts')); + assertGlobMatch(p, 'test/foo.js'); + assertGlobMatch(p, 'test/other/foo.js'); + assertGlobMatch(p, 'test/other/more/foo.js'); + assertNoGlobMatch(p, 'test/foo.ts'); + assertNoGlobMatch(p, 'test/other/foo.ts'); + assertNoGlobMatch(p, 'test/other/more/foo.ts'); p = '**/**/*.js'; - assert(glob.match(p, 'foo.js')); - assert(glob.match(p, 'folder/foo.js')); - assert(glob.match(p, '/node_modules/foo.js')); - assert(!glob.match(p, 'foo.jss')); - assert(!glob.match(p, 'some.js/test')); + assertGlobMatch(p, 'foo.js'); + assertGlobMatch(p, 'folder/foo.js'); + assertGlobMatch(p, '/node_modules/foo.js'); + assertNoGlobMatch(p, 'foo.jss'); + assertNoGlobMatch(p, 'some.js/test'); p = '**/node_modules/**/*.js'; - assert(!glob.match(p, 'foo.js')); - assert(!glob.match(p, 'folder/foo.js')); - assert(glob.match(p, 'node_modules/foo.js')); - assert(glob.match(p, 'node_modules/some/folder/foo.js')); - assert(!glob.match(p, 'node_modules/some/folder/foo.ts')); - assert(!glob.match(p, 'foo.jss')); - assert(!glob.match(p, 'some.js/test')); + assertNoGlobMatch(p, 'foo.js'); + assertNoGlobMatch(p, 'folder/foo.js'); + assertGlobMatch(p, 'node_modules/foo.js'); + assertGlobMatch(p, 'node_modules/some/folder/foo.js'); + assertNoGlobMatch(p, 'node_modules/some/folder/foo.ts'); + assertNoGlobMatch(p, 'foo.jss'); + assertNoGlobMatch(p, 'some.js/test'); p = '{**/node_modules/**,**/.git/**,**/bower_components/**}'; - assert(glob.match(p, 'node_modules')); - assert(glob.match(p, '/node_modules')); - assert(glob.match(p, '/node_modules/more')); - assert(glob.match(p, 'some/test/node_modules')); - assert(glob.match(p, 'some\\test\\node_modules')); - assert(glob.match(p, 'C:\\\\some\\test\\node_modules')); - assert(glob.match(p, 'C:\\\\some\\test\\node_modules\\more')); + assertGlobMatch(p, 'node_modules'); + assertGlobMatch(p, '/node_modules'); + assertGlobMatch(p, '/node_modules/more'); + assertGlobMatch(p, 'some/test/node_modules'); + assertGlobMatch(p, 'some\\test\\node_modules'); + assertGlobMatch(p, 'C:\\\\some\\test\\node_modules'); + assertGlobMatch(p, 'C:\\\\some\\test\\node_modules\\more'); - assert(glob.match(p, 'bower_components')); - assert(glob.match(p, 'bower_components/more')); - assert(glob.match(p, '/bower_components')); - assert(glob.match(p, 'some/test/bower_components')); - assert(glob.match(p, 'some\\test\\bower_components')); - assert(glob.match(p, 'C:\\\\some\\test\\bower_components')); - assert(glob.match(p, 'C:\\\\some\\test\\bower_components\\more')); + assertGlobMatch(p, 'bower_components'); + assertGlobMatch(p, 'bower_components/more'); + assertGlobMatch(p, '/bower_components'); + assertGlobMatch(p, 'some/test/bower_components'); + assertGlobMatch(p, 'some\\test\\bower_components'); + assertGlobMatch(p, 'C:\\\\some\\test\\bower_components'); + assertGlobMatch(p, 'C:\\\\some\\test\\bower_components\\more'); - assert(glob.match(p, '.git')); - assert(glob.match(p, '/.git')); - assert(glob.match(p, 'some/test/.git')); - assert(glob.match(p, 'some\\test\\.git')); - assert(glob.match(p, 'C:\\\\some\\test\\.git')); + assertGlobMatch(p, '.git'); + assertGlobMatch(p, '/.git'); + assertGlobMatch(p, 'some/test/.git'); + assertGlobMatch(p, 'some\\test\\.git'); + assertGlobMatch(p, 'C:\\\\some\\test\\.git'); - assert(!glob.match(p, 'tempting')); - assert(!glob.match(p, '/tempting')); - assert(!glob.match(p, 'some/test/tempting')); - assert(!glob.match(p, 'some\\test\\tempting')); - assert(!glob.match(p, 'C:\\\\some\\test\\tempting')); + assertNoGlobMatch(p, 'tempting'); + assertNoGlobMatch(p, '/tempting'); + assertNoGlobMatch(p, 'some/test/tempting'); + assertNoGlobMatch(p, 'some\\test\\tempting'); + assertNoGlobMatch(p, 'C:\\\\some\\test\\tempting'); p = '{**/package.json,**/project.json}'; - assert(glob.match(p, 'package.json')); - assert(glob.match(p, '/package.json')); - assert(!glob.match(p, 'xpackage.json')); - assert(!glob.match(p, '/xpackage.json')); + assertGlobMatch(p, 'package.json'); + assertGlobMatch(p, '/package.json'); + assertNoGlobMatch(p, 'xpackage.json'); + assertNoGlobMatch(p, '/xpackage.json'); }); test('issue 41724', function () { let p = 'some/**/*.js'; - assert(glob.match(p, 'some/foo.js')); - assert(glob.match(p, 'some/folder/foo.js')); - assert(!glob.match(p, 'something/foo.js')); - assert(!glob.match(p, 'something/folder/foo.js')); + assertGlobMatch(p, 'some/foo.js'); + assertGlobMatch(p, 'some/folder/foo.js'); + assertNoGlobMatch(p, 'something/foo.js'); + assertNoGlobMatch(p, 'something/folder/foo.js'); p = 'some/**/*'; - assert(glob.match(p, 'some/foo.js')); - assert(glob.match(p, 'some/folder/foo.js')); - assert(!glob.match(p, 'something/foo.js')); - assert(!glob.match(p, 'something/folder/foo.js')); + assertGlobMatch(p, 'some/foo.js'); + assertGlobMatch(p, 'some/folder/foo.js'); + assertNoGlobMatch(p, 'something/foo.js'); + assertNoGlobMatch(p, 'something/folder/foo.js'); }); test('brace expansion', function () { let p = '*.{html,js}'; - assert(glob.match(p, 'foo.js')); - assert(glob.match(p, 'foo.html')); - assert(!glob.match(p, 'folder/foo.js')); - assert(!glob.match(p, '/node_modules/foo.js')); - assert(!glob.match(p, 'foo.jss')); - assert(!glob.match(p, 'some.js/test')); + assertGlobMatch(p, 'foo.js'); + assertGlobMatch(p, 'foo.html'); + assertNoGlobMatch(p, 'folder/foo.js'); + assertNoGlobMatch(p, '/node_modules/foo.js'); + assertNoGlobMatch(p, 'foo.jss'); + assertNoGlobMatch(p, 'some.js/test'); p = '*.{html}'; - assert(glob.match(p, 'foo.html')); - assert(!glob.match(p, 'foo.js')); - assert(!glob.match(p, 'folder/foo.js')); - assert(!glob.match(p, '/node_modules/foo.js')); - assert(!glob.match(p, 'foo.jss')); - assert(!glob.match(p, 'some.js/test')); + assertGlobMatch(p, 'foo.html'); + assertNoGlobMatch(p, 'foo.js'); + assertNoGlobMatch(p, 'folder/foo.js'); + assertNoGlobMatch(p, '/node_modules/foo.js'); + assertNoGlobMatch(p, 'foo.jss'); + assertNoGlobMatch(p, 'some.js/test'); p = '{node_modules,testing}'; - assert(glob.match(p, 'node_modules')); - assert(glob.match(p, 'testing')); - assert(!glob.match(p, 'node_module')); - assert(!glob.match(p, 'dtesting')); + assertGlobMatch(p, 'node_modules'); + assertGlobMatch(p, 'testing'); + assertNoGlobMatch(p, 'node_module'); + assertNoGlobMatch(p, 'dtesting'); p = '**/{foo,bar}'; - assert(glob.match(p, 'foo')); - assert(glob.match(p, 'bar')); - assert(glob.match(p, 'test/foo')); - assert(glob.match(p, 'test/bar')); - assert(glob.match(p, 'other/more/foo')); - assert(glob.match(p, 'other/more/bar')); + assertGlobMatch(p, 'foo'); + assertGlobMatch(p, 'bar'); + assertGlobMatch(p, 'test/foo'); + assertGlobMatch(p, 'test/bar'); + assertGlobMatch(p, 'other/more/foo'); + assertGlobMatch(p, 'other/more/bar'); p = '{foo,bar}/**'; - assert(glob.match(p, 'foo')); - assert(glob.match(p, 'bar')); - assert(glob.match(p, 'foo/test')); - assert(glob.match(p, 'bar/test')); - assert(glob.match(p, 'foo/other/more')); - assert(glob.match(p, 'bar/other/more')); + assertGlobMatch(p, 'foo'); + assertGlobMatch(p, 'bar'); + assertGlobMatch(p, 'foo/test'); + assertGlobMatch(p, 'bar/test'); + assertGlobMatch(p, 'foo/other/more'); + assertGlobMatch(p, 'bar/other/more'); p = '{**/*.d.ts,**/*.js}'; - assert(glob.match(p, 'foo.js')); - assert(glob.match(p, 'testing/foo.js')); - assert(glob.match(p, 'testing\\foo.js')); - assert(glob.match(p, '/testing/foo.js')); - assert(glob.match(p, '\\testing\\foo.js')); - assert(glob.match(p, 'C:\\testing\\foo.js')); + assertGlobMatch(p, 'foo.js'); + assertGlobMatch(p, 'testing/foo.js'); + assertGlobMatch(p, 'testing\\foo.js'); + assertGlobMatch(p, '/testing/foo.js'); + assertGlobMatch(p, '\\testing\\foo.js'); + assertGlobMatch(p, 'C:\\testing\\foo.js'); - assert(glob.match(p, 'foo.d.ts')); - assert(glob.match(p, 'testing/foo.d.ts')); - assert(glob.match(p, 'testing\\foo.d.ts')); - assert(glob.match(p, '/testing/foo.d.ts')); - assert(glob.match(p, '\\testing\\foo.d.ts')); - assert(glob.match(p, 'C:\\testing\\foo.d.ts')); + assertGlobMatch(p, 'foo.d.ts'); + assertGlobMatch(p, 'testing/foo.d.ts'); + assertGlobMatch(p, 'testing\\foo.d.ts'); + assertGlobMatch(p, '/testing/foo.d.ts'); + assertGlobMatch(p, '\\testing\\foo.d.ts'); + assertGlobMatch(p, 'C:\\testing\\foo.d.ts'); - assert(!glob.match(p, 'foo.d')); - assert(!glob.match(p, 'testing/foo.d')); - assert(!glob.match(p, 'testing\\foo.d')); - assert(!glob.match(p, '/testing/foo.d')); - assert(!glob.match(p, '\\testing\\foo.d')); - assert(!glob.match(p, 'C:\\testing\\foo.d')); + assertNoGlobMatch(p, 'foo.d'); + assertNoGlobMatch(p, 'testing/foo.d'); + assertNoGlobMatch(p, 'testing\\foo.d'); + assertNoGlobMatch(p, '/testing/foo.d'); + assertNoGlobMatch(p, '\\testing\\foo.d'); + assertNoGlobMatch(p, 'C:\\testing\\foo.d'); p = '{**/*.d.ts,**/*.js,path/simple.jgs}'; - assert(glob.match(p, 'foo.js')); - assert(glob.match(p, 'testing/foo.js')); - assert(glob.match(p, 'testing\\foo.js')); - assert(glob.match(p, '/testing/foo.js')); - assert(glob.match(p, 'path/simple.jgs')); - assert(!glob.match(p, '/path/simple.jgs')); - assert(glob.match(p, '\\testing\\foo.js')); - assert(glob.match(p, 'C:\\testing\\foo.js')); + assertGlobMatch(p, 'foo.js'); + assertGlobMatch(p, 'testing/foo.js'); + assertGlobMatch(p, 'testing\\foo.js'); + assertGlobMatch(p, '/testing/foo.js'); + assertGlobMatch(p, 'path/simple.jgs'); + assertNoGlobMatch(p, '/path/simple.jgs'); + assertGlobMatch(p, '\\testing\\foo.js'); + assertGlobMatch(p, 'C:\\testing\\foo.js'); p = '{**/*.d.ts,**/*.js,foo.[0-9]}'; - assert(glob.match(p, 'foo.5')); - assert(glob.match(p, 'foo.8')); - assert(!glob.match(p, 'bar.5')); - assert(!glob.match(p, 'foo.f')); - assert(glob.match(p, 'foo.js')); + assertGlobMatch(p, 'foo.5'); + assertGlobMatch(p, 'foo.8'); + assertNoGlobMatch(p, 'bar.5'); + assertNoGlobMatch(p, 'foo.f'); + assertGlobMatch(p, 'foo.js'); p = 'prefix/{**/*.d.ts,**/*.js,foo.[0-9]}'; - assert(glob.match(p, 'prefix/foo.5')); - assert(glob.match(p, 'prefix/foo.8')); - assert(!glob.match(p, 'prefix/bar.5')); - assert(!glob.match(p, 'prefix/foo.f')); - assert(glob.match(p, 'prefix/foo.js')); + assertGlobMatch(p, 'prefix/foo.5'); + assertGlobMatch(p, 'prefix/foo.8'); + assertNoGlobMatch(p, 'prefix/bar.5'); + assertNoGlobMatch(p, 'prefix/foo.f'); + assertGlobMatch(p, 'prefix/foo.js'); }); test('expression support (single)', function () { @@ -465,57 +484,57 @@ suite('Glob', () => { test('brackets', function () { let p = 'foo.[0-9]'; - assert(glob.match(p, 'foo.5')); - assert(glob.match(p, 'foo.8')); - assert(!glob.match(p, 'bar.5')); - assert(!glob.match(p, 'foo.f')); + assertGlobMatch(p, 'foo.5'); + assertGlobMatch(p, 'foo.8'); + assertNoGlobMatch(p, 'bar.5'); + assertNoGlobMatch(p, 'foo.f'); p = 'foo.[^0-9]'; - assert(!glob.match(p, 'foo.5')); - assert(!glob.match(p, 'foo.8')); - assert(!glob.match(p, 'bar.5')); - assert(glob.match(p, 'foo.f')); + assertNoGlobMatch(p, 'foo.5'); + assertNoGlobMatch(p, 'foo.8'); + assertNoGlobMatch(p, 'bar.5'); + assertGlobMatch(p, 'foo.f'); p = 'foo.[!0-9]'; - assert(!glob.match(p, 'foo.5')); - assert(!glob.match(p, 'foo.8')); - assert(!glob.match(p, 'bar.5')); - assert(glob.match(p, 'foo.f')); + assertNoGlobMatch(p, 'foo.5'); + assertNoGlobMatch(p, 'foo.8'); + assertNoGlobMatch(p, 'bar.5'); + assertGlobMatch(p, 'foo.f'); p = 'foo.[0!^*?]'; - assert(!glob.match(p, 'foo.5')); - assert(!glob.match(p, 'foo.8')); - assert(glob.match(p, 'foo.0')); - assert(glob.match(p, 'foo.!')); - assert(glob.match(p, 'foo.^')); - assert(glob.match(p, 'foo.*')); - assert(glob.match(p, 'foo.?')); + assertNoGlobMatch(p, 'foo.5'); + assertNoGlobMatch(p, 'foo.8'); + assertGlobMatch(p, 'foo.0'); + assertGlobMatch(p, 'foo.!'); + assertGlobMatch(p, 'foo.^'); + assertGlobMatch(p, 'foo.*'); + assertGlobMatch(p, 'foo.?'); p = 'foo[/]bar'; - assert(!glob.match(p, 'foo/bar')); + assertNoGlobMatch(p, 'foo/bar'); p = 'foo.[[]'; - assert(glob.match(p, 'foo.[')); + assertGlobMatch(p, 'foo.['); p = 'foo.[]]'; - assert(glob.match(p, 'foo.]')); + assertGlobMatch(p, 'foo.]'); p = 'foo.[][!]'; - assert(glob.match(p, 'foo.]')); - assert(glob.match(p, 'foo.[')); - assert(glob.match(p, 'foo.!')); + assertGlobMatch(p, 'foo.]'); + assertGlobMatch(p, 'foo.['); + assertGlobMatch(p, 'foo.!'); p = 'foo.[]-]'; - assert(glob.match(p, 'foo.]')); - assert(glob.match(p, 'foo.-')); + assertGlobMatch(p, 'foo.]'); + assertGlobMatch(p, 'foo.-'); }); test('full path', function () { @@ -527,111 +546,111 @@ suite('Glob', () => { test('prefix agnostic', function () { let p = '**/*.js'; - assert(glob.match(p, 'foo.js')); - assert(glob.match(p, '/foo.js')); - assert(glob.match(p, '\\foo.js')); - assert(glob.match(p, 'testing/foo.js')); - assert(glob.match(p, 'testing\\foo.js')); - assert(glob.match(p, '/testing/foo.js')); - assert(glob.match(p, '\\testing\\foo.js')); - assert(glob.match(p, 'C:\\testing\\foo.js')); + assertGlobMatch(p, 'foo.js'); + assertGlobMatch(p, '/foo.js'); + assertGlobMatch(p, '\\foo.js'); + assertGlobMatch(p, 'testing/foo.js'); + assertGlobMatch(p, 'testing\\foo.js'); + assertGlobMatch(p, '/testing/foo.js'); + assertGlobMatch(p, '\\testing\\foo.js'); + assertGlobMatch(p, 'C:\\testing\\foo.js'); - assert(!glob.match(p, 'foo.ts')); - assert(!glob.match(p, 'testing/foo.ts')); - assert(!glob.match(p, 'testing\\foo.ts')); - assert(!glob.match(p, '/testing/foo.ts')); - assert(!glob.match(p, '\\testing\\foo.ts')); - assert(!glob.match(p, 'C:\\testing\\foo.ts')); + assertNoGlobMatch(p, 'foo.ts'); + assertNoGlobMatch(p, 'testing/foo.ts'); + assertNoGlobMatch(p, 'testing\\foo.ts'); + assertNoGlobMatch(p, '/testing/foo.ts'); + assertNoGlobMatch(p, '\\testing\\foo.ts'); + assertNoGlobMatch(p, 'C:\\testing\\foo.ts'); - assert(!glob.match(p, 'foo.js.txt')); - assert(!glob.match(p, 'testing/foo.js.txt')); - assert(!glob.match(p, 'testing\\foo.js.txt')); - assert(!glob.match(p, '/testing/foo.js.txt')); - assert(!glob.match(p, '\\testing\\foo.js.txt')); - assert(!glob.match(p, 'C:\\testing\\foo.js.txt')); + assertNoGlobMatch(p, 'foo.js.txt'); + assertNoGlobMatch(p, 'testing/foo.js.txt'); + assertNoGlobMatch(p, 'testing\\foo.js.txt'); + assertNoGlobMatch(p, '/testing/foo.js.txt'); + assertNoGlobMatch(p, '\\testing\\foo.js.txt'); + assertNoGlobMatch(p, 'C:\\testing\\foo.js.txt'); - assert(!glob.match(p, 'testing.js/foo')); - assert(!glob.match(p, 'testing.js\\foo')); - assert(!glob.match(p, '/testing.js/foo')); - assert(!glob.match(p, '\\testing.js\\foo')); - assert(!glob.match(p, 'C:\\testing.js\\foo')); + assertNoGlobMatch(p, 'testing.js/foo'); + assertNoGlobMatch(p, 'testing.js\\foo'); + assertNoGlobMatch(p, '/testing.js/foo'); + assertNoGlobMatch(p, '\\testing.js\\foo'); + assertNoGlobMatch(p, 'C:\\testing.js\\foo'); p = '**/foo.js'; - assert(glob.match(p, 'foo.js')); - assert(glob.match(p, '/foo.js')); - assert(glob.match(p, '\\foo.js')); - assert(glob.match(p, 'testing/foo.js')); - assert(glob.match(p, 'testing\\foo.js')); - assert(glob.match(p, '/testing/foo.js')); - assert(glob.match(p, '\\testing\\foo.js')); - assert(glob.match(p, 'C:\\testing\\foo.js')); + assertGlobMatch(p, 'foo.js'); + assertGlobMatch(p, '/foo.js'); + assertGlobMatch(p, '\\foo.js'); + assertGlobMatch(p, 'testing/foo.js'); + assertGlobMatch(p, 'testing\\foo.js'); + assertGlobMatch(p, '/testing/foo.js'); + assertGlobMatch(p, '\\testing\\foo.js'); + assertGlobMatch(p, 'C:\\testing\\foo.js'); }); test('cached properly', function () { let p = '**/*.js'; - assert(glob.match(p, 'foo.js')); - assert(glob.match(p, 'testing/foo.js')); - assert(glob.match(p, 'testing\\foo.js')); - assert(glob.match(p, '/testing/foo.js')); - assert(glob.match(p, '\\testing\\foo.js')); - assert(glob.match(p, 'C:\\testing\\foo.js')); + assertGlobMatch(p, 'foo.js'); + assertGlobMatch(p, 'testing/foo.js'); + assertGlobMatch(p, 'testing\\foo.js'); + assertGlobMatch(p, '/testing/foo.js'); + assertGlobMatch(p, '\\testing\\foo.js'); + assertGlobMatch(p, 'C:\\testing\\foo.js'); - assert(!glob.match(p, 'foo.ts')); - assert(!glob.match(p, 'testing/foo.ts')); - assert(!glob.match(p, 'testing\\foo.ts')); - assert(!glob.match(p, '/testing/foo.ts')); - assert(!glob.match(p, '\\testing\\foo.ts')); - assert(!glob.match(p, 'C:\\testing\\foo.ts')); + assertNoGlobMatch(p, 'foo.ts'); + assertNoGlobMatch(p, 'testing/foo.ts'); + assertNoGlobMatch(p, 'testing\\foo.ts'); + assertNoGlobMatch(p, '/testing/foo.ts'); + assertNoGlobMatch(p, '\\testing\\foo.ts'); + assertNoGlobMatch(p, 'C:\\testing\\foo.ts'); - assert(!glob.match(p, 'foo.js.txt')); - assert(!glob.match(p, 'testing/foo.js.txt')); - assert(!glob.match(p, 'testing\\foo.js.txt')); - assert(!glob.match(p, '/testing/foo.js.txt')); - assert(!glob.match(p, '\\testing\\foo.js.txt')); - assert(!glob.match(p, 'C:\\testing\\foo.js.txt')); + assertNoGlobMatch(p, 'foo.js.txt'); + assertNoGlobMatch(p, 'testing/foo.js.txt'); + assertNoGlobMatch(p, 'testing\\foo.js.txt'); + assertNoGlobMatch(p, '/testing/foo.js.txt'); + assertNoGlobMatch(p, '\\testing\\foo.js.txt'); + assertNoGlobMatch(p, 'C:\\testing\\foo.js.txt'); - assert(!glob.match(p, 'testing.js/foo')); - assert(!glob.match(p, 'testing.js\\foo')); - assert(!glob.match(p, '/testing.js/foo')); - assert(!glob.match(p, '\\testing.js\\foo')); - assert(!glob.match(p, 'C:\\testing.js\\foo')); + assertNoGlobMatch(p, 'testing.js/foo'); + assertNoGlobMatch(p, 'testing.js\\foo'); + assertNoGlobMatch(p, '/testing.js/foo'); + assertNoGlobMatch(p, '\\testing.js\\foo'); + assertNoGlobMatch(p, 'C:\\testing.js\\foo'); // Run again and make sure the regex are properly reused - assert(glob.match(p, 'foo.js')); - assert(glob.match(p, 'testing/foo.js')); - assert(glob.match(p, 'testing\\foo.js')); - assert(glob.match(p, '/testing/foo.js')); - assert(glob.match(p, '\\testing\\foo.js')); - assert(glob.match(p, 'C:\\testing\\foo.js')); + assertGlobMatch(p, 'foo.js'); + assertGlobMatch(p, 'testing/foo.js'); + assertGlobMatch(p, 'testing\\foo.js'); + assertGlobMatch(p, '/testing/foo.js'); + assertGlobMatch(p, '\\testing\\foo.js'); + assertGlobMatch(p, 'C:\\testing\\foo.js'); - assert(!glob.match(p, 'foo.ts')); - assert(!glob.match(p, 'testing/foo.ts')); - assert(!glob.match(p, 'testing\\foo.ts')); - assert(!glob.match(p, '/testing/foo.ts')); - assert(!glob.match(p, '\\testing\\foo.ts')); - assert(!glob.match(p, 'C:\\testing\\foo.ts')); + assertNoGlobMatch(p, 'foo.ts'); + assertNoGlobMatch(p, 'testing/foo.ts'); + assertNoGlobMatch(p, 'testing\\foo.ts'); + assertNoGlobMatch(p, '/testing/foo.ts'); + assertNoGlobMatch(p, '\\testing\\foo.ts'); + assertNoGlobMatch(p, 'C:\\testing\\foo.ts'); - assert(!glob.match(p, 'foo.js.txt')); - assert(!glob.match(p, 'testing/foo.js.txt')); - assert(!glob.match(p, 'testing\\foo.js.txt')); - assert(!glob.match(p, '/testing/foo.js.txt')); - assert(!glob.match(p, '\\testing\\foo.js.txt')); - assert(!glob.match(p, 'C:\\testing\\foo.js.txt')); + assertNoGlobMatch(p, 'foo.js.txt'); + assertNoGlobMatch(p, 'testing/foo.js.txt'); + assertNoGlobMatch(p, 'testing\\foo.js.txt'); + assertNoGlobMatch(p, '/testing/foo.js.txt'); + assertNoGlobMatch(p, '\\testing\\foo.js.txt'); + assertNoGlobMatch(p, 'C:\\testing\\foo.js.txt'); - assert(!glob.match(p, 'testing.js/foo')); - assert(!glob.match(p, 'testing.js\\foo')); - assert(!glob.match(p, '/testing.js/foo')); - assert(!glob.match(p, '\\testing.js\\foo')); - assert(!glob.match(p, 'C:\\testing.js\\foo')); + assertNoGlobMatch(p, 'testing.js/foo'); + assertNoGlobMatch(p, 'testing.js\\foo'); + assertNoGlobMatch(p, '/testing.js/foo'); + assertNoGlobMatch(p, '\\testing.js\\foo'); + assertNoGlobMatch(p, 'C:\\testing.js\\foo'); }); test('invalid glob', function () { let p = '**/*(.js'; - assert(!glob.match(p, 'foo.js')); + assertNoGlobMatch(p, 'foo.js'); }); test('split glob aware', function () { @@ -926,48 +945,48 @@ suite('Glob', () => { test('relative pattern - glob star', function () { if (isWindows) { let p: glob.IRelativePattern = { base: 'C:\\DNXConsoleApp\\foo', pattern: '**/*.cs', pathToRelative: (from, to) => path.relative(from, to) }; - assert(glob.match(p, 'C:\\DNXConsoleApp\\foo\\Program.cs')); - assert(glob.match(p, 'C:\\DNXConsoleApp\\foo\\bar\\Program.cs')); - assert(!glob.match(p, 'C:\\DNXConsoleApp\\foo\\Program.ts')); - assert(!glob.match(p, 'C:\\DNXConsoleApp\\Program.cs')); - assert(!glob.match(p, 'C:\\other\\DNXConsoleApp\\foo\\Program.ts')); + assertGlobMatch(p, 'C:\\DNXConsoleApp\\foo\\Program.cs'); + assertGlobMatch(p, 'C:\\DNXConsoleApp\\foo\\bar\\Program.cs'); + assertNoGlobMatch(p, 'C:\\DNXConsoleApp\\foo\\Program.ts'); + assertNoGlobMatch(p, 'C:\\DNXConsoleApp\\Program.cs'); + assertNoGlobMatch(p, 'C:\\other\\DNXConsoleApp\\foo\\Program.ts'); } else { let p: glob.IRelativePattern = { base: '/DNXConsoleApp/foo', pattern: '**/*.cs', pathToRelative: (from, to) => path.relative(from, to) }; - assert(glob.match(p, '/DNXConsoleApp/foo/Program.cs')); - assert(glob.match(p, '/DNXConsoleApp/foo/bar/Program.cs')); - assert(!glob.match(p, '/DNXConsoleApp/foo/Program.ts')); - assert(!glob.match(p, '/DNXConsoleApp/Program.cs')); - assert(!glob.match(p, '/other/DNXConsoleApp/foo/Program.ts')); + assertGlobMatch(p, '/DNXConsoleApp/foo/Program.cs'); + assertGlobMatch(p, '/DNXConsoleApp/foo/bar/Program.cs'); + assertNoGlobMatch(p, '/DNXConsoleApp/foo/Program.ts'); + assertNoGlobMatch(p, '/DNXConsoleApp/Program.cs'); + assertNoGlobMatch(p, '/other/DNXConsoleApp/foo/Program.ts'); } }); test('relative pattern - single star', function () { if (isWindows) { let p: glob.IRelativePattern = { base: 'C:\\DNXConsoleApp\\foo', pattern: '*.cs', pathToRelative: (from, to) => path.relative(from, to) }; - assert(glob.match(p, 'C:\\DNXConsoleApp\\foo\\Program.cs')); - assert(!glob.match(p, 'C:\\DNXConsoleApp\\foo\\bar\\Program.cs')); - assert(!glob.match(p, 'C:\\DNXConsoleApp\\foo\\Program.ts')); - assert(!glob.match(p, 'C:\\DNXConsoleApp\\Program.cs')); - assert(!glob.match(p, 'C:\\other\\DNXConsoleApp\\foo\\Program.ts')); + assertGlobMatch(p, 'C:\\DNXConsoleApp\\foo\\Program.cs'); + assertNoGlobMatch(p, 'C:\\DNXConsoleApp\\foo\\bar\\Program.cs'); + assertNoGlobMatch(p, 'C:\\DNXConsoleApp\\foo\\Program.ts'); + assertNoGlobMatch(p, 'C:\\DNXConsoleApp\\Program.cs'); + assertNoGlobMatch(p, 'C:\\other\\DNXConsoleApp\\foo\\Program.ts'); } else { let p: glob.IRelativePattern = { base: '/DNXConsoleApp/foo', pattern: '*.cs', pathToRelative: (from, to) => path.relative(from, to) }; - assert(glob.match(p, '/DNXConsoleApp/foo/Program.cs')); - assert(!glob.match(p, '/DNXConsoleApp/foo/bar/Program.cs')); - assert(!glob.match(p, '/DNXConsoleApp/foo/Program.ts')); - assert(!glob.match(p, '/DNXConsoleApp/Program.cs')); - assert(!glob.match(p, '/other/DNXConsoleApp/foo/Program.ts')); + assertGlobMatch(p, '/DNXConsoleApp/foo/Program.cs'); + assertNoGlobMatch(p, '/DNXConsoleApp/foo/bar/Program.cs'); + assertNoGlobMatch(p, '/DNXConsoleApp/foo/Program.ts'); + assertNoGlobMatch(p, '/DNXConsoleApp/Program.cs'); + assertNoGlobMatch(p, '/other/DNXConsoleApp/foo/Program.ts'); } }); test('relative pattern - single star with path', function () { if (isWindows) { let p: glob.IRelativePattern = { base: 'C:\\DNXConsoleApp\\foo', pattern: 'something/*.cs', pathToRelative: (from, to) => path.relative(from, to) }; - assert(glob.match(p, 'C:\\DNXConsoleApp\\foo\\something\\Program.cs')); - assert(!glob.match(p, 'C:\\DNXConsoleApp\\foo\\Program.cs')); + assertGlobMatch(p, 'C:\\DNXConsoleApp\\foo\\something\\Program.cs'); + assertNoGlobMatch(p, 'C:\\DNXConsoleApp\\foo\\Program.cs'); } else { let p: glob.IRelativePattern = { base: '/DNXConsoleApp/foo', pattern: 'something/*.cs', pathToRelative: (from, to) => path.relative(from, to) }; - assert(glob.match(p, '/DNXConsoleApp/foo/something/Program.cs')); - assert(!glob.match(p, '/DNXConsoleApp/foo/Program.cs')); + assertGlobMatch(p, '/DNXConsoleApp/foo/something/Program.cs'); + assertNoGlobMatch(p, '/DNXConsoleApp/foo/Program.cs'); } }); diff --git a/src/vs/workbench/services/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts index eba4967b3ef..cacc1dc7a68 100644 --- a/src/vs/workbench/services/files/electron-browser/fileService.ts +++ b/src/vs/workbench/services/files/electron-browser/fileService.ts @@ -234,7 +234,7 @@ export class FileService implements IFileService { const legacyWindowsWatcher = new WindowsWatcherService(this.contextService, watcherIgnoredPatterns, e => this._onFileChanges.fire(e), err => this.handleError(err), this.environmentService.verbose); this.activeWorkspaceFileChangeWatcher = toDisposable(legacyWindowsWatcher.startWatching()); } else { - const legacyUnixWatcher = new UnixWatcherService(this.contextService, watcherIgnoredPatterns, e => this._onFileChanges.fire(e), err => this.handleError(err), this.environmentService.verbose); + const legacyUnixWatcher = new UnixWatcherService(this.contextService, this.configurationService, e => this._onFileChanges.fire(e), err => this.handleError(err), this.environmentService.verbose); this.activeWorkspaceFileChangeWatcher = toDisposable(legacyUnixWatcher.startWatching()); } } diff --git a/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts b/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts index fc92f0581cb..550297d06f0 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts @@ -10,6 +10,8 @@ import * as fs from 'fs'; import * as gracefulFs from 'graceful-fs'; gracefulFs.gracefulify(fs); +import * as paths from 'vs/base/common/paths'; +import * as glob from 'vs/base/common/glob'; import { TPromise } from 'vs/base/common/winjs.base'; import { FileChangeType } from 'vs/platform/files/common/files'; @@ -18,158 +20,331 @@ import * as strings from 'vs/base/common/strings'; import { normalizeNFC } from 'vs/base/common/normalization'; import { realcaseSync } from 'vs/base/node/extfs'; import { isMacintosh } from 'vs/base/common/platform'; -import * as watcher from 'vs/workbench/services/files/node/watcher/common'; -import { IWatcherRequest, IWatcherService } from 'vs/workbench/services/files/node/watcher/unix/watcher'; -import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import * as watcherCommon from 'vs/workbench/services/files/node/watcher/common'; +import { IWatcherRequest, IWatcherService, IWatcherOptions } from 'vs/workbench/services/files/node/watcher/unix/watcher'; + +interface IWatcher { + requests: ExtendedWatcherRequest[]; + stop(): any; +} + +export interface IChockidarWatcherOptions { + pollingInterval?: number; +} + +interface ExtendedWatcherRequest extends IWatcherRequest { + parsedPattern?: glob.ParsedPattern; +} export class ChokidarWatcherService implements IWatcherService { private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms) private static readonly EVENT_SPAM_WARNING_THRESHOLD = 60 * 1000; // warn after certain time span of event spam + private _watchers: { [watchPath: string]: IWatcher }; + private _watcherCount: number; + + private _watcherPromise: TPromise; + private _options: IWatcherOptions & IChockidarWatcherOptions; + private spamCheckStartTime: number; private spamWarningLogged: boolean; private enospcErrorLogged: boolean; - private toDispose: IDisposable[] = []; + private _errorCallback: (error: Error) => void; + private _fileChangeCallback: (changes: watcherCommon.IRawFileChange[]) => void; + + public initialize(options: IWatcherOptions & IChockidarWatcherOptions): TPromise { + this._options = options; + this._watchers = Object.create(null); + this._watcherCount = 0; + this._watcherPromise = new TPromise((c, e, p) => { + this._errorCallback = (error) => { + this.stop(); + e(error); + }; + this._fileChangeCallback = p; + }, () => { + this.stop(); + }); + return this._watcherPromise; + } + + public setRoots(requests: IWatcherRequest[]): TPromise { + const watchers = Object.create(null); + const newRequests = []; + + const requestsByBasePath = normalizeRoots(requests); + + // evaluate new & remaining watchers + for (let basePath in requestsByBasePath) { + let watcher = this._watchers[basePath]; + if (watcher && isEqualRequests(watcher.requests, requestsByBasePath[basePath])) { + watchers[basePath] = watcher; + delete this._watchers[basePath]; + } else { + newRequests.push(basePath); + } + } + // stop all old watchers + for (let path in this._watchers) { + this._watchers[path].stop(); + } + // start all new watchers + for (let basePath of newRequests) { + let requests = requestsByBasePath[basePath]; + watchers[basePath] = this._watch(basePath, requests); + } + + this._watchers = watchers; + return TPromise.as(null); + } + + // for test purposes + public get wacherCount() { + return this._watcherCount; + } + + private _watch(basePath: string, requests: IWatcherRequest[]): IWatcher { + if (this._options.verboseLogging) { + console.log(`Start watching: ${basePath}]`); + } + + const pollingInterval = this._options.pollingInterval || 1000; - public watch(request: IWatcherRequest): TPromise { const watcherOpts: chokidar.IOptions = { ignoreInitial: true, ignorePermissionErrors: true, followSymlinks: true, // this is the default of chokidar and supports file events through symlinks - ignored: request.ignored, - interval: 1000, // while not used in normal cases, if any error causes chokidar to fallback to polling, increase its intervals - binaryInterval: 1000, + interval: pollingInterval, // while not used in normal cases, if any error causes chokidar to fallback to polling, increase its intervals + binaryInterval: pollingInterval, disableGlobbing: true // fix https://github.com/Microsoft/vscode/issues/4586 }; + // if there's only one request, use the built-in ignore-filterering + if (requests.length === 1) { + watcherOpts.ignored = requests[0].ignored; + } + // Chokidar fails when the basePath does not match case-identical to the path on disk // so we have to find the real casing of the path and do some path massaging to fix this // see https://github.com/paulmillr/chokidar/issues/418 - const originalBasePath = request.basePath; - const realBasePath = isMacintosh ? (realcaseSync(originalBasePath) || originalBasePath) : originalBasePath; + const realBasePath = isMacintosh ? (realcaseSync(basePath) || basePath) : basePath; const realBasePathLength = realBasePath.length; - const realBasePathDiffers = (originalBasePath !== realBasePath); + const realBasePathDiffers = (basePath !== realBasePath); if (realBasePathDiffers) { - console.warn(`Watcher basePath does not match version on disk and was corrected (original: ${originalBasePath}, real: ${realBasePath})`); + console.warn(`Watcher basePath does not match version on disk and was corrected (original: ${basePath}, real: ${realBasePath})`); } - const chokidarWatcher = chokidar.watch(realBasePath, watcherOpts); + let chokidarWatcher = chokidar.watch(realBasePath, watcherOpts); + this._watcherCount++; // Detect if for some reason the native watcher library fails to load if (isMacintosh && !chokidarWatcher.options.useFsEvents) { console.error('Watcher is not using native fsevents library and is falling back to unefficient polling.'); } - let undeliveredFileEvents: watcher.IRawFileChange[] = []; - const fileEventDelayer = new ThrottledDelayer(ChokidarWatcherService.FS_EVENT_DELAY); + let undeliveredFileEvents: watcherCommon.IRawFileChange[] = []; + let fileEventDelayer = new ThrottledDelayer(ChokidarWatcherService.FS_EVENT_DELAY); - this.toDispose.push(toDisposable(() => { - chokidarWatcher.close(); - fileEventDelayer.cancel(); - })); - - return new TPromise((c, e, p) => { - chokidarWatcher.on('all', (type: string, path: string) => { - if (isMacintosh) { - // Mac: uses NFD unicode form on disk, but we want NFC - // See also https://github.com/nodejs/node/issues/2165 - path = normalizeNFC(path); - } - - if (path.indexOf(realBasePath) < 0) { - return; // we really only care about absolute paths here in our basepath context here - } - - // Make sure to convert the path back to its original basePath form if the realpath is different - if (realBasePathDiffers) { - path = originalBasePath + path.substr(realBasePathLength); - } - - let event: watcher.IRawFileChange = null; - - // Change - if (type === 'change') { - event = { type: 0, path }; - } - - // Add - else if (type === 'add' || type === 'addDir') { - event = { type: 1, path }; - } - - // Delete - else if (type === 'unlink' || type === 'unlinkDir') { - event = { type: 2, path }; - } - - if (event) { - - // Logging - if (request.verboseLogging) { - console.log(event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', event.path); + const watcher: IWatcher = { + requests, + stop: () => { + try { + if (this._options.verboseLogging) { + console.log(`Stop watching: ${basePath}]`); } - - // Check for spam - const now = Date.now(); - if (undeliveredFileEvents.length === 0) { - this.spamWarningLogged = false; - this.spamCheckStartTime = now; - } else if (!this.spamWarningLogged && this.spamCheckStartTime + ChokidarWatcherService.EVENT_SPAM_WARNING_THRESHOLD < now) { - this.spamWarningLogged = true; - console.warn(strings.format('Watcher is busy catching up with {0} file changes in 60 seconds. Latest changed path is "{1}"', undeliveredFileEvents.length, event.path)); + if (chokidarWatcher) { + chokidarWatcher.close(); + this._watcherCount--; + chokidarWatcher = null; } + if (fileEventDelayer) { + fileEventDelayer.cancel(); + fileEventDelayer = null; + } + } catch (error) { + console.error(error.toString()); + } + } + }; - // Add to buffer - undeliveredFileEvents.push(event); + chokidarWatcher.on('all', (type: string, path: string) => { + if (isMacintosh) { + // Mac: uses NFD unicode form on disk, but we want NFC + // See also https://github.com/nodejs/node/issues/2165 + path = normalizeNFC(path); + } - // Delay and send buffer - fileEventDelayer.trigger(() => { - const events = undeliveredFileEvents; - undeliveredFileEvents = []; + if (path.indexOf(realBasePath) < 0) { + return; // we really only care about absolute paths here in our basepath context here + } - // Broadcast to clients normalized - const res = watcher.normalize(events); - p(res); + // Make sure to convert the path back to its original basePath form if the realpath is different + if (realBasePathDiffers) { + path = basePath + path.substr(realBasePathLength); + } - // Logging - if (request.verboseLogging) { - res.forEach(r => { - console.log(' >> normalized', r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', r.path); - }); - } + let eventType: FileChangeType; + switch (type) { + case 'change': + eventType = FileChangeType.UPDATED; + break; + case 'add': + case 'addDir': + eventType = FileChangeType.ADDED; + break; + case 'unlink': + case 'unlinkDir': + eventType = FileChangeType.DELETED; + break; + default: + return; + } - return TPromise.as(null); + if (isIgnored(path, watcher.requests)) { + return; + } + + let event = { type: eventType, path }; + + // Logging + if (this._options.verboseLogging) { + console.log(eventType === FileChangeType.ADDED ? '[ADDED]' : eventType === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', path); + } + + // Check for spam + const now = Date.now(); + if (undeliveredFileEvents.length === 0) { + this.spamWarningLogged = false; + this.spamCheckStartTime = now; + } else if (!this.spamWarningLogged && this.spamCheckStartTime + ChokidarWatcherService.EVENT_SPAM_WARNING_THRESHOLD < now) { + this.spamWarningLogged = true; + console.warn(strings.format('Watcher is busy catching up with {0} file changes in 60 seconds. Latest changed path is "{1}"', undeliveredFileEvents.length, event.path)); + } + + // Add to buffer + undeliveredFileEvents.push(event); + + // Delay and send buffer + fileEventDelayer.trigger(() => { + const events = undeliveredFileEvents; + undeliveredFileEvents = []; + + // Broadcast to clients normalized + const res = watcherCommon.normalize(events); + this._fileChangeCallback(res); + + // Logging + if (this._options.verboseLogging) { + res.forEach(r => { + console.log(' >> normalized', r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', r.path); }); } - }); - chokidarWatcher.on('error', (error: Error) => { - if (error) { - - // Specially handle ENOSPC errors that can happen when - // the watcher consumes so many file descriptors that - // we are running into a limit. We only want to warn - // once in this case to avoid log spam. - // See https://github.com/Microsoft/vscode/issues/7950 - if ((error).code === 'ENOSPC') { - if (!this.enospcErrorLogged) { - this.enospcErrorLogged = true; - e(new Error('Inotify limit reached (ENOSPC)')); - } - } else { - console.error(error.toString()); - } - } + return TPromise.as(null); }); - }, () => { - this.toDispose = dispose(this.toDispose); }); + + chokidarWatcher.on('error', (error: Error) => { + if (error) { + + // Specially handle ENOSPC errors that can happen when + // the watcher consumes so many file descriptors that + // we are running into a limit. We only want to warn + // once in this case to avoid log spam. + // See https://github.com/Microsoft/vscode/issues/7950 + if ((error).code === 'ENOSPC') { + if (!this.enospcErrorLogged) { + this.enospcErrorLogged = true; + this._errorCallback(new Error('Inotify limit reached (ENOSPC)')); + } + } else { + console.error(error.toString()); + } + } + }); + return watcher; } public stop(): TPromise { - this.toDispose = dispose(this.toDispose); + for (let path in this._watchers) { + let watcher = this._watchers[path]; + watcher.stop(); + } + this._watchers = Object.create(null); return TPromise.as(void 0); } + + +} + +function isIgnored(path: string, requests: ExtendedWatcherRequest[]): boolean { + for (let request of requests) { + if (request.basePath === path) { + return false; + } + if (paths.isEqualOrParent(path, request.basePath)) { + if (!request.parsedPattern) { + if (request.ignored && request.ignored.length > 0) { + let pattern = `{${request.ignored.map(i => i + '/**').join(',')}}`; + request.parsedPattern = glob.parse(pattern); + } else { + request.parsedPattern = () => false; + } + } + const relPath = path.substr(request.basePath.length + 1); + if (!request.parsedPattern(relPath)) { + return false; + } + } + } + return true; +} + +/** + * Normalizes a set of root paths by grouping by the most parent root path. + * equests with Sub paths are skipped if they have the same ignored set as the parent. + */ +export function normalizeRoots(requests: IWatcherRequest[]): { [basePath: string]: IWatcherRequest[] } { + requests = requests.sort((r1, r2) => r1.basePath.localeCompare(r2.basePath)); + let prevRequest: IWatcherRequest = null; + let result: { [basePath: string]: IWatcherRequest[] } = Object.create(null); + for (let request of requests) { + let basePath = request.basePath; + let ignored = (request.ignored || []).sort(); + if (prevRequest && (paths.isEqualOrParent(basePath, prevRequest.basePath))) { + if (!isEqualIgnore(ignored, prevRequest.ignored)) { + result[prevRequest.basePath].push({ basePath, ignored }); + } + } else { + prevRequest = { basePath, ignored }; + result[basePath] = [prevRequest]; + } + } + return result; +} + +function isEqualRequests(r1: IWatcherRequest[], r2: IWatcherRequest[]) { + if (r1.length !== r2.length) { + return false; + } + for (let k = 0; k < r1.length; k++) { + if (r1[k].basePath !== r2[k].basePath || !isEqualIgnore(r1[k].ignored, r2[k].ignored)) { + return false; + } + } + return true; +} + +function isEqualIgnore(i1: string[], i2: string[]) { + if (i1.length !== i2.length) { + return false; + } + for (let k = 0; k < i1.length; k++) { + if (i1[k] !== i2[k]) { + return false; + } + } + return true; } diff --git a/src/vs/workbench/services/files/node/watcher/unix/test/chockidarWatcherService.test.ts b/src/vs/workbench/services/files/node/watcher/unix/test/chockidarWatcherService.test.ts new file mode 100644 index 00000000000..09849f5f5c5 --- /dev/null +++ b/src/vs/workbench/services/files/node/watcher/unix/test/chockidarWatcherService.test.ts @@ -0,0 +1,327 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as assert from 'assert'; +import * as os from 'os'; +import * as path from 'path'; +import * as pfs from 'vs/base/node/pfs'; + +import { normalizeRoots, ChokidarWatcherService } from '../chokidarWatcherService'; +import { IWatcherRequest } from '../watcher'; + +import * as platform from 'vs/base/common/platform'; +import { Delayer } from 'vs/base/common/async'; +import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; +import { FileChangeType } from 'vs/platform/files/common/files'; + +function newRequest(basePath: string, ignored = []): IWatcherRequest { + return { basePath, ignored }; +} + +function assertNormalizedRootPath(inputPaths: string[], expectedPaths: string[]) { + const requests = inputPaths.map(path => newRequest(path)); + const actual = normalizeRoots(requests); + assert.deepEqual(Object.keys(actual).sort(), expectedPaths); +} + +function assertNormalizedRequests(inputRequests: IWatcherRequest[], expectedRequests: { [path: string]: IWatcherRequest[] }) { + const actual = normalizeRoots(inputRequests); + const actualPath = Object.keys(actual).sort(); + const expectedPaths = Object.keys(expectedRequests).sort(); + assert.deepEqual(actualPath, expectedPaths); + for (let path of actualPath) { + let a = expectedRequests[path].sort((r1, r2) => r1.basePath.localeCompare(r2.basePath)); + let e = expectedRequests[path].sort((r1, r2) => r1.basePath.localeCompare(r2.basePath)); + assert.deepEqual(a, e); + } +} + +function sort(changes: IRawFileChange[]) { + return changes.sort((c1, c2) => { + return c1.path.localeCompare(c2.path); + }); +} + +function wait(time: number) { + return new Delayer(time).trigger(() => { }); +} + +async function assertFileEvents(actuals: IRawFileChange[], expected: IRawFileChange[]) { + let repeats = 40; + while ((actuals.length < expected.length) && repeats-- > 0) { + await wait(50); + } + assert.deepEqual(sort(actuals), sort(expected)); + actuals.length = 0; +} + +suite('Chockidar normalizeRoots', () => { + test('should not impacts roots that don\'t overlap', () => { + if (platform.isWindows) { + assertNormalizedRootPath(['C:\\a'], ['C:\\a']); + assertNormalizedRootPath(['C:\\a', 'C:\\b'], ['C:\\a', 'C:\\b']); + assertNormalizedRootPath(['C:\\a', 'C:\\b', 'C:\\c\\d\\e'], ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']); + } else { + assertNormalizedRootPath(['/a'], ['/a']); + assertNormalizedRootPath(['/a', '/b'], ['/a', '/b']); + assertNormalizedRootPath(['/a', '/b', '/c/d/e'], ['/a', '/b', '/c/d/e']); + } + }); + + test('should remove sub-folders of other roots', () => { + if (platform.isWindows) { + assertNormalizedRootPath(['C:\\a', 'C:\\a\\b'], ['C:\\a']); + assertNormalizedRootPath(['C:\\a', 'C:\\b', 'C:\\a\\b'], ['C:\\a', 'C:\\b']); + assertNormalizedRootPath(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b'], ['C:\\a', 'C:\\b']); + assertNormalizedRootPath(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d'], ['C:\\a']); + } else { + assertNormalizedRootPath(['/a', '/a/b'], ['/a']); + assertNormalizedRootPath(['/a', '/b', '/a/b'], ['/a', '/b']); + assertNormalizedRootPath(['/b/a', '/a', '/b', '/a/b'], ['/a', '/b']); + assertNormalizedRootPath(['/a', '/a/b', '/a/c/d'], ['/a']); + assertNormalizedRootPath(['/a/c/d/e', '/a/b/d', '/a/c/d', '/a/c/e/f', '/a/b'], ['/a/b', '/a/c/d', '/a/c/e/f']); + } + }); + + test('should remove duplicates', () => { + if (platform.isWindows) { + assertNormalizedRootPath(['C:\\a', 'C:\\a\\', 'C:\\a'], ['C:\\a']); + } else { + assertNormalizedRootPath(['/a', '/a/', '/a'], ['/a']); + assertNormalizedRootPath(['/a', '/b', '/a/b'], ['/a', '/b']); + assertNormalizedRootPath(['/b/a', '/a', '/b', '/a/b'], ['/a', '/b']); + assertNormalizedRootPath(['/a', '/a/b', '/a/c/d'], ['/a']); + } + }); + + test('nested requests', () => { + let p1, p2, p3; + if (platform.isWindows) { + p1 = 'C:\\a'; + p2 = 'C:\\a\\b'; + p3 = 'C:\\a\\b\\c'; + } else { + p1 = '/a'; + p2 = '/a/b'; + p3 = '/a/b/c'; + } + const r1 = newRequest(p1, ['**/*.ts']); + const r2 = newRequest(p2, ['**/*.js']); + const r3 = newRequest(p3, ['**/*.ts']); + assertNormalizedRequests([r1, r2], { [p1]: [r1, r2] }); + assertNormalizedRequests([r2, r1], { [p1]: [r1, r2] }); + assertNormalizedRequests([r1, r2, r3], { [p1]: [r1, r2, r3] }); + assertNormalizedRequests([r1, r3], { [p1]: [r1] }); + assertNormalizedRequests([r2, r3], { [p2]: [r2, r3] }); + }); +}); + +suite('Chockidar watching', () => { + const tmpdir = os.tmpdir(); + const testDir = path.join(tmpdir, 'chockidartest-' + Date.now()); + const aFolder = path.join(testDir, 'a'); + const bFolder = path.join(testDir, 'b'); + const b2Folder = path.join(bFolder, 'b2'); + + const service = new ChokidarWatcherService(); + const result: IRawFileChange[] = []; + let error = null; + + suiteSetup(async () => { + await pfs.mkdirp(testDir); + await pfs.mkdirp(aFolder); + await pfs.mkdirp(bFolder); + await pfs.mkdirp(b2Folder); + + const promise = service.initialize({ verboseLogging: false, pollingInterval: 200 }); + promise.then(null, + e => { + console.log('set error', e); + error = e; + }, + p => { + if (Array.isArray(p)) { + result.push(...p); + } + } + ); + + }); + + suiteTeardown(async () => { + await pfs.del(testDir); + await service.stop(); + }); + + setup(() => { + result.length = 0; + assert.equal(error, null); + }); + + teardown(() => { + assert.equal(error, null); + }); + + test('simple file operations, single root, no ignore', async () => { + let request: IWatcherRequest = { basePath: testDir, ignored: [] }; + service.setRoots([request]); + await wait(300); + + assert.equal(service.wacherCount, 1); + + // create a file + let testFilePath = path.join(testDir, 'file.txt'); + await pfs.writeFile(testFilePath, ''); + await assertFileEvents(result, [{ path: testFilePath, type: FileChangeType.ADDED }]); + + // modify a file + await pfs.writeFile(testFilePath, 'Hello'); + await assertFileEvents(result, [{ path: testFilePath, type: FileChangeType.UPDATED }]); + + // create a folder + let testFolderPath = path.join(testDir, 'newFolder'); + await pfs.mkdirp(testFolderPath); + // copy a file + let copiedFilePath = path.join(testFolderPath, 'file2.txt'); + await pfs.copy(testFilePath, copiedFilePath); + await assertFileEvents(result, [{ path: copiedFilePath, type: FileChangeType.ADDED }, { path: testFolderPath, type: FileChangeType.ADDED }]); + + // delete a file + await pfs.del(copiedFilePath); + let renamedFilePath = path.join(testFolderPath, 'file3.txt'); + // move a file + await pfs.rename(testFilePath, renamedFilePath); + await assertFileEvents(result, [{ path: copiedFilePath, type: FileChangeType.DELETED }, { path: testFilePath, type: FileChangeType.DELETED }, { path: renamedFilePath, type: FileChangeType.ADDED }]); + + // delete a folder + await pfs.del(testFolderPath); + await assertFileEvents(result, [{ path: testFolderPath, type: FileChangeType.DELETED }, { path: renamedFilePath, type: FileChangeType.DELETED }]); + }); + + test('simple file operations, ignore', async () => { + let request: IWatcherRequest = { basePath: testDir, ignored: ['**/b', '**/*.js', '.git'] }; + service.setRoots([request]); + await wait(300); + + assert.equal(service.wacherCount, 1); + + // create various ignored files + let file1 = path.join(bFolder, 'file1.txt'); // hidden + await pfs.writeFile(file1, 'Hello'); + let file2 = path.join(b2Folder, 'file2.txt'); // hidden + await pfs.writeFile(file2, 'Hello'); + let folder1 = path.join(bFolder, 'folder1'); // hidden + await pfs.mkdirp(folder1); + let folder2 = path.join(aFolder, 'b'); // hidden + await pfs.mkdirp(folder2); + let folder3 = path.join(testDir, '.git'); // hidden + await pfs.mkdirp(folder3); + let folder4 = path.join(testDir, '.git1'); + await pfs.mkdirp(folder4); + let folder5 = path.join(aFolder, '.git'); + await pfs.mkdirp(folder5); + let file3 = path.join(aFolder, 'file3.js'); // hidden + await pfs.writeFile(file3, 'var x;'); + let file4 = path.join(aFolder, 'file4.txt'); + await pfs.writeFile(file4, 'Hello'); + await assertFileEvents(result, [{ path: file4, type: FileChangeType.ADDED }, { path: folder4, type: FileChangeType.ADDED }, { path: folder5, type: FileChangeType.ADDED }]); + + // move some files + let movedFile1 = path.join(folder2, 'file1.txt'); // from ignored to ignored + await pfs.rename(file1, movedFile1); + let movedFile2 = path.join(aFolder, 'file2.txt'); // from ignored to visible + await pfs.rename(file2, movedFile2); + let movedFile3 = path.join(aFolder, 'file3.txt'); // from ignored file ext to visible + await pfs.rename(file3, movedFile3); + await assertFileEvents(result, [{ path: movedFile2, type: FileChangeType.ADDED }, { path: movedFile3, type: FileChangeType.ADDED }]); + + // delete all files + await pfs.del(movedFile1); // hidden + await pfs.del(movedFile2); + await pfs.del(movedFile3); + await pfs.del(folder1); // hidden + await pfs.del(folder2); // hidden + await pfs.del(folder3); // hidden + await pfs.del(folder4); + await pfs.del(folder5); + await pfs.del(file4); + await assertFileEvents(result, [{ path: movedFile2, type: FileChangeType.DELETED }, { path: movedFile3, type: FileChangeType.DELETED }, { path: file4, type: FileChangeType.DELETED }, { path: folder4, type: FileChangeType.DELETED }, { path: folder5, type: FileChangeType.DELETED }]); + }); + + test('simple file operations, multiple roots', async () => { + let request1: IWatcherRequest = { basePath: aFolder, ignored: ['**/*.js'] }; + let request2: IWatcherRequest = { basePath: b2Folder, ignored: ['**/*.ts'] }; + service.setRoots([request1, request2]); + await wait(300); + + assert.equal(service.wacherCount, 2); + + // create some files + let folderPath1 = path.join(aFolder, 'folder1'); + await pfs.mkdirp(folderPath1); + let filePath1 = path.join(folderPath1, 'file1.json'); + await pfs.writeFile(filePath1, ''); + let filePath2 = path.join(folderPath1, 'file2.js'); // filtered + await pfs.writeFile(filePath2, ''); + let folderPath2 = path.join(b2Folder, 'folder2'); + await pfs.mkdirp(folderPath2); + let filePath3 = path.join(folderPath2, 'file3.ts'); // filtered + await pfs.writeFile(filePath3, ''); + let filePath4 = path.join(testDir, 'file4.json'); // outside roots + await pfs.writeFile(filePath4, ''); + + await assertFileEvents(result, [{ path: folderPath1, type: FileChangeType.ADDED }, { path: filePath1, type: FileChangeType.ADDED }, { path: folderPath2, type: FileChangeType.ADDED }]); + + // change roots + let request3: IWatcherRequest = { basePath: aFolder, ignored: ['**/*.json'] }; + service.setRoots([request3]); + await wait(300); + + assert.equal(service.wacherCount, 1); + + // delete all + await pfs.del(folderPath1); + await pfs.del(folderPath2); + await pfs.del(filePath4); + + await assertFileEvents(result, [{ path: folderPath1, type: FileChangeType.DELETED }, { path: filePath2, type: FileChangeType.DELETED }]); + }); + + test('simple file operations, nested roots', async () => { + let request1: IWatcherRequest = { basePath: testDir, ignored: ['**/b2'] }; + let request2: IWatcherRequest = { basePath: bFolder, ignored: ['**/b3'] }; + service.setRoots([request1, request2]); + await wait(300); + + assert.equal(service.wacherCount, 1); + + // create files + let filePath1 = path.join(bFolder, 'file1.xml'); // visible by both + await pfs.writeFile(filePath1, ''); + let filePath2 = path.join(b2Folder, 'file2.xml'); // filtered by root1, but visible by root2 + await pfs.writeFile(filePath2, ''); + let folderPath1 = path.join(b2Folder, 'b3'); // filtered + await pfs.mkdirp(folderPath1); + let filePath3 = path.join(folderPath1, 'file3.xml'); // filtered + await pfs.writeFile(filePath3, ''); + + await assertFileEvents(result, [{ path: filePath1, type: FileChangeType.ADDED }, { path: filePath2, type: FileChangeType.ADDED }]); + + let renamedFilePath2 = path.join(folderPath1, 'file2.xml'); + // move a file + await pfs.rename(filePath2, renamedFilePath2); + await assertFileEvents(result, [{ path: filePath2, type: FileChangeType.DELETED }]); + + // delete all + await pfs.del(folderPath1); + await pfs.del(filePath1); + + await assertFileEvents(result, [{ path: filePath1, type: FileChangeType.DELETED }]); + }); + +}); + diff --git a/src/vs/workbench/services/files/node/watcher/unix/watcher.ts b/src/vs/workbench/services/files/node/watcher/unix/watcher.ts index 6961b77f6a2..8ed35e6c792 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/watcher.ts +++ b/src/vs/workbench/services/files/node/watcher/unix/watcher.ts @@ -10,9 +10,13 @@ import { TPromise } from 'vs/base/common/winjs.base'; export interface IWatcherRequest { basePath: string; ignored: string[]; +} + +export interface IWatcherOptions { verboseLogging: boolean; } export interface IWatcherService { - watch(request: IWatcherRequest): TPromise; + initialize(options: IWatcherOptions): TPromise; + setRoots(roots: IWatcherRequest[]): TPromise; } diff --git a/src/vs/workbench/services/files/node/watcher/unix/watcherIpc.ts b/src/vs/workbench/services/files/node/watcher/unix/watcherIpc.ts index 9fcd515708f..bf2fba4664b 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/watcherIpc.ts +++ b/src/vs/workbench/services/files/node/watcher/unix/watcherIpc.ts @@ -7,10 +7,11 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IWatcherRequest, IWatcherService } from 'vs/workbench/services/files/node/watcher/unix/watcher'; +import { IWatcherRequest, IWatcherService, IWatcherOptions } from 'vs/workbench/services/files/node/watcher/unix/watcher'; export interface IWatcherChannel extends IChannel { - call(command: 'watch', request: IWatcherRequest): TPromise; + call(command: 'initialize', options: IWatcherOptions): TPromise; + call(command: 'setRoots', request: IWatcherRequest[]): TPromise; call(command: string, arg: any): TPromise; } @@ -20,7 +21,8 @@ export class WatcherChannel implements IWatcherChannel { call(command: string, arg: any): TPromise { switch (command) { - case 'watch': return this.service.watch(arg); + case 'initialize': return this.service.initialize(arg); + case 'setRoots': return this.service.setRoots(arg); } return undefined; } @@ -30,7 +32,11 @@ export class WatcherChannelClient implements IWatcherService { constructor(private channel: IWatcherChannel) { } - watch(request: IWatcherRequest): TPromise { - return this.channel.call('watch', request); + initialize(options: IWatcherOptions): TPromise { + return this.channel.call('initialize', options); + } + + setRoots(roots: IWatcherRequest[]): TPromise { + return this.channel.call('setRoots', roots); } } \ No newline at end of file diff --git a/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts b/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts index 160d25da051..e51c287854f 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/unix/watcherService.ts @@ -11,26 +11,31 @@ import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; import uri from 'vs/base/common/uri'; import { toFileChangesEvent, IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; import { IWatcherChannel, WatcherChannelClient } from 'vs/workbench/services/files/node/watcher/unix/watcherIpc'; -import { FileChangesEvent } from 'vs/platform/files/common/files'; +import { FileChangesEvent, IFilesConfiguration } from 'vs/platform/files/common/files'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { normalize } from 'path'; import { isPromiseCanceledError } from 'vs/base/common/errors'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Schemas } from 'vs/base/common/network'; export class FileWatcher { private static readonly MAX_RESTARTS = 5; private isDisposed: boolean; private restartCounter: number; + private service: WatcherChannelClient; + private toDispose: IDisposable[]; constructor( private contextService: IWorkspaceContextService, - private ignored: string[], + private configurationService: IConfigurationService, private onFileChanges: (changes: FileChangesEvent) => void, private errorLogger: (msg: string) => void, private verboseLogging: boolean ) { this.isDisposed = false; this.restartCounter = 0; + this.toDispose = []; } public startWatching(): () => void { @@ -48,17 +53,19 @@ export class FileWatcher { } } ); + this.toDispose.push(client); const channel = getNextTickChannel(client.getChannel('watcher')); - const service = new WatcherChannelClient(channel); + this.service = new WatcherChannelClient(channel); - // Start watching - const basePath: string = normalize(this.contextService.getWorkspace().folders[0].uri.fsPath); - service.watch({ basePath: basePath, ignored: this.ignored, verboseLogging: this.verboseLogging }).then(null, err => { + const options = { + verboseLogging: this.verboseLogging + }; + + this.service.initialize(options).then(null, err => { if (!this.isDisposed && !isPromiseCanceledError(err)) { return TPromise.wrapError(err); // the service lib uses the promise cancel error to indicate the process died, we do not want to bubble this up } - return void 0; }, (events: IRawFileChange[]) => this.onRawFileEvents(events)).done(() => { @@ -79,10 +86,41 @@ export class FileWatcher { } }); - return () => { - this.isDisposed = true; - client.dispose(); - }; + // Start watching + this.updateFolders(); + this.toDispose.push(this.contextService.onDidChangeWorkspaceFolders(() => this.updateFolders())); + this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('files.watcherExclude')) { + this.updateFolders(); + } + })); + + return () => this.dispose(); + } + + private updateFolders() { + if (this.isDisposed) { + return; + } + + this.service.setRoots(this.contextService.getWorkspace().folders.filter(folder => { + // Only workspace folders on disk + return folder.uri.scheme === Schemas.file; + }).map(folder => { + // Fetch the root's watcherExclude setting and return it + const configuration = this.configurationService.getValue({ + resource: folder.uri + }); + let ignored: string[] = []; + if (configuration.files && configuration.files.watcherExclude) { + ignored = Object.keys(configuration.files.watcherExclude).filter(k => !!configuration.files.watcherExclude[k]); + } + return { + basePath: folder.uri.fsPath, + ignored, + recursive: false + }; + })); } private onRawFileEvents(events: IRawFileChange[]): void { @@ -95,4 +133,9 @@ export class FileWatcher { this.onFileChanges(toFileChangesEvent(events)); } } + + private dispose(): void { + this.isDisposed = true; + this.toDispose = dispose(this.toDispose); + } } \ No newline at end of file