search.ts 12.5 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 10
import { Event } from 'vs/base/common/event';
import * as glob from 'vs/base/common/glob';
import { IDisposable } from 'vs/base/common/lifecycle';
11
import * as objects from 'vs/base/common/objects';
12
import * as paths from 'vs/base/common/paths';
13
import { getNLines } from 'vs/base/common/strings';
14
import { URI, UriComponents } from 'vs/base/common/uri';
J
Johannes Rieken 已提交
15 16
import { IFilesConfiguration } from 'vs/platform/files/common/files';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
E
Erich Gamma 已提交
17

18
export const VIEW_ID = 'workbench.view.search';
E
Erich Gamma 已提交
19

20 21
export const ISearchHistoryService = createDecorator<ISearchHistoryService>('searchHistoryService');
export const ISearchService = createDecorator<ISearchService>('searchService');
B
Benjamin Pasero 已提交
22

E
Erich Gamma 已提交
23 24 25 26
/**
 * A service that enables to search for files or with in files.
 */
export interface ISearchService {
27
	_serviceBrand: any;
28 29
	textSearch(query: ITextQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): Promise<ISearchComplete>;
	fileSearch(query: IFileQuery, token?: CancellationToken): Promise<ISearchComplete>;
30
	extendQuery(query: ITextQuery | IFileQuery): void;
R
Rob Lourens 已提交
31
	clearCache(cacheKey: string): Thenable<void>;
32
	registerSearchResultProvider(scheme: string, type: SearchProviderType, provider: ISearchResultProvider): IDisposable;
33 34
}

35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
export interface ISearchHistoryValues {
	search?: string[];
	replace?: string[];
	include?: string[];
	exclude?: string[];
}

export interface ISearchHistoryService {
	_serviceBrand: any;
	onDidClearHistory: Event<void>;
	clearHistory(): void;
	load(): ISearchHistoryValues;
	save(history: ISearchHistoryValues): void;
}

50 51 52
/**
 * TODO@roblou - split text from file search entirely, or share code in a more natural way.
 */
53
export const enum SearchProviderType {
54 55 56 57 58
	file,
	fileIndex,
	text
}

59
export interface ISearchResultProvider {
60 61
	textSearch(query: ITextQuery, onProgress?: (p: ISearchProgressItem) => void, token?: CancellationToken): Promise<ISearchComplete>;
	fileSearch(query: IFileQuery, token?: CancellationToken): Promise<ISearchComplete>;
R
Rob Lourens 已提交
62
	clearCache(cacheKey: string): Thenable<void>;
E
Erich Gamma 已提交
63 64
}

65
export interface IFolderQuery<U extends UriComponents=URI> {
66
	folder: U;
67 68
	excludePattern?: glob.IExpression;
	includePattern?: glob.IExpression;
R
Rob Lourens 已提交
69
	fileEncoding?: string;
70
	disregardIgnoreFiles?: boolean;
71
	disregardGlobalIgnoreFiles?: boolean;
72
	ignoreSymlinks?: boolean;
R
Rob Lourens 已提交
73 74
}

75
export interface ICommonQueryProps<U extends UriComponents> {
R
Rob Lourens 已提交
76 77 78
	/** For telemetry - indicates what is triggering the source */
	_reason?: string;

79 80 81
	folderQueries?: IFolderQuery<U>[];
	includePattern?: glob.IExpression;
	excludePattern?: glob.IExpression;
82
	extraFileResources?: U[];
83 84

	useRipgrep?: boolean;
E
Erich Gamma 已提交
85
	maxResults?: number;
86 87 88 89 90 91 92 93 94 95
	usingSearchPaths?: boolean;
}

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

	// TODO: Remove this!
	disregardExcludeSettings?: boolean;

