fileSearch.ts 23.6 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

C
Christof Marti 已提交
8
import * as childProcess from 'child_process';
J
Johannes Rieken 已提交
9 10
import { StringDecoder, NodeStringDecoder } from 'string_decoder';
import { toErrorMessage } from 'vs/base/common/errorMessage';
E
Erich Gamma 已提交
11
import fs = require('fs');
12 13
import path = require('path');
import { isEqualOrParent } from 'vs/base/common/paths';
B
Benjamin Pasero 已提交
14
import { Readable } from 'stream';
15
import { TPromise } from 'vs/base/common/winjs.base';
E
Erich Gamma 已提交
16

17
import scorer = require('vs/base/common/scorer');
18
import objects = require('vs/base/common/objects');
E
Erich Gamma 已提交
19
import arrays = require('vs/base/common/arrays');
C
Christof Marti 已提交
20
import platform = require('vs/base/common/platform');
E
Erich Gamma 已提交
21
import strings = require('vs/base/common/strings');
22
import types = require('vs/base/common/types');
E
Erich Gamma 已提交
23
import glob = require('vs/base/common/glob');
J
Johannes Rieken 已提交
24
import { IProgress, IUncachedSearchStats } from 'vs/platform/search/common/search';
E
Erich Gamma 已提交
25 26 27

import extfs = require('vs/base/node/extfs');
import flow = require('vs/base/node/flow');
28
import { IRawFileMatch, ISerializedSearchComplete, IRawSearch, ISearchEngine, IFolderSearch } from './search';
E
Erich Gamma 已提交
29

C
Christof Marti 已提交
30 31 32
enum Traversal {
	Node = 1,
	MacFind,
C
Christof Marti 已提交
33 34
	WindowsDir,
	LinuxFind
C
Christof Marti 已提交
35 36 37
}

interface IDirectoryEntry {
38
	base: string;
C
Christof Marti 已提交
39 40 41 42 43 44 45 46 47
	relativePath: string;
	basename: string;
}

interface IDirectoryTree {
	rootEntries: IDirectoryEntry[];
	pathToEntries: { [relativePath: string]: IDirectoryEntry[] };
}

E
Erich Gamma 已提交
48 49
export class FileWalker {
	private config: IRawSearch;
50
	private filePattern: string;
51
	private normalizedFilePatternLowercase: string;
C
Christof Marti 已提交
52
	private includePattern: glob.ParsedExpression;
E
Erich Gamma 已提交
53
	private maxResults: number;
54
	private maxFilesize: number;
E
Erich Gamma 已提交
55 56 57
	private isLimitHit: boolean;
	private resultCount: number;
	private isCanceled: boolean;
C
chrmarti 已提交
58 59 60
	private fileWalkStartTime: number;
	private directoriesWalked: number;
	private filesWalked: number;
C
Christof Marti 已提交
61 62 63 64 65
	private traversal: Traversal;
	private errors: string[];
	private cmdForkStartTime: number;
	private cmdForkResultTime: number;
	private cmdResultCount: number;
E
Erich Gamma 已提交
66

67
	private folderExcludePatterns: Map<string, AbsoluteAndRelativeParsedExpression>;
68 69
	private globalExcludePattern: glob.ParsedExpression;

E
Erich Gamma 已提交
70 71 72 73
	private walkedPaths: { [path: string]: boolean; };

