extHostSearch.ts 25.6 KB
Newer Older
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

7
import * as path from 'path';
8
import { CancellationTokenSource } from 'vs/base/common/cancellation';
9 10
import { toErrorMessage } from 'vs/base/common/errorMessage';
import * as glob from 'vs/base/common/glob';
11
import * as resources from 'vs/base/common/resources';
12 13 14
import * as strings from 'vs/base/common/strings';
import URI, { UriComponents } from 'vs/base/common/uri';
import { PPromise, TPromise } from 'vs/base/common/winjs.base';
15 16
import * as extfs from 'vs/base/node/extfs';
import * as pfs from 'vs/base/node/pfs';
17
import { IFileMatch, IFolderQuery, IPatternInfo, IRawSearchQuery, ISearchCompleteStats, ISearchQuery } from 'vs/platform/search/common/search';
18 19 20
import * as vscode from 'vscode';
import { ExtHostSearchShape, IMainContext, MainContext, MainThreadSearchShape } from './extHost.protocol';

A
Alex Dima 已提交
21 22 23 24
export interface ISchemeTransformer {
	transformOutgoing(scheme: string): string;
}

25 26 27 28 29 30
export class ExtHostSearch implements ExtHostSearchShape {

	private readonly _proxy: MainThreadSearchShape;
	private readonly _searchProvider = new Map<number, vscode.SearchProvider>();
	private _handlePool: number = 0;

31
	private _fileSearchManager: FileSearchManager;
32

33
	constructor(mainContext: IMainContext, private _schemeTransformer: ISchemeTransformer, private _extfs = extfs, private _pfs = pfs) {
34
		this._proxy = mainContext.getProxy(MainContext.MainThreadSearch);
35
		this._fileSearchManager = new FileSearchManager(this._pfs);
36 37
	}

A
Alex Dima 已提交
38 39 40 41 42 43 44
	private _transformScheme(scheme: string): string {
		if (this._schemeTransformer) {
			return this._schemeTransformer.transformOutgoing(scheme);
		}
		return scheme;
	}

45 46 47
	registerSearchProvider(scheme: string, provider: vscode.SearchProvider) {
		const handle = this._handlePool++;
		this._searchProvider.set(handle, provider);
A
Alex Dima 已提交
48
		this._proxy.$registerSearchProvider(handle, this._transformScheme(scheme));
49 50 51 52 53 54 55 56
		return {
			dispose: () => {
				this._searchProvider.delete(handle);
				this._proxy.$unregisterProvider(handle);
			}
		};
	}

57
	$provideFileSearchResults(handle: number, session: number, rawQuery: IRawSearchQuery): TPromise<ISearchCompleteStats> {
58 59 60 61 62 63
		const provider = this._searchProvider.get(handle);
		if (!provider.provideFileSearchResults) {
			return TPromise.as(undefined);
		}

		const query = reviveQuery(rawQuery);
R
Rob Lourens 已提交
64
		return this._fileSearchManager.fileSearch(query, provider).then(
65
			null,
66 67
			null,
			progress => {
R
Rob Lourens 已提交
68
				this._proxy.$handleFileMatch(handle, session, progress.map(p => p.resource));
69
			});
70 71
	}

72
	$provideTextSearchResults(handle: number, session: number, pattern: IPatternInfo, rawQuery: IRawSearchQuery): TPromise<ISearchCompleteStats> {
73 74 75 76
		const provider = this._searchProvider.get(handle);
		if (!provider.provideTextSearchResults) {
			return TPromise.as(undefined);
		}
77

78 79 80
		const query = reviveQuery(rawQuery);
		const engine = new TextSearchEngine(pattern, query, provider, this._extfs);
		return engine.search().then(
81
			null,
82 83
			null,
			progress => {
R
Rob Lourens 已提交
84
				this._proxy.$handleTextMatch(handle, session, progress);
85 86
			});
	}
87 88
}

