queryBuilder.ts 18.3 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
import * as arrays from 'vs/base/common/arrays';
R
Rob Lourens 已提交
7 8
import * as collections from 'vs/base/common/collections';
import * as glob from 'vs/base/common/glob';
9
import { untildify } from 'vs/base/common/labels';
10
import { Schemas } from 'vs/base/common/network';
11
import * as path from 'vs/base/common/path';
12
import { isEqual } from 'vs/base/common/resources';
13
import * as strings from 'vs/base/common/strings';
14
import { URI as uri } from 'vs/base/common/uri';
15 16
import { isMultilineRegexSource } from 'vs/editor/common/model/textModelSearch';
import * as nls from 'vs/nls';
J
Johannes Rieken 已提交
17
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
18
import { IWorkspaceContextService, IWorkspaceFolderData, toWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace';
19
import { IPathService } from 'vs/workbench/services/path/common/pathService';
20
import { getExcludes, ICommonQueryProps, IFileQuery, IFolderQuery, IPatternInfo, ISearchConfiguration, ITextQuery, ITextSearchPreviewOptions, pathIncludedInQuery, QueryType } from 'vs/workbench/services/search/common/search';
E
Erich Gamma 已提交
21

22 23 24 25
/**
 * One folder to search and a glob expression that should be applied.
 */
export interface IOneSearchPathPattern {
26
	searchPath: uri;
27
	pattern?: string;
28 29
}

30 31 32 33
/**
 * One folder to search and a set of glob expressions that should be applied.
 */
export interface ISearchPathPattern {
34
	searchPath: uri;
35
	pattern?: glob.IExpression;
36 37
}

38 39 40
/**
 * A set of search paths and a set of glob expressions that should be applied.
 */
41
export interface ISearchPathsInfo {
R
Rob Lourens 已提交
42
	searchPaths?: ISearchPathPattern[];
43
	pattern?: glob.IExpression;
R
Rob Lourens 已提交
44 45
}

46
export interface ICommonQueryBuilderOptions {
R
Rob Lourens 已提交
47
	_reason?: string;
48 49 50 51
	excludePattern?: string;
	includePattern?: string;
	extraFileResources?: uri[];

52 53 54
	/** Parse the special ./ syntax supported by the searchview, and expand foo to ** /foo */
	expandPatterns?: boolean;

55
	maxResults?: number;
56
	maxFileSize?: number;
57 58 59
	disregardIgnoreFiles?: boolean;
	disregardGlobalIgnoreFiles?: boolean;
	disregardExcludeSettings?: boolean;
60
	disregardSearchExcludeSettings?: boolean;
61 62 63 64 65 66 67 68 69 70 71 72 73
	ignoreSymlinks?: boolean;
}

export interface IFileQueryBuilderOptions extends ICommonQueryBuilderOptions {
	filePattern?: string;
	exists?: boolean;
	sortByScore?: boolean;
	cacheKey?: string;
}

export interface ITextQueryBuilderOptions extends ICommonQueryBuilderOptions {
	previewOptions?: ITextSearchPreviewOptions;
	fileEncoding?: string;
74 75
	beforeContext?: number;
	afterContext?: number;
R
Rob Lourens 已提交
76
	isSmartCase?: boolean;
77 78
}

E
Erich Gamma 已提交
79 80
export class QueryBuilder {

81
	constructor(
82 83
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
84
		@IPathService private readonly pathService: IPathService
85 86 87
	) {
	}

88
	text(contentPattern: IPatternInfo, folderResources?: uri[], options: ITextQueryBuilderOptions = {}): ITextQuery {
R
Rob Lourens 已提交
89
		contentPattern = this.getContentPattern(contentPattern, options);
R
Rob Lourens 已提交
90
		const searchConfig = this.configurationService.getValue<ISearchConfiguration>();
91

R
Rob Lourens 已提交
92
		const fallbackToPCRE = folderResources && folderResources.some(folder => {
93 94 95 96
			const folderConfig = this.configurationService.getValue<ISearchConfiguration>({ resource: folder });
			return !folderConfig.search.useRipgrep;
		});

97
		const commonQuery = this.commonQuery(folderResources?.map(toWorkspaceFolder), options);
98 99 100 101
		return <ITextQuery>{
			...commonQuery,
			type: QueryType.Text,
			contentPattern,
102 103 104 105
			previewOptions: options.previewOptions,
			maxFileSize: options.maxFileSize,
			usePCRE2: searchConfig.search.usePCRE2 || fallbackToPCRE || false,
			beforeContext: options.beforeContext,
106 107
			afterContext: options.afterContext,
			userDisabledExcludesAndIgnoreFiles: options.disregardExcludeSettings && options.disregardIgnoreFiles
108
		};
E
Erich Gamma 已提交
109 110
	}

111 112 113
	/**
	 * Adjusts input pattern for config
	 */
R
Rob Lourens 已提交
114
	private getContentPattern(inputPattern: IPatternInfo, options: ITextQueryBuilderOptions): IPatternInfo {
115 116
		const searchConfig = this.configurationService.getValue<ISearchConfiguration>();

R
Rob Lourens 已提交
117 118 119
		if (inputPattern.isRegExp) {
			inputPattern.pattern = inputPattern.pattern.replace(/\r?\n/g, '\\n');
		}
120

121 122 123 124 125
		const newPattern = {
			...inputPattern,
			wordSeparators: searchConfig.editor.wordSeparators
		};

R
Rob Lourens 已提交
126
		if (this.isCaseSensitive(inputPattern, options)) {
127 128 129 130 131 132 133 134 135 136
			newPattern.isCaseSensitive = true;
		}

		if (this.isMultiline(inputPattern)) {
			newPattern.isMultiline = true;
		}

		return newPattern;
	}

137 138
	file(folders: IWorkspaceFolderData[], options: IFileQueryBuilderOptions = {}): IFileQuery {
		const commonQuery = this.commonQuery(folders, options);
139 140 141 142 143 144 145 146
		return <IFileQuery>{
			...commonQuery,
			type: QueryType.File,
			filePattern: options.filePattern
				? options.filePattern.trim()
				: options.filePattern,
			exists: options.exists,
			sortByScore: options.sortByScore,
147
			cacheKey: options.cacheKey,
148
		};
E
Erich Gamma 已提交
149 150
	}

151
	private commonQuery(folderResources: IWorkspaceFolderData[] = [], options: ICommonQueryBuilderOptions = {}): ICommonQueryProps<uri> {
152 153
		let includeSearchPathsInfo: ISearchPathsInfo = {};
		if (options.includePattern) {
R
Rob Lourens 已提交
154
			const includePattern = normalizeSlashes(options.includePattern);
155
			includeSearchPathsInfo = options.expandPatterns ?
R
Rob Lourens 已提交
156 157
				this.parseSearchPaths(includePattern) :
				{ pattern: patternListToIExpression(includePattern) };
158 159 160 161
		}

		let excludeSearchPathsInfo: ISearchPathsInfo = {};
		if (options.excludePattern) {
R
Rob Lourens 已提交
162
			const excludePattern = normalizeSlashes(options.excludePattern);
163
			excludeSearchPathsInfo = options.expandPatterns ?
R
Rob Lourens 已提交
164 165
				this.parseSearchPaths(excludePattern) :
				{ pattern: patternListToIExpression(excludePattern) };
166
		}
167

168
		// Build folderQueries from searchPaths, if given, otherwise folderResources
169
		const includeFolderName = folderResources.length > 1;
170 171
		const folderQueries = (includeSearchPathsInfo.searchPaths && includeSearchPathsInfo.searchPaths.length ?
			includeSearchPathsInfo.searchPaths.map(searchPath => this.getFolderQueryForSearchPath(searchPath, options, excludeSearchPathsInfo)) :
172
			folderResources.map(folder => this.getFolderQueryForRoot(folder, options, excludeSearchPathsInfo, includeFolderName)))
173
			.filter(query => !!query) as IFolderQuery[];
E
Erich Gamma 已提交
174

175
		const queryProps: ICommonQueryProps<uri> = {
R
Rob Lourens 已提交
176
			_reason: options._reason,
177
			folderQueries,
178
			usingSearchPaths: !!(includeSearchPathsInfo.searchPaths && includeSearchPathsInfo.searchPaths.length),
179
			extraFileResources: options.extraFileResources,
180

181 182
			excludePattern: excludeSearchPathsInfo.pattern,
			includePattern: includeSearchPathsInfo.pattern,
183
			maxResults: options.maxResults
184
		};
185 186

		// Filter extraFileResources against global include/exclude patterns - they are already expected to not belong to a workspace
187
		const extraFileResources = options.extraFileResources && options.extraFileResources.filter(extraFile => pathIncludedInQuery(queryProps, extraFile.fsPath));
188
		queryProps.extraFileResources = extraFileResources && extraFileResources.length ? extraFileResources : undefined;
189

190
		return queryProps;
E
Erich Gamma 已提交
191
	}
R
Rob Lourens 已提交
192

193
	/**
194
	 * Resolve isCaseSensitive flag based on the query and the isSmartCase flag, for search providers that don't support smart case natively.
195
	 */
R
Rob Lourens 已提交
196 197
	private isCaseSensitive(contentPattern: IPatternInfo, options: ITextQueryBuilderOptions): boolean {
		if (options.isSmartCase) {
198 199
			if (contentPattern.isRegExp) {
				// Consider it case sensitive if it contains an unescaped capital letter
200
				if (strings.containsUppercaseCharacter(contentPattern.pattern, true)) {
201
					return true;
202
				}
203
			} else if (strings.containsUppercaseCharacter(contentPattern.pattern)) {
204
				return true;
205 206
			}
		}
207

M
Matt Bierner 已提交
208
		return !!contentPattern.isCaseSensitive;
209 210 211 212 213 214 215 216 217 218 219
	}

	private isMultiline(contentPattern: IPatternInfo): boolean {
		if (contentPattern.isMultiline) {
			return true;
		}

		if (contentPattern.isRegExp && isMultilineRegexSource(contentPattern.pattern)) {
			return true;
		}

220 221 222 223
		if (contentPattern.pattern.indexOf('\n') >= 0) {
			return true;
		}

224
		return !!contentPattern.isMultiline;
225 226
	}

R
Rob Lourens 已提交
227 228
	/**
	 * Take the includePattern as seen in the search viewlet, and split into components that look like searchPaths, and
R
Rob Lourens 已提交
229 230 231
	 * glob patterns. Glob patterns are expanded from 'foo/bar' to '{foo/bar/**, **\/foo/bar}.
	 *
	 * Public for test.
R
Rob Lourens 已提交
232
	 */
233
	parseSearchPaths(pattern: string): ISearchPathsInfo {
R
Rob Lourens 已提交
234
		const isSearchPath = (segment: string) => {
235
			// A segment is a search path if it is an absolute path or starts with ./, ../, .\, or ..\
236
			return path.isAbsolute(segment) || /^\.\.?([\/\\]|$)/.test(segment);
R
Rob Lourens 已提交
237
		};
R
Rob Lourens 已提交
238

239
		const segments = splitGlobPattern(pattern)
240
			.map(segment => {
241
				const userHome = this.pathService.resolvedUserHome;
242 243
				if (userHome) {
					return untildify(segment, userHome.scheme === Schemas.file ? userHome.fsPath : userHome.path);
244 245 246 247
				}

				return segment;
			});
R
Rob Lourens 已提交
248 249 250
		const groups = collections.groupBy(segments,
			segment => isSearchPath(segment) ? 'searchPaths' : 'exprSegments');

251
		const expandedExprSegments = (groups.exprSegments || [])
R
Rob Lourens 已提交
252 253
			.map(s => strings.rtrim(s, '/'))
			.map(s => strings.rtrim(s, '\\'))
R
Rob Lourens 已提交
254 255 256 257 258
			.map(p => {
				if (p[0] === '.') {
					p = '*' + p; // convert ".js" to "*.js"
				}

259
				return expandGlobalGlob(p);
R
Rob Lourens 已提交
260 261
			});

262
		const result: ISearchPathsInfo = {};
263 264 265
		const searchPaths = this.expandSearchPathPatterns(groups.searchPaths || []);
		if (searchPaths && searchPaths.length) {
			result.searchPaths = searchPaths;
R
Rob Lourens 已提交
266 267
		}

268
		const exprSegments = arrays.flatten(expandedExprSegments);
269
		const includePattern = patternListToIExpression(...exprSegments);
R
Rob Lourens 已提交
270
		if (includePattern) {
271
			result.pattern = includePattern;
R
Rob Lourens 已提交
272 273 274
		}

		return result;
R
Rob Lourens 已提交
275 276
	}

277
	private getExcludesForFolder(folderConfig: ISearchConfiguration, options: ICommonQueryBuilderOptions): glob.IExpression | undefined {
R
Rob Lourens 已提交
278
		return options.disregardExcludeSettings ?
R
Rob Lourens 已提交
279
			undefined :
280
			getExcludes(folderConfig, !options.disregardSearchExcludeSettings);
R
Rob Lourens 已提交
281
	}
282

R
Rob Lourens 已提交
283
	/**
284
	 * Split search paths (./ or ../ or absolute paths in the includePatterns) into absolute paths and globs applied to those paths
R
Rob Lourens 已提交
285
	 */
286
	private expandSearchPathPatterns(searchPaths: string[]): ISearchPathPattern[] {
287
		if (!searchPaths || !searchPaths.length) {
288
			// No workspace => ignore search paths
289
			return [];
290 291
		}

292 293 294 295
		const expandedSearchPaths = arrays.flatten(
			searchPaths.map(searchPath => {
				// 1 open folder => just resolve the search paths to absolute paths
				let { pathPortion, globPortion } = splitGlobFromPath(searchPath);
R
Rob Lourens 已提交
296

297 298 299
				if (globPortion) {
					globPortion = normalizeGlobPattern(globPortion);
				}
R
Rob Lourens 已提交
300

301
				// One pathPortion to multiple expanded search paths (e.g. duplicate matching workspace folders)
302 303 304 305 306 307 308 309 310 311
				const oneExpanded = this.expandOneSearchPath(pathPortion);

				// Expanded search paths to multiple resolved patterns (with ** and without)
				return arrays.flatten(
					oneExpanded.map(oneExpandedResult => this.resolveOneSearchPathPattern(oneExpandedResult, globPortion)));
			}));

		const searchPathPatternMap = new Map<string, ISearchPathPattern>();
		expandedSearchPaths.forEach(oneSearchPathPattern => {
			const key = oneSearchPathPattern.searchPath.toString();
312 313
			const existing = searchPathPatternMap.get(key);
			if (existing) {
314 315 316 317 318 319 320 321 322 323 324
				if (oneSearchPathPattern.pattern) {
					existing.pattern = existing.pattern || {};
					existing.pattern[oneSearchPathPattern.pattern] = true;
				}
			} else {
				searchPathPatternMap.set(key, {
					searchPath: oneSearchPathPattern.searchPath,
					pattern: oneSearchPathPattern.pattern ? patternListToIExpression(oneSearchPathPattern.pattern) : undefined
				});
			}
		});
325

326
		return Array.from(searchPathPatternMap.values());
327 328
	}

329
	/**
330
	 * Takes a searchPath like `./a/foo` or `../a/foo` and expands it to absolute paths for all the workspaces it matches.
331
	 */
332
	private expandOneSearchPath(searchPath: string): IOneSearchPathPattern[] {
333
		if (path.isAbsolute(searchPath)) {
R
Rob Lourens 已提交
334 335 336 337 338 339 340
			const workspaceFolders = this.workspaceContextService.getWorkspace().folders;
			if (workspaceFolders[0] && workspaceFolders[0].uri.scheme !== Schemas.file) {
				return [{
					searchPath: workspaceFolders[0].uri.with({ path: searchPath })
				}];
			}

341 342 343
			// Currently only local resources can be searched for with absolute search paths.
			// TODO convert this to a workspace folder + pattern, so excludes will be resolved properly for an absolute path inside a workspace folder
			return [{
344
				searchPath: uri.file(path.normalize(searchPath))
345
			}];
346
		}
347

348
		if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.FOLDER) {
R
Rob Lourens 已提交
349
			const workspaceUri = this.workspaceContextService.getWorkspace().folders[0].uri;
350 351

			searchPath = normalizeSlashes(searchPath);
352
			if (searchPath.startsWith('../') || searchPath === '..') {
R
Rob Lourens 已提交
353
				const resolvedPath = path.posix.resolve(workspaceUri.path, searchPath);
354 355 356 357 358
				return [{
					searchPath: workspaceUri.with({ path: resolvedPath })
				}];
			}

359 360 361 362 363 364
			const cleanedPattern = normalizeGlobPattern(searchPath);
			return [{
				searchPath: workspaceUri,
				pattern: cleanedPattern
			}];
		} else if (searchPath === './' || searchPath === '.\\') {
365
			return []; // ./ or ./**/foo makes sense for single-folder but not multi-folder workspaces
366
		} else {
367
			const relativeSearchPathMatch = searchPath.match(/\.[\/\\]([^\/\\]+)(?:[\/\\](.+))?/);
368 369
			if (relativeSearchPathMatch) {
				const searchPathRoot = relativeSearchPathMatch[1];
370
				const matchingRoots = this.workspaceContextService.getWorkspace().folders.filter(folder => folder.name === searchPathRoot);
371
				if (matchingRoots.length) {
R
Rob Lourens 已提交
372
					return matchingRoots.map(root => {
373 374 375 376 377
						const patternMatch = relativeSearchPathMatch[2];
						return {
							searchPath: root.uri,
							pattern: patternMatch && normalizeGlobPattern(patternMatch)
						};
R
Rob Lourens 已提交
378
					});
379
				} else {
380
					// No root folder with name
381
					const searchPathNotFoundError = nls.localize('search.noWorkspaceWithName', "Workspace folder does not exist: {0}", searchPathRoot);
382
					throw new Error(searchPathNotFoundError);
383
				}
384
			} else {
385
				// Malformed ./ search path, ignore
386 387 388
			}
		}

389
		return [];
390
	}
391

392 393 394 395 396 397 398 399 400 401 402
	private resolveOneSearchPathPattern(oneExpandedResult: IOneSearchPathPattern, globPortion?: string): IOneSearchPathPattern[] {
		const pattern = oneExpandedResult.pattern && globPortion ?
			`${oneExpandedResult.pattern}/${globPortion}` :
			oneExpandedResult.pattern || globPortion;

		const results = [
			{
				searchPath: oneExpandedResult.searchPath,
				pattern
			}];

403
		if (pattern && !pattern.endsWith('**')) {
404 405 406 407
			results.push({
				searchPath: oneExpandedResult.searchPath,
				pattern: pattern + '/**'
			});
408 409
		}

410 411 412
		return results;
	}

413
	private getFolderQueryForSearchPath(searchPath: ISearchPathPattern, options: ICommonQueryBuilderOptions, searchPathExcludes: ISearchPathsInfo): IFolderQuery | null {
414
		const rootConfig = this.getFolderQueryForRoot(toWorkspaceFolder(searchPath.searchPath), options, searchPathExcludes, false);
415 416 417 418
		if (!rootConfig) {
			return null;
		}

419 420 421
		return {
			...rootConfig,
			...{
422
				includePattern: searchPath.pattern
423
			}
424 425 426
		};
	}

427
	private getFolderQueryForRoot(folder: IWorkspaceFolderData, options: ICommonQueryBuilderOptions, searchPathExcludes: ISearchPathsInfo, includeFolderName: boolean): IFolderQuery | null {
428 429
		let thisFolderExcludeSearchPathPattern: glob.IExpression | undefined;
		if (searchPathExcludes.searchPaths) {
430
			const thisFolderExcludeSearchPath = searchPathExcludes.searchPaths.filter(sp => isEqual(sp.searchPath, folder.uri))[0];
431 432 433 434 435 436 437 438
			if (thisFolderExcludeSearchPath && !thisFolderExcludeSearchPath.pattern) {
				// entire folder is excluded
				return null;
			} else if (thisFolderExcludeSearchPath) {
				thisFolderExcludeSearchPathPattern = thisFolderExcludeSearchPath.pattern;
			}
		}

439
		const folderConfig = this.configurationService.getValue<ISearchConfiguration>({ resource: folder.uri });
440
		const settingExcludes = this.getExcludesForFolder(folderConfig, options);
441
		const excludePattern: glob.IExpression = {
442 443 444 445
			...(settingExcludes || {}),
			...(thisFolderExcludeSearchPathPattern || {})
		};

446
		return <IFolderQuery>{
447 448
			folder: folder.uri,
			folderName: includeFolderName ? folder.name : undefined,
449
			excludePattern: Object.keys(excludePattern).length > 0 ? excludePattern : undefined,
450
			fileEncoding: folderConfig.files && folderConfig.files.encoding,
451 452 453
			disregardIgnoreFiles: typeof options.disregardIgnoreFiles === 'boolean' ? options.disregardIgnoreFiles : !folderConfig.search.useIgnoreFiles,
			disregardGlobalIgnoreFiles: typeof options.disregardGlobalIgnoreFiles === 'boolean' ? options.disregardGlobalIgnoreFiles : !folderConfig.search.useGlobalIgnoreFiles,
			ignoreSymlinks: typeof options.ignoreSymlinks === 'boolean' ? options.ignoreSymlinks : !folderConfig.search.followSymlinks,
454 455
		};
	}
456 457 458 459 460 461 462 463
}

