fileSearch.ts 26.3 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';
11 12
import * as fs from 'fs';
import * as path from 'path';
13
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 18 19 20 21 22
import * as objects from 'vs/base/common/objects';
import * as arrays from 'vs/base/common/arrays';
import * as platform from 'vs/base/common/platform';
import * as strings from 'vs/base/common/strings';
import * as types from 'vs/base/common/types';
import * as glob from 'vs/base/common/glob';
J
Johannes Rieken 已提交
23
import { IProgress, IUncachedSearchStats } from 'vs/platform/search/common/search';
E
Erich Gamma 已提交
24

25 26
import * as extfs from 'vs/base/node/extfs';
import * as flow from 'vs/base/node/flow';
27
import { IRawFileMatch, ISerializedSearchComplete, IRawSearch, ISearchEngine, IFolderSearch } from './search';
28
import { spawnRipgrepCmd } from './ripgrepFileSearch';
29
import { rgErrorMsgForDisplay } from './ripgrepTextSearch';
E
Erich Gamma 已提交
30

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

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

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

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

71
	private folderExcludePatterns: Map<string, AbsoluteAndRelativeParsedExpression>;
72 73
	private globalExcludePattern: glob.ParsedExpression;

E
Erich Gamma 已提交
74 75 76 77
	private walkedPaths: { [path: string]: boolean; };

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

92 93
		if (this.filePattern) {
			this.normalizedFilePatternLowercase = strings.stripWildcards(this.filePattern).toLowerCase();
B
Benjamin Pasero 已提交
94
		}
95 96

		this.globalExcludePattern = config.excludePattern && glob.parse(config.excludePattern);
97
		this.folderExcludePatterns = new Map<string, AbsoluteAndRelativeParsedExpression>();
98 99

		config.folderQueries.forEach(folderQuery => {
100
			const folderExcludeExpression: glob.IExpression = objects.assign({}, folderQuery.excludePattern || {}, this.config.excludePattern || {});
101 102 103 104 105

			// Add excludes for other root folders
			config.folderQueries
				.map(rootFolderQuery => rootFolderQuery.folder)
				.filter(rootFolder => rootFolder !== folderQuery.folder)
106 107 108 109 110
				.forEach(otherRootFolder => {
					// Exclude nested root folders
					if (isEqualOrParent(otherRootFolder, folderQuery.folder)) {
						folderExcludeExpression[path.relative(folderQuery.folder, otherRootFolder)] = true;
					}
111 112
				});

113
			this.folderExcludePatterns.set(folderQuery.folder, new AbsoluteAndRelativeParsedExpression(folderExcludeExpression, folderQuery.folder));
114
		});
E
Erich Gamma 已提交
115 116 117 118 119 120
	}

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

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

124
		// Support that the file pattern is a full path to a file that exists