89
/**
90
 *  Computes the patterns that the provider handles. Discards sibling clauses and 'false' patterns
91 92 93 94 95 96 97 98 99 100 101 102 103 104
 */
function resolvePatternsForProvider(globalPattern: glob.IExpression, folderPattern: glob.IExpression): string[] {
	const merged = {
		...(globalPattern || {}),
		...(folderPattern || {})
	};

	return Object.keys(merged)
		.filter(key => {
			const value = merged[key];
			return typeof value === 'boolean' && value;
		});
}

105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
function reviveQuery(rawQuery: IRawSearchQuery): ISearchQuery {
	return {
		...rawQuery,
		...{
			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)
	};
}

122
class TextSearchResultsCollector {
123
	private _batchedCollector: BatchedCollector<IFileMatch>;
124

125
	private _currentFolderIdx: number;
126 127
	private _currentUri: URI;
	private _currentFileMatch: IFileMatch;
128

129 130
	constructor(private _onResult: (result: IFileMatch[]) => void) {
		this._batchedCollector = new BatchedCollector<IFileMatch>(512, items => this.sendItems(items));
131 132
	}

133
	add(data: vscode.TextSearchResult, folderIdx: number): void {
134
		// Collects TextSearchResults into IInternalFileMatches and collates using BatchedCollector.
135 136
		// This is efficient for ripgrep which sends results back one file at a time. It wouldn't be efficient for other search
		// providers that send results in random order. We could do this step afterwards instead.
137
		if (this._currentFileMatch && (this._currentFolderIdx !== folderIdx || resources.isEqual(this._currentUri, data.uri))) {
138 139 140 141 142 143
			this.pushToCollector();
			this._currentFileMatch = null;
		}

		if (!this._currentFileMatch) {
			this._currentFileMatch = {
144
				resource: data.uri,
145 146 147 148 149
				lineMatches: []
			};
		}

		// TODO@roblou - line text is sent for every match
R
Rob Lourens 已提交
150
		const matchRange = data.preview.match;
151 152
		this._currentFileMatch.lineMatches.push({
			lineNumber: data.range.start.line,
R
Rob Lourens 已提交
153 154
			preview: data.preview.text,
			offsetAndLengths: [[matchRange.start.character, matchRange.end.character - matchRange.start.character]]
155 156 157 158
		});
	}

	private pushToCollector(): void {
159 160 161
		const size = this._currentFileMatch ?
			this._currentFileMatch.lineMatches.reduce((acc, match) => acc + match.offsetAndLengths.length, 0) :
			0;
162 163 164 165 166 167 168 169
		this._batchedCollector.addItem(this._currentFileMatch, size);
	}

	flush(): void {
		this.pushToCollector();
		this._batchedCollector.flush();
	}

170 171
	private sendItems(items: IFileMatch[]): void {
		this._onResult(items);
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
	}
}

/**
 * Collects items that have a size - before the cumulative size of collected items reaches START_BATCH_AFTER_COUNT, the callback is called for every
 * set of items collected.
 * But after that point, the callback is called with batches of maxBatchSize.
 * If the batch isn't filled within some time, the callback is also called.
 */
class BatchedCollector<T> {
	private static readonly TIMEOUT = 4000;

	// After START_BATCH_AFTER_COUNT items have been collected, stop flushing on timeout
	private static readonly START_BATCH_AFTER_COUNT = 50;

	private totalNumberCompleted = 0;
	private batch: T[] = [];
	private batchSize = 0;
	private timeoutHandle: number;

192
	constructor(private maxBatchSize: number, private cb: (items: T[]) => void) {
193 194 195 196 197 198 199
	}

	addItem(item: T, size: number): void {
		if (!item) {
			return;
		}

200
		this.addItemToBatch(item, size);
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253
	}

	addItems(items: T[], size: number): void {
		if (!items) {
			return;
		}

		if (this.maxBatchSize > 0) {
			this.addItemsToBatch(items, size);
		} else {
			this.cb(items);
		}
	}

