fileSearch.ts 20.1 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 12
import fs = require('fs');
import paths = require('path');
B
Benjamin Pasero 已提交
13
import { Readable } from 'stream';
E
Erich Gamma 已提交
14

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

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

C
Christof Marti 已提交
27 28 29
enum Traversal {
	Node = 1,
	MacFind,
C
Christof Marti 已提交
30 31
	WindowsDir,
	LinuxFind
C
Christof Marti 已提交
32 33 34
}

interface IDirectoryEntry {
35
	base: string;
C
Christof Marti 已提交
36 37 38 39 40 41 42 43 44
	relativePath: string;
	basename: string;
}

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

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

	private walkedPaths: { [path: string]: boolean; };

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

82 83
		if (this.filePattern) {
			this.normalizedFilePatternLowercase = strings.stripWildcards(this.filePattern).toLowerCase();
B
Benjamin Pasero 已提交
84
		}
E
Erich Gamma 已提交
85 86 87 88 89 90
	}

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

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

94
		// Support that the file pattern is a full path to a file that exists
95
		this.checkFilePatternAbsoluteMatch((exists, size) => {
96 97 98
			if (this.isCanceled) {
				return done(null, this.isLimitHit);
			}
E
Erich Gamma 已提交
99

100 101
			// Report result from file pattern if matching
			if (exists) {
102 103
				this.resultCount++;
				onResult({
104
					relativePath: this.filePattern,
105
					basename: paths.basename(this.filePattern),
106 107
					size
				});
108 109 110 111

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

115 116 117
			// For each extra file
			if (extraFiles) {
				extraFiles.forEach(extraFilePath => {
118 119
					const basename = paths.basename(extraFilePath);
					if (this.excludePattern(extraFilePath, basename)) {
120
						return; // excluded
E
Erich Gamma 已提交
121 122
					}

123
					// File: Check for match on file pattern and include pattern
124
					this.matchFile(onResult, { relativePath: extraFilePath /* no workspace relative path */, basename });
125 126
				});
			}
127

C
Christof Marti 已提交
128
			let traverse = this.nodeJSTraversal;
C
Christof Marti 已提交
129 130 131
			if (!this.maxFilesize) {
				if (platform.isMacintosh) {
					this.traversal = Traversal.MacFind;
132
					traverse = this.findTraversal;
J
Johannes Rieken 已提交
133
					// Disable 'dir' for now (#11181, #11179, #11183, #11182).
134
				} /* else if (platform.isWindows) {
C
Christof Marti 已提交
135 136
					this.traversal = Traversal.WindowsDir;
					traverse = this.windowsDirTraversal;
D
Dirk Baeumer 已提交
137
				} */ else if (platform.isLinux) {
C
Christof Marti 已提交
138
					this.traversal = Traversal.LinuxFind;
139
					traverse = this.findTraversal;
C
Christof Marti 已提交
140 141 142 143 144 145
				}
			}

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

148
			// For each root folder
149 150
			flow.parallel<IFolderSearch, void>(folderQueries, (folderQuery: IFolderSearch, rootFolderDone: (err: Error, result: void) => void) => {
				this.call(traverse, this, folderQuery, onResult, (err?: Error) => {
C
Christof Marti 已提交
151
					if (err) {
C
Christof Marti 已提交
152
						if (isNodeTraversal) {
153
							rootFolderDone(err, undefined);
C
Christof Marti 已提交
154 155
						} else {
							// fallback
156
							const errorMessage = toErrorMessage(err);
157 158
							console.error(errorMessage);
							this.errors.push(errorMessage);
159
							this.nodeJSTraversal(folderQuery, onResult, err => rootFolderDone(err, undefined));
C
Christof Marti 已提交
160 161
						}
					} else {
162
						rootFolderDone(undefined, undefined);
163
					}
C
Christof Marti 已提交
164 165 166 167 168 169
				});
			}, (err, result) => {
				done(err ? err[0] : null, this.isLimitHit);
			});
		});
	}