	constructor(config: IRawSearch) {
		this.config = config;
74
		this.filePattern = config.filePattern;
C
Christof Marti 已提交
75
		this.includePattern = config.includePattern && glob.parse(config.includePattern);
E
Erich Gamma 已提交
76
		this.maxResults = config.maxResults || null;
77
		this.maxFilesize = config.maxFilesize || null;
E
Erich Gamma 已提交
78
		this.walkedPaths = Object.create(null);
79 80
		this.resultCount = 0;
		this.isLimitHit = false;
C
chrmarti 已提交
81 82
		this.directoriesWalked = 0;
		this.filesWalked = 0;
C
Christof Marti 已提交
83 84
		this.traversal = Traversal.Node;
		this.errors = [];
B
Benjamin Pasero 已提交
85

86 87
		if (this.filePattern) {
			this.normalizedFilePatternLowercase = strings.stripWildcards(this.filePattern).toLowerCase();
B
Benjamin Pasero 已提交
88
		}
89 90

		this.globalExcludePattern = config.excludePattern && glob.parse(config.excludePattern);
91
		this.folderExcludePatterns = new Map<string, AbsoluteAndRelativeParsedExpression>();
92 93

		config.folderQueries.forEach(folderQuery => {
94
			const folderExcludeExpression: glob.IExpression = objects.assign({}, folderQuery.excludePattern || {}, this.config.excludePattern || {});
95 96 97 98 99

			// Add excludes for other root folders
			config.folderQueries
				.map(rootFolderQuery => rootFolderQuery.folder)
				.filter(rootFolder => rootFolder !== folderQuery.folder)
100 101 102 103 104
				.forEach(otherRootFolder => {
					// Exclude nested root folders
					if (isEqualOrParent(otherRootFolder, folderQuery.folder)) {
						folderExcludeExpression[path.relative(folderQuery.folder, otherRootFolder)] = true;
					}
105 106
				});

107
			this.folderExcludePatterns.set(folderQuery.folder, new AbsoluteAndRelativeParsedExpression(folderExcludeExpression, folderQuery.folder));
108
		});
E
Erich Gamma 已提交
109 110 111 112 113 114
	}