96 97 98 99 100
	/**
	 * 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;
101 102
	sortByScore?: boolean;
	cacheKey?: string;
E
Erich Gamma 已提交
103 104
}

105 106
export interface ITextQueryProps<U extends UriComponents> extends ICommonQueryProps<U> {
	type: QueryType.Text;
107
	contentPattern: IPatternInfo;
108 109 110

	previewOptions?: ITextSearchPreviewOptions;
	maxFileSize?: number;
R
Rob Lourens 已提交
111
	usePCRE2?: boolean;
112 113
	afterContext?: number;
	beforeContext?: number;
114 115

	userDisabledExcludesAndIgnoreFiles?: boolean;
E
Erich Gamma 已提交
116 117
}

118
export type IFileQuery = IFileQueryProps<URI>;
119
export type IRawFileQuery = IFileQueryProps<UriComponents>;
120
export type ITextQuery = ITextQueryProps<URI>;
121 122 123 124
export type IRawTextQuery = ITextQueryProps<UriComponents>;

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

126
export const enum QueryType {
E
Erich Gamma 已提交
127 128 129
	File = 1,
	Text = 2
}
R
Rob Lourens 已提交
130

K
kieferrm 已提交
131
/* __GDPR__FRAGMENT__
K
kieferrm 已提交
132 133
	"IPatternInfo" : {
		"pattern" : { "classification": "CustomerContent", "purpose": "FeatureInsight" },
K
kieferrm 已提交
134 135
		"isRegExp": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
		"isWordMatch": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
K
kieferrm 已提交
136
		"wordSeparators": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
K
kieferrm 已提交
137 138 139
		"isMultiline": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
		"isCaseSensitive": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
		"isSmartCase": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
K
kieferrm 已提交
140 141
	}
*/
E
Erich Gamma 已提交
142 143 144 145
export interface IPatternInfo {
	pattern: string;
	isRegExp?: boolean;
	isWordMatch?: boolean;
146
	wordSeparators?: string;
S
Sandeep Somavarapu 已提交
147
	isMultiline?: boolean;
E
Erich Gamma 已提交
148
	isCaseSensitive?: boolean;
149
	isSmartCase?: boolean;
E
Erich Gamma 已提交
150 151
}

R
Rob Lourens 已提交
152 153 154 155
export interface IExtendedExtensionSearchOptions {
	usePCRE2?: boolean;
}

156
export interface IFileMatch<U extends UriComponents = URI> {
157
	resource?: U;
158
	results?: ITextSearchResult[];
E
Erich Gamma 已提交
159 160
}

161
export type IRawFileMatch2 = IFileMatch<UriComponents>;
162

163
export interface ITextSearchPreviewOptions {
R
Rob Lourens 已提交
164 165
	matchLines: number;
	charsPerLine: number;
166 167 168 169 170 171 172 173 174 175 176
}

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

export interface ITextSearchResultPreview {
	text: string;
177
	matches: ISearchRange | ISearchRange[];
178 179
}

180 181
export interface ITextSearchMatch {
	uri?: URI;
182
	ranges: ISearchRange | ISearchRange[];
183
	preview: ITextSearchResultPreview;
E
Erich Gamma 已提交
184 185
}

186 187 188 189 190 191 192 193 194 195 196 197
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 已提交
198 199 200
export interface IProgress {
	total?: number;
	worked?: number;
201 202 203
	message?: string;
}

204
export interface ISearchProgressItem extends IFileMatch, IProgress {
E
Erich Gamma 已提交
205 206 207
	// Marker interface to indicate the possible values for progress calls from the engine
}

208
export interface ISearchCompleteStats {
E
Erich Gamma 已提交
209
	limitHit?: boolean;
210
	stats?: IFileSearchStats | ITextSearchStats;
211 212 213
}

export interface ISearchComplete extends ISearchCompleteStats {
E
Erich Gamma 已提交
214
	results: IFileMatch[];
C
chrmarti 已提交
215 216
}

217 218 219 220
export interface ITextSearchStats {
	type: 'textSearchProvider' | 'searchProcess';
}

221
export interface IFileSearchStats {
222
	fromCache: boolean;
223
	detailStats: ISearchEngineStats | ICachedSearchStats | IFileSearchProviderStats | IFileIndexProviderStats;
224

225
	resultCount: number;
R
Rob Lourens 已提交
226
	type: 'fileIndexProvider' | 'fileSearchProvider' | 'searchProcess';
227
	sortingTime?: number;
228 229
}

230 231 232 233
export interface ICachedSearchStats {
	cacheWasResolved: boolean;
	cacheLookupTime: number;
	cacheFilterTime: number;
234 235 236
	cacheEntryCount: number;
}

237
export interface ISearchEngineStats {
C
Christof Marti 已提交
238
	traversal: string;
239
	fileWalkTime: number;
C
chrmarti 已提交
240 241
	directoriesWalked: number;
	filesWalked: number;
242
	cmdTime: number;
C
Christof Marti 已提交
243
	cmdResultCount?: number;
E
Erich Gamma 已提交
244 245
}

246 247 248 249 250 251 252 253 254 255 256 257 258
export interface IFileSearchProviderStats {
	providerTime: number;
	postProcessTime: number;
}

export interface IFileIndexProviderStats {
	providerTime: number;
	providerResultCount: number;
	fileWalkTime: number;
	directoriesWalked: number;
	filesWalked: number;
}

E
Erich Gamma 已提交
259
export class FileMatch implements IFileMatch {
260 261
	public results: ITextSearchResult[] = [];
	constructor(public resource: URI) {
E
Erich Gamma 已提交
262 263 264 265
		// empty
	}
}

