search.ts 17.4 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: any;
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 | undefined>;
	fileSearch(query: IFileQuery, token?: CancellationToken): Promise<ISearchComplete | undefined>;
J
Johannes Rieken 已提交
53
	clearCache(cacheKey: string): Promise<void>;
E
Erich Gamma 已提交
54 55
}

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;
E
Erich Gamma 已提交
135 136 137
	isCaseSensitive?: boolean;
}

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

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

147
export type IRawFileMatch2 = IFileMatch<UriComponents>;
148

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

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

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

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

172 173 174 175 176 177 178 179 180 181 182 183
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;
}

E
Erich Gamma 已提交
184 185 186
export interface IProgress {
	total?: number;
	worked?: number;
187 188 189
	message?: string;
}

R
Rob Lourens 已提交
190
export type ISearchProgressItem = IFileMatch | IProgress;
E
Erich Gamma 已提交
191

192
export interface ISearchCompleteStats {
E
Erich Gamma 已提交
193
	limitHit?: boolean;
194
	stats?: IFileSearchStats | ITextSearchStats;
195 196 197
}

export interface ISearchComplete extends ISearchCompleteStats {
E
Erich Gamma 已提交
198
	results: IFileMatch[];
C
chrmarti 已提交
199 200
}

201 202 203 204
export interface ITextSearchStats {
	type: 'textSearchProvider' | 'searchProcess';
}

205
export interface IFileSearchStats {
206
	fromCache: boolean;
207
	detailStats: ISearchEngineStats | ICachedSearchStats | IFileSearchProviderStats;
208

209
	resultCount: number;
210
	type: 'fileSearchProvider' | 'searchProcess';
211
	sortingTime?: number;
212 213
}

214 215 216 217
export interface ICachedSearchStats {
	cacheWasResolved: boolean;
	cacheLookupTime: number;
	cacheFilterTime: number;
218 219 220
	cacheEntryCount: number;
}

221 222
export interface ISearchEngineStats {
	fileWalkTime: number;
C
chrmarti 已提交
223 224
	directoriesWalked: number;
	filesWalked: number;
225
	cmdTime: number;
C
Christof Marti 已提交
226
	cmdResultCount?: number;
E
Erich Gamma 已提交
227 228
}

229 230 231 232 233
export interface IFileSearchProviderStats {
	providerTime: number;
	postProcessTime: number;
}

E
Erich Gamma 已提交
234
export class FileMatch implements IFileMatch {
R
Rob Lourens 已提交
235
	results: ITextSearchResult[] = [];
236
	constructor(public resource: URI) {
E
Erich Gamma 已提交
237 238 239 240
		// empty
	}
}

241
export class TextSearchMatch implements ITextSearchMatch {
242
	ranges: ISearchRange | ISearchRange[];
243 244
	preview: ITextSearchResultPreview;

245 246
	constructor(text: string, range: ISearchRange | ISearchRange[], previewOptions?: ITextSearchPreviewOptions) {
		this.ranges = range;
247

248
		if (previewOptions && previewOptions.matchLines === 1 && !Array.isArray(range)) {
249
			// 1 line preview requested
250
			text = getNLines(text, previewOptions.matchLines);
R
Rob Lourens 已提交
251
			const leadingChars = Math.floor(previewOptions.charsPerLine / 5);
252
			const previewStart = Math.max(range.startColumn - leadingChars, 0);
253 254 255 256 257
			const previewText = text.substring(previewStart, previewOptions.charsPerLine + previewStart);

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

			this.preview = {
260
				text: previewText,
261
				matches: new OneLineRange(0, range.startColumn - previewStart, endColInPreview)
262 263
			};
		} else {
264 265 266
			const firstMatchLine = Array.isArray(range) ? range[0].startLineNumber : range.startLineNumber;

			// n line, no preview requested, or multiple matches in the preview
267
			this.preview = {
268
				text,
269
				matches: mapArrayOrNot(range, r => new SearchRange(r.startLineNumber - firstMatchLine, r.startColumn, r.endLineNumber - firstMatchLine, r.endColumn))
270 271 272 273 274
			};
		}
	}
}

275
export class SearchRange implements ISearchRange {
276 277 278 279 280
	startLineNumber: number;
	startColumn: number;
	endLineNumber: number;
	endColumn: number;

281 282
	constructor(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number) {
		this.startLineNumber = startLineNumber;
283
		this.startColumn = startColumn;
284
		this.endLineNumber = endLineNumber;
285
		this.endColumn = endColumn;
E
Erich Gamma 已提交
286 287 288
	}
}

