fileSearch.ts 24.2 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

C
Christof Marti 已提交
6
import * as childProcess from 'child_process';
7 8
import * as fs from 'fs';
import * as path from 'path';
B
Benjamin Pasero 已提交
9
import { Readable } from 'stream';
10
import { NodeStringDecoder, StringDecoder } from 'string_decoder';
11
import * as arrays from 'vs/base/common/arrays';
12 13 14 15 16
import { toErrorMessage } from 'vs/base/common/errorMessage';
import * as glob from 'vs/base/common/glob';
import * as normalization from 'vs/base/common/normalization';
import * as objects from 'vs/base/common/objects';
import { isEqualOrParent } from 'vs/base/common/paths';
17
import * as platform from 'vs/base/common/platform';
18
import { StopWatch } from 'vs/base/common/stopwatch';
19 20
import * as strings from 'vs/base/common/strings';
import * as types from 'vs/base/common/types';
21
import { URI } from 'vs/base/common/uri';
22 23
import * as extfs from 'vs/base/node/extfs';
import * as flow from 'vs/base/node/flow';
24
import { IFileQuery, IFolderQuery, IProgress, ISearchEngineStats } from 'vs/platform/search/common/search';
25
import { IRawFileMatch, ISearchEngine, ISearchEngineSuccess } from 'vs/workbench/services/search/node/search';
26
import { spawnRipgrepCmd } from './ripgrepFileSearch';
E
Erich Gamma 已提交
27

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

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

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

46 47 48 49 50
const killCmds = new Set<() => void>();
process.on('exit', () => {
	killCmds.forEach(cmd => cmd());
});

E
Erich Gamma 已提交
51
export class FileWalker {
52
	private config: IFileQuery;
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;
63
	private fileWalkSW: StopWatch;
C
chrmarti 已提交
64 65
	private directoriesWalked: number;
	private filesWalked: number;
C
Christof Marti 已提交
66 67
	private traversal: Traversal;
	private errors: string[];
68
	private cmdSW: StopWatch;
C
Christof Marti 已提交
69
	private cmdResultCount: number;
E
Erich Gamma 已提交
70

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

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

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

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

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

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

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

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

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

122
	public walk(folderQueries: IFolderQuery[], extraFiles: URI[], onResult: (result: IRawFileMatch) => void, onMessage: (message: IProgress) => void, done: (error: Error, isLimitHit: boolean) => void): void {
123
		this.fileWalkSW = StopWatch.create(false);
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
		// For each extra file
		if (extraFiles) {
			extraFiles.forEach(extraFilePath => {
133 134
				const basename = path.basename(extraFilePath.fsPath);
				if (this.globalExcludePattern && this.globalExcludePattern(extraFilePath.fsPath, basename)) {
135 136
					return; // excluded
				}
E
Erich Gamma 已提交
137

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

143 144 145 146 147 148 149 150
		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;
R
Rob Lourens 已提交
151
			} else if (platform.isLinux) {
152 153
				this.traversal = Traversal.LinuxFind;
				traverse = this.cmdTraversal;
C
Christof Marti 已提交
154
			}
155
		}
C
Christof Marti 已提交
156

157 158
		const isNodeTraversal = traverse === this.nodeJSTraversal;
		if (!isNodeTraversal) {
159
			this.cmdSW = StopWatch.create(false);
160
		}
C
Christof Marti 已提交
161

162
		// For each root folder
163
		flow.parallel<IFolderQuery, void>(folderQueries, (folderQuery: IFolderQuery, rootFolderDone: (err: Error, result: void) => void) => {
164 165 166 167 168 169 170 171 172
			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 已提交
173
			});
174
		}, (errors, result) => {
175
			this.fileWalkSW.stop();
M
Matt Bierner 已提交
176
			const err = errors ? arrays.coalesce(errors)[0] : null;
177
			done(err, this.isLimitHit);
C
Christof Marti 已提交
178 179
		});
	}
180

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

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

