fileSearch.ts 25.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';
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
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';
21
import * as normalization from 'vs/base/common/normalization';
22 23
import * as types from 'vs/base/common/types';
import * as glob from 'vs/base/common/glob';
J
Johannes Rieken 已提交
24
import { IProgress, IUncachedSearchStats } from 'vs/platform/search/common/search';
E
Erich Gamma 已提交
25

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

130 131 132 133 134 135 136
		// For each extra file
		if (extraFiles) {
			extraFiles.forEach(extraFilePath => {
				const basename = path.basename(extraFilePath);
				if (this.globalExcludePattern && this.globalExcludePattern(extraFilePath, basename)) {
					return; // excluded
				}
E
Erich Gamma 已提交
137

138 139 140 141
				// File: Check for match on file pattern and include pattern
				this.matchFile(onResult, { relativePath: extraFilePath /* no workspace relative path */, basename });
			});
		}
142

143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
		let traverse = this.nodeJSTraversal;
		if (!this.maxFilesize) {
			if (this.useRipgrep) {
				this.traversal = Traversal.Ripgrep;
				traverse = this.cmdTraversal;
			} else if (platform.isMacintosh) {
				this.traversal = Traversal.MacFind;
				traverse = this.cmdTraversal;
				// Disable 'dir' for now (#11181, #11179, #11183, #11182).
			} /* else if (platform.isWindows) {
				this.traversal = Traversal.WindowsDir;
				traverse = this.windowsDirTraversal;
			} */ else if (platform.isLinux) {
				this.traversal = Traversal.LinuxFind;
				traverse = this.cmdTraversal;
C
Christof Marti 已提交
158
			}
159
		}
C
Christof Marti 已提交
160

161 162 163 164
		const isNodeTraversal = traverse === this.nodeJSTraversal;
		if (!isNodeTraversal) {
			this.cmdForkStartTime = Date.now();
		}
C
Christof Marti 已提交
165

166 167 168 169 170 171 172 173 174 175 176
		// For each root folder
		flow.parallel<IFolderSearch, void>(folderQueries, (folderQuery: IFolderSearch, rootFolderDone: (err: Error, result: void) => void) => {
			this.call(traverse, this, folderQuery, onResult, onMessage, (err?: Error) => {
				if (err) {
					const errorMessage = toErrorMessage(err);
					console.error(errorMessage);
					this.errors.push(errorMessage);
					rootFolderDone(err, undefined);
				} else {
					rootFolderDone(undefined, undefined);
				}
C
Christof Marti 已提交
177
			});
178 179 180
		}, (errors, result) => {
			const err = errors ? errors.filter(e => !!e)[0] : null;
			done(err, this.isLimitHit);
C
Christof Marti 已提交
181 182
		});
	}
183

C
Christof Marti 已提交
184 185 186 187 188 189 190 191
	private call(fun: Function, that: any, ...args: any[]): void {
		try {
			fun.apply(that, args);
		} catch (e) {
			args[args.length - 1](e);
		}
	}

192
	private cmdTraversal(folderQuery: IFolderSearch, onResult: (result: IRawFileMatch) => void, onMessage: (message: IProgress) => void, cb: (err?: Error) => void): void {
193
		const rootFolder = folderQuery.folder;
194
		const isMac = platform.isMacintosh;
195 196 197
		let cmd: childProcess.ChildProcess;
		const killCmd = () => cmd && cmd.kill();

198
		let done = (err?: Error) => {
199
			process.removeListener('exit', killCmd);
J
Johannes Rieken 已提交
200
			done = () => { };
201 202 203 204 205
			cb(err);
		};
		let leftover = '';
		let first = true;
		const tree = this.initDirectoryTree();
206

C
Christof Marti 已提交
207
		const useRipgrep = this.useRipgrep;
208
		let noSiblingsClauses: boolean;
C
Christof Marti 已提交
209
		let filePatternSeen = false;
210
		if (useRipgrep) {
211
			const ripgrep = spawnRipgrepCmd(this.config, folderQuery, this.config.includePattern, this.folderExcludePatterns.get(folderQuery.folder).expression);
212 213
			cmd = ripgrep.cmd;
			noSiblingsClauses = !Object.keys(ripgrep.siblingClauses).length;
214 215 216 217 218 219 220 221 222 223 224 225

			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 });
			});
