fileSearch.ts 8.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 14
import arrays = require('vs/base/common/arrays');
import strings = require('vs/base/common/strings');
import glob = require('vs/base/common/glob');
B
Benjamin Pasero 已提交
15
import {IProgress} from 'vs/platform/search/common/search';
E
Erich Gamma 已提交
16 17 18 19 20 21 22

import extfs = require('vs/base/node/extfs');
import flow = require('vs/base/node/flow');
import {ISerializedFileMatch, IRawSearch, ISearchEngine} from 'vs/workbench/services/search/node/rawSearchService';

export class FileWalker {
	private config: IRawSearch;
23
	private filePattern: string;
24
	private normalizedFilePatternLowercase: string;
E
Erich Gamma 已提交
25 26 27 28 29 30
	private excludePattern: glob.IExpression;
	private includePattern: glob.IExpression;
	private maxResults: number;
	private isLimitHit: boolean;
	private resultCount: number;
	private isCanceled: boolean;
31
	private searchInPath: boolean;
E
Erich Gamma 已提交
32 33 34 35 36

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

	constructor(config: IRawSearch) {
		this.config = config;
37
		this.filePattern = config.filePattern;
E
Erich Gamma 已提交
38 39 40 41
		this.excludePattern = config.excludePattern;
		this.includePattern = config.includePattern;
		this.maxResults = config.maxResults || null;
		this.walkedPaths = Object.create(null);
42 43
		this.resultCount = 0;
		this.isLimitHit = false;
B
Benjamin Pasero 已提交
44

45
		if (this.filePattern) {
B
Benjamin Pasero 已提交
46
			this.filePattern = strings.replaceAll(this.filePattern, '\\', '/'); // Normalize file patterns to forward slashes
47
			this.normalizedFilePatternLowercase = strings.stripWildcards(this.filePattern).toLowerCase();
B
Benjamin Pasero 已提交
48
		}
E
Erich Gamma 已提交
49 50 51 52 53 54
	}

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

55
	public walk(rootFolders: string[], extraFiles: string[], onResult: (result: ISerializedFileMatch) => void, done: (error: Error, isLimitHit: boolean) => void): void {
E
Erich Gamma 已提交
56

57
		// Support that the file pattern is a full path to a file that exists
58
		this.checkFilePatternAbsoluteMatch((exists) => {
59 60 61
			if (this.isCanceled) {
				return done(null, this.isLimitHit);
			}
E
Erich Gamma 已提交
62

63 64 65
			// Report result from file pattern if matching
			if (exists) {
				onResult({ path: this.filePattern });
66 67 68 69

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

73 74 75 76 77
			// For each extra file
			if (extraFiles) {
				extraFiles.forEach(extraFilePath => {
					if (glob.match(this.excludePattern, extraFilePath)) {
						return; // excluded
E
Erich Gamma 已提交
78 79
					}

80
					// File: Check for match on file pattern and include pattern
B
polish  
Benjamin Pasero 已提交
81
					this.matchFile(onResult, extraFilePath, extraFilePath /* no workspace relative path */);
82 83
				});
			}
84

85 86 87 88 89
			// For each root folder
			flow.parallel(rootFolders, (absolutePath, perEntryCallback) => {
				extfs.readdir(absolutePath, (error: Error, files: string[]) => {
					if (error || this.isCanceled || this.isLimitHit) {
						return perEntryCallback(null, null);
90 91
					}

92 93 94 95
					// Support relative paths to files from a root resource
					return this.checkFilePatternRelativeMatch(absolutePath, (match) => {
						if (this.isCanceled || this.isLimitHit) {
							return perEntryCallback(null, null);
E
Erich Gamma 已提交
96 97
						}

98 99 100
						// Report result from file pattern if matching
						if (match) {
							onResult({ path: match });
E
Erich Gamma 已提交
101 102
						}

B
Benjamin Pasero 已提交
103
						return this.doWalk(paths.normalize(absolutePath), '', files, onResult, perEntryCallback);
104
					});
105 106 107
				});
			}, (err, result) => {
				done(err ? err[0] : null, this.isLimitHit);
E
Erich Gamma 已提交
108
			});
109 110 111
		});
	}

112
	private checkFilePatternAbsoluteMatch(clb: (exists: boolean) => void): void {
113 114 115 116 117 118
		if (!this.filePattern || !paths.isAbsolute(this.filePattern)) {
			return clb(false);
		}

		return fs.stat(this.filePattern, (error, stat) => {
			return clb(!error && !stat.isDirectory()); // only existing files
E
Erich Gamma 已提交
119 120 121
		});
	}

122
	private checkFilePatternRelativeMatch(basePath: string, clb: (matchPath: string) => void): void {
B
polish  
Benjamin Pasero 已提交
123
		if (!this.filePattern || paths.isAbsolute(this.filePattern)) {
124 125 126 127 128 129 130 131 132 133
			return clb(null);
		}

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

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

B
Benjamin Pasero 已提交
134
	private doWalk(absolutePath: string, relativeParentPathWithSlashes: string, files: string[], onResult: (result: ISerializedFileMatch) => void, done: (error: Error, result: any) => void): void {
E
Erich Gamma 已提交
135 136 137 138 139 140 141 142 143 144 145 146 147

		// 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;
148
			if (this.config.filePattern === file) {
E
Erich Gamma 已提交
149 150 151 152
				siblings = [];
			}

			// Check exclude pattern
B
Benjamin Pasero 已提交
153 154
			let currentRelativePathWithSlashes = relativeParentPathWithSlashes ? [relativeParentPathWithSlashes, file].join('/') : file;
			if (glob.match(this.excludePattern, currentRelativePathWithSlashes, siblings)) {
E
Erich Gamma 已提交
155 156 157
				return clb(null);
			}

158
			// Use lstat to detect links
B
Benjamin Pasero 已提交
159 160
			let currentAbsolutePath = [absolutePath, file].join(paths.sep);
			fs.lstat(currentAbsolutePath, (error, lstat) => {
161 162 163
				if (error || this.isCanceled || this.isLimitHit) {
					return clb(null);
				}
E
Erich Gamma 已提交
164

165 166
				// Directory: Follow directories
				if (lstat.isDirectory()) {
E
Erich Gamma 已提交
167 168

					// to really prevent loops with links we need to resolve the real path of them
B
Benjamin Pasero 已提交
169
					return this.realPathIfNeeded(currentAbsolutePath, lstat, (error, realpath) => {
170 171
						if (error || this.isCanceled || this.isLimitHit) {
							return clb(null);
E
Erich Gamma 已提交
172 173 174 175 176 177
						}

						if (this.walkedPaths[realpath]) {
							return clb(null); // escape when there are cycles (can happen with symlinks)
						}

178 179
						this.walkedPaths[realpath] = true; // remember as walked

E
Erich Gamma 已提交
180
						// Continue walking
B
Benjamin Pasero 已提交
181
						return extfs.readdir(currentAbsolutePath, (error: Error, children: string[]): void => {
182 183 184 185
							if (error || this.isCanceled || this.isLimitHit) {
								return clb(null);
							}

B
Benjamin Pasero 已提交
186
							this.doWalk(currentAbsolutePath, currentRelativePathWithSlashes, children, onResult, clb);
187
						});
E
Erich Gamma 已提交
188 189 190
					});
				}

191
				// File: Check for match on file pattern and include pattern
192
				else {
B
Benjamin Pasero 已提交
193
					if (currentRelativePathWithSlashes === this.filePattern) {
194
						return clb(null); // ignore file if its path matches with the file pattern because checkFilePatternRelativeMatch() takes care of those
195
					}
E
Erich Gamma 已提交
196

B
Benjamin Pasero 已提交
197
					this.matchFile(onResult, currentAbsolutePath, currentRelativePathWithSlashes);
E
Erich Gamma 已提交
198 199 200 201 202 203 204 205 206 207 208 209 210 211
				}

				// Unwind
				return clb(null);
			});
		}, (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);
		});
	}

