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

'use strict';

import fs = require('fs');
import paths = require('path');

11
import scorer = require('vs/base/common/scorer');
E
Erich Gamma 已提交
12 13
import arrays = require('vs/base/common/arrays');
import strings = require('vs/base/common/strings');
14
import types = require('vs/base/common/types');
E
Erich Gamma 已提交
15
import glob = require('vs/base/common/glob');
16
import {IProgress, IUncachedSearchStats} from 'vs/platform/search/common/search';
E
Erich Gamma 已提交
17 18 19

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

export class FileWalker {
	private config: IRawSearch;
24
	private filePattern: string;
25
	private normalizedFilePatternLowercase: string;
E
Erich Gamma 已提交
26 27 28
	private excludePattern: glob.IExpression;
	private includePattern: glob.IExpression;
	private maxResults: number;
29
	private maxFilesize: number;
E
Erich Gamma 已提交
30 31 32
	private isLimitHit: boolean;
	private resultCount: number;
	private isCanceled: boolean;
C
chrmarti 已提交
33 34 35
	private fileWalkStartTime: number;
	private directoriesWalked: number;
	private filesWalked: number;
E
Erich Gamma 已提交
36 37 38 39 40

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

	constructor(config: IRawSearch) {
		this.config = config;
41
		this.filePattern = config.filePattern;
E
Erich Gamma 已提交
42 43 44
		this.excludePattern = config.excludePattern;
		this.includePattern = config.includePattern;
		this.maxResults = config.maxResults || null;
45
		this.maxFilesize = config.maxFilesize || null;
E
Erich Gamma 已提交
46
		this.walkedPaths = Object.create(null);
47 48
		this.resultCount = 0;
		this.isLimitHit = false;
C
chrmarti 已提交
49 50
		this.directoriesWalked = 0;
		this.filesWalked = 0;
B
Benjamin Pasero 已提交
51

52
		if (this.filePattern) {
B
Benjamin Pasero 已提交
53
			this.filePattern = this.filePattern.replace(/\\/g, '/'); // Normalize file patterns to forward slashes
54
			this.normalizedFilePatternLowercase = strings.stripWildcards(this.filePattern).toLowerCase();
B
Benjamin Pasero 已提交
55
		}
E
Erich Gamma 已提交
56 57 58 59 60 61
	}

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

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

65
		// Support that the file pattern is a full path to a file that exists
66
		this.checkFilePatternAbsoluteMatch((exists, size) => {
67 68 69
			if (this.isCanceled) {
				return done(null, this.isLimitHit);
			}
E
Erich Gamma 已提交
70

71 72
			// Report result from file pattern if matching
			if (exists) {
73 74 75 76 77 78
				this.resultCount++;
				onResult({
					absolutePath: this.filePattern,
					pathLabel: this.filePattern,
					size
				});
79 80 81 82

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

86 87 88 89 90
			// For each extra file
			if (extraFiles) {
				extraFiles.forEach(extraFilePath => {
					if (glob.match(this.excludePattern, extraFilePath)) {
						return; // excluded
E
Erich Gamma 已提交
91 92
					}

93
					// File: Check for match on file pattern and include pattern
B
polish  
Benjamin Pasero 已提交
94
					this.matchFile(onResult, extraFilePath, extraFilePath /* no workspace relative path */);
95 96
				});
			}
97

98 99
			// For each root folder
			flow.parallel(rootFolders, (absolutePath, perEntryCallback) => {
C
chrmarti 已提交
100
				this.directoriesWalked++;
101 102 103
				extfs.readdir(absolutePath, (error: Error, files: string[]) => {
					if (error || this.isCanceled || this.isLimitHit) {
						return perEntryCallback(null, null);
104 105
					}

106
					// Support relative paths to files from a root resource
107
					return this.checkFilePatternRelativeMatch(absolutePath, (match, size) => {
108 109
						if (this.isCanceled || this.isLimitHit) {
							return perEntryCallback(null, null);
E
Erich Gamma 已提交
110 111
						}

112 113
						// Report result from file pattern if matching
						if (match) {
114 115 116 117 118 119
							this.resultCount++;
							onResult({
								absolutePath: match,
								pathLabel: this.filePattern,
								size
							});
E
Erich Gamma 已提交
120 121
						}

B
Benjamin Pasero 已提交
122
						return this.doWalk(paths.normalize(absolutePath), '', files, onResult, perEntryCallback);
123
					});
124 125 126
				});
			}, (err, result) => {
				done(err ? err[0] : null, this.isLimitHit);
E
Erich Gamma 已提交
127
			});
128 129 130
		});
	}

131
	public getStats(): IUncachedSearchStats {
C
chrmarti 已提交
132
		return {
133
			fromCache: false,
C
chrmarti 已提交
134 135 136
			fileWalkStartTime: this.fileWalkStartTime,
			fileWalkResultTime: Date.now(),
			directoriesWalked: this.directoriesWalked,
137 138
			filesWalked: this.filesWalked,
			resultCount: this.resultCount
C
chrmarti 已提交
139 140 141
		};
	}

142
	private checkFilePatternAbsoluteMatch(clb: (exists: boolean, size?: number) => void): void {
143 144 145 146
		if (!this.filePattern || !paths.isAbsolute(this.filePattern)) {
			return clb(false);
		}

147
		return fs.stat(this.filePattern, (error, stat) => {
148
			return clb(!error && !stat.isDirectory(), stat && stat.size); // only existing files
E
Erich Gamma 已提交
149 150 151
		});
	}

152
	private checkFilePatternRelativeMatch(basePath: string, clb: (matchPath: string, size?: number) => void): void {
B
polish  
Benjamin Pasero 已提交
153
		if (!this.filePattern || paths.isAbsolute(this.filePattern)) {
154 155 156 157 158
			return clb(null);
		}

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

159
		return fs.stat(absolutePath, (error, stat) => {
160
			return clb(!error && !stat.isDirectory() ? absolutePath : null, stat && stat.size); // only existing files
161 162 163
		});
	}

164
	private doWalk(absolutePath: string, relativeParentPathWithSlashes: string, files: string[], onResult: (result: IRawFileMatch) => void, done: (error: Error, result: any) => void): void {
E
Erich Gamma 已提交
165 166 167 168 169 170 171 172 173 174 175 176 177

		// 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;
178
			if (this.config.filePattern === file) {
E
Erich Gamma 已提交
179 180 181 182
				siblings = [];
			}

			// Check exclude pattern
B
Benjamin Pasero 已提交
183
			let currentRelativePathWithSlashes = relativeParentPathWithSlashes ? [relativeParentPathWithSlashes, file].join('/') : file;
184
			if (glob.match(this.excludePattern, currentRelativePathWithSlashes, () => siblings)) {
E
Erich Gamma 已提交
185 186 187
				return clb(null);
			}

188
			// Use lstat to detect links
B
Benjamin Pasero 已提交
189
			let currentAbsolutePath = [absolutePath, file].join(paths.sep);
190
			fs.lstat(currentAbsolutePath, (error, lstat) => {
191 192 193
				if (error || this.isCanceled || this.isLimitHit) {
					return clb(null);
				}
E
Erich Gamma 已提交
194

195
				// If the path is a link, we must instead use fs.stat() to find out if the
196 197 198 199 200 201
				// 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 已提交
202

203 204
					// Directory: Follow directories
					if (stat.isDirectory()) {
C
chrmarti 已提交
205
						this.directoriesWalked++;
206

207 208
						// to really prevent loops with links we need to resolve the real path of them
						return this.realPathIfNeeded(currentAbsolutePath, lstat, (error, realpath) => {
209 210 211 212
							if (error || this.isCanceled || this.isLimitHit) {
								return clb(null);
							}

213 214 215
							if (this.walkedPaths[realpath]) {
								return clb(null); // escape when there are cycles (can happen with symlinks)
							}
E
Erich Gamma 已提交
216

217 218 219 220 221 222 223 224 225 226 227
							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);
								}

								this.doWalk(currentAbsolutePath, currentRelativePathWithSlashes, children, onResult, clb);
							});
						});
228
					}
