fileSearch.ts 25.4 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 objects = require('vs/base/common/objects');
E
Erich Gamma 已提交
18
import arrays = require('vs/base/common/arrays');
C
Christof Marti 已提交
19
import platform = require('vs/base/common/platform');
E
Erich Gamma 已提交
20
import strings = require('vs/base/common/strings');
21
import types = require('vs/base/common/types');
E
Erich Gamma 已提交
22
import glob = require('vs/base/common/glob');
J
Johannes Rieken 已提交
23
import { IProgress, IUncachedSearchStats } from 'vs/platform/search/common/search';
E
Erich Gamma 已提交
24 25 26

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

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

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

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

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

69
	private folderExcludePatterns: Map<string, AbsoluteAndRelativeParsedExpression>;
70 71
	private globalExcludePattern: glob.ParsedExpression;

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

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

89 90
		if (this.filePattern) {
			this.normalizedFilePatternLowercase = strings.stripWildcards(this.filePattern).toLowerCase();
B
Benjamin Pasero 已提交
91
		}
92 93

		this.globalExcludePattern = config.excludePattern && glob.parse(config.excludePattern);
94
		this.folderExcludePatterns = new Map<string, AbsoluteAndRelativeParsedExpression>();
95 96

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

235
		process.on('exit', killCmd);
236
		this.collectStdout(cmd, 'utf8', useRipgrep, (err: Error, stdout?: string, last?: boolean) => {
C
Christof Marti 已提交
237 238 239 240
			if (err) {
				done(err);
				return;
			}
241 242 243
			if (this.isLimitHit) {
				return;
			}
E
Erich Gamma 已提交
244

C
Christof Marti 已提交
245
			// Mac: uses NFD unicode form on disk, but we want NFC
246
			const normalized = leftover + (isMac ? strings.normalizeNFC(stdout) : stdout);
247
			const relativeFiles = normalized.split(useRipgrep ? '\n' : '\n./');
C
Christof Marti 已提交
248
			if (!useRipgrep && first && normalized.length >= 2) {
249 250
				first = false;
				relativeFiles[0] = relativeFiles[0].trim().substr(2);
C
Christof Marti 已提交
251 252
			}

253 254 255 256 257 258 259 260
			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 已提交
261 262
			}

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

268 269 270 271
			this.cmdResultCount += relativeFiles.length;

			if (useRipgrep && noSiblingsClauses) {
				for (const relativePath of relativeFiles) {
C
Christof Marti 已提交
272 273 274
					if (relativePath === this.filePattern) {
						filePatternSeen = true;
					}
275 276
					const basename = path.basename(relativePath);
					this.matchFile(onResult, { base: rootFolder, relativePath, basename });
277 278 279 280
					if (this.isLimitHit) {
						killCmd();
						break;
					}
281
				}
282
				if (last || this.isLimitHit) {
C
Christof Marti 已提交
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
					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();
					}
298 299 300 301
				}
				return;
			}

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

305 306 307
			if (last) {
				this.matchDirectoryTree(tree, rootFolder, onResult);
				done();
308
			}
C
Christof Marti 已提交
309 310 311
		});
	}

312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
	// 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();
	// 	});
	// }

339 340 341
	/**
	 * Public for testing.
	 */
342
	public spawnFindCmd(folderQuery: IFolderSearch) {
343
		const excludePattern = this.folderExcludePatterns.get(folderQuery.folder);
344 345
		const basenames = excludePattern.getBasenameTerms();
		const pathTerms = excludePattern.getPathTerms();
346
		let args = ['-L', '.'];
347
		if (basenames.length || pathTerms.length) {
348
			args.push('-not', '(', '(');
349
			for (const basename of basenames) {
350
				args.push('-name', basename);
351 352
				args.push('-o');
			}
353
			for (const path of pathTerms) {
354
				args.push('-path', path);
355
				args.push('-o');
356
			}
357
			args.pop();
358 359 360
			args.push(')', '-prune', ')');
		}
		args.push('-type', 'f');
361
		return childProcess.spawn('find', args, { cwd: folderQuery.folder });
362 363 364 365 366
	}

	/**
	 * Public for testing.
	 */
