fileSearch.ts 23.3 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
import * as fs from 'fs';
8
import * as path from 'vs/base/common/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
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';
B
Benjamin Pasero 已提交
16
import { isEqualOrParent } from 'vs/base/common/extpath';
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';
B
Benjamin Pasero 已提交
22
import { readdir } from 'vs/base/node/pfs';
23
import { IFileQuery, IFolderQuery, IProgressMessage, ISearchEngineStats, IRawFileMatch, ISearchEngine, ISearchEngineSuccess } from 'vs/workbench/services/search/common/search';
24
import { spawnRipgrepCmd } from './ripgrepFileSearch';
E
Erich Gamma 已提交
25

C
Christof Marti 已提交
26
interface IDirectoryEntry {
27
	base: string;
C
Christof Marti 已提交
28 29 30 31 32 33 34 35 36
	relativePath: string;
	basename: string;
}

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

37 38 39 40 41
const killCmds = new Set<() => void>();
process.on('exit', () => {
	killCmds.forEach(cmd => cmd());
});

E
Erich Gamma 已提交
42
export class FileWalker {
43
	private config: IFileQuery;
44
	private filePattern: string;
45
	private normalizedFilePatternLowercase: string;
46 47
	private includePattern: glob.ParsedExpression | undefined;
	private maxResults: number | null;
48
	private exists: boolean;
49
	private maxFilesize: number | null;
E
Erich Gamma 已提交
50 51 52
	private isLimitHit: boolean;
	private resultCount: number;
	private isCanceled: boolean;
53
	private fileWalkSW: StopWatch;
C
chrmarti 已提交
54 55
	private directoriesWalked: number;
	private filesWalked: number;
C
Christof Marti 已提交
56
	private errors: string[];
57
	private cmdSW: StopWatch;
C
Christof Marti 已提交
58
	private cmdResultCount: number;
E
Erich Gamma 已提交
59

60
	private folderExcludePatterns: Map<string, AbsoluteAndRelativeParsedExpression>;
61
	private globalExcludePattern: glob.ParsedExpression | undefined;
62

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

R
Rob Lourens 已提交
65
	constructor(config: IFileQuery) {
E
Erich Gamma 已提交
66
		this.config = config;
67
		this.filePattern = config.filePattern || '';
C
Christof Marti 已提交
68
		this.includePattern = config.includePattern && glob.parse(config.includePattern);
E
Erich Gamma 已提交
69
		this.maxResults = config.maxResults || null;
70
		this.exists = !!config.exists;
E
Erich Gamma 已提交
71
		this.walkedPaths = Object.create(null);
72 73
		this.resultCount = 0;
		this.isLimitHit = false;
C
chrmarti 已提交
74 75
		this.directoriesWalked = 0;
		this.filesWalked = 0;
C
Christof Marti 已提交
76
		this.errors = [];
B
Benjamin Pasero 已提交
77

78 79
		if (this.filePattern) {
			this.normalizedFilePatternLowercase = strings.stripWildcards(this.filePattern).toLowerCase();
B
Benjamin Pasero 已提交
80
		}
81 82

		this.globalExcludePattern = config.excludePattern && glob.parse(config.excludePattern);
83
		this.folderExcludePatterns = new Map<string, AbsoluteAndRelativeParsedExpression>();
84 85

		config.folderQueries.forEach(folderQuery => {
86
			const folderExcludeExpression: glob.IExpression = objects.assign({}, folderQuery.excludePattern || {}, this.config.excludePattern || {});
87 88

			// Add excludes for other root folders
89
			const fqPath = folderQuery.folder.fsPath;
90
			config.folderQueries
91 92
				.map(rootFolderQuery => rootFolderQuery.folder.fsPath)
				.filter(rootFolder => rootFolder !== fqPath)
93 94
				.forEach(otherRootFolder => {
					// Exclude nested root folders
95 96
					if (isEqualOrParent(otherRootFolder, fqPath)) {
						folderExcludeExpression[path.relative(fqPath, otherRootFolder)] = true;
97
					}
98 99
				});

100
			this.folderExcludePatterns.set(fqPath, new AbsoluteAndRelativeParsedExpression(folderExcludeExpression, fqPath));
101
		});
E
Erich Gamma 已提交
102 103
	}

R
Rob Lourens 已提交
104
	cancel(): void {
E
Erich Gamma 已提交
105 106 107
		this.isCanceled = true;
	}

108
	walk(folderQueries: IFolderQuery[], extraFiles: URI[], onResult: (result: IRawFileMatch) => void, onMessage: (message: IProgressMessage) => void, done: (error: Error | null, isLimitHit: boolean) => void): void {
109
		this.fileWalkSW = StopWatch.create(false);
E
Erich Gamma 已提交
110

111
		// Support that the file pattern is a full path to a file that exists
112 113 114
		if (this.isCanceled) {
			return done(null, this.isLimitHit);
		}
E
Erich Gamma 已提交
115

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

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

R
Rob Lourens 已提交
127
		this.cmdSW = StopWatch.create(false);
C
Christof Marti 已提交
128

129
		// For each root folder
B
Benjamin Pasero 已提交
130
		this.parallel<IFolderQuery, void>(folderQueries, (folderQuery: IFolderQuery, rootFolderDone: (err: Error | null, result: void) => void) => {
R
Rob Lourens 已提交
131
			this.call(this.cmdTraversal, this, folderQuery, onResult, onMessage, (err?: Error) => {
132 133 134 135 136 137
				if (err) {
					const errorMessage = toErrorMessage(err);
					console.error(errorMessage);
					this.errors.push(errorMessage);
					rootFolderDone(err, undefined);
				} else {
138
					rootFolderDone(null, undefined);
139
				}
C
Christof Marti 已提交
140
			});
141
		}, (errors, result) => {
142
			this.fileWalkSW.stop();
M
Matt Bierner 已提交
143
			const err = errors ? arrays.coalesce(errors)[0] : null;
144
			done(err, this.isLimitHit);
C
Christof Marti 已提交
145 146
		});
	}
147

B
Benjamin Pasero 已提交
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
	private parallel<T, E>(list: T[], fn: (item: T, callback: (err: Error | null, result: E | null) => void) => void, callback: (err: Array<Error | null> | null, result: E[]) => void): void {
		const results = new Array(list.length);
		const errors = new Array<Error | null>(list.length);
		let didErrorOccur = false;
		let doneCount = 0;

		if (list.length === 0) {
			return callback(null, []);
		}

		list.forEach((item, index) => {
			fn(item, (error, result) => {
				if (error) {
					didErrorOccur = true;
					results[index] = null;
					errors[index] = error;
				} else {
					results[index] = result;
					errors[index] = null;
				}

				if (++doneCount === list.length) {
					return callback(didErrorOccur ? errors : null, results);
				}
			});
		});
	}

176
	private call<F extends Function>(fun: F, that: any, ...args: any[]): void {
C
Christof Marti 已提交
177 178 179 180 181 182 183
		try {
			fun.apply(that, args);
		} catch (e) {
			args[args.length - 1](e);
		}
	}

184
	private cmdTraversal(folderQuery: IFolderQuery, onResult: (result: IRawFileMatch) => void, onMessage: (message: IProgressMessage) => void, cb: (err?: Error) => void): void {
185
		const rootFolder = folderQuery.folder.fsPath;
186
		const isMac = platform.isMacintosh;
187 188
		let cmd: childProcess.ChildProcess;
		const killCmd = () => cmd && cmd.kill();
189
		killCmds.add(killCmd);
190

191
		let done = (err?: Error) => {
192
			killCmds.delete(killCmd);
J
Johannes Rieken 已提交
193
			done = () => { };
194 195 196 197
			cb(err);
		};
		let leftover = '';
		const tree = this.initDirectoryTree();
198 199

		let noSiblingsClauses: boolean;
R
Rob Lourens 已提交
200 201 202 203 204 205 206 207 208 209 210
		const ripgrep = spawnRipgrepCmd(this.config, folderQuery, this.config.includePattern, this.folderExcludePatterns.get(folderQuery.folder.fsPath)!.expression);
		cmd = ripgrep.cmd;
		noSiblingsClauses = !Object.keys(ripgrep.siblingClauses).length;

		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)}`;
211
		}
R
Rob Lourens 已提交
212
		onMessage({ message: rgCmd });
213

214
		this.cmdResultCount = 0;
R
Rob Lourens 已提交
215
		this.collectStdout(cmd, 'utf8', onMessage, (err: Error, stdout?: string, last?: boolean) => {
C
Christof Marti 已提交
216 217 218 219
			if (err) {
				done(err);
				return;
			}
220
			if (this.isLimitHit) {
221
				done();
222 223
				return;
			}
E
Erich Gamma 已提交
224

C
Christof Marti 已提交
225
			// Mac: uses NFD unicode form on disk, but we want NFC
226
			const normalized = leftover + (isMac ? normalization.normalizeNFC(stdout || '') : stdout);
R
Rob Lourens 已提交
227
			const relativeFiles = normalized.split('\n');
C
Christof Marti 已提交
228

229 230 231 232 233 234 235
			if (last) {
				const n = relativeFiles.length;
				relativeFiles[n - 1] = relativeFiles[n - 1].trim();
				if (!relativeFiles[n - 1]) {
					relativeFiles.pop();
				}
			} else {
236
				leftover = relativeFiles.pop() || '';
C
Christof Marti 已提交
237 238
			}

239 240 241 242 243
			if (relativeFiles.length && relativeFiles[0].indexOf('\n') !== -1) {
				done(new Error('Splitting up files failed'));
				return;
			}

244 245
			this.cmdResultCount += relativeFiles.length;

R
Rob Lourens 已提交
246
			if (noSiblingsClauses) {
247 248 249
				for (const relativePath of relativeFiles) {
					const basename = path.basename(relativePath);
					this.matchFile(onResult, { base: rootFolder, relativePath, basename });
250 251 252 253
					if (this.isLimitHit) {
						killCmd();
						break;
					}
254
				}
255
				if (last || this.isLimitHit) {
256
					done();
257
				}
258

259 260 261
				return;
			}

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

265 266 267
			if (last) {
				this.matchDirectoryTree(tree, rootFolder, onResult);
				done();
268
			}
C
Christof Marti 已提交
269 270 271
		});
	}

272 273 274
	/**
	 * Public for testing.
	 */
R
Rob Lourens 已提交
275
	spawnFindCmd(folderQuery: IFolderQuery) {
276
		const excludePattern = this.folderExcludePatterns.get(folderQuery.folder.fsPath)!;
277 278
		const basenames = excludePattern.getBasenameTerms();
		const pathTerms = excludePattern.getPathTerms();
279
		const args = ['-L', '.'];
280
		if (basenames.length || pathTerms.length) {
281
			args.push('-not', '(', '(');
282
			for (const basename of basenames) {
283
				args.push('-name', basename);
284 285
				args.push('-o');
			}
286
			for (const path of pathTerms) {
287
				args.push('-path', path);
288
				args.push('-o');
289
			}
290
			args.pop();
291 292 293
			args.push(')', '-prune', ')');
		}
		args.push('-type', 'f');
294
		return childProcess.spawn('find', args, { cwd: folderQuery.folder.fsPath });
295 296 297 298 299
	}

	/**
	 * Public for testing.
	 */
R
Rob Lourens 已提交
300
	readStdout(cmd: childProcess.ChildProcess, encoding: string, cb: (err: Error | null, stdout?: string) => void): void {
301
		let all = '';
R
Rob Lourens 已提交
302
		this.collectStdout(cmd, encoding, () => { }, (err: Error, stdout?: string, last?: boolean) => {
303 304 305 306 307 308 309 310 311 312 313 314
			if (err) {
				cb(err);
				return;
			}

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

315
	private collectStdout(cmd: childProcess.ChildProcess, encoding: string, onMessage: (message: IProgressMessage) => void, cb: (err: Error | null, stdout?: string, last?: boolean) => void): void {
316
		let onData = (err: Error | null, stdout?: string, last?: boolean) => {
317
			if (err || last) {
318
				onData = () => { };
319 320 321 322

				if (this.cmdSW) {
					this.cmdSW.stop();
				}
323 324
			}
			cb(err, stdout, last);
C
Christof Marti 已提交
325 326
		};

327
		let gotData = false;
328 329 330
		if (cmd.stdout) {
			// Should be non-null, but #38195
			this.forwardData(cmd.stdout, encoding, onData);
331
			cmd.stdout.once('data', () => gotData = true);
332 333 334 335
		} else {
			onMessage({ message: 'stdout is null' });
		}

336 337 338 339 340 341 342
		let stderr: Buffer[];
		if (cmd.stderr) {
			// Should be non-null, but #38195
			stderr = this.collectData(cmd.stderr);
		} else {
			onMessage({ message: 'stderr is null' });
		}
343

344
		cmd.on('error', (err: Error) => {
345
			onData(err);
C
Christof Marti 已提交
346 347
		});

348
		cmd.on('close', (code: number) => {
349
			// ripgrep returns code=1 when no results are found
350
			let stderrText: string;
R
Rob Lourens 已提交
351
			if (!gotData && (stderrText = this.decodeData(stderr, encoding)) && rgErrorMsgForDisplay(stderrText)) {
352
				onData(new Error(`command failed with error code ${code}: ${this.decodeData(stderr, encoding)}`));
C
Christof Marti 已提交
353
			} else {
R
Rob Lourens 已提交
354
				if (this.exists && code === 0) {
355 356
					this.isLimitHit = true;
				}
357
				onData(null, '', true);
C
Christof Marti 已提交
358 359 360 361
			}
		});
	}

362
	private forwardData(stream: Readable, encoding: string, cb: (err: Error | null, stdout?: string) => void): NodeStringDecoder {
363 364 365 366 367 368 369
		const decoder = new StringDecoder(encoding);
		stream.on('data', (data: Buffer) => {
			cb(null, decoder.write(data));
		});
		return decoder;
	}

C
Christof Marti 已提交
370
	private collectData(stream: Readable): Buffer[] {
371 372
		const buffers: Buffer[] = [];
		stream.on('data', (data: Buffer) => {
C
Christof Marti 已提交
373 374 375 376 377
			buffers.push(data);
		});
		return buffers;
	}

C
Christof Marti 已提交
378 379 380 381 382
	private decodeData(buffers: Buffer[], encoding: string): string {
		const decoder = new StringDecoder(encoding);
		return buffers.map(buffer => decoder.write(buffer)).join('');
	}

383 384 385 386 387 388 389 390 391
	private initDirectoryTree(): IDirectoryTree {
		const tree: IDirectoryTree = {
			rootEntries: [],
			pathToEntries: Object.create(null)
		};
		tree.pathToEntries['.'] = tree.rootEntries;
		return tree;
	}

392
	private addDirectoryEntries({ pathToEntries }: IDirectoryTree, base: string, relativeFiles: string[], onResult: (result: IRawFileMatch) => void) {
C
Christof Marti 已提交
393 394
		// Support relative paths to files from a root resource (ignores excludes)
		if (relativeFiles.indexOf(this.filePattern) !== -1) {
395
			const basename = path.basename(this.filePattern);
396
			this.matchFile(onResult, { base: base, relativePath: this.filePattern, basename });
C
Christof Marti 已提交
397 398
		}

399
		function add(relativePath: string) {
400 401
			const basename = path.basename(relativePath);
			const dirname = path.dirname(relativePath);
C
Christof Marti 已提交
402 403 404 405 406 407
			let entries = pathToEntries[dirname];
			if (!entries) {
				entries = pathToEntries[dirname] = [];
				add(dirname);
			}
			entries.push({
408
				base,
C
Christof Marti 已提交
409 410 411
				relativePath,
				basename
			});
412 413
		}
		relativeFiles.forEach(add);
C
Christof Marti 已提交
414 415
	}

416
	private matchDirectoryTree({ rootEntries, pathToEntries }: IDirectoryTree, rootFolder: string, onResult: (result: IRawFileMatch) => void) {
C
Christof Marti 已提交
417
		const self = this;
418
		const excludePattern = this.folderExcludePatterns.get(rootFolder)!;
C
Christof Marti 已提交
419 420 421
		const filePattern = this.filePattern;
		function matchDirectory(entries: IDirectoryEntry[]) {
			self.directoriesWalked++;
422
			const hasSibling = glob.hasSiblingFn(() => entries.map(entry => entry.basename));
C
Christof Marti 已提交
423
			for (let i = 0, n = entries.length; i < n; i++) {
424
				const entry = entries[i];
425
				const { relativePath, basename } = entry;
C
Christof Marti 已提交
426 427 428 429 430

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

C
Christof Marti 已提交
435 436 437 438 439 440 441 442 443
				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
					}

444
					self.matchFile(onResult, entry);
C
Christof Marti 已提交
445
				}
446 447 448 449

				if (self.isLimitHit) {
					break;
				}
450
			}
C
Christof Marti 已提交
451 452 453 454
		}
		matchDirectory(rootEntries);
	}

R
Rob Lourens 已提交
455
	getStats(): ISearchEngineStats {
C
chrmarti 已提交
456
		return {
R
Rob Lourens 已提交
457
			cmdTime: this.cmdSW && this.cmdSW.elapsed(),
458
			fileWalkTime: this.fileWalkSW.elapsed(),
C
chrmarti 已提交
459
			directoriesWalked: this.directoriesWalked,
460
			filesWalked: this.filesWalked,
C
Christof Marti 已提交
461
			cmdResultCount: this.cmdResultCount
C
chrmarti 已提交
462 463 464
		};
	}

465
	private doWalk(folderQuery: IFolderQuery, relativeParentPath: string, files: string[], onResult: (result: IRawFileMatch) => void, done: (error?: Error) => void): void {
466
		const rootFolder = folderQuery.folder;
E
Erich Gamma 已提交
467 468

		// Execute tasks on each file in parallel to optimize throughput
469
		const hasSibling = glob.hasSiblingFn(() => files);
B
Benjamin Pasero 已提交
470
		this.parallel(files, (file: string, clb: (error: Error | null, _?: any) => void): void => {
E
Erich Gamma 已提交
471 472 473

			// Check canceled
			if (this.isCanceled || this.isLimitHit) {
474
				return clb(null);
E
Erich Gamma 已提交
475 476
			}

477
			// Check exclude pattern
E
Erich Gamma 已提交
478 479 480
			// 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
481
			const currentRelativePath = relativeParentPath ? [relativeParentPath, file].join(path.sep) : file;
482
			if (this.folderExcludePatterns.get(folderQuery.folder.fsPath)!.test(currentRelativePath, file, this.config.filePattern !== file ? hasSibling : undefined)) {
483
				return clb(null);
E
Erich Gamma 已提交
484 485
			}

486
			// Use lstat to detect links
487
			const currentAbsolutePath = [rootFolder.fsPath, currentRelativePath].join(path.sep);
488
			fs.lstat(currentAbsolutePath, (error, lstat) => {
489
				if (error || this.isCanceled || this.isLimitHit) {
490
					return clb(null);
491
				}
E
Erich Gamma 已提交
492

493
				// If the path is a link, we must instead use fs.stat() to find out if the
494 495 496 497
				// 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) {
498
						return clb(null);
499
					}
E
Erich Gamma 已提交
500

501 502
					// Directory: Follow directories
					if (stat.isDirectory()) {
C
chrmarti 已提交
503
						this.directoriesWalked++;
504

505 506
						// to really prevent loops with links we need to resolve the real path of them
						return this.realPathIfNeeded(currentAbsolutePath, lstat, (error, realpath) => {
507
							if (error || this.isCanceled || this.isLimitHit) {
508
								return clb(null);
509 510
							}

511
							realpath = realpath || '';
512
							if (this.walkedPaths[realpath]) {
513
								return clb(null); // escape when there are cycles (can happen with symlinks)
514
							}
E
Erich Gamma 已提交
515

516 517 518
							this.walkedPaths[realpath] = true; // remember as walked

							// Continue walking
B
Benjamin Pasero 已提交
519 520
							return readdir(currentAbsolutePath).then(children => {
								if (this.isCanceled || this.isLimitHit) {
521
									return clb(null);
522 523
								}

524
								this.doWalk(folderQuery, currentRelativePath, children, onResult, err => clb(err || null));
B
Benjamin Pasero 已提交
525 526
							}, error => {
								clb(null);
527 528
							});
						});
529
					}
E
Erich Gamma 已提交
530

531 532
					// File: Check for match on file pattern and include pattern
					else {
C
chrmarti 已提交
533
						this.filesWalked++;
C
Christof Marti 已提交
534
						if (currentRelativePath === this.filePattern) {
535
							return clb(null, undefined); // ignore file if its path matches with the file pattern because checkFilePatternRelativeMatch() takes care of those
536
						}
E
Erich Gamma 已提交
537

538
						if (this.maxFilesize && types.isNumber(stat.size) && stat.size > this.maxFilesize) {
539
							return clb(null, undefined); // ignore file if max file size is hit
540 541
						}

542
						this.matchFile(onResult, { base: rootFolder.fsPath, relativePath: currentRelativePath, basename: file, size: stat.size });
543 544 545
					}

					// Unwind
546
					return clb(null, undefined);
547
				});
E
Erich Gamma 已提交
548 549 550 551 552 553
			});
		}, (error: Error[]): void => {
			if (error) {
				error = arrays.coalesce(error); // find any error by removing null values first
			}

554
			return done(error && error.length > 0 ? error[0] : undefined);
E
Erich Gamma 已提交
555 556 557
		});
	}

558 559
	private matchFile(onResult: (result: IRawFileMatch) => void, candidate: IRawFileMatch): void {
		if (this.isFilePatternMatch(candidate.relativePath) && (!this.includePattern || this.includePattern(candidate.relativePath, candidate.basename))) {
560 561
			this.resultCount++;

562
			if (this.exists || (this.maxResults && this.resultCount > this.maxResults)) {
563 564 565 566
				this.isLimitHit = true;
			}

			if (!this.isLimitHit) {
567
				onResult(candidate);
568 569 570 571
			}
		}
	}

B
polish  
Benjamin Pasero 已提交
572
	private isFilePatternMatch(path: string): boolean {
573 574 575

		// Check for search pattern
		if (this.filePattern) {
576 577 578 579
			if (this.filePattern === '*') {
				return true; // support the all-matching wildcard
			}

580
			return strings.fuzzyContains(path, this.normalizedFilePatternLowercase);
581 582 583 584 585 586
		}

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

587
	private statLinkIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error | null, stat: fs.Stats) => void): void {
588
		if (lstat.isSymbolicLink()) {
589
			return fs.stat(path, clb); // stat the target the link points to
590 591 592 593 594
		}

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

595
	private realPathIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error | null, realpath?: string) => void): void {
596 597 598 599 600
		if (lstat.isSymbolicLink()) {
			return fs.realpath(path, (error, realpath) => {
				if (error) {
					return clb(error);
				}
E
Erich Gamma 已提交
601

602 603 604
				return clb(null, realpath);
			});
		}
E
Erich Gamma 已提交
605

606
		return clb(null, path);
E
Erich Gamma 已提交
607 608 609
	}
}

610
export class Engine implements ISearchEngine<IRawFileMatch> {
611 612
	private folderQueries: IFolderQuery[];
	private extraFiles: URI[];
E
Erich Gamma 已提交
613 614
	private walker: FileWalker;

615
	constructor(config: IFileQuery) {
616
		this.folderQueries = config.folderQueries;
617
		this.extraFiles = config.extraFileResources || [];
618

E
Erich Gamma 已提交
619 620 621
		this.walker = new FileWalker(config);
	}

622
	search(onResult: (result: IRawFileMatch) => void, onProgress: (progress: IProgressMessage) => void, done: (error: Error, complete: ISearchEngineSuccess) => void): void {
623
		this.walker.walk(this.folderQueries, this.extraFiles, onResult, onProgress, (err: Error, isLimitHit: boolean) => {
C
chrmarti 已提交
624 625 626 627 628
			done(err, {
				limitHit: isLimitHit,
				stats: this.walker.getStats()
			});
		});
E
Erich Gamma 已提交
629 630
	}

R
Rob Lourens 已提交
631
	cancel(): void {
E
Erich Gamma 已提交
632 633
		this.walker.cancel();
	}
634 635 636 637 638 639 640 641
}

/**
 * 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 {
642 643
	private absoluteParsedExpr: glob.ParsedExpression | undefined;
	private relativeParsedExpr: glob.ParsedExpression | undefined;
644

C
Christof Marti 已提交
645 646
	constructor(public expression: glob.IExpression, private root: string) {
		this.init(expression);
647 648 649 650 651 652
	}

	/**
	 * Split the IExpression into its absolute and relative components, and glob.parse them separately.
	 */
	private init(expr: glob.IExpression): void {
653 654
		let absoluteGlobExpr: glob.IExpression | undefined;
		let relativeGlobExpr: glob.IExpression | undefined;
655 656 657 658 659
		Object.keys(expr)
			.filter(key => expr[key])
			.forEach(key => {
				if (path.isAbsolute(key)) {
					absoluteGlobExpr = absoluteGlobExpr || glob.getEmptyExpression();
660
					absoluteGlobExpr[key] = expr[key];
661 662
				} else {
					relativeGlobExpr = relativeGlobExpr || glob.getEmptyExpression();
663
					relativeGlobExpr[key] = expr[key];
664 665
				}
			});
666 667 668 669 670

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

R
Rob Lourens 已提交
671
	test(_path: string, basename?: string, hasSibling?: (name: string) => boolean | Promise<boolean>): string | Promise<string | null> | undefined | null {
672 673
		return (this.relativeParsedExpr && this.relativeParsedExpr(_path, basename, hasSibling)) ||
			(this.absoluteParsedExpr && this.absoluteParsedExpr(path.join(this.root, _path), basename, hasSibling));
674 675
	}

R
Rob Lourens 已提交
676
	getBasenameTerms(): string[] {
M
Matt Bierner 已提交
677
		const basenameTerms: string[] = [];
678 679 680 681 682 683 684 685 686 687 688
		if (this.absoluteParsedExpr) {
			basenameTerms.push(...glob.getBasenameTerms(this.absoluteParsedExpr));
		}

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

		return basenameTerms;
	}

R
Rob Lourens 已提交
689
	getPathTerms(): string[] {
M
Matt Bierner 已提交
690
		const pathTerms: string[] = [];
691 692 693 694 695 696 697 698 699 700
		if (this.absoluteParsedExpr) {
			pathTerms.push(...glob.getPathTerms(this.absoluteParsedExpr));
		}

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

		return pathTerms;
	}
701
}
R
Rob Lourens 已提交
702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732

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