search.ts 17.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 { mapArrayOrNot } from 'vs/base/common/arrays';
import { CancellationToken } from 'vs/base/common/cancellation';
8 9
import * as glob from 'vs/base/common/glob';
import { IDisposable } from 'vs/base/common/lifecycle';
10
import * as objects from 'vs/base/common/objects';
B
Benjamin Pasero 已提交
11
import * as extpath from 'vs/base/common/extpath';
12
import { getNLines } from 'vs/base/common/strings';
13
import { URI, UriComponents } from 'vs/base/common/uri';
J
Johannes Rieken 已提交
14 15
import { IFilesConfiguration } from 'vs/platform/files/common/files';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
16 17
import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
import { Event } from 'vs/base/common/event';
18 19
import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExtensions } from 'vs/workbench/common/views';
import { Registry } from 'vs/platform/registry/common/platform';
E
Erich Gamma 已提交
20

21 22
export const VIEWLET_ID = 'workbench.view.search';
export const PANEL_ID = 'workbench.view.search';
23
export const VIEW_ID = 'workbench.view.search';
24 25 26 27
/**
 * Search viewlet container.
 */
export const VIEW_CONTAINER: ViewContainer = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer(VIEWLET_ID, true);
E
Erich Gamma 已提交
28

29
export const ISearchService = createDecorator<ISearchService>('searchService');
B
Benjamin Pasero 已提交
30

E
Erich Gamma 已提交
31 32 33 34
/**
 * A service that enables to search for files or with in files.
 */
export interface ISearchService {
35
	_serviceBrand: undefined;
36 37
	textSearch(query: ITextQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): Promise<ISearchComplete>;
	fileSearch(query: IFileQuery, token?: CancellationToken): Promise<ISearchComplete>;
J
Johannes Rieken 已提交
38
	clearCache(cacheKey: string): Promise<void>;
39
	registerSearchResultProvider(scheme: string, type: SearchProviderType, provider: ISearchResultProvider): IDisposable;
40 41
}

42 43 44
/**
 * TODO@roblou - split text from file search entirely, or share code in a more natural way.
 */
45
export const enum SearchProviderType {
46 47 48 49
	file,
	text
}

50
export interface ISearchResultProvider {
R
Rob Lourens 已提交
51 52
	textSearch(query: ITextQuery, onProgress?: (p: ISearchProgressItem) => void, token?: CancellationToken): Promise<ISearchComplete>;
	fileSearch(query: IFileQuery, token?: CancellationToken): Promise<ISearchComplete>;
J
Johannes Rieken 已提交
53
	clearCache(cacheKey: string): Promise<void>;
E
Erich Gamma 已提交
54 55
}

M
Matt Bierner 已提交
56
export interface IFolderQuery<U extends UriComponents = URI> {
57
	folder: U;
58 59
	excludePattern?: glob.IExpression;
	includePattern?: glob.IExpression;
R
Rob Lourens 已提交
60
	fileEncoding?: string;
61
	disregardIgnoreFiles?: boolean;
62
	disregardGlobalIgnoreFiles?: boolean;
63
	ignoreSymlinks?: boolean;
R
Rob Lourens 已提交
64 65
}

66
export interface ICommonQueryProps<U extends UriComponents> {
R
Rob Lourens 已提交
67 68 69
	/** For telemetry - indicates what is triggering the source */
	_reason?: string;

R
Rob Lourens 已提交
70
	folderQueries: IFolderQuery<U>[];
71 72
	includePattern?: glob.IExpression;
	excludePattern?: glob.IExpression;
73
	extraFileResources?: U[];
74

E
Erich Gamma 已提交
75
	maxResults?: number;
76 77 78 79 80 81 82
	usingSearchPaths?: boolean;
}

export interface IFileQueryProps<U extends UriComponents> extends ICommonQueryProps<U> {
	type: QueryType.File;
	filePattern?: string;

83 84 85 86 87
	/**
	 * If true no results will be returned. Instead `limitHit` will indicate if at least one result exists or not.
	 * Currently does not work with queries including a 'siblings clause'.
	 */
	exists?: boolean;
88 89
	sortByScore?: boolean;
	cacheKey?: string;
E
Erich Gamma 已提交
90 91
}