function splitGlobFromPath(searchPath: string): { pathPortion: string, globPortion?: string } {
	const globCharMatch = searchPath.match(/[\*\{\}\(\)\[\]\?]/);
	if (globCharMatch) {
		const globCharIdx = globCharMatch.index;
		const lastSlashMatch = searchPath.substr(0, globCharIdx).match(/[/|\\][^/\\]*$/);
		if (lastSlashMatch) {
464 465
			let pathPortion = searchPath.substr(0, lastSlashMatch.index);
			if (!pathPortion.match(/[/\\]/)) {
466
				// If the last slash was the only slash, then we now have '' or 'C:' or '.'. Append a slash.
467 468 469
				pathPortion += '/';
			}

470
			return {
471
				pathPortion,
M
Matt Bierner 已提交
472
				globPortion: searchPath.substr((lastSlashMatch.index || 0) + 1)
473 474 475 476 477 478 479 480
			};
		}
	}

	// No glob char, or malformed
	return {
		pathPortion: searchPath
	};
481
}
R
Rob Lourens 已提交
482

483
function patternListToIExpression(...patterns: string[]): glob.IExpression {
484 485
	return patterns.length ?
		patterns.reduce((glob, cur) => { glob[cur] = true; return glob; }, Object.create(null)) :
R
Rob Lourens 已提交
486
		undefined;
R
Rob Lourens 已提交
487 488 489 490 491 492 493
}

function splitGlobPattern(pattern: string): string[] {
	return glob.splitGlobAware(pattern, ',')
		.map(s => s.trim())
		.filter(s => !!s.length);
}
494 495

/**
C
ChaseKnowlden 已提交
496
 * Note - we used {} here previously but ripgrep can't handle nested {} patterns. See https://github.com/microsoft/vscode/issues/32761
497
 */
498
function expandGlobalGlob(pattern: string): string[] {
499
	const patterns = [
500 501 502
		`**/${pattern}/**`,
		`**/${pattern}`
	];
503 504

	return patterns.map(p => p.replace(/\*\*\/\*\*/g, '**'));
505
}
506 507 508 509 510 511 512 513 514 515 516 517 518

function normalizeSlashes(pattern: string): string {
	return pattern.replace(/\\/g, '/');
}

/**
 * Normalize slashes, remove `./` and trailing slashes
 */
function normalizeGlobPattern(pattern: string): string {
	return normalizeSlashes(pattern)
		.replace(/^\.\//, '')
		.replace(/\/+$/g, '');
}