fileSearch.ts 18.0 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 9
import * as childProcess from 'child_process';
import {StringDecoder} from 'string_decoder';
E
Erich Gamma 已提交
10 11
import fs = require('fs');
import paths = require('path');
C
Christof Marti 已提交
12
import {Readable} from "stream";
E
Erich Gamma 已提交
13

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

C
Christof Marti 已提交
127
			let traverse = this.nodeJSTraversal;
C
Christof Marti 已提交
128 129 130 131 132 133 134
			if (!this.maxFilesize) {
				if (platform.isMacintosh) {
					this.traversal = Traversal.MacFind;
					traverse = this.macFindTraversal;
				} else if (platform.isWindows) {
					this.traversal = Traversal.WindowsDir;
					traverse = this.windowsDirTraversal;
C
Christof Marti 已提交
135 136 137
				} else if (platform.isLinux) {
					this.traversal = Traversal.LinuxFind;
					traverse = this.linuxFindTraversal;
C
Christof Marti 已提交
138 139 140 141 142 143
				}
			}

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

146
			// For each root folder
147 148
			flow.parallel<string, void>(rootFolders, (rootFolder: string, rootFolderDone: (err?: Error) => void) => {
				traverse.call(this, rootFolder, onResult, (err?: Error) => {
C
Christof Marti 已提交
149
					if (err) {
C
Christof Marti 已提交
150
						if (isNodeTraversal) {
C
Christof Marti 已提交
151 152 153 154 155 156 157 158
							rootFolderDone(err);
						} else {
							// fallback
							this.errors.push(String(err));
							this.nodeJSTraversal(rootFolder, onResult, rootFolderDone);
						}
					} else {
						rootFolderDone();
159
					}
C
Christof Marti 已提交
160 161 162 163 164 165
				});
			}, (err, result) => {
				done(err ? err[0] : null, this.isLimitHit);
			});
		});
	}
166

C
Christof Marti 已提交
167 168
	private macFindTraversal(rootFolder: string, onResult: (result: IRawFileMatch) => void, done: (err?: Error) => void): void {
		const cmd = childProcess.spawn('find', ['-L', '.', '-type', 'f'], { cwd: rootFolder });
C
Christof Marti 已提交
169
		this.readStdout(cmd, 'utf8', (err: Error, stdout?: string) => {
C
Christof Marti 已提交
170 171 172 173
			if (err) {
				done(err);
				return;
			}
E
Erich Gamma 已提交
174

C
Christof Marti 已提交
175 176 177 178 179 180 181 182 183
			// Mac: uses NFD unicode form on disk, but we want NFC
			const relativeFiles = strings.normalizeNFC(stdout).split('\n./');
			relativeFiles[0] = relativeFiles[0].trim().substr(2);
			const n = relativeFiles.length;
			relativeFiles[n - 1] = relativeFiles[n - 1].trim();
			if (!relativeFiles[n - 1]) {
				relativeFiles.pop();
			}

C
Christof Marti 已提交
184
			this.matchFiles(rootFolder, relativeFiles, onResult);
C
Christof Marti 已提交
185

C
Christof Marti 已提交
186 187 188 189 190 191 192 193 194 195
			done();
		});
	}

	private windowsDirTraversal(rootFolder: string, onResult: (result: IRawFileMatch) => void, done: (err?: Error) => void): void {
		const cmd = childProcess.spawn('cmd', ['/U', '/c', 'dir', '/s', '/b', '/a-d'], { cwd: rootFolder });
		this.readStdout(cmd, 'ucs2', (err: Error, stdout?: string) => {
			if (err) {
				done(err);
				return;
C
Christof Marti 已提交
196 197
			}

C
Christof Marti 已提交
198 199 200 201 202 203 204 205 206
			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();
			}

			this.matchFiles(rootFolder, relativeFiles, onResult);
C
Christof Marti 已提交
207 208 209 210 211

			done();
		});
	}

C
Christof Marti 已提交
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
	private linuxFindTraversal(rootFolder: string, onResult: (result: IRawFileMatch) => void, done: (err?: Error) => void): void {
		const cmd = childProcess.spawn('find', ['-L', '.', '-type', 'f'], { cwd: rootFolder });
		this.readStdout(cmd, 'utf8', (err: Error, stdout?: string) => {
			if (err) {
				done(err);
				return;
			}

			const relativeFiles = stdout.split('\n./');
			relativeFiles[0] = relativeFiles[0].trim().substr(2);
			const n = relativeFiles.length;
			relativeFiles[n - 1] = relativeFiles[n - 1].trim();
			if (!relativeFiles[n - 1]) {
				relativeFiles.pop();
			}

			this.matchFiles(rootFolder, relativeFiles, onResult);

			done();
		});
	}