	public cancel(): void {
		this.isCanceled = true;
	}

115
	public walk(folderQueries: IFolderSearch[], extraFiles: string[], onResult: (result: IRawFileMatch) => void, done: (error: Error, isLimitHit: boolean) => void): void {
C
chrmarti 已提交
116
		this.fileWalkStartTime = Date.now();
E
Erich Gamma 已提交
117

118
		// Support that the file pattern is a full path to a file that exists
119
		this.checkFilePatternAbsoluteMatch((exists, size) => {
120 121 122
			if (this.isCanceled) {
				return done(null, this.isLimitHit);
			}
E
Erich Gamma 已提交
123

124 125
			// Report result from file pattern if matching
			if (exists) {
126 127
				this.resultCount++;
				onResult({
128
					relativePath: this.filePattern,
129
					basename: path.basename(this.filePattern),
130 131
					size
				});
132 133 134 135

				// Optimization: a match on an absolute path is a good result and we do not
				// continue walking the entire root paths array for other matches because
				// it is very unlikely that another file would match on the full absolute path
136
				return done(null, this.isLimitHit);
137
			}
E
Erich Gamma 已提交
138

139 140 141
			// For each extra file
			if (extraFiles) {
				extraFiles.forEach(extraFilePath => {
142
					const basename = path.basename(extraFilePath);
R
Rob Lourens 已提交
143
					if (this.globalExcludePattern && this.globalExcludePattern(extraFilePath, basename)) {
144
						return; // excluded
E
Erich Gamma 已提交
145 146
					}

147
					// File: Check for match on file pattern and include pattern
148
					this.matchFile(onResult, { relativePath: extraFilePath /* no workspace relative path */, basename });
149 150
				});
			}
151

C
Christof Marti 已提交
152
			let traverse = this.nodeJSTraversal;
C
Christof Marti 已提交
153 154 155
			if (!this.maxFilesize) {
				if (platform.isMacintosh) {
					this.traversal = Traversal.MacFind;
156
					traverse = this.findTraversal;
J
Johannes Rieken 已提交
157
					// Disable 'dir' for now (#11181, #11179, #11183, #11182).
158
				} /* else if (platform.isWindows) {
C
Christof Marti 已提交
159 160
					this.traversal = Traversal.WindowsDir;
					traverse = this.windowsDirTraversal;
D
Dirk Baeumer 已提交
161
				} */ else if (platform.isLinux) {
C
Christof Marti 已提交
162
					this.traversal = Traversal.LinuxFind;
163
					traverse = this.findTraversal;
C
Christof Marti 已提交
164 165 166 167 168 169
				}
			}

			const isNodeTraversal = traverse === this.nodeJSTraversal;
			if (!isNodeTraversal) {
				this.cmdForkStartTime = Date.now();
C
Christof Marti 已提交
170 171
			}

172
			// For each root folder
173 174
			flow.parallel<IFolderSearch, void>(folderQueries, (folderQuery: IFolderSearch, rootFolderDone: (err: Error, result: void) => void) => {
				this.call(traverse, this, folderQuery, onResult, (err?: Error) => {
C
Christof Marti 已提交
175
					if (err) {
C
Christof Marti 已提交
176
						if (isNodeTraversal) {
177
							rootFolderDone(err, undefined);
C
Christof Marti 已提交
178 179
						} else {
							// fallback
180
							const errorMessage = toErrorMessage(err);
181 182
							console.error(errorMessage);
							this.errors.push(errorMessage);
183
							this.nodeJSTraversal(folderQuery, onResult, err => rootFolderDone(err, undefined));
C
Christof Marti 已提交
184 185
						}
					} else {
186
						rootFolderDone(undefined, undefined);
187
					}
C
Christof Marti 已提交
188 189 190 191 192 193
				});
			}, (err, result) => {
				done(err ? err[0] : null, this.isLimitHit);
			});
		});
	}
194

C
Christof Marti 已提交
195 196 197 198 199 200 201 202
	private call(fun: Function, that: any, ...args: any[]): void {
		try {
			fun.apply(that, args);
		} catch (e) {
			args[args.length - 1](e);
		}
	}

203 204
	private findTraversal(folderQuery: IFolderSearch, onResult: (result: IRawFileMatch) => void, cb: (err?: Error) => void): void {
		const rootFolder = folderQuery.folder;
205 206
		const isMac = platform.isMacintosh;
		let done = (err?: Error) => {
J
Johannes Rieken 已提交
207
			done = () => { };
208 209 210 211 212
			cb(err);
		};
		let leftover = '';
		let first = true;
		const tree = this.initDirectoryTree();
213
		const cmd = this.spawnFindCmd(folderQuery);
214
		this.collectStdout(cmd, 'utf8', (err: Error, stdout?: string, last?: boolean) => {
C
Christof Marti 已提交
215 216 217 218
			if (err) {
				done(err);
				return;
			}
E
Erich Gamma 已提交
219

C
Christof Marti 已提交
220
			// Mac: uses NFD unicode form on disk, but we want NFC
221 222 223 224 225
			const normalized = leftover + (isMac ? strings.normalizeNFC(stdout) : stdout);
			const relativeFiles = normalized.split('\n./');
			if (first && normalized.length >= 2) {
				first = false;
				relativeFiles[0] = relativeFiles[0].trim().substr(2);
C
Christof Marti 已提交
226 227
			}

228 229 230 231 232 233 234 235
			if (last) {
				const n = relativeFiles.length;
				relativeFiles[n - 1] = relativeFiles[n - 1].trim();
				if (!relativeFiles[n - 1]) {
					relativeFiles.pop();
				}
			} else {
				leftover = relativeFiles.pop();
C
Christof Marti 已提交
236 237
			}

238 239 240 241 242
			if (relativeFiles.length && relativeFiles[0].indexOf('\n') !== -1) {
				done(new Error('Splitting up files failed'));
				return;
			}

243
			this.addDirectoryEntries(tree, rootFolder, relativeFiles, onResult);
C
Christof Marti 已提交
244

245 246 247
			if (last) {
				this.matchDirectoryTree(tree, rootFolder, onResult);
				done();
248
			}
C
Christof Marti 已提交
249 250 251
		});
	}

252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
	// protected windowsDirTraversal(rootFolder: string, onResult: (result: IRawFileMatch) => void, done: (err?: Error) => void): void {
	// 	const cmd = childProcess.spawn('cmd', ['/U', '/c', 'dir', '/s', '/b', '/a-d', rootFolder]);
	// 	this.readStdout(cmd, 'ucs2', (err: Error, stdout?: string) => {
	// 		if (err) {
	// 			done(err);
	// 			return;
	// 		}