367
	public readStdout(cmd: childProcess.ChildProcess, encoding: string, isRipgrep: boolean, cb: (err: Error, stdout?: string) => void): void {
368
		let all = '';
369
		this.collectStdout(cmd, encoding, isRipgrep, (err: Error, stdout?: string, last?: boolean) => {
370 371 372 373 374 375 376 377 378 379 380 381
			if (err) {
				cb(err);
				return;
			}

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

382
	private collectStdout(cmd: childProcess.ChildProcess, encoding: string, isRipgrep: boolean, cb: (err: Error, stdout?: string, last?: boolean) => void): void {
383 384
		let done = (err: Error, stdout?: string, last?: boolean) => {
			if (err || last) {
J
Johannes Rieken 已提交
385
				done = () => { };
386 387 388
				this.cmdForkResultTime = Date.now();
			}
			cb(err, stdout, last);
C
Christof Marti 已提交
389 390
		};

391
		this.forwardData(cmd.stdout, encoding, done);
C
Christof Marti 已提交
392 393
		const stderr = this.collectData(cmd.stderr);

394
		cmd.on('error', (err: Error) => {
C
Christof Marti 已提交
395 396 397
			done(err);
		});

398
		cmd.on('close', (code: number) => {
399 400
			// ripgrep returns code=1 when no results are found
			if (code !== 0 && ((isRipgrep && stderr.length) || !isRipgrep)) {
C
Christof Marti 已提交
401
				done(new Error(`command failed with error code ${code}: ${this.decodeData(stderr, encoding)}`));
C
Christof Marti 已提交
402
			} else {
403
				done(null, '', true);
C
Christof Marti 已提交
404 405 406 407
			}
		});
	}

408 409 410 411 412 413 414 415
	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 已提交
416
	private collectData(stream: Readable): Buffer[] {
417 418
		const buffers: Buffer[] = [];
		stream.on('data', (data: Buffer) => {
C
Christof Marti 已提交
419 420 421 422 423
			buffers.push(data);
		});
		return buffers;
	}

C
Christof Marti 已提交
424 425 426 427 428
	private decodeData(buffers: Buffer[], encoding: string): string {
		const decoder = new StringDecoder(encoding);
		return buffers.map(buffer => decoder.write(buffer)).join('');
	}

429 430 431 432 433 434 435 436 437
	private initDirectoryTree(): IDirectoryTree {
		const tree: IDirectoryTree = {
			rootEntries: [],
			pathToEntries: Object.create(null)
		};
		tree.pathToEntries['.'] = tree.rootEntries;
		return tree;
	}

438
	private addDirectoryEntries({ pathToEntries }: IDirectoryTree, base: string, relativeFiles: string[], onResult: (result: IRawFileMatch) => void) {
C
Christof Marti 已提交
439 440
		// Support relative paths to files from a root resource (ignores excludes)
		if (relativeFiles.indexOf(this.filePattern) !== -1) {
441
			const basename = path.basename(this.filePattern);
442
			this.matchFile(onResult, { base: base, relativePath: this.filePattern, basename });
C
Christof Marti 已提交
443 444
		}

445
		function add(relativePath: string) {
446 447
			const basename = path.basename(relativePath);
			const dirname = path.dirname(relativePath);
C
Christof Marti 已提交
448 449 450 451 452 453
			let entries = pathToEntries[dirname];
			if (!entries) {
				entries = pathToEntries[dirname] = [];
				add(dirname);
			}
			entries.push({
454
				base,
C
Christof Marti 已提交
455 456 457
				relativePath,
				basename
			});
458 459
		}
		relativeFiles.forEach(add);
C
Christof Marti 已提交
460 461
	}

462
	private matchDirectoryTree({ rootEntries, pathToEntries }: IDirectoryTree, rootFolder: string, onResult: (result: IRawFileMatch) => void) {
C
Christof Marti 已提交
463
		const self = this;
464
		const excludePattern = this.folderExcludePatterns.get(rootFolder);
C
Christof Marti 已提交
465 466 467 468
		const filePattern = this.filePattern;
		function matchDirectory(entries: IDirectoryEntry[]) {
			self.directoriesWalked++;
			for (let i = 0, n = entries.length; i < n; i++) {
469
				const entry = entries[i];
470
				const { relativePath, basename } = entry;
C
Christof Marti 已提交
471 472 473 474 475

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

C
Christof Marti 已提交
480 481 482 483 484 485 486 487 488
				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
					}

489
					self.matchFile(onResult, entry);
C
Christof Marti 已提交
490
				}
491 492 493 494

				if (self.isLimitHit) {
					break;
				}
C
Christof Marti 已提交
495 496 497 498 499
			};
		}
		matchDirectory(rootEntries);
	}

500
	private nodeJSTraversal(folderQuery: IFolderSearch, onResult: (result: IRawFileMatch) => void, done: (err?: Error) => void): void {
C
Christof Marti 已提交
501
		this.directoriesWalked++;
502
		extfs.readdir(folderQuery.folder, (error: Error, files: string[]) => {
C
Christof Marti 已提交
503 504 505 506 507
			if (error || this.isCanceled || this.isLimitHit) {
				return done();
			}

			// Support relative paths to files from a root resource (ignores excludes)
508
			return this.checkFilePatternRelativeMatch(folderQuery.folder, (match, size) => {
C
Christof Marti 已提交
509 510 511 512 513 514 515 516
				if (this.isCanceled || this.isLimitHit) {
					return done();
				}

				// Report result from file pattern if matching
				if (match) {
					this.resultCount++;
					onResult({
517
						base: folderQuery.folder,
518
						relativePath: this.filePattern,
519
						basename: path.basename(this.filePattern),
C
Christof Marti 已提交
520
						size
521
					});
C
Christof Marti 已提交
522 523
				}

524
				return this.doWalk(folderQuery, '', files, onResult, done);
E
Erich Gamma 已提交
525
			});
526 527 528
		});
	}

529
	public getStats(): IUncachedSearchStats {
C
chrmarti 已提交
530
		return {
531
			fromCache: false,
C
Christof Marti 已提交
532 533
			traversal: Traversal[this.traversal],
			errors: this.errors,
C
chrmarti 已提交
534 535 536
			fileWalkStartTime: this.fileWalkStartTime,
			fileWalkResultTime: Date.now(),
			directoriesWalked: this.directoriesWalked,
537
			filesWalked: this.filesWalked,
C
Christof Marti 已提交
538 539 540 541
			resultCount: this.resultCount,
			cmdForkStartTime: this.cmdForkStartTime,
			cmdForkResultTime: this.cmdForkResultTime,
			cmdResultCount: this.cmdResultCount
C
chrmarti 已提交
542 543 544
		};
	}

545
	private checkFilePatternAbsoluteMatch(clb: (exists: boolean, size?: number) => void): void {
546
		if (!this.filePattern || !path.isAbsolute(this.filePattern)) {
547 548 549
			return clb(false);
		}

550
		return fs.stat(this.filePattern, (error, stat) => {
551
			return clb(!error && !stat.isDirectory(), stat && stat.size); // only existing files
E
Erich Gamma 已提交
552 553 554
		});
	}

555
	private checkFilePatternRelativeMatch(basePath: string, clb: (matchPath: string, size?: number) => void): void {
556
		if (!this.filePattern || path.isAbsolute(this.filePattern)) {
557 558 559
			return clb(null);
		}

560
		const absolutePath = path.join(basePath, this.filePattern);
561

562
		return fs.stat(absolutePath, (error, stat) => {
563
			return clb(!error && !stat.isDirectory() ? absolutePath : null, stat && stat.size); // only existing files
564 565 566
		});
	}

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

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

			// Check canceled
			if (this.isCanceled || this.isLimitHit) {
575
				return clb(null, undefined);
E
Erich Gamma 已提交
576 577 578 579 580 581
			}

			// 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;
582
			if (this.config.filePattern === file) {
E
Erich Gamma 已提交
583 584 585 586
				siblings = [];
			}

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

592
			// Use lstat to detect links
593
			let currentAbsolutePath = [rootFolder, currentRelativePath].join(path.sep);
594
			fs.lstat(currentAbsolutePath, (error, lstat) => {
595
				if (error || this.isCanceled || this.isLimitHit) {
596
					return clb(null, undefined);
597
				}
E
Erich Gamma 已提交
598

599
				// If the path is a link, we must instead use fs.stat() to find out if the
600 601 602 603
				// 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) {
604
						return clb(null, undefined);
605
					}
E
Erich Gamma 已提交
606

607 608
					// Directory: Follow directories
					if (stat.isDirectory()) {
C
chrmarti 已提交
609
						this.directoriesWalked++;
610

611 612
						// to really prevent loops with links we need to resolve the real path of them
						return this.realPathIfNeeded(currentAbsolutePath, lstat, (error, realpath) => {
613
							if (error || this.isCanceled || this.isLimitHit) {
614
								return clb(null, undefined);
615 616
							}

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

621 622 623 624 625
							this.walkedPaths[realpath] = true; // remember as walked

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

629
								this.doWalk(folderQuery, currentRelativePath, children, onResult, err => clb(err, undefined));
630 631
							});
						});
632
					}