92 93
export interface ITextQueryProps<U extends UriComponents> extends ICommonQueryProps<U> {
	type: QueryType.Text;
94
	contentPattern: IPatternInfo;
95 96 97

	previewOptions?: ITextSearchPreviewOptions;
	maxFileSize?: number;
R
Rob Lourens 已提交
98
	usePCRE2?: boolean;
99 100
	afterContext?: number;
	beforeContext?: number;
101 102

	userDisabledExcludesAndIgnoreFiles?: boolean;
E
Erich Gamma 已提交
103 104
}

105
export type IFileQuery = IFileQueryProps<URI>;
106
export type IRawFileQuery = IFileQueryProps<UriComponents>;
107
export type ITextQuery = ITextQueryProps<URI>;
108 109 110 111
export type IRawTextQuery = ITextQueryProps<UriComponents>;

export type IRawQuery = IRawTextQuery | IRawFileQuery;
export type ISearchQuery = ITextQuery | IFileQuery;
112

113
export const enum QueryType {
E
Erich Gamma 已提交
114 115 116
	File = 1,
	Text = 2
}
R
Rob Lourens 已提交
117

K
kieferrm 已提交
118
/* __GDPR__FRAGMENT__
K
kieferrm 已提交
119 120
	"IPatternInfo" : {
		"pattern" : { "classification": "CustomerContent", "purpose": "FeatureInsight" },
K
kieferrm 已提交
121 122
		"isRegExp": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
		"isWordMatch": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
K
kieferrm 已提交
123
		"wordSeparators": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
K
kieferrm 已提交
124 125 126
		"isMultiline": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
		"isCaseSensitive": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
		"isSmartCase": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
K
kieferrm 已提交
127 128
	}
*/
E
Erich Gamma 已提交
129 130 131 132
export interface IPatternInfo {
	pattern: string;
	isRegExp?: boolean;
	isWordMatch?: boolean;
133
	wordSeparators?: string;
S
Sandeep Somavarapu 已提交
134
	isMultiline?: boolean;
135
	isUnicode?: boolean;
E
Erich Gamma 已提交
136 137 138
	isCaseSensitive?: boolean;
}

R
Rob Lourens 已提交
139 140 141 142
export interface IExtendedExtensionSearchOptions {
	usePCRE2?: boolean;
}

143
export interface IFileMatch<U extends UriComponents = URI> {
R
Rob Lourens 已提交
144
	resource: U;
145
	results?: ITextSearchResult[];
E
Erich Gamma 已提交
146 147
}

148
export type IRawFileMatch2 = IFileMatch<UriComponents>;
149

150
export interface ITextSearchPreviewOptions {
R
Rob Lourens 已提交
151 152
	matchLines: number;
	charsPerLine: number;
153 154 155 156 157 158 159 160 161 162 163
}

export interface ISearchRange {
	readonly startLineNumber: number;
	readonly startColumn: number;
	readonly endLineNumber: number;
	readonly endColumn: number;
}

export interface ITextSearchResultPreview {
	text: string;
164
	matches: ISearchRange | ISearchRange[];
165 166
}

167 168
export interface ITextSearchMatch {
	uri?: URI;
169
	ranges: ISearchRange | ISearchRange[];
170
	preview: ITextSearchResultPreview;
E
Erich Gamma 已提交
171 172
}

173 174 175 176 177 178 179 180 181 182 183 184
export interface ITextSearchContext {
	uri?: URI;
	text: string;
	lineNumber: number;
}

export type ITextSearchResult = ITextSearchMatch | ITextSearchContext;

export function resultIsMatch(result: ITextSearchResult): result is ITextSearchMatch {
	return !!(<ITextSearchMatch>result).preview;
}

185
export interface IProgressMessage {
186 187 188
	message?: string;
}

189 190 191 192 193 194 195 196 197
export type ISearchProgressItem = IFileMatch | IProgressMessage;

export function isFileMatch(p: ISearchProgressItem): p is IFileMatch {
	return !!(<IFileMatch>p).resource;
}

export function isProgressMessage(p: ISearchProgressItem): p is IProgressMessage {
	return !isFileMatch(p);
}
E
Erich Gamma 已提交
198