	// 		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();
	// 		}

	// 		if (relativeFiles.length && relativeFiles[0].indexOf('\n') !== -1) {
	// 			done(new Error('Splitting up files failed'));
	// 			return;
	// 		}

	// 		this.matchFiles(rootFolder, relativeFiles, onResult);

	// 		done();
	// 	});
	// }

279 280 281
	/**
	 * Public for testing.
	 */
282
	public spawnFindCmd(folderQuery: IFolderSearch) {
283
		const excludePattern = this.folderExcludePatterns.get(folderQuery.folder);
284 285
		const basenames = excludePattern.getBasenameTerms();
		const pathTerms = excludePattern.getPathTerms();
286
		let args = ['-L', '.'];
287
		if (basenames.length || pathTerms.length) {
288
			args.push('-not', '(', '(');
289
			for (const basename of basenames) {
290
				args.push('-name', basename);
291 292
				args.push('-o');
			}
293
			for (const path of pathTerms) {
294
				args.push('-path', path);
295
				args.push('-o');
296
			}
297
			args.pop();
298 299 300
			args.push(')', '-prune', ')');
		}
		args.push('-type', 'f');
301
		return childProcess.spawn('find', args, { cwd: folderQuery.folder });
302 303 304 305 306 307
	}

	/**
	 * Public for testing.
	 */
	public readStdout(cmd: childProcess.ChildProcess, encoding: string, cb: (err: Error, stdout?: string) => void): void {
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
		let all = '';
		this.collectStdout(cmd, encoding, (err: Error, stdout?: string, last?: boolean) => {
			if (err) {
				cb(err);
				return;
			}

			all += stdout;
			if (last) {
				cb(null, all);
			}
		});
	}

