search.ts 11.9 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';
E
Erich Gamma 已提交
16

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

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

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

32 33 34
/**
 * TODO@roblou - split text from file search entirely, or share code in a more natural way.
 */
35
export const enum SearchProviderType {
36 37 38 39 40
	file,
	fileIndex,
	text
}

41
export interface ISearchResultProvider {
R
Rob Lourens 已提交
42 43
	textSearch(query: ITextQuery, onProgress?: (p: ISearchProgressItem) => void, token?: CancellationToken): Promise<ISearchComplete | undefined>;
	fileSearch(query: IFileQuery, token?: CancellationToken): Promise<ISearchComplete | undefined>;
J
Johannes Rieken 已提交
44
	clearCache(cacheKey: string): Promise<void>;
E
Erich Gamma 已提交
45 46
}

47
export interface IFolderQuery<U extends UriComponents=URI> {
48
	folder: U;
49 50
	excludePattern?: glob.IExpression;
	includePattern?: glob.IExpression;
R
Rob Lourens 已提交
51
	fileEncoding?: string;
52
	disregardIgnoreFiles?: boolean;
53
	disregardGlobalIgnoreFiles?: boolean;
54
	ignoreSymlinks?: boolean;
R
Rob Lourens 已提交
55 56
}

57
export interface ICommonQueryProps<U extends UriComponents> {
R
Rob Lourens 已提交
58 59 60
	/** For telemetry - indicates what is triggering the source */
	_reason?: string;

R
Rob Lourens 已提交
61
	folderQueries: IFolderQuery<U>[];
62 63
	includePattern?: glob.IExpression;
	excludePattern?: glob.IExpression;
64
	extraFileResources?: U[];
65

E
Erich Gamma 已提交
66
	maxResults?: number;
67 68 69 70 71 72 73
	usingSearchPaths?: boolean;
}

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

74 75 76 77 78
	/**
	 * 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;
79 80
	sortByScore?: boolean;
	cacheKey?: string;
E
Erich Gamma 已提交
81 82
}

83 84
export interface ITextQueryProps<U extends UriComponents> extends ICommonQueryProps<U> {
	type: QueryType.Text;
85
	contentPattern: IPatternInfo;
86 87 88

	previewOptions?: ITextSearchPreviewOptions;
	maxFileSize?: number;
R
Rob Lourens 已提交
89
	usePCRE2?: boolean;
90 91
	afterContext?: number;
	beforeContext?: number;
92 93

	userDisabledExcludesAndIgnoreFiles?: boolean;
E
Erich Gamma 已提交
94 95
}

96
export type IFileQuery = IFileQueryProps<URI>;
97
export type IRawFileQuery = IFileQueryProps<UriComponents>;
98
export type ITextQuery = ITextQueryProps<URI>;
99 100 101 102
export type IRawTextQuery = ITextQueryProps<UriComponents>;

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

104
export const enum QueryType {
E
Erich Gamma 已提交
105 106 107
	File = 1,
	Text = 2
}
R
Rob Lourens 已提交
108

K
kieferrm 已提交
109
/* __GDPR__FRAGMENT__
K
kieferrm 已提交
110 111
	"IPatternInfo" : {
		"pattern" : { "classification": "CustomerContent", "purpose": "FeatureInsight" },
K
kieferrm 已提交
112 113
		"isRegExp": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
		"isWordMatch": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
K
kieferrm 已提交
114
		"wordSeparators": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
K
kieferrm 已提交
115 116 117
		"isMultiline": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
		"isCaseSensitive": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
		"isSmartCase": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
K
kieferrm 已提交
118 119
	}
*/
E
Erich Gamma 已提交
120 121 122 123
export interface IPatternInfo {
	pattern: string;
	isRegExp?: boolean;
	isWordMatch?: boolean;
124
	wordSeparators?: string;
S
Sandeep Somavarapu 已提交
125
	isMultiline?: boolean;
E
Erich Gamma 已提交
126 127 128
	isCaseSensitive?: boolean;
}

R
Rob Lourens 已提交
129 130 131 132
export interface IExtendedExtensionSearchOptions {
	usePCRE2?: boolean;
}

133
export interface IFileMatch<U extends UriComponents = URI> {
R
Rob Lourens 已提交
134
	resource: U;
135
	results?: ITextSearchResult[];
E
Erich Gamma 已提交
136 137
}

138
export type IRawFileMatch2 = IFileMatch<UriComponents>;
139

140
export interface ITextSearchPreviewOptions {
R
Rob Lourens 已提交
141 142
	matchLines: number;
	charsPerLine: number;
143 144 145 146 147 148 149 150 151 152 153
}

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

export interface ITextSearchResultPreview {
	text: string;
154
	matches: ISearchRange | ISearchRange[];
155 156
}

157 158
export interface ITextSearchMatch {
	uri?: URI;
159
	ranges: ISearchRange | ISearchRange[];
160
	preview: ITextSearchResultPreview;
E
Erich Gamma 已提交
161 162
}

163 164 165 166 167 168 169 170 171 172 173 174
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 已提交
175 176 177
export interface IProgress {
	total?: number;
	worked?: number;
178 179 180
	message?: string;
}

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

183
export interface ISearchCompleteStats {
E
Erich Gamma 已提交
184
	limitHit?: boolean;
185
	stats?: IFileSearchStats | ITextSearchStats;
186 187 188
}

export interface ISearchComplete extends ISearchCompleteStats {
E
Erich Gamma 已提交
189
	results: IFileMatch[];
C
chrmarti 已提交
190 191
}

192 193 194 195
export interface ITextSearchStats {
	type: 'textSearchProvider' | 'searchProcess';
}

196
export interface IFileSearchStats {
197
	fromCache: boolean;
198
	detailStats: ISearchEngineStats | ICachedSearchStats | IFileSearchProviderStats | IFileIndexProviderStats;
199

200
	resultCount: number;
R
Rob Lourens 已提交
201
	type: 'fileIndexProvider' | 'fileSearchProvider' | 'searchProcess';
202
	sortingTime?: number;
203 204
}

205 206 207 208
export interface ICachedSearchStats {
	cacheWasResolved: boolean;
	cacheLookupTime: number;
	cacheFilterTime: number;
209 210 211
	cacheEntryCount: number;
}

212 213
export interface ISearchEngineStats {
	fileWalkTime: number;
C
chrmarti 已提交
214 215
	directoriesWalked: number;
	filesWalked: number;
216
	cmdTime: number;
C
Christof Marti 已提交
217
	cmdResultCount?: number;
E
Erich Gamma 已提交
218 219
}

220 221 222 223 224 225 226 227 228 229 230 231 232
export interface IFileSearchProviderStats {
	providerTime: number;
	postProcessTime: number;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	return allExcludes;
339
}
340

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

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

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

	return true;
}
R
Rob Lourens 已提交
364 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

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