199
export interface ISearchCompleteStats {
E
Erich Gamma 已提交
200
	limitHit?: boolean;
201
	stats?: IFileSearchStats | ITextSearchStats;
202 203 204
}

export interface ISearchComplete extends ISearchCompleteStats {
E
Erich Gamma 已提交
205
	results: IFileMatch[];
C
chrmarti 已提交
206 207
}

208 209 210 211
export interface ITextSearchStats {
	type: 'textSearchProvider' | 'searchProcess';
}

212
export interface IFileSearchStats {
213
	fromCache: boolean;
214
	detailStats: ISearchEngineStats | ICachedSearchStats | IFileSearchProviderStats;
215

216
	resultCount: number;
217
	type: 'fileSearchProvider' | 'searchProcess';
218
	sortingTime?: number;
219 220
}

221 222 223 224
export interface ICachedSearchStats {
	cacheWasResolved: boolean;
	cacheLookupTime: number;
	cacheFilterTime: number;
225 226 227
	cacheEntryCount: number;
}

228 229
export interface ISearchEngineStats {
	fileWalkTime: number;
C
chrmarti 已提交
230 231
	directoriesWalked: number;
	filesWalked: number;
232
	cmdTime: number;
C
Christof Marti 已提交
233
	cmdResultCount?: number;
E
Erich Gamma 已提交
234 235
}

236 237 238 239 240
export interface IFileSearchProviderStats {
	providerTime: number;
	postProcessTime: number;
}

E
Erich Gamma 已提交
241
export class FileMatch implements IFileMatch {
R
Rob Lourens 已提交
242
	results: ITextSearchResult[] = [];
243
	constructor(public resource: URI) {
E
Erich Gamma 已提交
244 245 246 247
		// empty
	}
}

248
export class TextSearchMatch implements ITextSearchMatch {
249
	ranges: ISearchRange | ISearchRange[];
250 251
	preview: ITextSearchResultPreview;

252 253
	constructor(text: string, range: ISearchRange | ISearchRange[], previewOptions?: ITextSearchPreviewOptions) {
		this.ranges = range;
254

255 256 257
		if (previewOptions && previewOptions.matchLines === 1 && (!Array.isArray(range) || range.length === 1)) {
			const oneRange = Array.isArray(range) ? range[0] : range;

258
			// 1 line preview requested
259
			text = getNLines(text, previewOptions.matchLines);
R
Rob Lourens 已提交
260
			const leadingChars = Math.floor(previewOptions.charsPerLine / 5);
261
			const previewStart = Math.max(oneRange.startColumn - leadingChars, 0);
262 263
			const previewText = text.substring(previewStart, previewOptions.charsPerLine + previewStart);

264 265
			const endColInPreview = (oneRange.endLineNumber - oneRange.startLineNumber + 1) <= previewOptions.matchLines ?
				Math.min(previewText.length, oneRange.endColumn - previewStart) :  // if number of match lines will not be trimmed by previewOptions
266
				previewText.length; // if number of lines is trimmed
267

268
			const oneLineRange = new OneLineRange(0, oneRange.startColumn - previewStart, endColInPreview);
269
			this.preview = {
270
				text: previewText,
271
				matches: Array.isArray(range) ? [oneLineRange] : oneLineRange
272 273
			};
		} else {
274 275 276
			const firstMatchLine = Array.isArray(range) ? range[0].startLineNumber : range.startLineNumber;

			// n line, no preview requested, or multiple matches in the preview
277
			this.preview = {
278
				text,
279
				matches: mapArrayOrNot(range, r => new SearchRange(r.startLineNumber - firstMatchLine, r.startColumn, r.endLineNumber - firstMatchLine, r.endColumn))
280 281 282 283 284
			};
		}
	}
}

285
export class SearchRange implements ISearchRange {
286 287 288 289 290
	startLineNumber: number;
	startColumn: number;
	endLineNumber: number;
	endColumn: number;

291 292
	constructor(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number) {
		this.startLineNumber = startLineNumber;
293
		this.startColumn = startColumn;
294
		this.endLineNumber = endLineNumber;
295
		this.endColumn = endColumn;
E
Erich Gamma 已提交
296 297 298
	}
}

299 300 301 302 303 304
export class OneLineRange extends SearchRange {
	constructor(lineNumber: number, startColumn: number, endColumn: number) {
		super(lineNumber, startColumn, lineNumber, endColumn);
	}
}