	private addItemToBatch(item: T, size: number): void {
		this.batch.push(item);
		this.batchSize += size;
		this.onUpdate();
	}

	private addItemsToBatch(item: T[], size: number): void {
		this.batch = this.batch.concat(item);
		this.batchSize += size;
		this.onUpdate();
	}

	private onUpdate(): void {
		if (this.totalNumberCompleted < BatchedCollector.START_BATCH_AFTER_COUNT) {
			// Flush because we aren't batching yet
			this.flush();
		} else if (this.batchSize >= this.maxBatchSize) {
			// Flush because the batch is full
			this.flush();
		} else if (!this.timeoutHandle) {
			// No timeout running, start a timeout to flush
			this.timeoutHandle = setTimeout(() => {
				this.flush();
			}, BatchedCollector.TIMEOUT);
		}
	}

	flush(): void {
		if (this.batchSize) {
			this.totalNumberCompleted += this.batchSize;
			this.cb(this.batch);
			this.batch = [];
			this.batchSize = 0;

			if (this.timeoutHandle) {
				clearTimeout(this.timeoutHandle);
				this.timeoutHandle = 0;
			}
		}
254 255
	}
}
256 257

interface IDirectoryEntry {
258
	base: URI;
259 260 261 262 263 264 265 266 267
	relativePath: string;
	basename: string;
}

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

268
interface IInternalFileMatch {
269 270
	base: URI;
	relativePath?: string; // Not present for extraFiles or absolute path matches
271 272 273 274
	basename: string;
	size?: number;
}

275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
class QueryGlobTester {

	private _excludeExpression: glob.IExpression;
	private _parsedExcludeExpression: glob.ParsedExpression;

	private _parsedIncludeExpression: glob.ParsedExpression;

	constructor(config: ISearchQuery, folderQuery: IFolderQuery) {
		this._excludeExpression = {
			...(config.excludePattern || {}),
			...(folderQuery.excludePattern || {})
		};
		this._parsedExcludeExpression = glob.parse(this._excludeExpression);

		// Empty includeExpression means include nothing, so no {} shortcuts
		let includeExpression: glob.IExpression = config.includePattern;
		if (folderQuery.includePattern) {
			if (includeExpression) {
				includeExpression = {
					...includeExpression,
					...folderQuery.includePattern
				};
			} else {
				includeExpression = folderQuery.includePattern;
			}
		}

		if (includeExpression) {
			this._parsedIncludeExpression = glob.parse(includeExpression);
		}
	}

	/**
	 * Guaranteed sync - siblingsFn should not return a promise.
	 */
	public includedInQuerySync(testPath: string, basename?: string, siblingsFn?: () => string[]): boolean {
		if (this._parsedExcludeExpression && this._parsedExcludeExpression(testPath, basename, siblingsFn)) {
			return false;
		}

		if (this._parsedIncludeExpression && !this._parsedIncludeExpression(testPath, basename, siblingsFn)) {
			return false;
		}

		return true;
	}

	/**
	 * Guaranteed async.
	 */
325 326 327 328 329 330 331 332 333
	public includedInQuery(testPath: string, basename?: string, siblingsFn?: () => string[] | TPromise<string[]>): TPromise<boolean> {
		const excludeP = this._parsedExcludeExpression ?
			TPromise.as(this._parsedExcludeExpression(testPath, basename, siblingsFn)).then(result => !!result) :
			TPromise.wrap(false);

		return excludeP.then(excluded => {
			if (excluded) {
				return false;
			}
334

335
			return this._parsedIncludeExpression ?
R
Rob Lourens 已提交
336
				TPromise.as(this._parsedIncludeExpression(testPath, basename, siblingsFn)).then(result => !!result) :
337 338 339 340
				TPromise.wrap(true);
		}).then(included => {
			return included;
		});
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
	}