	private collectStdout(cmd: childProcess.ChildProcess, encoding: string, cb: (err: Error, stdout?: string, last?: boolean) => void): void {
		let done = (err: Error, stdout?: string, last?: boolean) => {
			if (err || last) {
J
Johannes Rieken 已提交
325
				done = () => { };
326 327 328
				this.cmdForkResultTime = Date.now();
			}
			cb(err, stdout, last);
C
Christof Marti 已提交
329 330
		};

331
		this.forwardData(cmd.stdout, encoding, done);
C
Christof Marti 已提交
332 333
		const stderr = this.collectData(cmd.stderr);

334
		cmd.on('error', (err: Error) => {
C
Christof Marti 已提交
335 336 337
			done(err);
		});

338
		cmd.on('close', (code: number) => {
C
Christof Marti 已提交
339
			if (code !== 0) {
C
Christof Marti 已提交
340
				done(new Error(`find failed with error code ${code}: ${this.decodeData(stderr, encoding)}`));
C
Christof Marti 已提交
341
			} else {
342
				done(null, '', true);
C
Christof Marti 已提交
343 344 345 346
			}
		});
	}

347 348 349 350 351 352 353 354
	private forwardData(stream: Readable, encoding: string, cb: (err: Error, stdout?: string) => void): NodeStringDecoder {
		const decoder = new StringDecoder(encoding);
		stream.on('data', (data: Buffer) => {
			cb(null, decoder.write(data));
		});
		return decoder;
	}

C
Christof Marti 已提交
355
	private collectData(stream: Readable): Buffer[] {
356 357
		const buffers: Buffer[] = [];
		stream.on('data', (data: Buffer) => {
C
Christof Marti 已提交
358 359 360 361 362
			buffers.push(data);
		});
		return buffers;
	}

C
Christof Marti 已提交
363 364 365 366 367
	private decodeData(buffers: Buffer[], encoding: string): string {
		const decoder = new StringDecoder(encoding);
		return buffers.map(buffer => decoder.write(buffer)).join('');
	}

368 369 370 371 372 373 374 375 376
	private initDirectoryTree(): IDirectoryTree {
		const tree: IDirectoryTree = {
			rootEntries: [],
			pathToEntries: Object.create(null)
		};
		tree.pathToEntries['.'] = tree.rootEntries;
		return tree;
	}

377
	private addDirectoryEntries({ pathToEntries }: IDirectoryTree, base: string, relativeFiles: string[], onResult: (result: IRawFileMatch) => void) {
378
		this.cmdResultCount += relativeFiles.length;
C
Christof Marti 已提交
379 380 381

		// Support relative paths to files from a root resource (ignores excludes)
		if (relativeFiles.indexOf(this.filePattern) !== -1) {
382
			const basename = path.basename(this.filePattern);
383
			this.matchFile(onResult, { base: base, relativePath: this.filePattern, basename });
C
Christof Marti 已提交
384 385
		}

386
		function add(relativePath: string) {
387 388
			const basename = path.basename(relativePath);
			const dirname = path.dirname(relativePath);
C
Christof Marti 已提交
389 390 391 392 393 394
			let entries = pathToEntries[dirname];
			if (!entries) {
				entries = pathToEntries[dirname] = [];
				add(dirname);
			}
			entries.push({
395
				base,
C
Christof Marti 已提交
396 397 398
				relativePath,
				basename
			});
399 400
		}
		relativeFiles.forEach(add);
C
Christof Marti 已提交
401 402
	}

403
	private matchDirectoryTree({ rootEntries, pathToEntries }: IDirectoryTree, rootFolder: string, onResult: (result: IRawFileMatch) => void) {
C
Christof Marti 已提交
404
		const self = this;
405
		const excludePattern = this.folderExcludePatterns.get(rootFolder);
C
Christof Marti 已提交
406 407 408 409
		const filePattern = this.filePattern;
		function matchDirectory(entries: IDirectoryEntry[]) {
			self.directoriesWalked++;
			for (let i = 0, n = entries.length; i < n; i++) {
410
				const entry = entries[i];
411
				const { relativePath, basename } = entry;
C
Christof Marti 已提交
412 413 414 415 416

				// Check exclude pattern
				// If the user searches for the exact file name, we adjust the glob matching
				// to ignore filtering by siblings because the user seems to know what she
				// is searching for and we want to include the result in that case anyway
417
				if (excludePattern.test(relativePath, basename, () => filePattern !== basename ? entries.map(entry => entry.basename) : [])) {
C
Christof Marti 已提交
418 419
					continue;
				}
E
Erich Gamma 已提交
420

C
Christof Marti 已提交
421 422 423 424 425 426 427 428 429
				const sub = pathToEntries[relativePath];
				if (sub) {
					matchDirectory(sub);
				} else {
					self.filesWalked++;
					if (relativePath === filePattern) {
						continue; // ignore file if its path matches with the file pattern because that is already matched above
					}

430
					self.matchFile(onResult, entry);
C
Christof Marti 已提交
431 432 433 434 435 436
				}
			};
		}
		matchDirectory(rootEntries);
	}

437
	private nodeJSTraversal(folderQuery: IFolderSearch, onResult: (result: IRawFileMatch) => void, done: (err?: Error) => void): void {
C
Christof Marti 已提交
438
		this.directoriesWalked++;
439
		extfs.readdir(folderQuery.folder, (error: Error, files: string[]) => {
C
Christof Marti 已提交
440 441 442 443 444
			if (error || this.isCanceled || this.isLimitHit) {
				return done();
			}

			// Support relative paths to files from a root resource (ignores excludes)
445
			return this.checkFilePatternRelativeMatch(folderQuery.folder, (match, size) => {
C
Christof Marti 已提交
446 447 448 449 450 451 452 453
				if (this.isCanceled || this.isLimitHit) {
					return done();
				}

				// Report result from file pattern if matching
				if (match) {
					this.resultCount++;
					onResult({
454
						base: folderQuery.folder,
455
						relativePath: this.filePattern,
456
						basename: path.basename(this.filePattern),
C
Christof Marti 已提交
457
						size
458
					});
C
Christof Marti 已提交
459 460
				}

461
				return this.doWalk(folderQuery, '', files, onResult, done);
E
Erich Gamma 已提交
462
			});
463 464 465
		});
	}

466
	public getStats(): IUncachedSearchStats {
C
chrmarti 已提交
467
		return {
468
			fromCache: false,
C
Christof Marti 已提交
469 470
			traversal: Traversal[this.traversal],
			errors: this.errors,
C
chrmarti 已提交
471 472 473
			fileWalkStartTime: this.fileWalkStartTime,
			fileWalkResultTime: Date.now(),
			directoriesWalked: this.directoriesWalked,
474
			filesWalked: this.filesWalked,
C
Christof Marti 已提交
475 476 477 478
			resultCount: this.resultCount,
			cmdForkStartTime: this.cmdForkStartTime,
			cmdForkResultTime: this.cmdForkResultTime,
			cmdResultCount: this.cmdResultCount
C
chrmarti 已提交
479 480 481
		};
	}

482
	private checkFilePatternAbsoluteMatch(clb: (exists: boolean, size?: number) => void): void {
483
		if (!this.filePattern || !path.isAbsolute(this.filePattern)) {
484 485 486
			return clb(false);
		}

487
		return fs.stat(this.filePattern, (error, stat) => {
488
			return clb(!error && !stat.isDirectory(), stat && stat.size); // only existing files
E
Erich Gamma 已提交
489 490 491
		});
	}

492
	private checkFilePatternRelativeMatch(basePath: string, clb: (matchPath: string, size?: number) => void): void {
493
		if (!this.filePattern || path.isAbsolute(this.filePattern)) {
494 495 496
			return clb(null);
		}

497
		const absolutePath = path.join(basePath, this.filePattern);
498

499
		return fs.stat(absolutePath, (error, stat) => {
500
			return clb(!error && !stat.isDirectory() ? absolutePath : null, stat && stat.size); // only existing files
501 502 503
		});
	}

504 505
	private doWalk(folderQuery: IFolderSearch, relativeParentPath: string, files: string[], onResult: (result: IRawFileMatch) => void, done: (error: Error) => void): void {
		const rootFolder = folderQuery.folder;
E
Erich Gamma 已提交
506 507

		// Execute tasks on each file in parallel to optimize throughput
508
		flow.parallel(files, (file: string, clb: (error: Error, result: {}) => void): void => {
E
Erich Gamma 已提交
509 510 511

			// Check canceled
			if (this.isCanceled || this.isLimitHit) {
512
				return clb(null, undefined);
E
Erich Gamma 已提交
513 514 515 516 517 518
			}

			// If the user searches for the exact file name, we adjust the glob matching
			// to ignore filtering by siblings because the user seems to know what she
			// is searching for and we want to include the result in that case anyway
			let siblings = files;
519
			if (this.config.filePattern === file) {
E
Erich Gamma 已提交
520 521 522 523
				siblings = [];
			}

			// Check exclude pattern
524
			let currentRelativePath = relativeParentPath ? [relativeParentPath, file].join(path.sep) : file;
525
			if (this.folderExcludePatterns.get(folderQuery.folder).test(currentRelativePath, file, () => siblings)) {
526
				return clb(null, undefined);
E
Erich Gamma 已提交
527 528
			}

529
			// Use lstat to detect links
530
			let currentAbsolutePath = [rootFolder, currentRelativePath].join(path.sep);
531
			fs.lstat(currentAbsolutePath, (error, lstat) => {
532
				if (error || this.isCanceled || this.isLimitHit) {
533
					return clb(null, undefined);
534
				}
E
Erich Gamma 已提交
535

536
				// If the path is a link, we must instead use fs.stat() to find out if the
537 538 539 540
				// link is a directory or not because lstat will always return the stat of
				// the link which is always a file.
				this.statLinkIfNeeded(currentAbsolutePath, lstat, (error, stat) => {
					if (error || this.isCanceled || this.isLimitHit) {
541
						return clb(null, undefined);
542
					}
E
Erich Gamma 已提交
543

544 545
					// Directory: Follow directories
					if (stat.isDirectory()) {
C
chrmarti 已提交
546
						this.directoriesWalked++;
547

548 549
						// to really prevent loops with links we need to resolve the real path of them
						return this.realPathIfNeeded(currentAbsolutePath, lstat, (error, realpath) => {
550
							if (error || this.isCanceled || this.isLimitHit) {
551
								return clb(null, undefined);
552 553
							}

554
							if (this.walkedPaths[realpath]) {
555
								return clb(null, undefined); // escape when there are cycles (can happen with symlinks)
556
							}
E
Erich Gamma 已提交
557

558 559 560 561 562
							this.walkedPaths[realpath] = true; // remember as walked

							// Continue walking
							return extfs.readdir(currentAbsolutePath, (error: Error, children: string[]): void => {
								if (error || this.isCanceled || this.isLimitHit) {
563
									return clb(null, undefined);
564 565
								}

566
								this.doWalk(folderQuery, currentRelativePath, children, onResult, err => clb(err, undefined));
567 568
							});
						});
569
					}
E
Erich Gamma 已提交
570

571 572
					// File: Check for match on file pattern and include pattern
					else {
C
chrmarti 已提交
573
						this.filesWalked++;
C
Christof Marti 已提交
574
						if (currentRelativePath === this.filePattern) {
575
							return clb(null, undefined); // ignore file if its path matches with the file pattern because checkFilePatternRelativeMatch() takes care of those
576
						}
E
Erich Gamma 已提交
577

578
						if (this.maxFilesize && types.isNumber(stat.size) && stat.size > this.maxFilesize) {
579
							return clb(null, undefined); // ignore file if max file size is hit
580 581
						}

582
						this.matchFile(onResult, { base: rootFolder, relativePath: currentRelativePath, basename: file, size: stat.size });
583 584 585
					}

					// Unwind
586
					return clb(null, undefined);
587
				});
E
Erich Gamma 已提交
588 589 590 591 592 593
			});
		}, (error: Error[]): void => {
			if (error) {
				error = arrays.coalesce(error); // find any error by removing null values first
			}

C
Christof Marti 已提交
594
			return done(error && error.length > 0 ? error[0] : null);
E
Erich Gamma 已提交
595 596 597
		});
	}

598 599
	private matchFile(onResult: (result: IRawFileMatch) => void, candidate: IRawFileMatch): void {
		if (this.isFilePatternMatch(candidate.relativePath) && (!this.includePattern || this.includePattern(candidate.relativePath, candidate.basename))) {
600 601 602 603 604 605 606
			this.resultCount++;

			if (this.maxResults && this.resultCount > this.maxResults) {
				this.isLimitHit = true;
			}

			if (!this.isLimitHit) {
607
				onResult(candidate);
608 609 610 611
			}
		}
	}

B
polish  
Benjamin Pasero 已提交
612
	private isFilePatternMatch(path: string): boolean {
613 614 615

		// Check for search pattern
		if (this.filePattern) {
616 617 618 619
			if (this.filePattern === '*') {
				return true; // support the all-matching wildcard
			}

620
			return scorer.matches(path, this.normalizedFilePatternLowercase);
621 622 623 624 625 626
		}

		// No patterns means we match all
		return true;
	}

627
	private statLinkIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error, stat: fs.Stats) => void): void {
628
		if (lstat.isSymbolicLink()) {
629
			return fs.stat(path, clb); // stat the target the link points to
630 631 632 633 634
		}

		return clb(null, lstat); // not a link, so the stat is already ok for us
	}