125
		this.checkFilePatternAbsoluteMatch((exists, size) => {
126 127 128
			if (this.isCanceled) {
				return done(null, this.isLimitHit);
			}
E
Erich Gamma 已提交
129

130 131
			// Report result from file pattern if matching
			if (exists) {
132 133
				this.resultCount++;
				onResult({
134
					relativePath: this.filePattern,
135
					basename: path.basename(this.filePattern),
136 137
					size
				});
138 139 140 141

				// 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
142
				return done(null, this.isLimitHit);
143
			}
E
Erich Gamma 已提交
144

145 146 147
			// For each extra file
			if (extraFiles) {
				extraFiles.forEach(extraFilePath => {
148
					const basename = path.basename(extraFilePath);
R
Rob Lourens 已提交
149
					if (this.globalExcludePattern && this.globalExcludePattern(extraFilePath, basename)) {
150
						return; // excluded
E
Erich Gamma 已提交
151 152
					}

153
					// File: Check for match on file pattern and include pattern
154
					this.matchFile(onResult, { relativePath: extraFilePath /* no workspace relative path */, basename });
155 156
				});
			}
157

C
Christof Marti 已提交
158
			let traverse = this.nodeJSTraversal;
C
Christof Marti 已提交
159
			if (!this.maxFilesize) {
C
Christof Marti 已提交
160
				if (this.useRipgrep) {
161 162 163
					this.traversal = Traversal.Ripgrep;
					traverse = this.cmdTraversal;
				} else if (platform.isMacintosh) {
C
Christof Marti 已提交
164
					this.traversal = Traversal.MacFind;
165
					traverse = this.cmdTraversal;
J
Johannes Rieken 已提交
166
					// Disable 'dir' for now (#11181, #11179, #11183, #11182).
167
				} /* else if (platform.isWindows) {
C
Christof Marti 已提交
168 169
					this.traversal = Traversal.WindowsDir;
					traverse = this.windowsDirTraversal;
D
Dirk Baeumer 已提交
170
				} */ else if (platform.isLinux) {
C
Christof Marti 已提交
171
					this.traversal = Traversal.LinuxFind;
172
					traverse = this.cmdTraversal;
C
Christof Marti 已提交
173 174 175 176 177 178
				}
			}

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

181
			// For each root folder
182
			flow.parallel<IFolderSearch, void>(folderQueries, (folderQuery: IFolderSearch, rootFolderDone: (err: Error, result: void) => void) => {
183
				this.call(traverse, this, folderQuery, onResult, onMessage, (err?: Error) => {
C
Christof Marti 已提交
184
					if (err) {
C
Christof Marti 已提交
185 186 187 188
						const errorMessage = toErrorMessage(err);
						console.error(errorMessage);
						this.errors.push(errorMessage);
						rootFolderDone(err, undefined);
C
Christof Marti 已提交
189
					} else {
190
						rootFolderDone(undefined, undefined);
191
					}
C
Christof Marti 已提交
192
				});
R
Rob Lourens 已提交
193 194 195
			}, (errors, result) => {
				const err = errors ? errors.filter(e => !!e)[0] : null;
				done(err, this.isLimitHit);
C
Christof Marti 已提交
196 197 198
			});
		});
	}
199

C
Christof Marti 已提交
200 201 202 203 204 205 206 207
	private call(fun: Function, that: any, ...args: any[]): void {
		try {
			fun.apply(that, args);
		} catch (e) {
			args[args.length - 1](e);
		}
	}

208
	private cmdTraversal(folderQuery: IFolderSearch, onResult: (result: IRawFileMatch) => void, onMessage: (message: IProgress) => void, cb: (err?: Error) => void): void {
209
		const rootFolder = folderQuery.folder;
210
		const isMac = platform.isMacintosh;
211 212 213
		let cmd: childProcess.ChildProcess;
		const killCmd = () => cmd && cmd.kill();

214
		let done = (err?: Error) => {
215
			process.removeListener('exit', killCmd);
J
Johannes Rieken 已提交
216
			done = () => { };
217 218 219 220 221
			cb(err);
		};
		let leftover = '';
		let first = true;
		const tree = this.initDirectoryTree();
222

C
Christof Marti 已提交
223
		const useRipgrep = this.useRipgrep;
224
		let noSiblingsClauses: boolean;
C
Christof Marti 已提交
225
		let filePatternSeen = false;
226
		if (useRipgrep) {
227
			const ripgrep = spawnRipgrepCmd(this.config, folderQuery, this.config.includePattern, this.folderExcludePatterns.get(folderQuery.folder).expression);
228 229
			cmd = ripgrep.cmd;
			noSiblingsClauses = !Object.keys(ripgrep.siblingClauses).length;
230 231 232 233 234 235 236 237 238 239 240 241

			process.nextTick(() => {
				const escapedArgs = ripgrep.rgArgs.args
					.map(arg => arg.match(/^-/) ? arg : `'${arg}'`)
					.join(' ');

				let rgCmd = `rg ${escapedArgs}\n - cwd: ${ripgrep.cwd}`;
				if (ripgrep.rgArgs.siblingClauses) {
					rgCmd += `\n - Sibling clauses: ${JSON.stringify(ripgrep.rgArgs.siblingClauses)}`;
				}
				onMessage({ message: rgCmd });
			});
242 243 244 245
		} else {
			cmd = this.spawnFindCmd(folderQuery);
		}

246
		process.on('exit', killCmd);
247
		this.collectStdout(cmd, 'utf8', useRipgrep, (err: Error, stdout?: string, last?: boolean) => {
C
Christof Marti 已提交
248 249 250 251
			if (err) {
				done(err);
				return;
			}
252
			if (this.isLimitHit) {
253
				done();
254 255
				return;
			}
E
Erich Gamma 已提交
256

C
Christof Marti 已提交
257
			// Mac: uses NFD unicode form on disk, but we want NFC
258
			const normalized = leftover + (isMac ? strings.normalizeNFC(stdout) : stdout);
259
			const relativeFiles = normalized.split(useRipgrep ? '\n' : '\n./');
C
Christof Marti 已提交
260
			if (!useRipgrep && first && normalized.length >= 2) {
261 262
				first = false;
				relativeFiles[0] = relativeFiles[0].trim().substr(2);
C
Christof Marti 已提交
263 264
			}

265 266 267 268 269 270 271 272
			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 已提交
273 274
			}

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

280 281 282 283
			this.cmdResultCount += relativeFiles.length;

			if (useRipgrep && noSiblingsClauses) {
				for (const relativePath of relativeFiles) {
C
Christof Marti 已提交
284 285 286
					if (relativePath === this.filePattern) {
						filePatternSeen = true;
					}
287 288
					const basename = path.basename(relativePath);
					this.matchFile(onResult, { base: rootFolder, relativePath, basename });
289 290 291 292
					if (this.isLimitHit) {
						killCmd();
						break;
					}
293
				}
294
				if (last || this.isLimitHit) {
C
Christof Marti 已提交
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
					if (!filePatternSeen) {
						this.checkFilePatternRelativeMatch(folderQuery.folder, (match, size) => {
							if (match) {
								this.resultCount++;
								onResult({
									base: folderQuery.folder,
									relativePath: this.filePattern,
									basename: path.basename(this.filePattern),
								});
							}
							done();
						});
					} else {
						done();
					}
310 311 312 313
				}
				return;
			}

314
			// TODO: Optimize siblings clauses with ripgrep here.
315
			this.addDirectoryEntries(tree, rootFolder, relativeFiles, onResult);
C
Christof Marti 已提交
316

317 318 319
			if (last) {
				this.matchDirectoryTree(tree, rootFolder, onResult);
				done();
320
			}
C
Christof Marti 已提交
321 322 323
		});
	}