196
		let done = (err?: Error) => {
197
			killCmds.delete(killCmd);
J
Johannes Rieken 已提交
198
			done = () => { };
199 200 201 202 203
			cb(err);
		};
		let leftover = '';
		let first = true;
		const tree = this.initDirectoryTree();
204

C
Christof Marti 已提交
205
		const useRipgrep = this.useRipgrep;
206 207
		let noSiblingsClauses: boolean;
		if (useRipgrep) {
208
			const ripgrep = spawnRipgrepCmd(this.config, folderQuery, this.config.includePattern, this.folderExcludePatterns.get(folderQuery.folder.fsPath).expression);
209 210
			cmd = ripgrep.cmd;
			noSiblingsClauses = !Object.keys(ripgrep.siblingClauses).length;
211

212 213 214 215 216 217 218 219 220
			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 });
221 222 223 224
		} else {
			cmd = this.spawnFindCmd(folderQuery);
		}

225
		this.cmdResultCount = 0;
226
		this.collectStdout(cmd, 'utf8', useRipgrep, onMessage, (err: Error, stdout?: string, last?: boolean) => {
C
Christof Marti 已提交
227 228 229 230
			if (err) {
				done(err);
				return;
			}
231
			if (this.isLimitHit) {
232
				done();
233 234
				return;
			}
E
Erich Gamma 已提交
235

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

244 245 246 247 248 249 250 251
			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 已提交
252 253
			}

254 255 256 257 258
			if (relativeFiles.length && relativeFiles[0].indexOf('\n') !== -1) {
				done(new Error('Splitting up files failed'));
				return;
			}

259 260 261 262 263 264
			this.cmdResultCount += relativeFiles.length;

			if (useRipgrep && noSiblingsClauses) {
				for (const relativePath of relativeFiles) {
					const basename = path.basename(relativePath);
					this.matchFile(onResult, { base: rootFolder, relativePath, basename });
265 266 267 268
					if (this.isLimitHit) {
						killCmd();
						break;
					}
269
				}
270
				if (last || this.isLimitHit) {
271
					done();
272
				}
273

274 275 276
				return;
			}

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

280 281 282
			if (last) {
				this.matchDirectoryTree(tree, rootFolder, onResult);
				done();
283
			}
C
Christof Marti 已提交
284 285 286
		});
	}

287 288 289
	/**
	 * Public for testing.
	 */
290 291
	public spawnFindCmd(folderQuery: IFolderQuery) {
		const excludePattern = this.folderExcludePatterns.get(folderQuery.folder.fsPath);
292 293
		const basenames = excludePattern.getBasenameTerms();
		const pathTerms = excludePattern.getPathTerms();
294
		let args = ['-L', '.'];
295
		if (basenames.length || pathTerms.length) {
296
			args.push('-not', '(', '(');
297
			for (const basename of basenames) {
298
				args.push('-name', basename);
299 300
				args.push('-o');
			}
301
			for (const path of pathTerms) {
302
				args.push('-path', path);
303
				args.push('-o');
304
			}
305
			args.pop();
306 307 308
			args.push(')', '-prune', ')');
		}
		args.push('-type', 'f');
309
		return childProcess.spawn('find', args, { cwd: folderQuery.folder.fsPath });
310 311 312 313 314
	}

	/**
	 * Public for testing.
	 */