170

C
Christof Marti 已提交
171 172 173 174 175 176 177 178
	private call(fun: Function, that: any, ...args: any[]): void {
		try {
			fun.apply(that, args);
		} catch (e) {
			args[args.length - 1](e);
		}
	}

179 180
	private findTraversal(folderQuery: IFolderSearch, onResult: (result: IRawFileMatch) => void, cb: (err?: Error) => void): void {
		const rootFolder = folderQuery.folder;
181 182
		const isMac = platform.isMacintosh;
		let done = (err?: Error) => {
J
Johannes Rieken 已提交
183
			done = () => { };
184 185 186 187 188
			cb(err);
		};
		let leftover = '';
		let first = true;
		const tree = this.initDirectoryTree();
189
		const cmd = this.spawnFindCmd(rootFolder, this.excludePattern);
190
		this.collectStdout(cmd, 'utf8', (err: Error, stdout?: string, last?: boolean) => {
C
Christof Marti 已提交
191 192 193 194
			if (err) {
				done(err);
				return;
			}
E
Erich Gamma 已提交
195

C
Christof Marti 已提交
196
			// Mac: uses NFD unicode form on disk, but we want NFC
197 198 199 200 201
			const normalized = leftover + (isMac ? strings.normalizeNFC(stdout) : stdout);
			const relativeFiles = normalized.split('\n./');
			if (first && normalized.length >= 2) {
				first = false;
				relativeFiles[0] = relativeFiles[0].trim().substr(2);
C
Christof Marti 已提交
202 203
			}

204 205 206 207 208 209 210 211
			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 已提交
212 213
			}

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

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

221 222 223
			if (last) {
				this.matchDirectoryTree(tree, rootFolder, onResult);
				done();
224
			}
C
Christof Marti 已提交
225 226 227
		});
	}

228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
	// 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();
	// 	});
	// }