	public hasSiblingExcludeClauses(): boolean {
		return hasSiblingClauses(this._excludeExpression);
	}
}

function hasSiblingClauses(pattern: glob.IExpression): boolean {
	for (let key in pattern) {
		if (typeof pattern[key] !== 'boolean') {
			return true;
		}
	}

	return false;
}

358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375
class TextSearchEngine {

	private activeCancellationTokens = new Set<CancellationTokenSource>();
	private collector: TextSearchResultsCollector;

	private isLimitHit: boolean;
	private resultCount = 0;
	private isCanceled: boolean;

	constructor(private pattern: IPatternInfo, private config: ISearchQuery, private provider: vscode.SearchProvider, private _extfs: typeof extfs) {
	}

	public cancel(): void {
		this.isCanceled = true;
		this.activeCancellationTokens.forEach(t => t.cancel());
		this.activeCancellationTokens = new Set();
	}

376
	public search(): PPromise<{ limitHit: boolean }, IFileMatch[]> {
377 378
		const folderQueries = this.config.folderQueries;

379 380
		return new PPromise<{ limitHit: boolean }, IFileMatch[]>((resolve, reject, _onResult) => {
			this.collector = new TextSearchResultsCollector(_onResult);
381

382
			const onResult = (match: vscode.TextSearchResult, folderIdx: number) => {
383 384 385 386 387 388 389 390
				if (this.isCanceled) {
					return;
				}

				if (this.resultCount >= this.config.maxResults) {
					this.isLimitHit = true;
					this.cancel();
				}
391 392 393 394 395

				if (!this.isLimitHit) {
					this.resultCount++;
					this.collector.add(match, folderIdx);
				}
396 397 398
			};

			// For each root folder
399 400
			PPromise.join(folderQueries.map((fq, i) => {
				return this.searchInFolder(fq).then(null, null, r => onResult(r, i));
401 402
			})).then(() => {
				this.collector.flush();
403
				resolve({ limitHit: this.isLimitHit });
404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
			}, (errs: Error[]) => {
				const errMsg = errs
					.map(err => toErrorMessage(err))
					.filter(msg => !!msg)[0];

				reject(new Error(errMsg));
			});
		});
	}

	private searchInFolder(folderQuery: IFolderQuery<URI>): PPromise<void, vscode.TextSearchResult> {
		let cancellation = new CancellationTokenSource();
		return new PPromise((resolve, reject, onResult) => {

			const queryTester = new QueryGlobTester(this.config, folderQuery);
			const testingPs = [];
			const progress = {
				report: (result: vscode.TextSearchResult) => {
422
					const siblingFn = folderQuery.folder.scheme === 'file' && (() => {
423
						return this.readdir(path.dirname(result.uri.fsPath));
424
					});
425

426
					const relativePath = path.relative(folderQuery.folder.fsPath, result.uri.fsPath);
427
					testingPs.push(
428
						queryTester.includedInQuery(relativePath, path.basename(relativePath), siblingFn)
429 430 431 432 433
							.then(included => {
								if (included) {
									onResult(result);
								}
							}));
434 435 436 437 438 439 440
				}
			};

			const searchOptions = this.getSearchOptionsForFolder(folderQuery);
			new TPromise(resolve => process.nextTick(resolve))
				.then(() => {
					this.activeCancellationTokens.add(cancellation);
441
					return this.provider.provideTextSearchResults(patternInfoToQuery(this.pattern), searchOptions, progress, cancellation.token);
442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480
				})
				.then(() => {
					this.activeCancellationTokens.delete(cancellation);
					return TPromise.join(testingPs);
				})
				.then(
					() => {
						cancellation.dispose();
						resolve(null);
					},
					err => {
						cancellation.dispose();
						reject(err);
					});
		});
	}

	private readdir(dirname: string): TPromise<string[]> {
		return new TPromise((resolve, reject) => {
			this._extfs.readdir(dirname, (err, files) => {
				if (err) {
					return reject(err);
				}

				resolve(files);
			});
		});
	}