315
	public readStdout(cmd: childProcess.ChildProcess, encoding: string, isRipgrep: boolean, cb: (err: Error, stdout?: string) => void): void {
316
		let all = '';
317
		this.collectStdout(cmd, encoding, isRipgrep, () => { }, (err: Error, stdout?: string, last?: boolean) => {
318 319 320 321 322 323 324 325 326 327 328 329
			if (err) {
				cb(err);
				return;
			}

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

330 331
	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) => {
332
			if (err || last) {
333
				onData = () => { };
334 335 336 337

				if (this.cmdSW) {
					this.cmdSW.stop();
				}
338 339
			}
			cb(err, stdout, last);
C
Christof Marti 已提交
340 341
		};

342
		let gotData = false;
343 344 345
		if (cmd.stdout) {
			// Should be non-null, but #38195
			this.forwardData(cmd.stdout, encoding, onData);
346
			cmd.stdout.once('data', () => gotData = true);
347 348 349 350
		} else {
			onMessage({ message: 'stdout is null' });
		}

351 352 353 354 355 356 357
		let stderr: Buffer[];
		if (cmd.stderr) {
			// Should be non-null, but #38195
			stderr = this.collectData(cmd.stderr);
		} else {
			onMessage({ message: 'stderr is null' });
		}
358

359
		cmd.on('error', (err: Error) => {
360
			onData(err);
C
Christof Marti 已提交
361 362
		});

363
		cmd.on('close', (code: number) => {
364
			// ripgrep returns code=1 when no results are found
365 366
			let stderrText: string;
			if (isRipgrep ? (!gotData && (stderrText = this.decodeData(stderr, encoding)) && rgErrorMsgForDisplay(stderrText)) : code !== 0) {
367
				onData(new Error(`command failed with error code ${code}: ${this.decodeData(stderr, encoding)}`));
C
Christof Marti 已提交
368
			} else {
369 370 371
				if (isRipgrep && this.exists && code === 0) {
					this.isLimitHit = true;
				}
372
				onData(null, '', true);
C
Christof Marti 已提交
373 374 375 376
			}
		});
	}

377 378 379 380 381 382 383 384
	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 已提交
385
	private collectData(stream: Readable): Buffer[] {
386 387
		const buffers: Buffer[] = [];
		stream.on('data', (data: Buffer) => {
C
Christof Marti 已提交
388 389 390 391 392
			buffers.push(data);
		});
		return buffers;
	}

C
Christof Marti 已提交
393 394 395 396 397
	private decodeData(buffers: Buffer[], encoding: string): string {
		const decoder = new StringDecoder(encoding);
		return buffers.map(buffer => decoder.write(buffer)).join('');
	}

398 399 400 401 402 403 404 405 406
	private initDirectoryTree(): IDirectoryTree {
		const tree: IDirectoryTree = {
			rootEntries: [],
			pathToEntries: Object.create(null)
		};
		tree.pathToEntries['.'] = tree.rootEntries;
		return tree;
	}

407
	private addDirectoryEntries({ pathToEntries }: IDirectoryTree, base: string, relativeFiles: string[], onResult: (result: IRawFileMatch) => void) {
C
Christof Marti 已提交
408 409
		// Support relative paths to files from a root resource (ignores excludes)
		if (relativeFiles.indexOf(this.filePattern) !== -1) {
410
			const basename = path.basename(this.filePattern);
411
			this.matchFile(onResult, { base: base, relativePath: this.filePattern, basename });
C
Christof Marti 已提交
412 413
		}

414
		function add(relativePath: string) {
415 416
			const basename = path.basename(relativePath);
			const dirname = path.dirname(relativePath);
C
Christof Marti 已提交
417 418 419 420 421 422
			let entries = pathToEntries[dirname];
			if (!entries) {
				entries = pathToEntries[dirname] = [];
				add(dirname);
			}
			entries.push({
423
				base,
C
Christof Marti 已提交
424 425 426
				relativePath,
				basename
			});
427 428
		}
		relativeFiles.forEach(add);
C
Christof Marti 已提交
429 430
	}

431
	private matchDirectoryTree({ rootEntries, pathToEntries }: IDirectoryTree, rootFolder: string, onResult: (result: IRawFileMatch) => void) {
C
Christof Marti 已提交
432
		const self = this;
433
		const excludePattern = this.folderExcludePatterns.get(rootFolder);
C
Christof Marti 已提交
434 435 436
		const filePattern = this.filePattern;
		function matchDirectory(entries: IDirectoryEntry[]) {
			self.directoriesWalked++;
437
			const hasSibling = glob.hasSiblingFn(() => entries.map(entry => entry.basename));
C
Christof Marti 已提交
438
			for (let i = 0, n = entries.length; i < n; i++) {
439
				const entry = entries[i];
440
				const { relativePath, basename } = entry;
C
Christof Marti 已提交
441 442 443 444 445

				// 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
446
				if (excludePattern.test(relativePath, basename, filePattern !== basename ? hasSibling : undefined)) {
C
Christof Marti 已提交
447 448
					continue;
				}
E
Erich Gamma 已提交
449

C
Christof Marti 已提交
450 451 452 453 454 455 456 457 458
				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
					}

459
					self.matchFile(onResult, entry);
C
Christof Marti 已提交
460
				}
461 462 463 464

				if (self.isLimitHit) {
					break;
				}
465
			}
C
Christof Marti 已提交
466 467 468 469
		}
		matchDirectory(rootEntries);
	}