255 256 257 258 259
	/**
	 * Public for testing.
	 */
	public spawnFindCmd(rootFolder: string, excludePattern: glob.ParsedExpression) {
		const basenames = glob.getBasenameTerms(excludePattern);
260
		const paths = glob.getPathTerms(excludePattern);
261
		let args = ['-L', '.'];
262
		if (basenames.length || paths.length) {
263
			args.push('-not', '(', '(');
264
			for (const basename of basenames) {
265
				args.push('-name', basename);
266 267
				args.push('-o');
			}
268 269
			for (const path of paths) {
				args.push('-path', path);
270
				args.push('-o');
271
			}
272
			args.pop();
273 274 275 276 277 278 279 280 281 282
			args.push(')', '-prune', ')');
		}
		args.push('-type', 'f');
		return childProcess.spawn('find', args, { cwd: rootFolder });
	}

	/**
	 * Public for testing.
	 */
	public readStdout(cmd: childProcess.ChildProcess, encoding: string, cb: (err: Error, stdout?: string) => void): void {
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
		let all = '';
		this.collectStdout(cmd, encoding, (err: Error, stdout?: string, last?: boolean) => {
			if (err) {
				cb(err);
				return;
			}

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

	private collectStdout(cmd: childProcess.ChildProcess, encoding: string, cb: (err: Error, stdout?: string, last?: boolean) => void): void {
		let done = (err: Error, stdout?: string, last?: boolean) => {
			if (err || last) {
J
Johannes Rieken 已提交
300
				done = () => { };
301 302 303
				this.cmdForkResultTime = Date.now();
			}
			cb(err, stdout, last);
C
Christof Marti 已提交
304 305
		};

306
		this.forwardData(cmd.stdout, encoding, done);
C
Christof Marti 已提交
307 308
		const stderr = this.collectData(cmd.stderr);

309
		cmd.on('error', (err: Error) => {
C
Christof Marti 已提交
310 311 312
			done(err);
		});

313
		cmd.on('close', (code: number) => {
C
Christof Marti 已提交
314
			if (code !== 0) {
C
Christof Marti 已提交
315
				done(new Error(`find failed with error code ${code}: ${this.decodeData(stderr, encoding)}`));
C
Christof Marti 已提交
316
			} else {
317
				done(null, '', true);
C
Christof Marti 已提交
318 319 320 321
			}
		});
	}

322 323 324 325 326 327 328 329
	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 已提交
330
	private collectData(stream: Readable): Buffer[] {
331 332
		const buffers: Buffer[] = [];
		stream.on('data', (data: Buffer) => {
C
Christof Marti 已提交
333 334 335 336 337
			buffers.push(data);
		});
		return buffers;
	}

C
Christof Marti 已提交
338 339 340 341 342
	private decodeData(buffers: Buffer[], encoding: string): string {
		const decoder = new StringDecoder(encoding);
		return buffers.map(buffer => decoder.write(buffer)).join('');
	}

343 344 345 346 347 348 349 350 351
	private initDirectoryTree(): IDirectoryTree {
		const tree: IDirectoryTree = {
			rootEntries: [],
			pathToEntries: Object.create(null)
		};
		tree.pathToEntries['.'] = tree.rootEntries;
		return tree;
	}

352
	private addDirectoryEntries({ pathToEntries }: IDirectoryTree, base: string, relativeFiles: string[], onResult: (result: IRawFileMatch) => void) {
353
		this.cmdResultCount += relativeFiles.length;
C
Christof Marti 已提交
354 355 356

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

361
		function add(relativePath: string) {
C
Christof Marti 已提交
362 363 364 365 366 367 368 369
			const basename = paths.basename(relativePath);
			const dirname = paths.dirname(relativePath);
			let entries = pathToEntries[dirname];
			if (!entries) {
				entries = pathToEntries[dirname] = [];
				add(dirname);
			}
			entries.push({
370
				base,
C
Christof Marti 已提交
371 372 373
				relativePath,
				basename
			});
374 375
		}
		relativeFiles.forEach(add);
C
Christof Marti 已提交
376 377
	}

378
	private matchDirectoryTree({ rootEntries, pathToEntries }: IDirectoryTree, rootFolder: string, onResult: (result: IRawFileMatch) => void) {
C
Christof Marti 已提交
379 380 381 382 383 384
		const self = this;
		const excludePattern = this.excludePattern;
		const filePattern = this.filePattern;
		function matchDirectory(entries: IDirectoryEntry[]) {
			self.directoriesWalked++;
			for (let i = 0, n = entries.length; i < n; i++) {
385
				const entry = entries[i];
386
				const { relativePath, basename } = entry;
C
Christof Marti 已提交
387 388 389 390 391

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

C
Christof Marti 已提交
396 397 398 399 400 401 402 403 404
				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
					}

405
					self.matchFile(onResult, entry);
C
Christof Marti 已提交
406 407 408 409 410 411
				}
			};
		}
		matchDirectory(rootEntries);
	}

412
	private nodeJSTraversal(folderQuery: IFolderSearch, onResult: (result: IRawFileMatch) => void, done: (err?: Error) => void): void {
C
Christof Marti 已提交
413
		this.directoriesWalked++;
414
		extfs.readdir(folderQuery.folder, (error: Error, files: string[]) => {
C
Christof Marti 已提交
415 416 417 418 419
			if (error || this.isCanceled || this.isLimitHit) {
				return done();
			}

			// Support relative paths to files from a root resource (ignores excludes)
420
			return this.checkFilePatternRelativeMatch(folderQuery.folder, (match, size) => {
C
Christof Marti 已提交
421 422 423 424 425 426 427 428
				if (this.isCanceled || this.isLimitHit) {
					return done();
				}

				// Report result from file pattern if matching
				if (match) {
					this.resultCount++;
					onResult({
429
						base: folderQuery.folder,
430
						relativePath: this.filePattern,
431
						basename: paths.basename(this.filePattern),
C
Christof Marti 已提交
432
						size
433
					});
C
Christof Marti 已提交
434 435
				}

436
				return this.doWalk(folderQuery, '', files, onResult, done);
E
Erich Gamma 已提交
437
			});
438 439 440
		});
	}

441
	public getStats(): IUncachedSearchStats {
C
chrmarti 已提交
442
		return {
443
			fromCache: false,
C
Christof Marti 已提交
444 445
			traversal: Traversal[this.traversal],
			errors: this.errors,
C
chrmarti 已提交
446 447 448
			fileWalkStartTime: this.fileWalkStartTime,
			fileWalkResultTime: Date.now(),
			directoriesWalked: this.directoriesWalked,
449
			filesWalked: this.filesWalked,
C
Christof Marti 已提交
450 451 452 453
			resultCount: this.resultCount,
			cmdForkStartTime: this.cmdForkStartTime,
			cmdForkResultTime: this.cmdForkResultTime,
			cmdResultCount: this.cmdResultCount
C
chrmarti 已提交
454 455 456
		};
	}

457
	private checkFilePatternAbsoluteMatch(clb: (exists: boolean, size?: number) => void): void {
458 459 460 461
		if (!this.filePattern || !paths.isAbsolute(this.filePattern)) {
			return clb(false);
		}

462
		return fs.stat(this.filePattern, (error, stat) => {
463
			return clb(!error && !stat.isDirectory(), stat && stat.size); // only existing files
E
Erich Gamma 已提交
464 465 466
		});
	}

467
	private checkFilePatternRelativeMatch(basePath: string, clb: (matchPath: string, size?: number) => void): void {
B
polish  
Benjamin Pasero 已提交
468
		if (!this.filePattern || paths.isAbsolute(this.filePattern)) {
469 470 471 472 473
			return clb(null);
		}

		const absolutePath = paths.join(basePath, this.filePattern);

474
		return fs.stat(absolutePath, (error, stat) => {
475
			return clb(!error && !stat.isDirectory() ? absolutePath : null, stat && stat.size); // only existing files
476 477 478
		});
	}

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

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

			// Check canceled
			if (this.isCanceled || this.isLimitHit) {
487
				return clb(null, undefined);
E
Erich Gamma 已提交
488 489 490 491 492 493
			}

			// 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;
494
			if (this.config.filePattern === file) {
E
Erich Gamma 已提交
495 496 497 498
				siblings = [];
			}

			// Check exclude pattern
C
Christof Marti 已提交
499
			let currentRelativePath = relativeParentPath ? [relativeParentPath, file].join(paths.sep) : file;
500
			if (this.excludePattern(currentRelativePath, file, () => siblings)) {
501
				return clb(null, undefined);
E
Erich Gamma 已提交
502 503
			}

504
			// Use lstat to detect links
C
Christof Marti 已提交
505
			let currentAbsolutePath = [rootFolder, currentRelativePath].join(paths.sep);
506
			fs.lstat(currentAbsolutePath, (error, lstat) => {
507
				if (error || this.isCanceled || this.isLimitHit) {
508
					return clb(null, undefined);
509
				}
E
Erich Gamma 已提交
510

511
				// If the path is a link, we must instead use fs.stat() to find out if the
512 513 514 515
				// 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) {
516
						return clb(null, undefined);
517
					}
E
Erich Gamma 已提交
518

519 520
					// Directory: Follow directories
					if (stat.isDirectory()) {
C
chrmarti 已提交
521
						this.directoriesWalked++;
522

523 524
						// to really prevent loops with links we need to resolve the real path of them
						return this.realPathIfNeeded(currentAbsolutePath, lstat, (error, realpath) => {
525
							if (error || this.isCanceled || this.isLimitHit) {
526
								return clb(null, undefined);
527 528
							}

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

533 534 535 536 537
							this.walkedPaths[realpath] = true; // remember as walked

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

541
								this.doWalk(folderQuery, currentRelativePath, children, onResult, err => clb(err, undefined));
542 543
							});
						});
544
					}