	private getSearchOptionsForFolder(fq: IFolderQuery<URI>): vscode.TextSearchOptions {
		const includes = resolvePatternsForProvider(this.config.includePattern, fq.includePattern);
		const excludes = resolvePatternsForProvider(this.config.excludePattern, fq.excludePattern);

		return {
			folder: URI.from(fq.folder),
			excludes,
			includes,
			useIgnoreFiles: !this.config.disregardIgnoreFiles,
			followSymlinks: !this.config.ignoreSymlinks,
481
			encoding: this.config.fileEncoding,
482 483
			maxFileSize: this.config.maxFileSize,
			maxResults: this.config.maxResults
484 485 486 487
		};
	}
}

488 489 490 491 492 493 494 495 496
function patternInfoToQuery(patternInfo: IPatternInfo): vscode.TextSearchQuery {
	return <vscode.TextSearchQuery>{
		isCaseSensitive: patternInfo.isCaseSensitive || false,
		isRegExp: patternInfo.isRegExp || false,
		isWordMatch: patternInfo.isWordMatch || false,
		pattern: patternInfo.pattern
	};
}

R
Rob Lourens 已提交
497
class FileSearchEngine {
498 499 500 501 502 503 504 505 506
	private filePattern: string;
	private normalizedFilePatternLowercase: string;
	private includePattern: glob.ParsedExpression;
	private maxResults: number;
	private exists: boolean;
	private isLimitHit: boolean;
	private resultCount: number;
	private isCanceled: boolean;

507 508
	private activeCancellationTokens: Set<CancellationTokenSource>;

509 510
	private globalExcludePattern: glob.ParsedExpression;

511
	constructor(private config: ISearchQuery, private provider: vscode.SearchProvider, private _pfs: typeof pfs) {
512 513 514 515 516 517
		this.filePattern = config.filePattern;
		this.includePattern = config.includePattern && glob.parse(config.includePattern);
		this.maxResults = config.maxResults || null;
		this.exists = config.exists;
		this.resultCount = 0;
		this.isLimitHit = false;
518
		this.activeCancellationTokens = new Set<CancellationTokenSource>();
519 520 521 522 523 524 525 526 527 528

		if (this.filePattern) {
			this.normalizedFilePatternLowercase = strings.stripWildcards(this.filePattern).toLowerCase();
		}

		this.globalExcludePattern = config.excludePattern && glob.parse(config.excludePattern);
	}