470
	private nodeJSTraversal(folderQuery: IFolderQuery, onResult: (result: IRawFileMatch) => void, onMessage: (message: IProgress) => void, done: (err?: Error) => void): void {
C
Christof Marti 已提交
471
		this.directoriesWalked++;
472
		extfs.readdir(folderQuery.folder.fsPath, (error: Error, files: string[]) => {
C
Christof Marti 已提交
473 474 475 476
			if (error || this.isCanceled || this.isLimitHit) {
				return done();
			}

477 478 479
			if (this.isCanceled || this.isLimitHit) {
				return done();
			}
C
Christof Marti 已提交
480

481
			return this.doWalk(folderQuery, '', files, onResult, done);
482 483 484
		});
	}

485
	public getStats(): ISearchEngineStats {
C
chrmarti 已提交
486
		return {
R
Rob Lourens 已提交
487
			cmdTime: this.cmdSW && this.cmdSW.elapsed(),
488
			fileWalkTime: this.fileWalkSW.elapsed(),
C
Christof Marti 已提交
489
			traversal: Traversal[this.traversal],
C
chrmarti 已提交
490
			directoriesWalked: this.directoriesWalked,
491
			filesWalked: this.filesWalked,
C
Christof Marti 已提交
492
			cmdResultCount: this.cmdResultCount
C
chrmarti 已提交
493 494 495
		};
	}

496
	private doWalk(folderQuery: IFolderQuery, relativeParentPath: string, files: string[], onResult: (result: IRawFileMatch) => void, done: (error: Error) => void): void {
497
		const rootFolder = folderQuery.folder;
E
Erich Gamma 已提交
498 499

		// Execute tasks on each file in parallel to optimize throughput
500
		const hasSibling = glob.hasSiblingFn(() => files);
501
		flow.parallel(files, (file: string, clb: (error: Error, result: {}) => void): void => {
E
Erich Gamma 已提交
502 503 504

			// Check canceled
			if (this.isCanceled || this.isLimitHit) {
505
				return clb(null, undefined);
E
Erich Gamma 已提交
506 507
			}

508
			// Check exclude pattern
E
Erich Gamma 已提交
509 510 511
			// 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
512
			let currentRelativePath = relativeParentPath ? [relativeParentPath, file].join(path.sep) : file;
513
			if (this.folderExcludePatterns.get(folderQuery.folder.fsPath).test(currentRelativePath, file, this.config.filePattern !== file ? hasSibling : undefined)) {
514
				return clb(null, undefined);
E
Erich Gamma 已提交
515 516
			}

517
			// Use lstat to detect links
518
			let currentAbsolutePath = [rootFolder.fsPath, currentRelativePath].join(path.sep);
519
			fs.lstat(currentAbsolutePath, (error, lstat) => {
520
				if (error || this.isCanceled || this.isLimitHit) {
521
					return clb(null, undefined);
522
				}
E
Erich Gamma 已提交
523

524
				// If the path is a link, we must instead use fs.stat() to find out if the
525 526 527 528
				// 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) {
529
						return clb(null, undefined);
530
					}
E
Erich Gamma 已提交
531

532 533
					// Directory: Follow directories
					if (stat.isDirectory()) {
C
chrmarti 已提交
534
						this.directoriesWalked++;
535

536 537
						// to really prevent loops with links we need to resolve the real path of them
						return this.realPathIfNeeded(currentAbsolutePath, lstat, (error, realpath) => {
538
							if (error || this.isCanceled || this.isLimitHit) {
539
								return clb(null, undefined);
540 541
							}

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

546 547 548 549 550
							this.walkedPaths[realpath] = true; // remember as walked

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

554
								this.doWalk(folderQuery, currentRelativePath, children, onResult, err => clb(err, undefined));
555 556
							});
						});
557
					}
E
Erich Gamma 已提交
558

559 560
					// File: Check for match on file pattern and include pattern
					else {
C
chrmarti 已提交
561
						this.filesWalked++;
C
Christof Marti 已提交
562
						if (currentRelativePath === this.filePattern) {
563
							return clb(null, undefined); // ignore file if its path matches with the file pattern because checkFilePatternRelativeMatch() takes care of those
564
						}
E
Erich Gamma 已提交
565

566
						if (this.maxFilesize && types.isNumber(stat.size) && stat.size > this.maxFilesize) {
567
							return clb(null, undefined); // ignore file if max file size is hit
568 569
						}

570
						this.matchFile(onResult, { base: rootFolder.fsPath, relativePath: currentRelativePath, basename: file, size: stat.size });
571 572 573
					}

					// Unwind
574
					return clb(null, undefined);
575
				});