C
Christof Marti 已提交
234
	private readStdout(cmd: childProcess.ChildProcess, encoding: string, cb: (err: Error, stdout?: string) => void): void {
C
Christof Marti 已提交
235 236
		let done = (err: Error, stdout?: string) => {
			done = () => {};
C
Christof Marti 已提交
237
			this.cmdForkResultTime = Date.now();
C
Christof Marti 已提交
238 239 240 241 242 243
			cb(err, stdout);
		};

		const stdout = this.collectData(cmd.stdout);
		const stderr = this.collectData(cmd.stderr);

244
		cmd.on('error', (err: Error) => {
C
Christof Marti 已提交
245 246 247
			done(err);
		});

248
		cmd.on('close', (code: number) => {
C
Christof Marti 已提交
249
			if (code !== 0) {
C
Christof Marti 已提交
250
				done(new Error(`find failed with error code ${code}: ${this.decodeData(stderr, encoding)}`));
C
Christof Marti 已提交
251
			} else {
C
Christof Marti 已提交
252
				done(null, this.decodeData(stdout, encoding));
C
Christof Marti 已提交
253 254 255 256 257
			}
		});
	}

	private collectData(stream: Readable): Buffer[] {
258 259
		const buffers: Buffer[] = [];
		stream.on('data', (data: Buffer) => {
C
Christof Marti 已提交
260 261 262 263 264
			buffers.push(data);
		});
		return buffers;
	}

C
Christof Marti 已提交
265 266 267 268 269 270 271 272 273 274
	private decodeData(buffers: Buffer[], encoding: string): string {
		const decoder = new StringDecoder(encoding);
		return buffers.map(buffer => decoder.write(buffer)).join('');
	}

	private matchFiles(rootFolder: string, relativeFiles: string[], onResult: (result: IRawFileMatch) => void) {
		this.cmdResultCount = relativeFiles.length;

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

279
		const tree = this.buildDirectoryTree(rootFolder, relativeFiles);
C
Christof Marti 已提交
280
		this.matchDirectoryTree(rootFolder, tree, onResult);
C
Christof Marti 已提交
281 282
	}

283
	private buildDirectoryTree(base: string, relativeFilePaths: string[]): IDirectoryTree {
C
Christof Marti 已提交
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
		const tree: IDirectoryTree = {
			rootEntries: [],
			pathToEntries: Object.create(null)
		};
		const {pathToEntries} = tree;
		pathToEntries['.'] = tree.rootEntries;
		relativeFilePaths.forEach(function add(relativePath: string) {
			const basename = paths.basename(relativePath);
			const dirname = paths.dirname(relativePath);
			let entries = pathToEntries[dirname];
			if (!entries) {
				entries = pathToEntries[dirname] = [];
				add(dirname);
			}
			entries.push({
299
				base,
C
Christof Marti 已提交
300 301 302 303 304 305 306 307 308 309 310 311 312 313
				relativePath,
				basename
			});
		});
		return tree;
	}

	private matchDirectoryTree(rootFolder: string, { rootEntries, pathToEntries }: IDirectoryTree, onResult: (result: IRawFileMatch) => void) {
		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++) {
314 315
				const entry = entries[i];
				const {relativePath, basename} = entry;
C
Christof Marti 已提交
316 317 318 319 320

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

C
Christof Marti 已提交
325 326 327 328 329 330 331 332 333
				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
					}

334
					self.matchFile(onResult, entry);
C
Christof Marti 已提交
335 336 337 338 339 340
				}
			};
		}
		matchDirectory(rootEntries);
	}

