rawSearchService.ts 15.8 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.
 *--------------------------------------------------------------------------------------------*/

6 7
import * as fs from 'fs';
import * as gracefulFs from 'graceful-fs';
8
import { basename, dirname, join, sep } from 'vs/base/common/path';
9
import * as arrays from 'vs/base/common/arrays';
10
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
11
import { CancellationToken } from 'vs/base/common/cancellation';
12
import { canceled } from 'vs/base/common/errors';
13
import { Emitter, Event } from 'vs/base/common/event';
14
import * as objects from 'vs/base/common/objects';
15
import { StopWatch } from 'vs/base/common/stopwatch';
16
import * as strings from 'vs/base/common/strings';
17
import { URI, UriComponents } from 'vs/base/common/uri';
18
import { compareItemsByScore, IItemAccessor, prepareQuery, ScorerCache } from 'vs/base/common/fuzzyScorer';
19
import { MAX_FILE_SIZE } from 'vs/base/node/pfs';
20
import { ICachedSearchStats, IFileQuery, IFileSearchStats, IFolderQuery, IProgressMessage, IRawFileQuery, IRawQuery, IRawTextQuery, ITextQuery, IFileSearchProgressItem, IRawFileMatch, IRawSearchService, ISearchEngine, ISearchEngineSuccess, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedSearchSuccess, isFilePatternMatch } from 'vs/workbench/services/search/common/search';
21
import { Engine as FileSearchEngine } from 'vs/workbench/services/search/node/fileSearch';
R
Rob Lourens 已提交
22
import { TextSearchEngineAdapter } from 'vs/workbench/services/search/node/textSearchAdapter';
23 24

gracefulFs.gracefulify(fs);
25

26 27
export type IProgressCallback = (p: ISerializedSearchProgressItem) => void;
export type IFileProgressCallback = (p: IFileSearchProgressItem) => void;
28