E
Erich Gamma 已提交
576 577 578 579 580 581
			});
		}, (error: Error[]): void => {
			if (error) {
				error = arrays.coalesce(error); // find any error by removing null values first
			}

C
Christof Marti 已提交
582
			return done(error && error.length > 0 ? error[0] : null);
E
Erich Gamma 已提交
583 584 585
		});
	}

586 587
	private matchFile(onResult: (result: IRawFileMatch) => void, candidate: IRawFileMatch): void {
		if (this.isFilePatternMatch(candidate.relativePath) && (!this.includePattern || this.includePattern(candidate.relativePath, candidate.basename))) {
588 589
			this.resultCount++;

590
			if (this.exists || (this.maxResults && this.resultCount > this.maxResults)) {
591 592 593 594
				this.isLimitHit = true;
			}

			if (!this.isLimitHit) {
595
				onResult(candidate);
596 597 598 599
			}
		}
	}

B
polish  
Benjamin Pasero 已提交
600
	private isFilePatternMatch(path: string): boolean {
601 602 603

		// Check for search pattern
		if (this.filePattern) {
604 605 606 607
			if (this.filePattern === '*') {
				return true; // support the all-matching wildcard
			}

608
			return strings.fuzzyContains(path, this.normalizedFilePatternLowercase);
609 610 611 612 613 614
		}

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

615
	private statLinkIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error, stat: fs.Stats) => void): void {
616
		if (lstat.isSymbolicLink()) {
617
			return fs.stat(path, clb); // stat the target the link points to
618 619 620 621 622
		}

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

623
	private realPathIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error, realpath?: string) => void): void {
624 625 626 627 628
		if (lstat.isSymbolicLink()) {
			return fs.realpath(path, (error, realpath) => {
				if (error) {
					return clb(error);
				}
E
Erich Gamma 已提交
629

630 631 632
				return clb(null, realpath);
			});
		}
E
Erich Gamma 已提交
633

634
		return clb(null, path);
E
Erich Gamma 已提交
635 636 637
	}
}

638
export class Engine implements ISearchEngine<IRawFileMatch> {
639 640
	private folderQueries: IFolderQuery[];
	private extraFiles: URI[];
E
Erich Gamma 已提交
641 642
	private walker: FileWalker;

643
	constructor(config: IFileQuery) {
644
		this.folderQueries = config.folderQueries;
645
		this.extraFiles = config.extraFileResources;
646

E
Erich Gamma 已提交
647 648 649
		this.walker = new FileWalker(config);
	}

650
	public search(onResult: (result: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISearchEngineSuccess) => void): void {
651
		this.walker.walk(this.folderQueries, this.extraFiles, onResult, onProgress, (err: Error, isLimitHit: boolean) => {
C
chrmarti 已提交
652 653 654 655 656
			done(err, {
				limitHit: isLimitHit,
				stats: this.walker.getStats()
			});
		});
E
Erich Gamma 已提交
657 658 659 660 661
	}