226 227 228 229
		} else {
			cmd = this.spawnFindCmd(folderQuery);
		}

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

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

249 250 251 252 253 254 255 256
			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 已提交
257 258
			}

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

264 265 266 267
			this.cmdResultCount += relativeFiles.length;

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

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

301 302 303
			if (last) {
				this.matchDirectoryTree(tree, rootFolder, onResult);
				done();
304
			}
C
Christof Marti 已提交
305 306 307
		});
	}

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

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

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

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

378 379
	private collectStdout(cmd: childProcess.ChildProcess, encoding: string, isRipgrep: boolean, onMessage: (message: IProgress) => void, cb: (err: Error, stdout?: string, last?: boolean) => void): void {
		let onData = (err: Error, stdout?: string, last?: boolean) => {
380
			if (err || last) {
381
				onData = () => { };
382 383 384
				this.cmdForkResultTime = Date.now();
			}
			cb(err, stdout, last);
C
Christof Marti 已提交
385 386
		};

387 388 389 390 391 392 393
		if (cmd.stdout) {
			// Should be non-null, but #38195
			this.forwardData(cmd.stdout, encoding, onData);
		} else {
			onMessage({ message: 'stdout is null' });
		}

C
Christof Marti 已提交
394 395
		const stderr = this.collectData(cmd.stderr);

396 397 398
		let gotData = false;
		cmd.stdout.once('data', () => gotData = true);

399
		cmd.on('error', (err: Error) => {
400
			onData(err);
C
Christof Marti 已提交
401 402
		});

403
		cmd.on('close', (code: number) => {
404
			// ripgrep returns code=1 when no results are found
405 406
			let stderrText, displayMsg: string;
			if (isRipgrep ? (!gotData && (stderrText = this.decodeData(stderr, encoding)) && (displayMsg = rgErrorMsgForDisplay(stderrText))) : code !== 0) {
407
				onData(new Error(`command failed with error code ${code}: ${this.decodeData(stderr, encoding)}`));
C
Christof Marti 已提交
408
			} else {
409 410 411
				if (isRipgrep && this.exists && code === 0) {
					this.isLimitHit = true;
				}
412
				onData(null, '', true);
C
Christof Marti 已提交
413 414 415 416
			}
		});
	}

417 418 419 420 421 422 423 424
	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 已提交
425
	private collectData(stream: Readable): Buffer[] {
426 427
		const buffers: Buffer[] = [];
		stream.on('data', (data: Buffer) => {
C
Christof Marti 已提交
428 429 430 431 432
			buffers.push(data);
		});
		return buffers;
	}

C
Christof Marti 已提交
433 434 435 436 437
	private decodeData(buffers: Buffer[], encoding: string): string {
		const decoder = new StringDecoder(encoding);
		return buffers.map(buffer => decoder.write(buffer)).join('');
	}

438 439 440 441 442 443 444 445 446
	private initDirectoryTree(): IDirectoryTree {
		const tree: IDirectoryTree = {
			rootEntries: [],
			pathToEntries: Object.create(null)
		};
		tree.pathToEntries['.'] = tree.rootEntries;
		return tree;
	}

447
	private addDirectoryEntries({ pathToEntries }: IDirectoryTree, base: string, relativeFiles: string[], onResult: (result: IRawFileMatch) => void) {
C
Christof Marti 已提交
448 449
		// Support relative paths to files from a root resource (ignores excludes)
		if (relativeFiles.indexOf(this.filePattern) !== -1) {
450
			const basename = path.basename(this.filePattern);
451
			this.matchFile(onResult, { base: base, relativePath: this.filePattern, basename });
C
Christof Marti 已提交
452 453
		}

454
		function add(relativePath: string) {
455 456
			const basename = path.basename(relativePath);
			const dirname = path.dirname(relativePath);
C
Christof Marti 已提交
457 458 459 460 461 462
			let entries = pathToEntries[dirname];
			if (!entries) {
				entries = pathToEntries[dirname] = [];
				add(dirname);
			}
			entries.push({
463
				base,
C
Christof Marti 已提交
464 465 466
				relativePath,
				basename
			});
467 468
		}
		relativeFiles.forEach(add);
C
Christof Marti 已提交
469 470
	}