289 290 291 292 293 294
export class OneLineRange extends SearchRange {
	constructor(lineNumber: number, startColumn: number, endColumn: number) {
		super(lineNumber, startColumn, lineNumber, endColumn);
	}
}

295 296 297 298 299 300 301
export interface ISearchConfigurationProperties {
	exclude: glob.IExpression;
	useRipgrep: boolean;
	/**
	 * Use ignore file for file search.
	 */
	useIgnoreFiles: boolean;
P
pkoushik 已提交
302
	useGlobalIgnoreFiles: boolean;
303 304
	followSymlinks: boolean;
	smartCase: boolean;
305
	globalFindClipboard: boolean;
306
	location: 'sidebar' | 'panel';
307
	useReplacePreview: boolean;
308
	showLineNumbers: boolean;
R
Rob Lourens 已提交
309
	usePCRE2: boolean;
R
Rob Lourens 已提交
310
	actionsPosition: 'auto' | 'right';
311
	maintainFileSearchCache: boolean;
312
	collapseResults: 'auto' | 'alwaysCollapse' | 'alwaysExpand';
313 314
}

E
Erich Gamma 已提交
315
export interface ISearchConfiguration extends IFilesConfiguration {
316
	search: ISearchConfigurationProperties;
317 318 319
	editor: {
		wordSeparators: string;
	};
B
Benjamin Pasero 已提交
320 321
}

322
export function getExcludes(configuration: ISearchConfiguration, includeSearchExcludes = true): glob.IExpression | undefined {
B
Benjamin Pasero 已提交
323
	const fileExcludes = configuration && configuration.files && configuration.files.exclude;
324
	const searchExcludes = includeSearchExcludes && configuration && configuration.search && configuration.search.exclude;
B
Benjamin Pasero 已提交
325 326

	if (!fileExcludes && !searchExcludes) {
R
Rob Lourens 已提交
327
		return undefined;
B
Benjamin Pasero 已提交
328 329 330 331 332 333
	}

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

334
	let allExcludes: glob.IExpression = Object.create(null);
S
Sandeep Somavarapu 已提交
335
	// clone the config as it could be frozen
J
Johannes Rieken 已提交
336 337
	allExcludes = objects.mixin(allExcludes, objects.deepClone(fileExcludes));
	allExcludes = objects.mixin(allExcludes, objects.deepClone(searchExcludes), true);
B
Benjamin Pasero 已提交
338 339

	return allExcludes;
340
}
341

342
export function pathIncludedInQuery(queryProps: ICommonQueryProps<URI>, fsPath: string): boolean {
343
	if (queryProps.excludePattern && glob.match(queryProps.excludePattern, fsPath)) {
344 345 346
		return false;
	}

347
	if (queryProps.includePattern && !glob.match(queryProps.includePattern, fsPath)) {
348 349 350 351
		return false;
	}

	// If searchPaths are being used, the extra file must be in a subfolder and match the pattern, if present
352 353
	if (queryProps.usingSearchPaths) {
		return !!queryProps.folderQueries && queryProps.folderQueries.every(fq => {
354
			const searchPath = fq.folder.fsPath;
B
Benjamin Pasero 已提交
355
			if (extpath.isEqualOrParent(fsPath, searchPath)) {
356 357 358 359 360 361 362 363 364
				return !fq.includePattern || !!glob.match(fq.includePattern, fsPath);
			} else {
				return false;
			}
		});
	}

	return true;
}
R
Rob Lourens 已提交
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393

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));
}
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 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 467 468 469 470 471 472 473 474 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 520 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
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> {
	search: (onResult: (matches: T) => void, onProgress: (progress: IProgress) => void, done: (error: Error | null, complete: ISearchEngineSuccess) => void) => void;
	cancel: () => void;
}

export interface ISerializedSearchSuccess {
	type: 'success';
	limitHit: boolean;
	stats: IFileSearchStats | ITextSearchStats | null;
}

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 {
	path?: string;
	results?: ITextSearchResult[];
	numMatches?: number;
}

// Type of the possible values for progress calls from the engine
export type ISerializedSearchProgressItem = ISerializedFileMatch | ISerializedFileMatch[] | IProgress;
export type IFileSearchProgressItem = IRawFileMatch | IRawFileMatch[] | IProgress;


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;

	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 | 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;
}