diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index 364fd10f24a2328feb95162025b070a60fe859a2..c376f9307905d6b4e7cdf71d63c67db2359a59eb 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -26,6 +26,7 @@ import {IRawFileMatch, ISerializedSearchComplete, IRawSearch, ISearchEngine} fro enum Traversal { Node = 1, MacFind, + WindowsDir } interface IDirectoryEntry { @@ -76,7 +77,6 @@ export class FileWalker { this.errors = []; if (this.filePattern) { - this.filePattern = this.filePattern.replace(/\\/g, '/'); // Normalize file patterns to forward slashes this.normalizedFilePatternLowercase = strings.stripWildcards(this.filePattern).toLowerCase(); } } @@ -98,8 +98,7 @@ export class FileWalker { if (exists) { this.resultCount++; onResult({ - absolutePath: this.filePattern, - pathLabel: this.filePattern, + path: this.filePattern, size }); @@ -117,21 +116,31 @@ export class FileWalker { } // File: Check for match on file pattern and include pattern - this.matchFile(onResult, extraFilePath, extraFilePath /* no workspace relative path */); + this.matchFile(onResult, null, extraFilePath /* no workspace relative path */); }); } let traverse = this.nodeJSTraversal; - if (!this.maxFilesize && platform.isMacintosh) { - this.traversal = Traversal.MacFind; - traverse = this.macFindTraversal; + if (!this.maxFilesize) { + if (platform.isMacintosh) { + this.traversal = Traversal.MacFind; + traverse = this.macFindTraversal; + } else if (platform.isWindows) { + this.traversal = Traversal.WindowsDir; + traverse = this.windowsDirTraversal; + } + } + + const isNodeTraversal = traverse === this.nodeJSTraversal; + if (!isNodeTraversal) { + this.cmdForkStartTime = Date.now(); } // For each root folder flow.parallel(rootFolders, (rootFolder, rootFolderDone: (err?: Error) => void) => { traverse.call(this, rootFolder, onResult, err => { if (err) { - if (traverse === this.nodeJSTraversal) { + if (isNodeTraversal) { rootFolderDone(err); } else { // fallback @@ -149,9 +158,8 @@ export class FileWalker { } private macFindTraversal(rootFolder: string, onResult: (result: IRawFileMatch) => void, done: (err?: Error) => void): void { - this.cmdForkStartTime = Date.now(); const cmd = childProcess.spawn('find', ['-L', '.', '-type', 'f'], { cwd: rootFolder }); - this.readStdout(cmd, (err: Error, stdout?: string) => { + this.readStdout(cmd, 'utf8', (err: Error, stdout?: string) => { if (err) { done(err); return; @@ -166,23 +174,38 @@ export class FileWalker { relativeFiles.pop(); } - this.cmdResultCount = relativeFiles.length; + this.matchFiles(rootFolder, relativeFiles, onResult); - // Support relative paths to files from a root resource (ignores excludes) - if (relativeFiles.indexOf(this.filePattern) !== -1) { - this.matchFile(onResult, [rootFolder, this.filePattern].join(paths.sep), this.filePattern); + done(); + }); + } + + private windowsDirTraversal(rootFolder: string, onResult: (result: IRawFileMatch) => void, done: (err?: Error) => void): void { + const cmd = childProcess.spawn('cmd', ['/U', '/c', 'dir', '/s', '/b', '/a-d'], { cwd: rootFolder }); + this.readStdout(cmd, 'ucs2', (err: Error, stdout?: string) => { + if (err) { + done(err); + return; } - const tree = this.buildDirectoryTree(relativeFiles); - this.matchDirectoryTree(rootFolder, tree, onResult); + const relativeFiles = stdout.split(`\r\n${rootFolder}\\`); + relativeFiles[0] = relativeFiles[0].trim().substr(rootFolder.length + 1); + const n = relativeFiles.length; + relativeFiles[n - 1] = relativeFiles[n - 1].trim(); + if (!relativeFiles[n - 1]) { + relativeFiles.pop(); + } + + this.matchFiles(rootFolder, relativeFiles, onResult); done(); }); } - private readStdout(cmd: childProcess.ChildProcess, cb: (err: Error, stdout?: string) => void): void { + private readStdout(cmd: childProcess.ChildProcess, encoding: string, cb: (err: Error, stdout?: string) => void): void { let done = (err: Error, stdout?: string) => { done = () => {}; + this.cmdForkResultTime = Date.now(); cb(err, stdout); }; @@ -195,10 +218,9 @@ export class FileWalker { cmd.on('close', code => { if (code !== 0) { - done(new Error(`find failed with error code ${code}: ${this.decodeData(stderr)}`)); + done(new Error(`find failed with error code ${code}: ${this.decodeData(stderr, encoding)}`)); } else { - this.cmdForkResultTime = Date.now(); - done(null, this.decodeData(stdout)); + done(null, this.decodeData(stdout, encoding)); } }); } @@ -211,10 +233,21 @@ export class FileWalker { return buffers; } - private decodeData(buffers: Buffer[]): string { - const decoder = new StringDecoder('utf8'); - return buffers.map(data => decoder.write(data)) - .reduce((all, current) => all + current, ''); + private decodeData(buffers: Buffer[], encoding: string): string { + const decoder = new StringDecoder(encoding); + return buffers.map(buffer => decoder.write(buffer)).join(''); + } + + private matchFiles(rootFolder: string, relativeFiles: string[], onResult: (result: IRawFileMatch) => void) { + this.cmdResultCount = relativeFiles.length; + + // Support relative paths to files from a root resource (ignores excludes) + if (relativeFiles.indexOf(this.filePattern) !== -1) { + this.matchFile(onResult, rootFolder, this.filePattern); + } + + const tree = this.buildDirectoryTree(relativeFiles); + this.matchDirectoryTree(rootFolder, tree, onResult); } private buildDirectoryTree(relativeFilePaths: string[]): IDirectoryTree { @@ -248,7 +281,7 @@ export class FileWalker { self.directoriesWalked++; for (let i = 0, n = entries.length; i < n; i++) { const entry = entries[i]; - const relativePath = entry.relativePath; // assumes slashes as separator + const relativePath = entry.relativePath; // Check exclude pattern // If the user searches for the exact file name, we adjust the glob matching @@ -267,22 +300,22 @@ export class FileWalker { continue; // ignore file if its path matches with the file pattern because that is already matched above } - self.matchFile(onResult, [rootFolder, relativePath].join(paths.sep), relativePath); + self.matchFile(onResult, rootFolder, relativePath); } }; } matchDirectory(rootEntries); } - private nodeJSTraversal(absolutePath: string, onResult: (result: IRawFileMatch) => void, done: (err?: Error) => void): void { + private nodeJSTraversal(rootFolder: string, onResult: (result: IRawFileMatch) => void, done: (err?: Error) => void): void { this.directoriesWalked++; - extfs.readdir(absolutePath, (error: Error, files: string[]) => { + extfs.readdir(rootFolder, (error: Error, files: string[]) => { if (error || this.isCanceled || this.isLimitHit) { return done(); } // Support relative paths to files from a root resource (ignores excludes) - return this.checkFilePatternRelativeMatch(absolutePath, (match, size) => { + return this.checkFilePatternRelativeMatch(rootFolder, (match, size) => { if (this.isCanceled || this.isLimitHit) { return done(); } @@ -291,13 +324,13 @@ export class FileWalker { if (match) { this.resultCount++; onResult({ - absolutePath: match, - pathLabel: this.filePattern, + base: rootFolder, + path: this.filePattern, size }); } - return this.doWalk(paths.normalize(absolutePath), '', files, onResult, done); + return this.doWalk(rootFolder, '', files, onResult, done); }); }); } @@ -340,7 +373,7 @@ export class FileWalker { }); } - private doWalk(absolutePath: string, relativeParentPathWithSlashes: string, files: string[], onResult: (result: IRawFileMatch) => void, done: (error: Error) => void): void { + private doWalk(rootFolder: string, relativeParentPath: string, files: string[], onResult: (result: IRawFileMatch) => void, done: (error: Error) => void): void { // Execute tasks on each file in parallel to optimize throughput flow.parallel(files, (file: string, clb: (error: Error) => void): void => { @@ -359,13 +392,13 @@ export class FileWalker { } // Check exclude pattern - let currentRelativePathWithSlashes = relativeParentPathWithSlashes ? [relativeParentPathWithSlashes, file].join('/') : file; - if (this.excludePattern(currentRelativePathWithSlashes, () => siblings)) { + let currentRelativePath = relativeParentPath ? [relativeParentPath, file].join(paths.sep) : file; + if (this.excludePattern(currentRelativePath, () => siblings)) { return clb(null); } // Use lstat to detect links - let currentAbsolutePath = [absolutePath, file].join(paths.sep); + let currentAbsolutePath = [rootFolder, currentRelativePath].join(paths.sep); fs.lstat(currentAbsolutePath, (error, lstat) => { if (error || this.isCanceled || this.isLimitHit) { return clb(null); @@ -401,7 +434,7 @@ export class FileWalker { return clb(null); } - this.doWalk(currentAbsolutePath, currentRelativePathWithSlashes, children, onResult, clb); + this.doWalk(rootFolder, currentRelativePath, children, onResult, clb); }); }); } @@ -409,7 +442,7 @@ export class FileWalker { // File: Check for match on file pattern and include pattern else { this.filesWalked++; - if (currentRelativePathWithSlashes === this.filePattern) { + if (currentRelativePath === this.filePattern) { return clb(null); // ignore file if its path matches with the file pattern because checkFilePatternRelativeMatch() takes care of those } @@ -417,7 +450,7 @@ export class FileWalker { return clb(null); // ignore file if max file size is hit } - this.matchFile(onResult, currentAbsolutePath, currentRelativePathWithSlashes, stat.size); + this.matchFile(onResult, rootFolder, currentRelativePath, stat.size); } // Unwind @@ -433,8 +466,8 @@ export class FileWalker { }); } - private matchFile(onResult: (result: IRawFileMatch) => void, absolutePath: string, relativePathWithSlashes: string, size?: number): void { - if (this.isFilePatternMatch(relativePathWithSlashes) && (!this.includePattern || this.includePattern(relativePathWithSlashes))) { + private matchFile(onResult: (result: IRawFileMatch) => void, base: string, path: string, size?: number): void { + if (this.isFilePatternMatch(path) && (!this.includePattern || this.includePattern(path))) { this.resultCount++; if (this.maxResults && this.resultCount > this.maxResults) { @@ -443,8 +476,8 @@ export class FileWalker { if (!this.isLimitHit) { onResult({ - absolutePath, - pathLabel: relativePathWithSlashes, + base, + path, size }); } diff --git a/src/vs/workbench/services/search/node/rawSearchService.ts b/src/vs/workbench/services/search/node/rawSearchService.ts index a2e77f3929cac7bc00bc0d6784bfc189784e6ab7..8b104f4dc74f934e0134c4151da567d37d0decac 100644 --- a/src/vs/workbench/services/search/node/rawSearchService.ts +++ b/src/vs/workbench/services/search/node/rawSearchService.ts @@ -67,9 +67,9 @@ export class SearchService implements IRawSearchService { searchPromise = this.doSearch(engine, batchSize) .then(c, e, progress => { if (Array.isArray(progress)) { - p(progress.map(m => ({ path: m.absolutePath }))); - } else if ((progress).absolutePath) { - p({ path: (progress).absolutePath }); + p(progress.map(m => this.rawMatchToSearchItem(m))); + } else if ((progress).path) { + p(this.rawMatchToSearchItem(progress)); } else { p(progress); } @@ -77,6 +77,10 @@ export class SearchService implements IRawSearchService { }, () => searchPromise.cancel()); } + private rawMatchToSearchItem(match: IRawFileMatch): ISerializedFileMatch { + return { path: match.base ? [match.base, match.path].join(paths.nativeSep) : match.path }; + } + private doSortedSearch(engine: ISearchEngine, config: IRawSearch, batchSize?: number): PPromise> { let searchPromise; return new PPromise((c, e, p) => { @@ -178,7 +182,7 @@ export class SearchService implements IRawSearchService { const normalizedSearchValue = strings.stripWildcards(filePattern).toLowerCase(); const compare = (elementA: IRawFileMatch, elementB: IRawFileMatch) => compareByScore(elementA, elementB, FileMatchAccessor, filePattern, normalizedSearchValue, cache.scorerCache); const filteredWrappers = arrays.top(results, compare, config.maxResults); - return filteredWrappers.map(result => ({ path: result.absolutePath })); + return filteredWrappers.map(result => this.rawMatchToSearchItem(result)); } private sendProgress(results: ISerializedFileMatch[], progressCb: (batch: ISerializedFileMatch[]) => void, batchSize?: number) { @@ -218,13 +222,12 @@ export class SearchService implements IRawSearchService { // Pattern match on results and adjust highlights let results: IRawFileMatch[] = []; - const normalizedSearchValue = searchValue.replace(/\\/g, '/'); // Normalize file patterns to forward slashes - const normalizedSearchValueLowercase = strings.stripWildcards(normalizedSearchValue).toLowerCase(); + const normalizedSearchValueLowercase = strings.stripWildcards(searchValue).toLowerCase(); for (let i = 0; i < cachedEntries.length; i++) { let entry = cachedEntries[i]; // Check if this entry is a match for the search value - if (!scorer.matches(entry.pathLabel, normalizedSearchValueLowercase)) { + if (!scorer.matches(entry.path, normalizedSearchValueLowercase)) { continue; } @@ -285,12 +288,12 @@ class FileMatchAccessor { public static getLabel(match: IFileMatch): string { if (!match.label) { - match.label = paths.basename(match.absolutePath); + match.label = paths.basename(match.path); } return match.label; } public static getResourcePath(match: IFileMatch): string { - return match.absolutePath; + return match.path; } } diff --git a/src/vs/workbench/services/search/node/search.ts b/src/vs/workbench/services/search/node/search.ts index 765effe27689e5383365ce0d50e9d15858561eb1..88e67fe808b9f9f80f65bfc296fedeaaf9d57e37 100644 --- a/src/vs/workbench/services/search/node/search.ts +++ b/src/vs/workbench/services/search/node/search.ts @@ -30,9 +30,9 @@ export interface IRawSearchService { } export interface IRawFileMatch { - absolutePath: string; - pathLabel: string; - size: number; + base?: string; + path: string; + size?: number; } export interface ISearchEngine { diff --git a/src/vs/workbench/services/search/node/textSearch.ts b/src/vs/workbench/services/search/node/textSearch.ts index 4b44fefb8bbd2c47ba34d53ce95a94eca94a3452..09ec52c39e9cf8853fa59ff7cb084f8207b5d606 100644 --- a/src/vs/workbench/services/search/node/textSearch.ts +++ b/src/vs/workbench/services/search/node/textSearch.ts @@ -5,11 +5,12 @@ 'use strict'; -import strings = require('vs/base/common/strings'); +import * as strings from 'vs/base/common/strings'; -import fs = require('fs'); +import * as fs from 'fs'; +import * as path from 'path'; -import baseMime = require('vs/base/common/mime'); +import * as baseMime from 'vs/base/common/mime'; import {ILineMatch, IProgress} from 'vs/platform/search/common/search'; import {detectMimeAndEncodingFromBuffer} from 'vs/base/node/mime'; import {FileWalker} from 'vs/workbench/services/search/node/fileSearch'; @@ -110,6 +111,7 @@ export class Engine implements ISearchEngine { return unwind(size); }; + const absolutePath = result.base ? [result.base, result.path].join(path.sep) : result.path; let perLineCallback = (line: string, lineNumber: number) => { if (this.limitReached || this.isCanceled) { return; // return early if canceled or limit reached @@ -126,7 +128,7 @@ export class Engine implements ISearchEngine { } if (fileMatch === null) { - fileMatch = new FileMatch(result.absolutePath); + fileMatch = new FileMatch(absolutePath); } if (lineMatch === null) { @@ -141,7 +143,7 @@ export class Engine implements ISearchEngine { }; // Read lines buffered to support large files - this.readlinesAsync(result.absolutePath, perLineCallback, { bufferLength: 8096, encoding: this.fileEncoding }, doneCallback); + this.readlinesAsync(absolutePath, perLineCallback, { bufferLength: 8096, encoding: this.fileEncoding }, doneCallback); }, (error, isLimitHit) => { this.walkerIsDone = true; this.walkerError = error; 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 e743a9395142ce47545e6e077ad65dcd48f331a4..5236ae63fbde04fcc45765e604999e4137d9fcb7 100644 --- a/src/vs/workbench/services/search/test/node/search.test.ts +++ b/src/vs/workbench/services/search/test/node/search.test.ts @@ -159,7 +159,7 @@ suite('Search', () => { }, () => { }, (error) => { assert.ok(!error); assert.equal(count, 1); - assert.strictEqual(path.basename(res.absolutePath), 'site.less'); + assert.strictEqual(path.basename(res.path), 'site.less'); done(); }); }); @@ -256,7 +256,7 @@ suite('Search', () => { }, () => { }, (error) => { assert.ok(!error); assert.equal(count, 1); - assert.equal(path.basename(res.absolutePath), '汉语.txt'); + assert.equal(path.basename(res.path), '汉语.txt'); done(); }); }); @@ -296,7 +296,7 @@ suite('Search', () => { }, () => { }, (error) => { assert.ok(!error); assert.equal(count, 1); - assert.equal(path.basename(res.absolutePath), 'site.css'); + assert.equal(path.basename(res.path), 'site.css'); done(); }); }); @@ -317,7 +317,7 @@ suite('Search', () => { }, () => { }, (error) => { assert.ok(!error); assert.equal(count, 1); - assert.equal(path.basename(res.absolutePath), 'company.js'); + assert.equal(path.basename(res.path), 'company.js'); done(); }); }); @@ -339,7 +339,7 @@ suite('Search', () => { }, () => { }, (error) => { assert.ok(!error); assert.equal(count, 1); - assert.equal(path.basename(res.absolutePath), 'company.js'); + assert.equal(path.basename(res.path), 'company.js'); done(); }); }); @@ -365,7 +365,7 @@ suite('Search', () => { }, () => { }, (error) => { assert.ok(!error); assert.equal(count, 1); - assert.equal(path.basename(res.absolutePath), 'company.js'); + assert.equal(path.basename(res.path), 'company.js'); done(); }); }); @@ -392,7 +392,7 @@ suite('Search', () => { }, () => { }, (error) => { assert.ok(!error); assert.equal(count, 1); - assert.equal(path.basename(res.absolutePath), 'site.css'); + assert.equal(path.basename(res.path), 'site.css'); done(); }); }); diff --git a/src/vs/workbench/services/search/test/node/searchService.test.ts b/src/vs/workbench/services/search/test/node/searchService.test.ts index 1674c1a513de1ce3a063fa5fa9fe792af92a0e33..0b00ad262ec6d488fc566557800622b1ea01eff1 100644 --- a/src/vs/workbench/services/search/test/node/searchService.test.ts +++ b/src/vs/workbench/services/search/test/node/searchService.test.ts @@ -5,7 +5,8 @@ 'use strict'; -import assert = require('assert'); +import * as assert from 'assert'; +import {normalize} from 'path'; import {IProgress, IUncachedSearchStats} from 'vs/platform/search/common/search'; import {ISearchEngine, IRawSearch, IRawFileMatch, ISerializedFileMatch, ISerializedSearchComplete} from 'vs/workbench/services/search/node/search'; @@ -67,18 +68,18 @@ class TestSearchEngine implements ISearchEngine { suite('SearchService', () => { const rawSearch: IRawSearch = { - rootFolders: ['/some/where'], + rootFolders: [normalize('/some/where')], filePattern: 'a' }; const rawMatch: IRawFileMatch = { - absolutePath: '/some/where', - pathLabel: 'where', + base: normalize('/some'), + path: 'where', size: 123 }; const match: ISerializedFileMatch = { - path: '/some/where' + path: normalize('/some/where') }; test('Individual results', function () { @@ -122,7 +123,7 @@ suite('SearchService', () => { }); test('Collect batched results', function () { - const path = '/some/where'; + const uriPath = '/some/where'; let i = 25; const Engine = TestSearchEngine.bind(null, () => i-- && rawMatch); const service = new RawSearchService(); @@ -134,16 +135,16 @@ suite('SearchService', () => { assert.strictEqual(result.results.length, 25, 'Result'); assert.strictEqual(progressResults.length, 25, 'Progress'); }, null, match => { - assert.strictEqual(match.resource.path, path); + assert.strictEqual(match.resource.path, uriPath); progressResults.push(match); }); }); test('Sorted results', function () { const paths = ['bab', 'bbc', 'abb']; - const matches = paths.map(path => ({ - absolutePath: `/some/where/${path}`, - pathLabel: path, + const matches: IRawFileMatch[] = paths.map(path => ({ + base: normalize('/some/where'), + path, size: 3 })); let i = 0; @@ -152,13 +153,13 @@ suite('SearchService', () => { const results = []; return service.doFileSearch(Engine, { - rootFolders: ['/some/where'], + rootFolders: [normalize('/some/where')], filePattern: 'bb', sortByScore: true, maxResults: 2 }, 1).then(() => { assert.notStrictEqual(typeof TestSearchEngine.last.config.maxResults, 'number'); - assert.deepStrictEqual(results, ['/some/where/bbc', '/some/where/bab']); + assert.deepStrictEqual(results, [normalize('/some/where/bbc'), normalize('/some/where/bab')]); }, null, value => { if (Array.isArray(value)) { results.push(...value.map(v => v.path)); @@ -175,7 +176,7 @@ suite('SearchService', () => { const results = []; return service.doFileSearch(Engine, { - rootFolders: ['/some/where'], + rootFolders: [normalize('/some/where')], filePattern: 'a', sortByScore: true, maxResults: 23 @@ -196,9 +197,9 @@ suite('SearchService', () => { test('Cached results', function () { const paths = ['bcb', 'bbc', 'aab']; - const matches = paths.map(path => ({ - absolutePath: `/some/where/${path}`, - pathLabel: path, + const matches: IRawFileMatch[] = paths.map(path => ({ + base: normalize('/some/where'), + path, size: 3 })); let i = 0; @@ -207,13 +208,13 @@ suite('SearchService', () => { const results = []; return service.doFileSearch(Engine, { - rootFolders: ['/some/where'], + rootFolders: [normalize('/some/where')], filePattern: 'b', sortByScore: true, cacheKey: 'x' }, -1).then(complete => { assert.strictEqual(complete.stats.fromCache, false); - assert.deepStrictEqual(results, ['/some/where/bcb', '/some/where/bbc', '/some/where/aab']); + assert.deepStrictEqual(results, [normalize('/some/where/bcb'), normalize('/some/where/bbc'), normalize('/some/where/aab')]); }, null, value => { if (Array.isArray(value)) { results.push(...value.map(v => v.path)); @@ -223,13 +224,13 @@ suite('SearchService', () => { }).then(() => { const results = []; return service.doFileSearch(Engine, { - rootFolders: ['/some/where'], + rootFolders: [normalize('/some/where')], filePattern: 'bc', sortByScore: true, cacheKey: 'x' }, -1).then(complete => { assert.ok(complete.stats.fromCache); - assert.deepStrictEqual(results, ['/some/where/bcb', '/some/where/bbc']); + assert.deepStrictEqual(results, [normalize('/some/where/bcb'), normalize('/some/where/bbc')]); }, null, value => { if (Array.isArray(value)) { results.push(...value.map(v => v.path)); @@ -241,19 +242,19 @@ suite('SearchService', () => { return service.clearCache('x'); }).then(() => { matches.push({ - absolutePath: '/some/where/bc', - pathLabel: 'bc', + base: normalize('/some/where'), + path: 'bc', size: 3 }); const results = []; return service.doFileSearch(Engine, { - rootFolders: ['/some/where'], + rootFolders: [normalize('/some/where')], filePattern: 'bc', sortByScore: true, cacheKey: 'x' }, -1).then(complete => { assert.strictEqual(complete.stats.fromCache, false); - assert.deepStrictEqual(results, ['/some/where/bc']); + assert.deepStrictEqual(results, [normalize('/some/where/bc')]); }, null, value => { if (Array.isArray(value)) { results.push(...value.map(v => v.path));