305 306 307 308 309 310 311
export interface ISearchConfigurationProperties {
	exclude: glob.IExpression;
	useRipgrep: boolean;
	/**
	 * Use ignore file for file search.
	 */
	useIgnoreFiles: boolean;
P
pkoushik 已提交
312
	useGlobalIgnoreFiles: boolean;
313 314
	followSymlinks: boolean;
	smartCase: boolean;
315
	globalFindClipboard: boolean;
316
	location: 'sidebar' | 'panel';
317
	useReplacePreview: boolean;
318
	showLineNumbers: boolean;
R
Rob Lourens 已提交
319
	usePCRE2: boolean;
R
Rob Lourens 已提交
320
	actionsPosition: 'auto' | 'right';
321
	maintainFileSearchCache: boolean;
322
	collapseResults: 'auto' | 'alwaysCollapse' | 'alwaysExpand';
323 324
}

E
Erich Gamma 已提交
325
export interface ISearchConfiguration extends IFilesConfiguration {
326
	search: ISearchConfigurationProperties;
327 328 329
	editor: {
		wordSeparators: string;
	};
B
Benjamin Pasero 已提交
330 331
}

332
export function getExcludes(configuration: ISearchConfiguration, includeSearchExcludes = true): glob.IExpression | undefined {
B
Benjamin Pasero 已提交
333
	const fileExcludes = configuration && configuration.files && configuration.files.exclude;
334
	const searchExcludes = includeSearchExcludes && configuration && configuration.search && configuration.search.exclude;
B
Benjamin Pasero 已提交
335 336

	if (!fileExcludes && !searchExcludes) {
R
Rob Lourens 已提交
337
		return undefined;
B
Benjamin Pasero 已提交
338 339 340 341 342 343
	}

	if (!fileExcludes || !searchExcludes) {
		return fileExcludes || searchExcludes;
	}

344
	let allExcludes: glob.IExpression = Object.create(null);
S
Sandeep Somavarapu 已提交
345
	// clone the config as it could be frozen
J
Johannes Rieken 已提交
346 347
	allExcludes = objects.mixin(allExcludes, objects.deepClone(fileExcludes));
	allExcludes = objects.mixin(allExcludes, objects.deepClone(searchExcludes), true);
B
Benjamin Pasero 已提交
348 349

	return allExcludes;
350
}
351

352
export function pathIncludedInQuery(queryProps: ICommonQueryProps<URI>, fsPath: string): boolean {
353
	if (queryProps.excludePattern && glob.match(queryProps.excludePattern, fsPath)) {
354 355 356
		return false;
	}

357
	if (queryProps.includePattern && !glob.match(queryProps.includePattern, fsPath)) {
358 359 360 361
		return false;
	}

	// If searchPaths are being used, the extra file must be in a subfolder and match the pattern, if present
362 363
	if (queryProps.usingSearchPaths) {
		return !!queryProps.folderQueries && queryProps.folderQueries.every(fq => {
364
			const searchPath = fq.folder.fsPath;
B
Benjamin Pasero 已提交
365
			if (extpath.isEqualOrParent(fsPath, searchPath)) {
366 367 368 369 370 371 372 373 374
				return !fq.includePattern || !!glob.match(fq.includePattern, fsPath);
			} else {
				return false;
			}
		});
	}

	return true;
}
R
Rob Lourens 已提交
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403

export enum SearchErrorCode {
	unknownEncoding = 1,
	regexParseError,
	globParseError,
	invalidLiteral,
	rgProcessError,
	other
}

export class SearchError extends Error {
	constructor(message: string, readonly code?: SearchErrorCode) {
		super(message);
	}
}

export function deserializeSearchError(errorMsg: string): SearchError {
	try {
		const details = JSON.parse(errorMsg);
		return new SearchError(details.message, details.code);
	} catch (e) {
		return new SearchError(errorMsg, SearchErrorCode.other);
	}
}

export function serializeSearchError(searchError: SearchError): Error {
	const details = { message: searchError.message, code: searchError.code };
	return new Error(JSON.stringify(details));
}
404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422
export interface ITelemetryEvent {
	eventName: string;
	data: ITelemetryData;
}