E
Erich Gamma 已提交
229

230 231
					// File: Check for match on file pattern and include pattern
					else {
C
chrmarti 已提交
232
						this.filesWalked++;
233 234 235
						if (currentRelativePathWithSlashes === this.filePattern) {
							return clb(null); // ignore file if its path matches with the file pattern because checkFilePatternRelativeMatch() takes care of those
						}
E
Erich Gamma 已提交
236

237 238 239 240
						if (this.maxFilesize && types.isNumber(stat.size) && stat.size > this.maxFilesize) {
							return clb(null); // ignore file if max file size is hit
						}

241
						this.matchFile(onResult, currentAbsolutePath, currentRelativePathWithSlashes, stat.size);
242 243 244 245 246
					}

					// Unwind
					return clb(null);
				});
E
Erich Gamma 已提交
247 248 249 250 251 252 253 254 255 256
			});
		}, (error: Error[]): void => {
			if (error) {
				error = arrays.coalesce(error); // find any error by removing null values first
			}

			return done(error && error.length > 0 ? error[0] : null, null);
		});
	}

257
	private matchFile(onResult: (result: IRawFileMatch) => void, absolutePath: string, relativePathWithSlashes: string, size?: number): void {
B
Benjamin Pasero 已提交
258
		if (this.isFilePatternMatch(relativePathWithSlashes) && (!this.includePattern || glob.match(this.includePattern, relativePathWithSlashes))) {
259 260 261 262 263 264 265 266
			this.resultCount++;

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

			if (!this.isLimitHit) {
				onResult({
267 268 269 270
					absolutePath,
					pathLabel: relativePathWithSlashes,
					size
				});
271 272 273 274
			}
		}
	}