E
Erich Gamma 已提交
29 30
export class SearchService implements IRawSearchService {

31
	private static readonly BATCH_SIZE = 512;
C
chrmarti 已提交
32

33
	private caches: { [cacheKey: string]: Cache; } = Object.create(null);
E
Erich Gamma 已提交
34

R
Rob Lourens 已提交
35
	fileSearch(config: IRawFileQuery): Event<ISerializedSearchProgressItem | ISerializedSearchComplete> {
36
		let promise: CancelablePromise<ISerializedSearchSuccess>;
37

38
		const query = reviveQuery(config);
39
		const emitter = new Emitter<ISerializedSearchProgressItem | ISerializedSearchComplete>({
40
			onFirstListenerDidAdd: () => {
41
				promise = createCancelablePromise(token => {
42
					return this.doFileSearchWithEngine(FileSearchEngine, query, p => emitter.fire(p), token);
43
				});
44 45 46

				promise.then(
					c => emitter.fire(c),
47
					err => emitter.fire({ type: 'error', error: { message: err.message, stack: err.stack } }));
48 49 50 51 52
			},
			onLastListenerRemove: () => {
				promise.cancel();
			}
		});
C
Christof Marti 已提交
53

54
		return emitter.event;
E
Erich Gamma 已提交
55 56
	}

R
Rob Lourens 已提交
57
	textSearch(rawQuery: IRawTextQuery): Event<ISerializedSearchProgressItem | ISerializedSearchComplete> {
58
		let promise: CancelablePromise<ISerializedSearchComplete>;
59

60
		const query = reviveQuery(rawQuery);
61
		const emitter = new Emitter<ISerializedSearchProgressItem | ISerializedSearchComplete>({
62
			onFirstListenerDidAdd: () => {
63
				promise = createCancelablePromise(token => {
64
					return this.ripgrepTextSearch(query, p => emitter.fire(p), token);
65
				});
66 67 68

				promise.then(
					c => emitter.fire(c),
69
					err => emitter.fire({ type: 'error', error: { message: err.message, stack: err.stack } }));
70 71 72 73 74 75 76
			},
			onLastListenerRemove: () => {
				promise.cancel();
			}
		});

		return emitter.event;
77 78
	}

79 80
	private ripgrepTextSearch(config: ITextQuery, progressCallback: IProgressCallback, token: CancellationToken): Promise<ISerializedSearchSuccess> {
		config.maxFileSize = MAX_FILE_SIZE;
R
Rob Lourens 已提交
81
		const engine = new TextSearchEngineAdapter(config);
82

83
		return engine.search(token, progressCallback, progressCallback);
84 85
	}

86
	doFileSearch(config: IFileQuery, progressCallback: IProgressCallback, token?: CancellationToken): Promise<ISerializedSearchSuccess> {
87 88 89
		return this.doFileSearchWithEngine(FileSearchEngine, config, progressCallback, token);
	}

90
	doFileSearchWithEngine(EngineClass: { new(config: IFileQuery): ISearchEngine<IRawFileMatch>; }, config: IFileQuery, progressCallback: IProgressCallback, token?: CancellationToken, batchSize = SearchService.BATCH_SIZE): Promise<ISerializedSearchSuccess> {
91
		let resultCount = 0;
92 93
		const fileProgressCallback: IFileProgressCallback = progress => {
			if (Array.isArray(progress)) {
94
				resultCount += progress.length;
95 96
				progressCallback(progress.map(m => this.rawMatchToSearchItem(m)));
			} else if ((<IRawFileMatch>progress).relativePath) {
97
				resultCount++;
98 99
				progressCallback(this.rawMatchToSearchItem(<IRawFileMatch>progress));
			} else {
100
				progressCallback(<IProgressMessage>progress);
101 102
			}
		};
103 104

		if (config.sortByScore) {
105
			let sortedSearch = this.trySortedSearchFromCache(config, fileProgressCallback, token);
106 107 108
			if (!sortedSearch) {
				const walkerConfig = config.maxResults ? objects.assign({}, config, { maxResults: null }) : config;
				const engine = new EngineClass(walkerConfig);
109
				sortedSearch = this.doSortedSearch(engine, config, progressCallback, fileProgressCallback, token);
110 111
			}

112
			return new Promise<ISerializedSearchSuccess>((c, e) => {
R
Rob Lourens 已提交
113
				sortedSearch!.then(([result, rawMatches]) => {
114 115 116 117
					const serializedMatches = rawMatches.map(rawMatch => this.rawMatchToSearchItem(rawMatch));
					this.sendProgress(serializedMatches, progressCallback, batchSize);
					c(result);
				}, e);
118
			});
119 120
		}

121 122
		const engine = new EngineClass(config);

123 124 125 126 127
		return this.doSearch(engine, fileProgressCallback, batchSize, token).then(complete => {
			return <ISerializedSearchSuccess>{
				limitHit: complete.limitHit,
				type: 'success',
				stats: {
128
					detailStats: complete.stats,
R
Rob Lourens 已提交
129
					type: 'searchProcess',
130 131
					fromCache: false,
					resultCount,
132
					sortingTime: undefined
133 134 135
				}
			};
		});
136 137
	}

C
Christof Marti 已提交
138
	private rawMatchToSearchItem(match: IRawFileMatch): ISerializedFileMatch {
139
		return { path: match.base ? join(match.base, match.relativePath) : match.relativePath };
C
Christof Marti 已提交
140 141
	}

142
	private doSortedSearch(engine: ISearchEngine<IRawFileMatch>, config: IFileQuery, progressCallback: IProgressCallback, fileProgressCallback: IFileProgressCallback, token?: CancellationToken): Promise<[ISerializedSearchSuccess, IRawFileMatch[]]> {
143 144
		const emitter = new Emitter<IFileSearchProgressItem>();

145
		let allResultsPromise = createCancelablePromise(token => {
146
			let results: IRawFileMatch[] = [];
147 148 149 150 151 152 153 154 155 156

			const innerProgressCallback: IFileProgressCallback = progress => {
				if (Array.isArray(progress)) {
					results = progress;
				} else {
					fileProgressCallback(progress);
					emitter.fire(progress);
				}
			};

157
			return this.doSearch(engine, innerProgressCallback, -1, token)
158
				.then<[ISearchEngineSuccess, IRawFileMatch[]]>(result => {
159 160
					return [result, results];
				});
161 162 163 164 165
		});

		let cache: Cache;
		if (config.cacheKey) {
			cache = this.getOrCreateCache(config.cacheKey);
166
			const cacheRow: ICacheRow = {
167
				promise: allResultsPromise,
168 169
				event: emitter.event,
				resolved: false
170
			};
R
Rob Lourens 已提交
171
			cache.resultsToSearchCache[config.filePattern || ''] = cacheRow;
172 173 174
			allResultsPromise.then(() => {
				cacheRow.resolved = true;
			}, err => {
R
Rob Lourens 已提交
175
				delete cache.resultsToSearchCache[config.filePattern || ''];
176
			});
177

178
			allResultsPromise = this.preventCancellation(allResultsPromise);
179 180
		}

181 182 183 184 185
		return allResultsPromise.then(([result, results]) => {
			const scorerCache: ScorerCache = cache ? cache.scorerCache : Object.create(null);
			const sortSW = (typeof config.maxResults !== 'number' || config.maxResults > 0) && StopWatch.create(false);
			return this.sortResults(config, results, scorerCache, token)
				.then<[ISerializedSearchSuccess, IRawFileMatch[]]>(sortedResults => {
186
					// sortingTime: -1 indicates a "sorted" search that was not sorted, i.e. populating the cache when quickaccess is opened.
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
					// Contrasting with findFiles which is not sorted and will have sortingTime: undefined
					const sortingTime = sortSW ? sortSW.elapsed() : -1;

					return [{
						type: 'success',
						stats: {
							detailStats: result.stats,
							sortingTime,
							fromCache: false,
							type: 'searchProcess',
							workspaceFolderCount: config.folderQueries.length,
							resultCount: sortedResults.length
						},
						limitHit: result.limitHit || typeof config.maxResults === 'number' && results.length > config.maxResults
					} as ISerializedSearchSuccess, sortedResults];
				});
		});
204 205 206 207 208 209 210 211 212 213
	}