C
Christof Marti 已提交
341
	private nodeJSTraversal(rootFolder: string, onResult: (result: IRawFileMatch) => void, done: (err?: Error) => void): void {
C
Christof Marti 已提交
342
		this.directoriesWalked++;
C
Christof Marti 已提交
343
		extfs.readdir(rootFolder, (error: Error, files: string[]) => {
C
Christof Marti 已提交
344 345 346 347 348
			if (error || this.isCanceled || this.isLimitHit) {
				return done();
			}

			// Support relative paths to files from a root resource (ignores excludes)
C
Christof Marti 已提交
349
			return this.checkFilePatternRelativeMatch(rootFolder, (match, size) => {
C
Christof Marti 已提交
350 351 352 353 354 355 356 357
				if (this.isCanceled || this.isLimitHit) {
					return done();
				}

				// Report result from file pattern if matching
				if (match) {
					this.resultCount++;
					onResult({
C
Christof Marti 已提交
358
						base: rootFolder,
359
						relativePath: this.filePattern,
360
						basename: paths.basename(this.filePattern),
C
Christof Marti 已提交
361
						size
362
					});
C
Christof Marti 已提交
363 364
				}

C
Christof Marti 已提交
365
				return this.doWalk(rootFolder, '', files, onResult, done);
E
Erich Gamma 已提交
366
			});
367 368 369
		});
	}

370
	public getStats(): IUncachedSearchStats {
C
chrmarti 已提交
371
		return {
372
			fromCache: false,
C
Christof Marti 已提交
373 374
			traversal: Traversal[this.traversal],
			errors: this.errors,
C
chrmarti 已提交
375 376 377
			fileWalkStartTime: this.fileWalkStartTime,
			fileWalkResultTime: Date.now(),
			directoriesWalked: this.directoriesWalked,
378
			filesWalked: this.filesWalked,
C
Christof Marti 已提交
379 380 381 382
			resultCount: this.resultCount,
			cmdForkStartTime: this.cmdForkStartTime,
			cmdForkResultTime: this.cmdForkResultTime,
			cmdResultCount: this.cmdResultCount
C
chrmarti 已提交
383 384 385
		};
	}

386
	private checkFilePatternAbsoluteMatch(clb: (exists: boolean, size?: number) => void): void {
387 388 389 390
		if (!this.filePattern || !paths.isAbsolute(this.filePattern)) {
			return clb(false);
		}

391
		return fs.stat(this.filePattern, (error, stat) => {
392
			return clb(!error && !stat.isDirectory(), stat && stat.size); // only existing files
E
Erich Gamma 已提交
393 394 395
		});
	}

396
	private checkFilePatternRelativeMatch(basePath: string, clb: (matchPath: string, size?: number) => void): void {
B
polish  
Benjamin Pasero 已提交
397
		if (!this.filePattern || paths.isAbsolute(this.filePattern)) {
398 399 400 401 402
			return clb(null);
		}

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

403
		return fs.stat(absolutePath, (error, stat) => {
404
			return clb(!error && !stat.isDirectory() ? absolutePath : null, stat && stat.size); // only existing files
405 406 407
		});
	}

C
Christof Marti 已提交
408
	private doWalk(rootFolder: string, relativeParentPath: string, files: string[], onResult: (result: IRawFileMatch) => void, done: (error: Error) => void): void {
E
Erich Gamma 已提交
409 410 411 412 413 414 415 416 417 418 419 420 421

		// Execute tasks on each file in parallel to optimize throughput
		flow.parallel(files, (file: string, clb: (error: Error) => void): void => {

			// Check canceled
			if (this.isCanceled || this.isLimitHit) {
				return clb(null);
			}

			// 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;
422
			if (this.config.filePattern === file) {
E
Erich Gamma 已提交
423 424 425 426
				siblings = [];
			}

			// Check exclude pattern
C
Christof Marti 已提交
427
			let currentRelativePath = relativeParentPath ? [relativeParentPath, file].join(paths.sep) : file;
428
			if (this.excludePattern(currentRelativePath, file, () => siblings)) {
E
Erich Gamma 已提交
429 430 431
				return clb(null);
			}

432
			// Use lstat to detect links
C
Christof Marti 已提交
433
			let currentAbsolutePath = [rootFolder, currentRelativePath].join(paths.sep);
434
			fs.lstat(currentAbsolutePath, (error, lstat) => {
435 436 437
				if (error || this.isCanceled || this.isLimitHit) {
					return clb(null);
				}
E
Erich Gamma 已提交
438

439
				// If the path is a link, we must instead use fs.stat() to find out if the
440 441 442 443 444 445
				// 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) {
						return clb(null);
					}
E
Erich Gamma 已提交
446

447 448
					// Directory: Follow directories
					if (stat.isDirectory()) {
C
chrmarti 已提交
449
						this.directoriesWalked++;
450

451 452
						// to really prevent loops with links we need to resolve the real path of them
						return this.realPathIfNeeded(currentAbsolutePath, lstat, (error, realpath) => {
453 454 455 456
							if (error || this.isCanceled || this.isLimitHit) {
								return clb(null);
							}

457 458 459
							if (this.walkedPaths[realpath]) {
								return clb(null); // escape when there are cycles (can happen with symlinks)
							}
E
Erich Gamma 已提交
460

461 462 463 464 465 466 467 468
							this.walkedPaths[realpath] = true; // remember as walked

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

C
Christof Marti 已提交
469
								this.doWalk(rootFolder, currentRelativePath, children, onResult, clb);
470 471
							});
						});
472
					}
E
Erich Gamma 已提交
473

474 475
					// File: Check for match on file pattern and include pattern
					else {
C
chrmarti 已提交
476
						this.filesWalked++;
C
Christof Marti 已提交
477
						if (currentRelativePath === this.filePattern) {
478 479
							return clb(null); // ignore file if its path matches with the file pattern because checkFilePatternRelativeMatch() takes care of those
						}
E
Erich Gamma 已提交
480

481 482 483 484
						if (this.maxFilesize && types.isNumber(stat.size) && stat.size > this.maxFilesize) {
							return clb(null); // ignore file if max file size is hit
						}

485
						this.matchFile(onResult, { base: rootFolder, relativePath: currentRelativePath, basename: file, size: stat.size });
486 487 488 489 490
					}

					// Unwind
					return clb(null);
				});