	public cancel(): void {
		this.isCanceled = true;
529 530
		this.activeCancellationTokens.forEach(t => t.cancel());
		this.activeCancellationTokens = new Set();
531 532
	}

R
Rob Lourens 已提交
533
	public search(): PPromise<{ isLimitHit: boolean }, IInternalFileMatch> {
534 535
		const folderQueries = this.config.folderQueries;

536 537 538 539 540 541
		return new PPromise<{ isLimitHit: boolean }, IInternalFileMatch>((resolve, reject, _onResult) => {
			const onResult = (match: IInternalFileMatch) => {
				this.resultCount++;
				_onResult(match);
			};

R
Rob Lourens 已提交
542 543 544 545 546
			// Support that the file pattern is a full path to a file that exists
			this.checkFilePatternAbsoluteMatch().then(({ exists, size }) => {
				if (this.isCanceled) {
					return resolve({ isLimitHit: this.isLimitHit });
				}
547

R
Rob Lourens 已提交
548 549 550
				// Report result from file pattern if matching
				if (exists) {
					onResult({
551
						base: URI.file(this.filePattern),
R
Rob Lourens 已提交
552 553
						basename: path.basename(this.filePattern),
						size
554
					});
555

R
Rob Lourens 已提交
556 557 558 559
					// 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
					return resolve({ isLimitHit: this.isLimitHit });
560 561
				}

R
Rob Lourens 已提交
562 563 564
				// For each extra file
				if (this.config.extraFileResources) {
					this.config.extraFileResources
565 566 567 568
						.forEach(extraFile => {
							const extraFileStr = extraFile.toString(); // ?
							const basename = path.basename(extraFileStr);
							if (this.globalExcludePattern && this.globalExcludePattern(extraFileStr, basename)) {
R
Rob Lourens 已提交
569 570 571 572
								return; // excluded
							}

							// File: Check for match on file pattern and include pattern
573
							this.matchFile(onResult, { base: extraFile, basename });
R
Rob Lourens 已提交
574
						});
575 576
				}

R
Rob Lourens 已提交
577 578 579 580
				// For each root folder
				PPromise.join(folderQueries.map(fq => {
					return this.searchInFolder(fq).then(null, null, onResult);
				})).then(() => {
581
					resolve({ isLimitHit: this.isLimitHit });
R
Rob Lourens 已提交
582 583 584 585 586 587 588 589 590 591
				}, (errs: Error[]) => {
					const errMsg = errs
						.map(err => toErrorMessage(err))
						.filter(msg => !!msg)[0];

					reject(new Error(errMsg));
				});
			});
		});
	}
592

R
Rob Lourens 已提交
593 594 595 596 597
	private searchInFolder(fq: IFolderQuery<URI>): PPromise<void, IInternalFileMatch> {
		let cancellation = new CancellationTokenSource();
		return new PPromise((resolve, reject, onResult) => {
			const options = this.getSearchOptionsForFolder(fq);
			const tree = this.initDirectoryTree();
598

599 600
			const queryTester = new QueryGlobTester(this.config, fq);
			const noSiblingsClauses = !queryTester.hasSiblingExcludeClauses();
601

602
			const onProviderResult = (result: URI) => {
603 604 605 606
				if (this.isCanceled) {
					return;
				}

607
				const relativePath = path.relative(fq.folder.fsPath, result.fsPath);
608

609 610
				if (noSiblingsClauses) {
					const basename = path.basename(result.fsPath);
611
					this.matchFile(onResult, { base: fq.folder, relativePath, basename });
612

R
Rob Lourens 已提交
613 614 615 616
					return;
				}

				// TODO: Optimize siblings clauses with ripgrep here.
617
				this.addDirectoryEntries(tree, fq.folder, relativePath, onResult);
R
Rob Lourens 已提交
618 619
			};

620 621 622
			new TPromise(resolve => process.nextTick(resolve))
				.then(() => {
					this.activeCancellationTokens.add(cancellation);
623
					return this.provider.provideFileSearchResults(
R
Rob Lourens 已提交
624
						{ cacheKey: this.config.cacheKey, pattern: this.config.filePattern || '' },
625 626 627
						options,
						{ report: onProviderResult },
						cancellation.token);
628
				})
R
Rob Lourens 已提交
629
				.then(() => {
630 631 632 633 634
					this.activeCancellationTokens.delete(cancellation);
					if (this.isCanceled) {
						return null;
					}

R
Rob Lourens 已提交
635
					if (noSiblingsClauses && this.isLimitHit) {
636 637 638 639 640 641 642 643 644 645
						// If the limit was hit, check whether filePattern is an exact relative match because it must be included
						return this.checkFilePatternRelativeMatch(fq.folder).then(({ exists, size }) => {
							if (exists) {
								onResult({
									base: fq.folder,
									relativePath: this.filePattern,
									basename: path.basename(this.filePattern),
								});
							}
						});
R
Rob Lourens 已提交
646
					}
647

648
					this.matchDirectoryTree(tree, queryTester, onResult);
R
Rob Lourens 已提交
649 650 651 652 653 654 655 656 657 658 659 660
					return null;
				}).then(
					() => {
						cancellation.dispose();
						resolve(undefined);
					},
					err => {
						cancellation.dispose();
						reject(err);
					});
		});
	}
661

R
Rob Lourens 已提交
662
	private getSearchOptionsForFolder(fq: IFolderQuery<URI>): vscode.FileSearchOptions {
663 664
		const includes = resolvePatternsForProvider(this.config.includePattern, fq.includePattern);
		const excludes = resolvePatternsForProvider(this.config.excludePattern, fq.excludePattern);
665

R
Rob Lourens 已提交
666 667 668 669 670
		return {
			folder: fq.folder,
			excludes,
			includes,
			useIgnoreFiles: !this.config.disregardIgnoreFiles,
671 672
			followSymlinks: !this.config.ignoreSymlinks,
			maxResults: this.config.maxResults
R
Rob Lourens 已提交
673
		};
674 675 676 677 678 679 680 681 682 683 684
	}