	private getOrCreateCache(cacheKey: string): Cache {
		const existing = this.caches[cacheKey];
		if (existing) {
			return existing;
		}
		return this.caches[cacheKey] = new Cache();
	}

R
Rob Lourens 已提交
214
	private trySortedSearchFromCache(config: IFileQuery, progressCallback: IFileProgressCallback, token?: CancellationToken): Promise<[ISerializedSearchSuccess, IRawFileMatch[]]> | undefined {
215 216
		const cache = config.cacheKey && this.caches[config.cacheKey];
		if (!cache) {
217
			return undefined;
218 219
		}

R
Rob Lourens 已提交
220
		const cached = this.getResultsFromCache(cache, config.filePattern || '', progressCallback, token);
221
		if (cached) {
222
			return cached.then(([result, results, cacheStats]) => {
223
				const sortSW = StopWatch.create(false);
224 225
				return this.sortResults(config, results, cache.scorerCache, token)
					.then<[ISerializedSearchSuccess, IRawFileMatch[]]>(sortedResults => {
226 227
						const sortingTime = sortSW.elapsed();
						const stats: IFileSearchStats = {
228
							fromCache: true,
229
							detailStats: cacheStats,
230 231 232
							type: 'searchProcess',
							resultCount: results.length,
							sortingTime
233
						};
234

235 236 237 238
						return [
							{
								type: 'success',
								limitHit: result.limitHit || typeof config.maxResults === 'number' && results.length > config.maxResults,
239
								stats
240 241 242 243
							} as ISerializedSearchSuccess,
							sortedResults
						];
					});
244 245
			});
		}
246
		return undefined;
247 248
	}

249
	private sortResults(config: IFileQuery, results: IRawFileMatch[], scorerCache: ScorerCache, token?: CancellationToken): Promise<IRawFileMatch[]> {
250 251 252 253
		// we use the same compare function that is used later when showing the results using fuzzy scoring
		// this is very important because we are also limiting the number of results by config.maxResults
		// and as such we want the top items to be included in this result set if the number of items
		// exceeds config.maxResults.
R
Rob Lourens 已提交
254
		const query = prepareQuery(config.filePattern || '');
255
		const compare = (matchA: IRawFileMatch, matchB: IRawFileMatch) => compareItemsByScore(matchA, matchB, query, true, FileMatchItemAccessor, scorerCache);
256

257
		const maxResults = typeof config.maxResults === 'number' ? config.maxResults : Number.MAX_VALUE;
R
Rob Lourens 已提交
258
		return arrays.topAsync(results, compare, maxResults, 10000, token);
259 260
	}

261
	private sendProgress(results: ISerializedFileMatch[], progressCb: IProgressCallback, batchSize: number) {
262 263 264 265 266 267 268 269 270
		if (batchSize && batchSize > 0) {
			for (let i = 0; i < results.length; i += batchSize) {
				progressCb(results.slice(i, i + batchSize));
			}
		} else {
			progressCb(results);
		}
	}

R
Rob Lourens 已提交
271
	private getResultsFromCache(cache: Cache, searchValue: string, progressCallback: IFileProgressCallback, token?: CancellationToken): Promise<[ISearchEngineSuccess, IRawFileMatch[], ICachedSearchStats]> | null {
272
		const cacheLookupSW = StopWatch.create(false);
273

274
		// Find cache entries by prefix of search value
275
		const hasPathSep = searchValue.indexOf(sep) >= 0;
R
Rob Lourens 已提交
276
		let cachedRow: ICacheRow | undefined;
277
		for (const previousSearch in cache.resultsToSearchCache) {
278 279
			// If we narrow down, we might be able to reuse the cached results
			if (strings.startsWith(searchValue, previousSearch)) {
280
				if (hasPathSep && previousSearch.indexOf(sep) < 0 && previousSearch !== '') {
281 282 283
					continue; // since a path character widens the search for potential more matches, require it in previous search too
				}

284 285 286
				const row = cache.resultsToSearchCache[previousSearch];
				cachedRow = {
					promise: this.preventCancellation(row.promise),
287 288
					event: row.event,
					resolved: row.resolved
289
				};
290 291 292 293
				break;
			}
		}

294
		if (!cachedRow) {
295 296 297
			return null;
		}

298
		const cacheLookupTime = cacheLookupSW.elapsed();
299
		const cacheFilterSW = StopWatch.create(false);
300

301
		const listener = cachedRow.event(progressCallback);
302 303 304 305 306
		if (token) {
			token.onCancellationRequested(() => {
				listener.dispose();
			});
		}
307

308
		return cachedRow.promise.then<[ISearchEngineSuccess, IRawFileMatch[], ICachedSearchStats]>(([complete, cachedEntries]) => {
309 310 311
			if (token && token.isCancellationRequested) {
				throw canceled();
			}
312

313
			// Pattern match on results
314
			const results: IRawFileMatch[] = [];
315
			const normalizedSearchValueLowercase = prepareQuery(searchValue).normalizedLowercase;
316
			for (const entry of cachedEntries) {
317

318
				// Check if this entry is a match for the search value
319
				if (!isFilePatternMatch(entry, normalizedSearchValueLowercase)) {
320
					continue;
321
				}
322

323 324 325 326
				results.push(entry);
			}

			return [complete, results, {
R
Rob Lourens 已提交
327
				cacheWasResolved: cachedRow!.resolved,
328 329 330 331
				cacheLookupTime,
				cacheFilterTime: cacheFilterSW.elapsed(),
				cacheEntryCount: cachedEntries.length
			}];
332
		});
333 334
	}

335 336


337 338
	private doSearch(engine: ISearchEngine<IRawFileMatch>, progressCallback: IFileProgressCallback, batchSize: number, token?: CancellationToken): Promise<ISearchEngineSuccess> {
		return new Promise<ISearchEngineSuccess>((c, e) => {
339
			let batch: IRawFileMatch[] = [];
340 341 342 343
			if (token) {
				token.onCancellationRequested(() => engine.cancel());
			}

E
Erich Gamma 已提交
344 345
			engine.search((match) => {
				if (match) {
C
chrmarti 已提交
346 347 348
					if (batchSize) {
						batch.push(match);
						if (batchSize > 0 && batch.length >= batchSize) {
349
							progressCallback(batch);
C
chrmarti 已提交
350 351 352
							batch = [];
						}
					} else {
353
						progressCallback(match);
C
chrmarti 已提交
354
					}
E
Erich Gamma 已提交
355 356
				}
			}, (progress) => {
357
				progressCallback(progress);
358
			}, (error, complete) => {
C
chrmarti 已提交
359
				if (batch.length) {
360
					progressCallback(batch);
C
chrmarti 已提交
361
				}
362

E
Erich Gamma 已提交
363 364 365
				if (error) {
					e(error);
				} else {
366
					c(complete);
E
Erich Gamma 已提交
367 368
				}
			});
369
		});
E
Erich Gamma 已提交
370
	}
371

R
Rob Lourens 已提交
372
	clearCache(cacheKey: string): Promise<void> {
373
		delete this.caches[cacheKey];
374
		return Promise.resolve(undefined);
375
	}
376

377 378 379 380 381 382
	/**
	 * Return a CancelablePromise which is not actually cancelable
	 * TODO@rob - Is this really needed?
	 */
	private preventCancellation<C>(promise: CancelablePromise<C>): CancelablePromise<C> {
		return new class implements CancelablePromise<C> {
383
			get [Symbol.toStringTag]() { return this.toString(); }
384 385 386
			cancel() {
				// Do nothing
			}
387
			then<TResult1 = C, TResult2 = never>(resolve?: ((value: C) => TResult1 | Promise<TResult1>) | undefined | null, reject?: ((reason: any) => TResult2 | Promise<TResult2>) | undefined | null): Promise<TResult1 | TResult2> {
388 389
				return promise.then(resolve, reject);
			}
390
			catch(reject?: any) {
391 392
				return this.then(undefined, reject);
			}
393
			finally(onFinally: any) {
394 395
				return promise.finally(onFinally);
			}
396
		};
397
	}
398 399
}