324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
	// 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();
	// 	});
	// }

351 352 353
	/**
	 * Public for testing.
	 */
354
	public spawnFindCmd(folderQuery: IFolderSearch) {
355
		const excludePattern = this.folderExcludePatterns.get(folderQuery.folder);
356 357
		const basenames = excludePattern.getBasenameTerms();
		const pathTerms = excludePattern.getPathTerms();
358
		let args = ['-L', '.'];
359
		if (basenames.length || pathTerms.length) {
360
			args.push('-not', '(', '(');
361
			for (const basename of basenames) {
362
				args.push('-name', basename);
363 364
				args.push('-o');
			}
365
			for (const path of pathTerms) {
366
				args.push('-path', path);
367
				args.push('-o');
368
			}
369
			args.pop();
370 371 372
			args.push(')', '-prune', ')');
		}
		args.push('-type', 'f');
373
		return childProcess.spawn('find', args, { cwd: folderQuery.folder });
374 375 376 377 378
	}

	/**
	 * Public for testing.
	 */
379
	public readStdout(cmd: childProcess.ChildProcess, encoding: string, isRipgrep: boolean, cb: (err: Error, stdout?: string) => void): void {
380
		let all = '';
381
		this.collectStdout(cmd, encoding, isRipgrep, (err: Error, stdout?: string, last?: boolean) => {
382 383 384 385 386 387 388 389 390 391 392 393
			if (err) {
				cb(err);
				return;
			}

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

394
	private collectStdout(cmd: childProcess.ChildProcess, encoding: string, isRipgrep: boolean, cb: (err: Error, stdout?: string, last?: boolean) => void): void {
395 396
		let done = (err: Error, stdout?: string, last?: boolean) => {
			if (err || last) {
J
Johannes Rieken 已提交
397
				done = () => { };
398 399 400
				this.cmdForkResultTime = Date.now();
			}
			cb(err, stdout, last);
C
Christof Marti 已提交
401 402
		};

403
		this.forwardData(cmd.stdout, encoding, done);
C
Christof Marti 已提交
404 405
		const stderr = this.collectData(cmd.stderr);

406 407 408
		let gotData = false;
		cmd.stdout.once('data', () => gotData = true);

409
		cmd.on('error', (err: Error) => {
C
Christof Marti 已提交
410 411 412
			done(err);
		});

413
		cmd.on('close', (code: number) => {
414
			// ripgrep returns code=1 when no results are found
415 416
			let stderrText, displayMsg: string;
			if (isRipgrep ? (!gotData && (stderrText = this.decodeData(stderr, encoding)) && (displayMsg = rgErrorMsgForDisplay(stderrText))) : code !== 0) {
C
Christof Marti 已提交
417
				done(new Error(`command failed with error code ${code}: ${this.decodeData(stderr, encoding)}`));
C
Christof Marti 已提交
418
			} else {
419 420 421
				if (isRipgrep && this.exists && code === 0) {
					this.isLimitHit = true;
				}
422
				done(null, '', true);
C
Christof Marti 已提交
423 424 425 426
			}
		});
	}

427 428 429 430 431 432 433 434
	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 已提交
435
	private collectData(stream: Readable): Buffer[] {
436 437
		const buffers: Buffer[] = [];
		stream.on('data', (data: Buffer) => {
C
Christof Marti 已提交
438 439 440 441 442
			buffers.push(data);
		});
		return buffers;
	}

C
Christof Marti 已提交
443 444 445 446 447
	private decodeData(buffers: Buffer[], encoding: string): string {
		const decoder = new StringDecoder(encoding);
		return buffers.map(buffer => decoder.write(buffer)).join('');
	}

448 449 450 451 452 453 454 455 456
	private initDirectoryTree(): IDirectoryTree {
		const tree: IDirectoryTree = {
			rootEntries: [],
			pathToEntries: Object.create(null)
		};
		tree.pathToEntries['.'] = tree.rootEntries;
		return tree;
	}

457
	private addDirectoryEntries({ pathToEntries }: IDirectoryTree, base: string, relativeFiles: string[], onResult: (result: IRawFileMatch) => void) {
C
Christof Marti 已提交
458 459
		// Support relative paths to files from a root resource (ignores excludes)
		if (relativeFiles.indexOf(this.filePattern) !== -1) {
460
			const basename = path.basename(this.filePattern);
461
			this.matchFile(onResult, { base: base, relativePath: this.filePattern, basename });
C
Christof Marti 已提交
462 463
		}

464
		function add(relativePath: string) {
465 466
			const basename = path.basename(relativePath);
			const dirname = path.dirname(relativePath);
C
Christof Marti 已提交
467 468 469 470 471 472
			let entries = pathToEntries[dirname];
			if (!entries) {
				entries = pathToEntries[dirname] = [];
				add(dirname);
			}
			entries.push({
473
				base,
C
Christof Marti 已提交
474 475 476
				relativePath,
				basename
			});
477 478
		}
		relativeFiles.forEach(add);
C
Christof Marti 已提交
479 480
	}

481
	private matchDirectoryTree({ rootEntries, pathToEntries }: IDirectoryTree, rootFolder: string, onResult: (result: IRawFileMatch) => void) {
C
Christof Marti 已提交
482
		const self = this;
483
		const excludePattern = this.folderExcludePatterns.get(rootFolder);
C
Christof Marti 已提交
484 485 486 487
		const filePattern = this.filePattern;
		function matchDirectory(entries: IDirectoryEntry[]) {
			self.directoriesWalked++;
			for (let i = 0, n = entries.length; i < n; i++) {
488
				const entry = entries[i];
489
				const { relativePath, basename } = entry;
C
Christof Marti 已提交
490 491 492 493 494

				// 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
495
				if (excludePattern.test(relativePath, basename, () => filePattern !== basename ? entries.map(entry => entry.basename) : [])) {
C
Christof Marti 已提交
496 497
					continue;
				}
E
Erich Gamma 已提交
498

C
Christof Marti 已提交
499 500 501 502 503 504 505 506 507
				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
					}

508
					self.matchFile(onResult, entry);
C
Christof Marti 已提交
509
				}
510 511 512 513

				if (self.isLimitHit) {
					break;
				}
514
			}
C
Christof Marti 已提交
515 516 517 518
		}
		matchDirectory(rootEntries);
	}

519
	private nodeJSTraversal(folderQuery: IFolderSearch, onResult: (result: IRawFileMatch) => void, onMessage: (message: IProgress) => void, done: (err?: Error) => void): void {
C
Christof Marti 已提交
520
		this.directoriesWalked++;
521
		extfs.readdir(folderQuery.folder, (error: Error, files: string[]) => {
C
Christof Marti 已提交
522 523 524 525 526
			if (error || this.isCanceled || this.isLimitHit) {
				return done();
			}

			// Support relative paths to files from a root resource (ignores excludes)
527
			return this.checkFilePatternRelativeMatch(folderQuery.folder, (match, size) => {
C
Christof Marti 已提交
528 529 530 531 532 533 534 535
				if (this.isCanceled || this.isLimitHit) {
					return done();
				}

				// Report result from file pattern if matching
				if (match) {
					this.resultCount++;
					onResult({
536
						base: folderQuery.folder,
537
						relativePath: this.filePattern,
538
						basename: path.basename(this.filePattern),
C
Christof Marti 已提交
539
						size
540
					});
C
Christof Marti 已提交
541 542
				}

543
				return this.doWalk(folderQuery, '', files, onResult, done);
E
Erich Gamma 已提交
544
			});
545 546 547
		});
	}

548
	public getStats(): IUncachedSearchStats {
C
chrmarti 已提交
549
		return {
550
			fromCache: false,
C
Christof Marti 已提交
551 552
			traversal: Traversal[this.traversal],
			errors: this.errors,
C
chrmarti 已提交
553 554 555
			fileWalkStartTime: this.fileWalkStartTime,
			fileWalkResultTime: Date.now(),
			directoriesWalked: this.directoriesWalked,
556
			filesWalked: this.filesWalked,
C
Christof Marti 已提交
557 558 559 560
			resultCount: this.resultCount,
			cmdForkStartTime: this.cmdForkStartTime,
			cmdForkResultTime: this.cmdForkResultTime,
			cmdResultCount: this.cmdResultCount
C
chrmarti 已提交
561 562 563
		};
	}

564
	private checkFilePatternAbsoluteMatch(clb: (exists: boolean, size?: number) => void): void {
565
		if (!this.filePattern || !path.isAbsolute(this.filePattern)) {
566 567 568
			return clb(false);
		}

569
		return fs.stat(this.filePattern, (error, stat) => {
570
			return clb(!error && !stat.isDirectory(), stat && stat.size); // only existing files
E
Erich Gamma 已提交
571 572 573
		});
	}

574
	private checkFilePatternRelativeMatch(basePath: string, clb: (matchPath: string, size?: number) => void): void {
575
		if (!this.filePattern || path.isAbsolute(this.filePattern)) {
576 577 578
			return clb(null);
		}

579
		const absolutePath = path.join(basePath, this.filePattern);
580

581
		return fs.stat(absolutePath, (error, stat) => {
582
			return clb(!error && !stat.isDirectory() ? absolutePath : null, stat && stat.size); // only existing files
583 584 585
		});
	}

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

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

			// Check canceled
			if (this.isCanceled || this.isLimitHit) {
594
				return clb(null, undefined);
E
Erich Gamma 已提交
595 596 597 598 599 600
			}

			// 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;
601
			if (this.config.filePattern === file) {
E
Erich Gamma 已提交
602 603 604 605
				siblings = [];
			}

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

611
			// Use lstat to detect links
612
			let currentAbsolutePath = [rootFolder, currentRelativePath].join(path.sep);
613
			fs.lstat(currentAbsolutePath, (error, lstat) => {
614
				if (error || this.isCanceled || this.isLimitHit) {
615
					return clb(null, undefined);
616
				}
E
Erich Gamma 已提交
617

618
				// If the path is a link, we must instead use fs.stat() to find out if the
619 620 621 622
				// 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) {
623
						return clb(null, undefined);
624
					}
E
Erich Gamma 已提交
625

626 627
					// Directory: Follow directories
					if (stat.isDirectory()) {
C
chrmarti 已提交
628
						this.directoriesWalked++;
629

630 631
						// to really prevent loops with links we need to resolve the real path of them
						return this.realPathIfNeeded(currentAbsolutePath, lstat, (error, realpath) => {
632
							if (error || this.isCanceled || this.isLimitHit) {
633
								return clb(null, undefined);
634 635
							}

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

640 641 642 643 644
							this.walkedPaths[realpath] = true; // remember as walked

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

648
								this.doWalk(folderQuery, currentRelativePath, children, onResult, err => clb(err, undefined));
649 650
							});
						});
651
					}