	private initDirectoryTree(): IDirectoryTree {
		const tree: IDirectoryTree = {
			rootEntries: [],
			pathToEntries: Object.create(null)
		};
		tree.pathToEntries['.'] = tree.rootEntries;
		return tree;
	}

685
	private addDirectoryEntries({ pathToEntries }: IDirectoryTree, base: URI, relativeFile: string, onResult: (result: IInternalFileMatch) => void) {
686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709
		// Support relative paths to files from a root resource (ignores excludes)
		if (relativeFile === this.filePattern) {
			const basename = path.basename(this.filePattern);
			this.matchFile(onResult, { base: base, relativePath: this.filePattern, basename });
		}

		function add(relativePath: string) {
			const basename = path.basename(relativePath);
			const dirname = path.dirname(relativePath);
			let entries = pathToEntries[dirname];
			if (!entries) {
				entries = pathToEntries[dirname] = [];
				add(dirname);
			}
			entries.push({
				base,
				relativePath,
				basename
			});
		}

		add(relativeFile);
	}

710
	private matchDirectoryTree({ rootEntries, pathToEntries }: IDirectoryTree, queryTester: QueryGlobTester, onResult: (result: IInternalFileMatch) => void) {
711 712 713 714 715 716 717 718 719 720 721
		const self = this;
		const filePattern = this.filePattern;
		function matchDirectory(entries: IDirectoryEntry[]) {
			for (let i = 0, n = entries.length; i < n; i++) {
				const entry = entries[i];
				const { relativePath, basename } = entry;

				// 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
722
				if (!queryTester.includedInQuerySync(relativePath, basename, () => filePattern !== basename ? entries.map(entry => entry.basename) : [])) {
723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744
					continue;
				}

				const sub = pathToEntries[relativePath];
				if (sub) {
					matchDirectory(sub);
				} else {
					if (relativePath === filePattern) {
						continue; // ignore file if its path matches with the file pattern because that is already matched above
					}

					self.matchFile(onResult, entry);
				}

				if (self.isLimitHit) {
					break;
				}
			}
		}
		matchDirectory(rootEntries);
	}

R
Rob Lourens 已提交
745 746
	/**
	 * Return whether the file pattern is an absolute path to a file that exists.
R
Rob Lourens 已提交
747
	 * TODO@roblou delete to match fileSearch.ts
R
Rob Lourens 已提交
748 749
	 */
	private checkFilePatternAbsoluteMatch(): TPromise<{ exists: boolean, size?: number }> {
750
		if (!this.filePattern || !path.isAbsolute(this.filePattern)) {
R
Rob Lourens 已提交
751
			return TPromise.wrap({ exists: false });
752 753
		}

754
		return this._pfs.stat(this.filePattern)
R
Rob Lourens 已提交
755 756 757 758 759 760 761 762 763 764
			.then(stat => {
				return {
					exists: !stat.isDirectory(),
					size: stat.size
				};
			}, err => {
				return {
					exists: false
				};
			});
765 766
	}

767 768
	private checkFilePatternRelativeMatch(base: URI): TPromise<{ exists: boolean, size?: number }> {
		if (!this.filePattern || path.isAbsolute(this.filePattern) || base.scheme !== 'file') {
R
Rob Lourens 已提交
769
			return TPromise.wrap({ exists: false });
770 771
		}

772
		const absolutePath = path.join(base.fsPath, this.filePattern);
773
		return this._pfs.stat(absolutePath).then(stat => {
R
Rob Lourens 已提交
774 775 776 777 778 779 780 781
			return {
				exists: !stat.isDirectory(),
				size: stat.size
			};
		}, err => {
			return {
				exists: false
			};
782 783 784
		});
	}

785
	private matchFile(onResult: (result: IInternalFileMatch) => void, candidate: IInternalFileMatch): void {
786
		if (this.isFilePatternMatch(candidate.relativePath) && (!this.includePattern || this.includePattern(candidate.relativePath, candidate.basename))) {
787
			if (this.exists || (this.maxResults && this.resultCount >= this.maxResults)) {
788
				this.isLimitHit = true;
789
				this.cancel();
790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812
			}

			if (!this.isLimitHit) {
				onResult(candidate);
			}
		}
	}