E
Erich Gamma 已提交
545

546 547
					// File: Check for match on file pattern and include pattern
					else {
C
chrmarti 已提交
548
						this.filesWalked++;
C
Christof Marti 已提交
549
						if (currentRelativePath === this.filePattern) {
550
							return clb(null, undefined); // ignore file if its path matches with the file pattern because checkFilePatternRelativeMatch() takes care of those
551
						}
E
Erich Gamma 已提交
552

553
						if (this.maxFilesize && types.isNumber(stat.size) && stat.size > this.maxFilesize) {
554
							return clb(null, undefined); // ignore file if max file size is hit
555 556
						}

557
						this.matchFile(onResult, { base: rootFolder, relativePath: currentRelativePath, basename: file, size: stat.size });
558 559 560
					}

					// Unwind
561
					return clb(null, undefined);
562
				});
E
Erich Gamma 已提交
563 564 565 566 567 568
			});
		}, (error: Error[]): void => {
			if (error) {
				error = arrays.coalesce(error); // find any error by removing null values first
			}

C
Christof Marti 已提交
569
			return done(error && error.length > 0 ? error[0] : null);
E
Erich Gamma 已提交
570 571 572
		});
	}

573 574
	private matchFile(onResult: (result: IRawFileMatch) => void, candidate: IRawFileMatch): void {
		if (this.isFilePatternMatch(candidate.relativePath) && (!this.includePattern || this.includePattern(candidate.relativePath, candidate.basename))) {
575 576 577 578 579 580 581
			this.resultCount++;

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

			if (!this.isLimitHit) {
582
				onResult(candidate);
583 584 585 586
			}
		}
	}