	public cancel(): void {
		this.walker.cancel();
	}
662 663 664 665 666 667 668 669 670 671 672
}

/**
 * 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 已提交
673 674
	constructor(public expression: glob.IExpression, private root: string) {
		this.init(expression);
675 676 677 678 679 680 681 682
	}

	/**
	 * 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;
683 684 685 686 687
		Object.keys(expr)
			.filter(key => expr[key])
			.forEach(key => {
				if (path.isAbsolute(key)) {
					absoluteGlobExpr = absoluteGlobExpr || glob.getEmptyExpression();
688
					absoluteGlobExpr[key] = expr[key];
689 690
				} else {
					relativeGlobExpr = relativeGlobExpr || glob.getEmptyExpression();
691
					relativeGlobExpr[key] = expr[key];
692 693
				}
			});
694 695 696 697 698

		this.absoluteParsedExpr = absoluteGlobExpr && glob.parse(absoluteGlobExpr, { trimForExclusions: true });
		this.relativeParsedExpr = relativeGlobExpr && glob.parse(relativeGlobExpr, { trimForExclusions: true });
	}

699
	public test(_path: string, basename?: string, hasSibling?: (name: string) => boolean | Promise<boolean>): string | Promise<string> {
700 701
		return (this.relativeParsedExpr && this.relativeParsedExpr(_path, basename, hasSibling)) ||
			(this.absoluteParsedExpr && this.absoluteParsedExpr(path.join(this.root, _path), basename, hasSibling));
702 703 704
	}

	public getBasenameTerms(): string[] {
M
Matt Bierner 已提交
705
		const basenameTerms: string[] = [];
706 707 708 709 710 711 712 713 714 715 716 717
		if (this.absoluteParsedExpr) {
			basenameTerms.push(...glob.getBasenameTerms(this.absoluteParsedExpr));
		}

		if (this.relativeParsedExpr) {
			basenameTerms.push(...glob.getBasenameTerms(this.relativeParsedExpr));
		}

		return basenameTerms;
	}

	public getPathTerms(): string[] {
M
Matt Bierner 已提交
718
		const pathTerms: string[] = [];
719 720 721 722 723 724 725 726 727 728
		if (this.absoluteParsedExpr) {
			pathTerms.push(...glob.getPathTerms(this.absoluteParsedExpr));
		}

		if (this.relativeParsedExpr) {
			pathTerms.push(...glob.getPathTerms(this.relativeParsedExpr));
		}

		return pathTerms;
	}
729
}
R
Rob Lourens 已提交
730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760

export function rgErrorMsgForDisplay(msg: string): string | undefined {
	const lines = msg.trim().split('\n');
	const firstLine = lines[0].trim();

	if (strings.startsWith(firstLine, 'Error parsing regex')) {
		return firstLine;
	}

	if (strings.startsWith(firstLine, 'regex parse error')) {
		return strings.uppercaseFirstLetter(lines[lines.length - 1].trim());
	}

	if (strings.startsWith(firstLine, 'error parsing glob') ||
		strings.startsWith(firstLine, 'unsupported encoding')) {
		// Uppercase first letter
		return firstLine.charAt(0).toUpperCase() + firstLine.substr(1);
	}

	if (firstLine === `Literal '\\n' not allowed.`) {
		// I won't localize this because none of the Ripgrep error messages are localized
		return `Literal '\\n' currently not supported`;
	}

	if (strings.startsWith(firstLine, 'Literal ')) {
		// Other unsupported chars
		return firstLine;
	}

	return undefined;
}