E
Erich Gamma 已提交
633

634 635
					// File: Check for match on file pattern and include pattern
					else {
C
chrmarti 已提交
636
						this.filesWalked++;
C
Christof Marti 已提交
637
						if (currentRelativePath === this.filePattern) {
638
							return clb(null, undefined); // ignore file if its path matches with the file pattern because checkFilePatternRelativeMatch() takes care of those
639
						}
E
Erich Gamma 已提交
640

641
						if (this.maxFilesize && types.isNumber(stat.size) && stat.size > this.maxFilesize) {
642
							return clb(null, undefined); // ignore file if max file size is hit
643 644
						}

645
						this.matchFile(onResult, { base: rootFolder, relativePath: currentRelativePath, basename: file, size: stat.size });
646 647 648
					}

					// Unwind
649
					return clb(null, undefined);
650
				});
E
Erich Gamma 已提交
651 652 653 654 655 656
			});
		}, (error: Error[]): void => {
			if (error) {
				error = arrays.coalesce(error); // find any error by removing null values first
			}

C
Christof Marti 已提交
657
			return done(error && error.length > 0 ? error[0] : null);
E
Erich Gamma 已提交
658 659 660
		});
	}

661 662
	private matchFile(onResult: (result: IRawFileMatch) => void, candidate: IRawFileMatch): void {
		if (this.isFilePatternMatch(candidate.relativePath) && (!this.includePattern || this.includePattern(candidate.relativePath, candidate.basename))) {
663 664 665 666 667 668 669
			this.resultCount++;

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

			if (!this.isLimitHit) {
670
				onResult(candidate);
671 672 673 674
			}
		}
	}