B
polish  
Benjamin Pasero 已提交
587
	private isFilePatternMatch(path: string): boolean {
588 589 590

		// Check for search pattern
		if (this.filePattern) {
591 592 593 594
			if (this.filePattern === '*') {
				return true; // support the all-matching wildcard
			}

595
			return scorer.matches(path, this.normalizedFilePatternLowercase);
596 597 598 599 600 601
		}

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

602
	private statLinkIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error, stat: fs.Stats) => void): void {
603
		if (lstat.isSymbolicLink()) {
604
			return fs.stat(path, clb); // stat the target the link points to
605 606 607 608 609
		}

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

610
	private realPathIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error, realpath?: string) => void): void {
611 612 613 614 615
		if (lstat.isSymbolicLink()) {
			return fs.realpath(path, (error, realpath) => {
				if (error) {
					return clb(error);
				}
E
Erich Gamma 已提交
616

617 618 619
				return clb(null, realpath);
			});
		}
E
Erich Gamma 已提交
620

621
		return clb(null, path);
E
Erich Gamma 已提交
622 623 624
	}
}

625
export class Engine implements ISearchEngine<IRawFileMatch> {
626
	private folderQueries: IFolderSearch[];
627
	private extraFiles: string[];
E
Erich Gamma 已提交
628 629 630
	private walker: FileWalker;

	constructor(config: IRawSearch) {
631
		this.folderQueries = config.folderQueries;
632 633
		this.extraFiles = config.extraFiles;

E
Erich Gamma 已提交
634 635 636
		this.walker = new FileWalker(config);
	}

637
	public search(onResult: (result: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void {
638
		this.walker.walk(this.folderQueries, this.extraFiles, onResult, (err: Error, isLimitHit: boolean) => {
C
chrmarti 已提交
639 640 641 642 643
			done(err, {
				limitHit: isLimitHit,
				stats: this.walker.getStats()
			});
		});
E
Erich Gamma 已提交
644 645 646 647 648
	}

	public cancel(): void {
		this.walker.cancel();
	}
649
}