E
Erich Gamma 已提交
652

653 654
					// File: Check for match on file pattern and include pattern
					else {
C
chrmarti 已提交
655
						this.filesWalked++;
C
Christof Marti 已提交
656
						if (currentRelativePath === this.filePattern) {
657
							return clb(null, undefined); // ignore file if its path matches with the file pattern because checkFilePatternRelativeMatch() takes care of those
658
						}
E
Erich Gamma 已提交
659

660
						if (this.maxFilesize && types.isNumber(stat.size) && stat.size > this.maxFilesize) {
661
							return clb(null, undefined); // ignore file if max file size is hit
662 663
						}

664
						this.matchFile(onResult, { base: rootFolder, relativePath: currentRelativePath, basename: file, size: stat.size });
665 666 667
					}

					// Unwind
668
					return clb(null, undefined);
669
				});
E
Erich Gamma 已提交
670 671 672 673 674 675
			});
		}, (error: Error[]): void => {
			if (error) {
				error = arrays.coalesce(error); // find any error by removing null values first
			}

C
Christof Marti 已提交
676
			return done(error && error.length > 0 ? error[0] : null);
E
Erich Gamma 已提交
677 678 679
		});
	}

680 681
	private matchFile(onResult: (result: IRawFileMatch) => void, candidate: IRawFileMatch): void {
		if (this.isFilePatternMatch(candidate.relativePath) && (!this.includePattern || this.includePattern(candidate.relativePath, candidate.basename))) {
682 683
			this.resultCount++;

684
			if (this.exists || (this.maxResults && this.resultCount > this.maxResults)) {
685 686 687 688
				this.isLimitHit = true;
			}

			if (!this.isLimitHit) {
689
				onResult(candidate);
690 691 692 693
			}
		}
	}