400
interface ICacheRow {
401
	// TODO@roblou - never actually canceled
402 403
	promise: CancelablePromise<[ISearchEngineSuccess, IRawFileMatch[]]>;
	resolved: boolean;
404 405 406
	event: Event<IFileSearchProgressItem>;
}

407 408
class Cache {

R
Rob Lourens 已提交
409
	resultsToSearchCache: { [searchValue: string]: ICacheRow; } = Object.create(null);
410

R
Rob Lourens 已提交
411
	scorerCache: ScorerCache = Object.create(null);
412 413
}

B
Benjamin Pasero 已提交
414
const FileMatchItemAccessor = new class implements IItemAccessor<IRawFileMatch> {
415

R
Rob Lourens 已提交
416
	getItemLabel(match: IRawFileMatch): string {
417
		return basename(match.relativePath); // e.g. myFile.txt
418
	}
419

R
Rob Lourens 已提交
420
	getItemDescription(match: IRawFileMatch): string {
421
		return dirname(match.relativePath); // e.g. some/path/to/file
422 423
	}

R
Rob Lourens 已提交
424
	getItemPath(match: IRawFileMatch): string {
425
		return match.relativePath; // e.g. some/path/to/file/myFile.txt
426
	}
B
Benjamin Pasero 已提交
427
};
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444

function reviveQuery<U extends IRawQuery>(rawQuery: U): U extends IRawTextQuery ? ITextQuery : IFileQuery {
	return {
		...<any>rawQuery, // TODO
		...{
			folderQueries: rawQuery.folderQueries && rawQuery.folderQueries.map(reviveFolderQuery),
			extraFileResources: rawQuery.extraFileResources && rawQuery.extraFileResources.map(components => URI.revive(components))
		}
	};
}

function reviveFolderQuery(rawFolderQuery: IFolderQuery<UriComponents>): IFolderQuery<URI> {
	return {
		...rawFolderQuery,
		folder: URI.revive(rawFolderQuery.folder)
	};
}