	private isFilePatternMatch(path: string): boolean {
		// Check for search pattern
		if (this.filePattern) {
			if (this.filePattern === '*') {
				return true; // support the all-matching wildcard
			}

			return strings.fuzzyContains(path, this.normalizedFilePatternLowercase);
		}

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

R
Rob Lourens 已提交
813
class FileSearchManager {
814 815 816

	private static readonly BATCH_SIZE = 512;

817
	constructor(private _pfs: typeof pfs) { }
818

R
Rob Lourens 已提交
819
	public fileSearch(config: ISearchQuery, provider: vscode.SearchProvider): PPromise<ISearchCompleteStats, IFileMatch[]> {
820
		let searchP: PPromise;
R
Rob Lourens 已提交
821
		return new PPromise<ISearchCompleteStats, IFileMatch[]>((c, e, p) => {
822
			const engine = new FileSearchEngine(config, provider, this._pfs);
823
			searchP = this.doSearch(engine, provider, FileSearchManager.BATCH_SIZE).then(c, e, progress => {
R
Rob Lourens 已提交
824
				p(progress.map(m => this.rawMatchToSearchItem(m)));
825
			});
826
		}, () => {
827 828 829
			if (searchP) {
				searchP.cancel();
			}
830 831 832
		});
	}

833
	private rawMatchToSearchItem(match: IInternalFileMatch): IFileMatch {
R
Rob Lourens 已提交
834
		return {
835
			resource: resources.joinPath(match.base, match.relativePath)
R
Rob Lourens 已提交
836
		};
837 838
	}

R
Rob Lourens 已提交
839 840
	private doSearch(engine: FileSearchEngine, provider: vscode.SearchProvider, batchSize: number): PPromise<ISearchCompleteStats, IInternalFileMatch[]> {
		return new PPromise<ISearchCompleteStats, IInternalFileMatch[]>((c, e, p) => {
841
			let batch: IInternalFileMatch[] = [];
842
			engine.search().then(result => {
R
Rob Lourens 已提交
843 844 845 846 847
				if (batch.length) {
					p(batch);
				}

				c({
R
Rob Lourens 已提交
848
					limitHit: result.isLimitHit
R
Rob Lourens 已提交
849 850 851 852 853 854 855 856
				});
			}, error => {
				if (batch.length) {
					p(batch);
				}

				e(error);
			}, match => {
857
				if (match) {
R
Rob Lourens 已提交
858 859 860 861
					batch.push(match);
					if (batchSize > 0 && batch.length >= batchSize) {
						p(batch);
						batch = [];
862 863
					}
				}
R
Rob Lourens 已提交
864
			});
865
		}, () => {
R
Rob Lourens 已提交
866
			engine.cancel();
867 868
		});
	}
869
}