B
polish  
Benjamin Pasero 已提交
694
	private isFilePatternMatch(path: string): boolean {
695 696 697

		// Check for search pattern
		if (this.filePattern) {
698 699 700 701
			if (this.filePattern === '*') {
				return true; // support the all-matching wildcard
			}

702
			return strings.fuzzyContains(path, this.normalizedFilePatternLowercase);
703 704 705 706 707 708
		}

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

709
	private statLinkIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error, stat: fs.Stats) => void): void {
710
		if (lstat.isSymbolicLink()) {
711
			return fs.stat(path, clb); // stat the target the link points to
712 713 714 715 716
		}

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

717
	private realPathIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error, realpath?: string) => void): void {
718 719 720 721 722
		if (lstat.isSymbolicLink()) {
			return fs.realpath(path, (error, realpath) => {
				if (error) {
					return clb(error);
				}
E
Erich Gamma 已提交
723

724 725 726
				return clb(null, realpath);
			});
		}
E
Erich Gamma 已提交
727

728
		return clb(null, path);
E
Erich Gamma 已提交
729 730 731
	}
}

732
export class Engine implements ISearchEngine<IRawFileMatch> {
733
	private folderQueries: IFolderSearch[];
734
	private extraFiles: string[];
E
Erich Gamma 已提交
735 736 737
	private walker: FileWalker;