E
Erich Gamma 已提交
491 492 493 494 495 496
			});
		}, (error: Error[]): void => {
			if (error) {
				error = arrays.coalesce(error); // find any error by removing null values first
			}

C
Christof Marti 已提交
497
			return done(error && error.length > 0 ? error[0] : null);
E
Erich Gamma 已提交
498 499 500
		});
	}

501 502
	private matchFile(onResult: (result: IRawFileMatch) => void, candidate: IRawFileMatch): void {
		if (this.isFilePatternMatch(candidate.relativePath) && (!this.includePattern || this.includePattern(candidate.relativePath, candidate.basename))) {
503 504 505 506 507 508 509
			this.resultCount++;

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

			if (!this.isLimitHit) {
510
				onResult(candidate);
511 512 513 514
			}
		}
	}

B
polish  
Benjamin Pasero 已提交
515
	private isFilePatternMatch(path: string): boolean {
516 517 518

		// Check for search pattern
		if (this.filePattern) {
519 520 521 522
			if (this.filePattern === '*') {
				return true; // support the all-matching wildcard
			}

523
			return scorer.matches(path, this.normalizedFilePatternLowercase);
524 525 526 527 528 529
		}

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

530
	private statLinkIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error, stat: fs.Stats) => void): void {
531
		if (lstat.isSymbolicLink()) {
532
			return fs.stat(path, clb); // stat the target the link points to
533 534 535 536 537
		}

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

538
	private realPathIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error, realpath?: string) => void): void {
539 540 541 542 543
		if (lstat.isSymbolicLink()) {
			return fs.realpath(path, (error, realpath) => {
				if (error) {
					return clb(error);
				}
E
Erich Gamma 已提交
544

545 546 547
				return clb(null, realpath);
			});
		}
E
Erich Gamma 已提交
548

549
		return clb(null, path);
E
Erich Gamma 已提交
550 551 552
	}
}

553
export class Engine implements ISearchEngine<IRawFileMatch> {
554 555
	private rootFolders: string[];
	private extraFiles: string[];
E
Erich Gamma 已提交
556 557 558
	private walker: FileWalker;

	constructor(config: IRawSearch) {
559 560 561
		this.rootFolders = config.rootFolders;
		this.extraFiles = config.extraFiles;

E
Erich Gamma 已提交
562 563 564
		this.walker = new FileWalker(config);
	}

565
	public search(onResult: (result: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void {
C
chrmarti 已提交
566 567 568 569 570 571
		this.walker.walk(this.rootFolders, this.extraFiles, onResult, (err: Error, isLimitHit: boolean) => {
			done(err, {
				limitHit: isLimitHit,
				stats: this.walker.getStats()
			});
		});
E
Erich Gamma 已提交
572 573 574 575 576
	}

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