export interface IRawSearchService {
	fileSearch(search: IRawFileQuery): Event<ISerializedSearchProgressItem | ISerializedSearchComplete>;
	textSearch(search: IRawTextQuery): Event<ISerializedSearchProgressItem | ISerializedSearchComplete>;
	clearCache(cacheKey: string): Promise<void>;
}

export interface IRawFileMatch {
	base?: string;
	relativePath: string;
	basename: string;
	size?: number;
}

export interface ISearchEngine<T> {
423
	search: (onResult: (matches: T) => void, onProgress: (progress: IProgressMessage) => void, done: (error: Error | null, complete: ISearchEngineSuccess) => void) => void;
424 425 426 427 428 429
	cancel: () => void;
}

export interface ISerializedSearchSuccess {
	type: 'success';
	limitHit: boolean;
R
Rob Lourens 已提交
430
	stats?: IFileSearchStats | ITextSearchStats;
431 432 433 434 435 436 437 438 439 440 441 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
}

export interface ISearchEngineSuccess {
	limitHit: boolean;
	stats: ISearchEngineStats;
}

export interface ISerializedSearchError {
	type: 'error';
	error: {
		message: string,
		stack: string
	};
}

export type ISerializedSearchComplete = ISerializedSearchSuccess | ISerializedSearchError;

export function isSerializedSearchComplete(arg: ISerializedSearchProgressItem | ISerializedSearchComplete): arg is ISerializedSearchComplete {
	if ((arg as any).type === 'error') {
		return true;
	} else if ((arg as any).type === 'success') {
		return true;
	} else {
		return false;
	}
}

export function isSerializedSearchSuccess(arg: ISerializedSearchComplete): arg is ISerializedSearchSuccess {
	return arg.type === 'success';
}

export function isSerializedFileMatch(arg: ISerializedSearchProgressItem): arg is ISerializedFileMatch {
	return !!(<ISerializedFileMatch>arg).path;
}

export interface ISerializedFileMatch {
R
Rob Lourens 已提交
467
	path: string;
468 469 470 471 472
	results?: ITextSearchResult[];
	numMatches?: number;
}

// Type of the possible values for progress calls from the engine
473 474
export type ISerializedSearchProgressItem = ISerializedFileMatch | ISerializedFileMatch[] | IProgressMessage;
export type IFileSearchProgressItem = IRawFileMatch | IRawFileMatch[] | IProgressMessage;
475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519


export class SerializableFileMatch implements ISerializedFileMatch {
	path: string;
	results: ITextSearchMatch[];

	constructor(path: string) {
		this.path = path;
		this.results = [];
	}

	addMatch(match: ITextSearchMatch): void {
		this.results.push(match);
	}

	serialize(): ISerializedFileMatch {
		return {
			path: this.path,
			results: this.results,
			numMatches: this.results.length
		};
	}
}

/**
 *  Computes the patterns that the provider handles. Discards sibling clauses and 'false' patterns
 */
export function resolvePatternsForProvider(globalPattern: glob.IExpression | undefined, folderPattern: glob.IExpression | undefined): string[] {
	const merged = {
		...(globalPattern || {}),
		...(folderPattern || {})
	};

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

export class QueryGlobTester {

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

520
	private _parsedIncludeExpression: glob.ParsedExpression | null = null;
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596

	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 | undefined = 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.
	 */
	includedInQuerySync(testPath: string, basename?: string, hasSibling?: (name: string) => boolean): boolean {
		if (this._parsedExcludeExpression && this._parsedExcludeExpression(testPath, basename, hasSibling)) {
			return false;
		}

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

		return true;
	}

	/**
	 * Guaranteed async.
	 */
	includedInQuery(testPath: string, basename?: string, hasSibling?: (name: string) => boolean | Promise<boolean>): Promise<boolean> {
		const excludeP = this._parsedExcludeExpression ?
			Promise.resolve(this._parsedExcludeExpression(testPath, basename, hasSibling)).then(result => !!result) :
			Promise.resolve(false);

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

			return this._parsedIncludeExpression ?
				Promise.resolve(this._parsedIncludeExpression(testPath, basename, hasSibling)).then(result => !!result) :
				Promise.resolve(true);
		}).then(included => {
			return included;
		});
	}

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

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

	return false;
}