635
	private realPathIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error, realpath?: string) => void): void {
636 637 638 639 640
		if (lstat.isSymbolicLink()) {
			return fs.realpath(path, (error, realpath) => {
				if (error) {
					return clb(error);
				}
E
Erich Gamma 已提交
641

642 643 644
				return clb(null, realpath);
			});
		}
E
Erich Gamma 已提交
645

646
		return clb(null, path);
E
Erich Gamma 已提交
647 648 649
	}
}

650
export class Engine implements ISearchEngine<IRawFileMatch> {
651
	private folderQueries: IFolderSearch[];
652
	private extraFiles: string[];
E
Erich Gamma 已提交
653 654 655
	private walker: FileWalker;

	constructor(config: IRawSearch) {
656
		this.folderQueries = config.folderQueries;
657 658
		this.extraFiles = config.extraFiles;

E
Erich Gamma 已提交
659 660 661
		this.walker = new FileWalker(config);
	}

662
	public search(onResult: (result: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void {
663
		this.walker.walk(this.folderQueries, this.extraFiles, onResult, (err: Error, isLimitHit: boolean) => {
C
chrmarti 已提交
664 665 666 667 668
			done(err, {
				limitHit: isLimitHit,
				stats: this.walker.getStats()
			});
		});
E
Erich Gamma 已提交
669 670 671 672 673
	}

	public cancel(): void {
		this.walker.cancel();
	}
674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694
}

/**
 * This class exists to provide one interface on top of two ParsedExpressions, one for absolute expressions and one for relative expressions.
 * The absolute and relative expressions don't "have" to be kept separate, but this keeps us from having to path.join every single
 * file searched, it's only used for a text search with a searchPath
 */
class AbsoluteAndRelativeParsedExpression {
	private absoluteParsedExpr: glob.ParsedExpression;
	private relativeParsedExpr: glob.ParsedExpression;

	constructor(expr: glob.IExpression, private root: string) {
		this.init(expr);
	}

	/**
	 * Split the IExpression into its absolute and relative components, and glob.parse them separately.
	 */
	private init(expr: glob.IExpression): void {
		let absoluteGlobExpr: glob.IExpression;
		let relativeGlobExpr: glob.IExpression;
695 696 697 698 699 700 701 702 703 704 705
		Object.keys(expr)
			.filter(key => expr[key])
			.forEach(key => {
				if (path.isAbsolute(key)) {
					absoluteGlobExpr = absoluteGlobExpr || glob.getEmptyExpression();
					absoluteGlobExpr[key] = true;
				} else {
					relativeGlobExpr = relativeGlobExpr || glob.getEmptyExpression();
					relativeGlobExpr[key] = true;
				}
			});
706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740

		this.absoluteParsedExpr = absoluteGlobExpr && glob.parse(absoluteGlobExpr, { trimForExclusions: true });
		this.relativeParsedExpr = relativeGlobExpr && glob.parse(relativeGlobExpr, { trimForExclusions: true });
	}

	public test(_path: string, basename?: string, siblingsFn?: () => string[] | TPromise<string[]>): string | TPromise<string> {
		return (this.relativeParsedExpr && this.relativeParsedExpr(_path, basename, siblingsFn)) ||
			(this.absoluteParsedExpr && this.absoluteParsedExpr(path.join(this.root, _path), basename, siblingsFn));
	}

	public getBasenameTerms(): string[] {
		const basenameTerms = [];
		if (this.absoluteParsedExpr) {
			basenameTerms.push(...glob.getBasenameTerms(this.absoluteParsedExpr));
		}

		if (this.relativeParsedExpr) {
			basenameTerms.push(...glob.getBasenameTerms(this.relativeParsedExpr));
		}

		return basenameTerms;
	}

	public getPathTerms(): string[] {
		const pathTerms = [];
		if (this.absoluteParsedExpr) {
			pathTerms.push(...glob.getPathTerms(this.absoluteParsedExpr));
		}

		if (this.relativeParsedExpr) {
			pathTerms.push(...glob.getPathTerms(this.relativeParsedExpr));
		}

		return pathTerms;
	}
741
}