B
polish  
Benjamin Pasero 已提交
275
	private isFilePatternMatch(path: string): boolean {
276 277 278

		// Check for search pattern
		if (this.filePattern) {
279 280 281 282
			if (this.filePattern === '*') {
				return true; // support the all-matching wildcard
			}

283
			return scorer.matches(path, this.normalizedFilePatternLowercase);
284 285 286 287 288 289
		}

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

290
	private statLinkIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error, stat: fs.Stats) => void): void {
291
		if (lstat.isSymbolicLink()) {
292
			return fs.stat(path, clb); // stat the target the link points to
293 294 295 296 297
		}

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

298
	private realPathIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error, realpath?: string) => void): void {
299 300 301 302 303
		if (lstat.isSymbolicLink()) {
			return fs.realpath(path, (error, realpath) => {
				if (error) {
					return clb(error);
				}
E
Erich Gamma 已提交
304

305 306 307
				return clb(null, realpath);
			});
		}
E
Erich Gamma 已提交
308

309
		return clb(null, path);
E
Erich Gamma 已提交
310 311 312
	}
}

313
export class Engine implements ISearchEngine<IRawFileMatch> {
314 315
	private rootFolders: string[];
	private extraFiles: string[];
E
Erich Gamma 已提交
316 317 318
	private walker: FileWalker;

	constructor(config: IRawSearch) {
319 320 321
		this.rootFolders = config.rootFolders;
		this.extraFiles = config.extraFiles;

E
Erich Gamma 已提交
322 323 324
		this.walker = new FileWalker(config);
	}

325
	public search(onResult: (result: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void {
C
chrmarti 已提交
326 327 328 329 330 331
		this.walker.walk(this.rootFolders, this.extraFiles, onResult, (err: Error, isLimitHit: boolean) => {
			done(err, {
				limitHit: isLimitHit,
				stats: this.walker.getStats()
			});
		});
E
Erich Gamma 已提交
332 333 334 335 336
	}

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