471
	private matchDirectoryTree({ rootEntries, pathToEntries }: IDirectoryTree, rootFolder: string, onResult: (result: IRawFileMatch) => void) {
C
Christof Marti 已提交
472
		const self = this;
473
		const excludePattern = this.folderExcludePatterns.get(rootFolder);
C
Christof Marti 已提交
474 475 476 477
		const filePattern = this.filePattern;
		function matchDirectory(entries: IDirectoryEntry[]) {
			self.directoriesWalked++;
			for (let i = 0, n = entries.length; i < n; i++) {
478
				const entry = entries[i];
479
				const { relativePath, basename } = entry;
C
Christof Marti 已提交
480 481 482 483 484

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

C
Christof Marti 已提交
489 490 491 492 493 494 495 496 497
				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
					}

498
					self.matchFile(onResult, entry);
C
Christof Marti 已提交
499
				}
500 501 502 503

				if (self.isLimitHit) {
					break;
				}
504
			}
C
Christof Marti 已提交
505 506 507 508
		}
		matchDirectory(rootEntries);
	}

509
	private nodeJSTraversal(folderQuery: IFolderSearch, onResult: (result: IRawFileMatch) => void, onMessage: (message: IProgress) => void, done: (err?: Error) => void): void {
C
Christof Marti 已提交
510
		this.directoriesWalked++;
511
		extfs.readdir(folderQuery.folder, (error: Error, files: string[]) => {
C
Christof Marti 已提交
512 513 514 515 516
			if (error || this.isCanceled || this.isLimitHit) {
				return done();
			}

			// Support relative paths to files from a root resource (ignores excludes)
517
			return this.checkFilePatternRelativeMatch(folderQuery.folder, (match, size) => {
C
Christof Marti 已提交
518 519 520 521 522 523 524 525
				if (this.isCanceled || this.isLimitHit) {
					return done();
				}

				// Report result from file pattern if matching
				if (match) {
					this.resultCount++;
					onResult({
526
						base: folderQuery.folder,
527
						relativePath: this.filePattern,
528
						basename: path.basename(this.filePattern),
C
Christof Marti 已提交
529
						size
530
					});
C
Christof Marti 已提交
531 532
				}

533
				return this.doWalk(folderQuery, '', files, onResult, done);
E
Erich Gamma 已提交
534
			});
535 536 537
		});
	}

538
	public getStats(): IUncachedSearchStats {
C
chrmarti 已提交
539
		return {
540
			fromCache: false,
C
Christof Marti 已提交
541 542
			traversal: Traversal[this.traversal],
			errors: this.errors,
C
chrmarti 已提交
543 544 545
			fileWalkStartTime: this.fileWalkStartTime,
			fileWalkResultTime: Date.now(),
			directoriesWalked: this.directoriesWalked,
546
			filesWalked: this.filesWalked,
C
Christof Marti 已提交
547 548 549 550
			resultCount: this.resultCount,
			cmdForkStartTime: this.cmdForkStartTime,
			cmdForkResultTime: this.cmdForkResultTime,
			cmdResultCount: this.cmdResultCount
C
chrmarti 已提交
551 552 553
		};
	}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

664
			if (this.exists || (this.maxResults && this.resultCount > this.maxResults)) {
665 666 667 668
				this.isLimitHit = true;
			}

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

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

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

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

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

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

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

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

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

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

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

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

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

724
	public search(onResult: (result: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchSuccess) => void): void {
725
		this.walker.walk(this.folderQueries, this.extraFiles, onResult, onProgress, (err: Error, isLimitHit: boolean) => {
C
chrmarti 已提交
726
			done(err, {
727
				type: 'success',
C
chrmarti 已提交
728 729 730 731
				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
}