fileSearch.ts 25.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';
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 183 184 185
						const errorMessage = toErrorMessage(err);
						console.error(errorMessage);
						this.errors.push(errorMessage);
						rootFolderDone(err, undefined);
C
Christof Marti 已提交
186
					} else {
187
						rootFolderDone(undefined, undefined);
188
					}
C
Christof Marti 已提交
189 190 191 192 193 194
				});
			}, (err, result) => {
				done(err ? err[0] : null, this.isLimitHit);
			});
		});
	}
195

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

204
	private cmdTraversal(folderQuery: IFolderSearch, onResult: (result: IRawFileMatch) => void, cb: (err?: Error) => void): void {
205
		const rootFolder = folderQuery.folder;
206
		const isMac = platform.isMacintosh;
207 208 209
		let cmd: childProcess.ChildProcess;
		const killCmd = () => cmd && cmd.kill();

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

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

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

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

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

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

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

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

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

300 301 302
			if (last) {
				this.matchDirectoryTree(tree, rootFolder, onResult);
				done();
303
			}
C
Christof Marti 已提交
304 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
	// 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();
	// 	});
	// }

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

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

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

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

386
		this.forwardData(cmd.stdout, encoding, done);
C
Christof Marti 已提交
387 388
		const stderr = this.collectData(cmd.stderr);

389
		cmd.on('error', (err: Error) => {
C
Christof Marti 已提交
390 391 392
			done(err);
		});

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

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

C
Christof Marti 已提交
419 420 421 422 423
	private decodeData(buffers: Buffer[], encoding: string): string {
		const decoder = new StringDecoder(encoding);
		return buffers.map(buffer => decoder.write(buffer)).join('');
	}

424 425 426 427 428 429 430 431 432
	private initDirectoryTree(): IDirectoryTree {
		const tree: IDirectoryTree = {
			rootEntries: [],
			pathToEntries: Object.create(null)
		};
		tree.pathToEntries['.'] = tree.rootEntries;
		return tree;
	}

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

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

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

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

C
Christof Marti 已提交
475 476 477 478 479 480 481 482 483
				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
					}

484
					self.matchFile(onResult, entry);
C
Christof Marti 已提交
485
				}
486 487 488 489

				if (self.isLimitHit) {
					break;
				}
C
Christof Marti 已提交
490 491 492 493 494
			};
		}
		matchDirectory(rootEntries);
	}

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

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

				// Report result from file pattern if matching
				if (match) {
					this.resultCount++;
					onResult({
512
						base: folderQuery.folder,
513
						relativePath: this.filePattern,
514
						basename: path.basename(this.filePattern),
C
Christof Marti 已提交
515
						size
516
					});
C
Christof Marti 已提交
517 518
				}

519
				return this.doWalk(folderQuery, '', files, onResult, done);
E
Erich Gamma 已提交
520
			});
521 522 523
		});
	}

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

540
	private checkFilePatternAbsoluteMatch(clb: (exists: boolean, size?: number) => void): void {
541
		if (!this.filePattern || !path.isAbsolute(this.filePattern)) {
542 543 544
			return clb(false);
		}

545
		return fs.stat(this.filePattern, (error, stat) => {
546
			return clb(!error && !stat.isDirectory(), stat && stat.size); // only existing files
E
Erich Gamma 已提交
547 548 549
		});
	}

550
	private checkFilePatternRelativeMatch(basePath: string, clb: (matchPath: string, size?: number) => void): void {
551
		if (!this.filePattern || path.isAbsolute(this.filePattern)) {
552 553 554
			return clb(null);
		}

555
		const absolutePath = path.join(basePath, this.filePattern);
556

557
		return fs.stat(absolutePath, (error, stat) => {
558
			return clb(!error && !stat.isDirectory() ? absolutePath : null, stat && stat.size); // only existing files
559 560 561
		});
	}

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

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

			// Check canceled
			if (this.isCanceled || this.isLimitHit) {
570
				return clb(null, undefined);
E
Erich Gamma 已提交
571 572 573 574 575 576
			}

			// 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;
577
			if (this.config.filePattern === file) {
E
Erich Gamma 已提交
578 579 580 581
				siblings = [];
			}

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

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

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

602 603
					// Directory: Follow directories
					if (stat.isDirectory()) {
C
chrmarti 已提交
604
						this.directoriesWalked++;
605

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

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

616 617 618 619 620
							this.walkedPaths[realpath] = true; // remember as walked

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

624
								this.doWalk(folderQuery, currentRelativePath, children, onResult, err => clb(err, undefined));
625 626
							});
						});
627
					}
E
Erich Gamma 已提交
628

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

636
						if (this.maxFilesize && types.isNumber(stat.size) && stat.size > this.maxFilesize) {
637
							return clb(null, undefined); // ignore file if max file size is hit
638 639
						}

640
						this.matchFile(onResult, { base: rootFolder, relativePath: currentRelativePath, basename: file, size: stat.size });
641 642 643
					}

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

C
Christof Marti 已提交
652
			return done(error && error.length > 0 ? error[0] : null);
E
Erich Gamma 已提交
653 654 655
		});
	}

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

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

			if (!this.isLimitHit) {
665
				onResult(candidate);
666 667 668 669
			}
		}
	}

B
polish  
Benjamin Pasero 已提交
670
	private isFilePatternMatch(path: string): boolean {
671 672 673

		// Check for search pattern
		if (this.filePattern) {
674 675 676 677
			if (this.filePattern === '*') {
				return true; // support the all-matching wildcard
			}

678
			return strings.fuzzyContains(path, this.normalizedFilePatternLowercase);
679 680 681 682 683 684
		}

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

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

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

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

700 701 702
				return clb(null, realpath);
			});
		}
E
Erich Gamma 已提交
703

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

708
export class Engine implements ISearchEngine<IRawFileMatch> {
709
	private folderQueries: IFolderSearch[];
710
	private extraFiles: string[];
E
Erich Gamma 已提交
711 712 713
	private walker: FileWalker;

	constructor(config: IRawSearch) {
714
		this.folderQueries = config.folderQueries;
715 716
		this.extraFiles = config.extraFiles;

E
Erich Gamma 已提交
717 718 719
		this.walker = new FileWalker(config);
	}

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

	public cancel(): void {
		this.walker.cancel();
	}
732 733 734 735 736 737 738 739 740 741 742
}

/**
 * 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 已提交
743 744
	constructor(public expression: glob.IExpression, private root: string) {
		this.init(expression);
745 746 747 748 749 750 751 752
	}

	/**
	 * 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;
753 754 755 756 757
		Object.keys(expr)
			.filter(key => expr[key])
			.forEach(key => {
				if (path.isAbsolute(key)) {
					absoluteGlobExpr = absoluteGlobExpr || glob.getEmptyExpression();
758
					absoluteGlobExpr[key] = expr[key];
759 760
				} else {
					relativeGlobExpr = relativeGlobExpr || glob.getEmptyExpression();
761
					relativeGlobExpr[key] = expr[key];
762 763
				}
			});
764 765 766 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

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