B
polish  
Benjamin Pasero 已提交
675
	private isFilePatternMatch(path: string): boolean {
676 677 678

		// Check for search pattern
		if (this.filePattern) {
679 680 681 682
			if (this.filePattern === '*') {
				return true; // support the all-matching wildcard
			}

683
			return strings.fuzzyContains(path, this.normalizedFilePatternLowercase);
684 685 686 687 688 689
		}

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

690
	private statLinkIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error, stat: fs.Stats) => void): void {
691
		if (lstat.isSymbolicLink()) {
692
			return fs.stat(path, clb); // stat the target the link points to
693 694 695 696 697
		}

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

698
	private realPathIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error, realpath?: string) => void): void {
699 700 701 702 703
		if (lstat.isSymbolicLink()) {
			return fs.realpath(path, (error, realpath) => {
				if (error) {
					return clb(error);
				}
E
Erich Gamma 已提交
704

705 706 707
				return clb(null, realpath);
			});
		}
E
Erich Gamma 已提交
708

709
		return clb(null, path);
E
Erich Gamma 已提交
710 711 712
	}
}

713
export class Engine implements ISearchEngine<IRawFileMatch> {
714
	private folderQueries: IFolderSearch[];
715
	private extraFiles: string[];
E
Erich Gamma 已提交
716 717 718
	private walker: FileWalker;

	constructor(config: IRawSearch) {
719
		this.folderQueries = config.folderQueries;
720 721
		this.extraFiles = config.extraFiles;

E
Erich Gamma 已提交
722 723 724
		this.walker = new FileWalker(config);
	}

725
	public search(onResult: (result: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void {
726
		this.walker.walk(this.folderQueries, this.extraFiles, onResult, (err: Error, isLimitHit: boolean) => {
C
chrmarti 已提交
727 728 729 730 731
			done(err, {
				limitHit: isLimitHit,
				stats: this.walker.getStats()
			});
		});
E
Erich Gamma 已提交
732 733 734 735 736
	}

	public cancel(): void {
		this.walker.cancel();
	}
737 738 739 740 741 742 743 744 745 746 747
}

/**
 * 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 已提交
748 749
	constructor(public expression: glob.IExpression, private root: string) {
		this.init(expression);
750 751 752 753 754 755 756 757
	}

	/**
	 * 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;
758 759 760 761 762
		Object.keys(expr)
			.filter(key => expr[key])
			.forEach(key => {
				if (path.isAbsolute(key)) {
					absoluteGlobExpr = absoluteGlobExpr || glob.getEmptyExpression();
763
					absoluteGlobExpr[key] = expr[key];
764 765
				} else {
					relativeGlobExpr = relativeGlobExpr || glob.getEmptyExpression();
766
					relativeGlobExpr[key] = expr[key];
767 768
				}
			});
769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803

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