B
Benjamin Pasero 已提交
212 213
	private matchFile(onResult: (result: ISerializedFileMatch) => void, absolutePath: string, relativePathWithSlashes: string): void {
		if (this.isFilePatternMatch(relativePathWithSlashes) && (!this.includePattern || glob.match(this.includePattern, relativePathWithSlashes))) {
214 215 216 217 218 219 220 221 222 223 224 225 226 227
			this.resultCount++;

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

			if (!this.isLimitHit) {
				onResult({
					path: absolutePath
				});
			}
		}
	}

B
polish  
Benjamin Pasero 已提交
228
	private isFilePatternMatch(path: string): boolean {
229 230 231

		// Check for search pattern
		if (this.filePattern) {
232
			return scorer.matches(path, this.normalizedFilePatternLowercase);
233 234 235 236 237 238
		}

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

239 240 241 242 243 244
	private realPathIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error, realpath?: string) => void): void {
		if (lstat.isSymbolicLink()) {
			return fs.realpath(path, (error, realpath) => {
				if (error) {
					return clb(error);
				}
E
Erich Gamma 已提交
245

246 247 248
				return clb(null, realpath);
			});
		}
E
Erich Gamma 已提交
249

250
		return clb(null, path);
E
Erich Gamma 已提交
251 252 253 254
	}
}

export class Engine implements ISearchEngine {
255 256
	private rootFolders: string[];
	private extraFiles: string[];
E
Erich Gamma 已提交
257 258 259
	private walker: FileWalker;

	constructor(config: IRawSearch) {
260 261 262
		this.rootFolders = config.rootFolders;
		this.extraFiles = config.extraFiles;

E
Erich Gamma 已提交
263 264 265 266
		this.walker = new FileWalker(config);
	}

	public search(onResult: (result: ISerializedFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, isLimitHit: boolean) => void): void {
267
		this.walker.walk(this.rootFolders, this.extraFiles, onResult, done);
E
Erich Gamma 已提交
268 269 270 271 272
	}

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