	constructor(config: IRawSearch) {
738
		this.folderQueries = config.folderQueries;
739 740
		this.extraFiles = config.extraFiles;

E
Erich Gamma 已提交
741 742 743
		this.walker = new FileWalker(config);
	}

744
	public search(onResult: (result: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void {
745
		this.walker.walk(this.folderQueries, this.extraFiles, onResult, onProgress, (err: Error, isLimitHit: boolean) => {
C
chrmarti 已提交
746 747 748 749 750
			done(err, {
				limitHit: isLimitHit,
				stats: this.walker.getStats()
			});
		});
E
Erich Gamma 已提交
751 752 753 754 755
	}

	public cancel(): void {
		this.walker.cancel();
	}
756 757 758 759 760 761 762 763 764 765 766
}

/**
 * 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;

C
Christof Marti 已提交
767 768
	constructor(public expression: glob.IExpression, private root: string) {
		this.init(expression);
769 770 771 772 773 774 775 776
	}

	/**
	 * 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;
777 778 779 780 781
		Object.keys(expr)
			.filter(key => expr[key])
			.forEach(key => {
				if (path.isAbsolute(key)) {
					absoluteGlobExpr = absoluteGlobExpr || glob.getEmptyExpression();
782
					absoluteGlobExpr[key] = expr[key];
783 784
				} else {
					relativeGlobExpr = relativeGlobExpr || glob.getEmptyExpression();
785
					relativeGlobExpr[key] = expr[key];
786 787
				}
			});
788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822

		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;
	}
823
}