266
export class TextSearchMatch implements ITextSearchMatch {
267
	ranges: ISearchRange | ISearchRange[];
268 269
	preview: ITextSearchResultPreview;

270 271
	constructor(text: string, range: ISearchRange | ISearchRange[], previewOptions?: ITextSearchPreviewOptions) {
		this.ranges = range;
272

273
		if (previewOptions && previewOptions.matchLines === 1 && !Array.isArray(range)) {
274
			// 1 line preview requested
275
			text = getNLines(text, previewOptions.matchLines);
R
Rob Lourens 已提交
276
			const leadingChars = Math.floor(previewOptions.charsPerLine / 5);
277
			const previewStart = Math.max(range.startColumn - leadingChars, 0);
278 279 280 281 282
			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
283 284

			this.preview = {
285
				text: previewText,
286
				matches: new OneLineRange(0, range.startColumn - previewStart, endColInPreview)
287 288
			};
		} else {
289 290 291
			const firstMatchLine = Array.isArray(range) ? range[0].startLineNumber : range.startLineNumber;

			// n line, no preview requested, or multiple matches in the preview
292
			this.preview = {
293
				text,
294
				matches: mapArrayOrNot(range, r => new SearchRange(r.startLineNumber - firstMatchLine, r.startColumn, r.endLineNumber - firstMatchLine, r.endColumn))
295 296 297 298 299
			};
		}
	}
}

300
export class SearchRange implements ISearchRange {
301 302 303 304 305
	startLineNumber: number;
	startColumn: number;
	endLineNumber: number;
	endColumn: number;

306 307
	constructor(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number) {
		this.startLineNumber = startLineNumber;
308
		this.startColumn = startColumn;
309
		this.endLineNumber = endLineNumber;
310
		this.endColumn = endColumn;
E
Erich Gamma 已提交
311 312 313
	}
}

314 315 316 317 318 319
export class OneLineRange extends SearchRange {
	constructor(lineNumber: number, startColumn: number, endColumn: number) {
		super(lineNumber, startColumn, lineNumber, endColumn);
	}
}

320 321 322
export interface ISearchConfigurationProperties {
	exclude: glob.IExpression;
	useRipgrep: boolean;
323
	useLegacySearch: boolean;
324 325 326 327
	/**
	 * Use ignore file for file search.
	 */
	useIgnoreFiles: boolean;
P
pkoushik 已提交
328
	useGlobalIgnoreFiles: boolean;
329 330
	followSymlinks: boolean;
	smartCase: boolean;
331
	globalFindClipboard: boolean;
332
	location: 'sidebar' | 'panel';
333
	useReplacePreview: boolean;
334
	showLineNumbers: boolean;
R
Rob Lourens 已提交
335
	usePCRE2: boolean;
336
	actionsPosition: 'auto' | 'right';
337 338
}

E
Erich Gamma 已提交
339
export interface ISearchConfiguration extends IFilesConfiguration {
340
	search: ISearchConfigurationProperties;
341 342 343
	editor: {
		wordSeparators: string;
	};
B
Benjamin Pasero 已提交
344 345
}

346
export function getExcludes(configuration: ISearchConfiguration): glob.IExpression | undefined {
B
Benjamin Pasero 已提交
347 348 349 350
	const fileExcludes = configuration && configuration.files && configuration.files.exclude;
	const searchExcludes = configuration && configuration.search && configuration.search.exclude;

	if (!fileExcludes && !searchExcludes) {
R
Rob Lourens 已提交
351
		return undefined;
B
Benjamin Pasero 已提交
352 353 354 355 356 357
	}

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

358
	let allExcludes: glob.IExpression = Object.create(null);
S
Sandeep Somavarapu 已提交
359
	// clone the config as it could be frozen
J
Johannes Rieken 已提交
360 361
	allExcludes = objects.mixin(allExcludes, objects.deepClone(fileExcludes));
	allExcludes = objects.mixin(allExcludes, objects.deepClone(searchExcludes), true);
B
Benjamin Pasero 已提交
362 363

	return allExcludes;
364
}
365

366
export function pathIncludedInQuery(queryProps: ICommonQueryProps<URI>, fsPath: string): boolean {
367
	if (queryProps.excludePattern && glob.match(queryProps.excludePattern, fsPath)) {
368 369 370
		return false;
	}

371
	if (queryProps.includePattern && !glob.match(queryProps.includePattern, fsPath)) {
372 373 374 375
		return false;
	}

	// If searchPaths are being used, the extra file must be in a subfolder and match the pattern, if present
376 377
	if (queryProps.usingSearchPaths) {
		return !!queryProps.folderQueries && queryProps.folderQueries.every(fq => {
378 379 380 381 382 383 384 385 386 387 388
			const searchPath = fq.folder.fsPath;
			if (paths.isEqualOrParent(fsPath, searchPath)) {
				return !fq.includePattern || !!glob.match(fq.includePattern, fsPath);
			} else {
				return false;
			}
		});
	}

	return true;
}
R
Rob Lourens 已提交
389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417

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