queryBuilder.ts 14.7 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 10
import { untildify } from 'vs/base/common/labels';
import * as objects from 'vs/base/common/objects';
11
import * as paths from 'vs/base/common/paths';
12
import * as resources 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 { IEnvironmentService } from 'vs/platform/environment/common/environment';
19 20
import { getExcludes, ICommonQueryProps, IFileQuery, IFolderQuery, IPatternInfo, ISearchConfiguration, ITextQuery, ITextSearchPreviewOptions, pathIncludedInQuery, QueryType } from 'vs/platform/search/common/search';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
E
Erich Gamma 已提交
21

R
Rob Lourens 已提交
22
export interface ISearchPathPattern {
23 24 25 26
	searchPath: uri;
	pattern?: string;
}

R
Rob Lourens 已提交
27 28
export interface ISearchPathsResult {
	searchPaths?: ISearchPathPattern[];
29
	pattern?: glob.IExpression;
R
Rob Lourens 已提交
30 31
}

32
export interface ICommonQueryBuilderOptions {
R
Rob Lourens 已提交
33
	_reason?: string;
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
	excludePattern?: string;
	includePattern?: string;
	extraFileResources?: uri[];

	maxResults?: number;
	useRipgrep?: boolean;
	disregardIgnoreFiles?: boolean;
	disregardGlobalIgnoreFiles?: boolean;
	disregardExcludeSettings?: boolean;
	ignoreSymlinks?: boolean;
}

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

export interface ITextQueryBuilderOptions extends ICommonQueryBuilderOptions {
	previewOptions?: ITextSearchPreviewOptions;
	fileEncoding?: string;
	maxFileSize?: number;
}

E
Erich Gamma 已提交
59 60
export class QueryBuilder {

61 62
	constructor(
		@IConfigurationService private configurationService: IConfigurationService,
63 64 65
		@IWorkspaceContextService private workspaceContextService: IWorkspaceContextService,
		@IEnvironmentService private environmentService: IEnvironmentService
	) { }
E
Erich Gamma 已提交
66

67 68 69
	text(contentPattern: IPatternInfo, folderResources?: uri[], options?: ITextQueryBuilderOptions): ITextQuery {
		contentPattern.isCaseSensitive = this.isCaseSensitive(contentPattern);
		contentPattern.isMultiline = this.isMultiline(contentPattern);
R
Rob Lourens 已提交
70 71
		const searchConfig = this.configurationService.getValue<ISearchConfiguration>();
		contentPattern.wordSeparators = searchConfig.editor.wordSeparators;
72

73 74 75 76 77
		const fallbackToPCRE = !folderResources || folderResources.some(folder => {
			const folderConfig = this.configurationService.getValue<ISearchConfiguration>({ resource: folder });
			return !folderConfig.search.useRipgrep;
		});

78 79 80 81 82 83
		const commonQuery = this.commonQuery(folderResources, options);
		return <ITextQuery>{
			...commonQuery,
			type: QueryType.Text,
			contentPattern,
			previewOptions: options && options.previewOptions,
R
Rob Lourens 已提交
84
			maxFileSize: options && options.maxFileSize,
85
			usePCRE2: searchConfig.search.usePCRE2 || fallbackToPCRE
86
		};
E
Erich Gamma 已提交
87 88
	}

89 90 91 92 93 94 95 96 97 98 99 100
	file(folderResources?: uri[], options?: IFileQueryBuilderOptions): IFileQuery {
		const commonQuery = this.commonQuery(folderResources, options);
		return <IFileQuery>{
			...commonQuery,
			type: QueryType.File,
			filePattern: options.filePattern
				? options.filePattern.trim()
				: options.filePattern,
			exists: options.exists,
			sortByScore: options.sortByScore,
			cacheKey: options.cacheKey
		};
E
Erich Gamma 已提交
101 102
	}

103
	private commonQuery(folderResources?: uri[], options: ICommonQueryBuilderOptions = {}): ICommonQueryProps<uri> {
104 105
		let { searchPaths, pattern: includePattern } = this.parseSearchPaths(options.includePattern);
		let excludePattern = this.parseExcludePattern(options.excludePattern);
106

107
		// Build folderQueries from searchPaths, if given, otherwise folderResources
108
		let folderQueries = folderResources && folderResources.map(uri => this.getFolderQueryForRoot(uri, options));
109 110
		if (searchPaths && searchPaths.length) {
			const allRootExcludes = folderQueries && this.mergeExcludesFromFolderQueries(folderQueries);
111
			folderQueries = searchPaths.map(searchPath => this.getFolderQueryForSearchPath(searchPath)); // TODO Rob
112 113
			if (allRootExcludes) {
				excludePattern = objects.mixin(excludePattern || Object.create(null), allRootExcludes);
114
			}
115
		}
E
Erich Gamma 已提交
116

117
		const useRipgrep = !folderResources || folderResources.every(folder => {
118
			const folderConfig = this.configurationService.getValue<ISearchConfiguration>({ resource: folder });
119
			return !folderConfig.search.disableRipgrep;
R
Rob Lourens 已提交
120
		});
121

122
		const queryProps: ICommonQueryProps<uri> = {
R
Rob Lourens 已提交
123
			_reason: options._reason,
R
Rob Lourens 已提交
124
			folderQueries: folderQueries || [],
125 126
			usingSearchPaths: !!(searchPaths && searchPaths.length),
			extraFileResources: options.extraFileResources,
127

R
Rob Lourens 已提交
128
			excludePattern,
129
			includePattern,
130
			maxResults: options.maxResults,
131
			useRipgrep
132
		};
133 134

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

138
		return queryProps;
E
Erich Gamma 已提交
139
	}
R
Rob Lourens 已提交
140

141
	/**
142
	 * Resolve isCaseSensitive flag based on the query and the isSmartCase flag, for search providers that don't support smart case natively.
143
	 */
144
	private isCaseSensitive(contentPattern: IPatternInfo): boolean {
145 146 147
		if (contentPattern.isSmartCase) {
			if (contentPattern.isRegExp) {
				// Consider it case sensitive if it contains an unescaped capital letter
148
				if (strings.containsUppercaseCharacter(contentPattern.pattern, true)) {
149
					return true;
150
				}
151
			} else if (strings.containsUppercaseCharacter(contentPattern.pattern)) {
152
				return true;
153 154
			}
		}
155 156 157 158 159 160 161 162 163 164 165 166 167 168

		return contentPattern.isCaseSensitive;
	}

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

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

		return false;
169 170
	}

R
Rob Lourens 已提交
171 172
	/**
	 * Take the includePattern as seen in the search viewlet, and split into components that look like searchPaths, and
R
Rob Lourens 已提交
173 174 175
	 * glob patterns. Glob patterns are expanded from 'foo/bar' to '{foo/bar/**, **\/foo/bar}.
	 *
	 * Public for test.
R
Rob Lourens 已提交
176
	 */
R
Rob Lourens 已提交
177
	public parseSearchPaths(pattern: string): ISearchPathsResult {
R
Rob Lourens 已提交
178
		const isSearchPath = (segment: string) => {
179 180
			// A segment is a search path if it is an absolute path or starts with ./, ../, .\, or ..\
			return paths.isAbsolute(segment) || /^\.\.?[\/\\]/.test(segment);
R
Rob Lourens 已提交
181
		};
R
Rob Lourens 已提交
182

183 184
		const segments = splitGlobPattern(pattern)
			.map(segment => untildify(segment, this.environmentService.userHome));
R
Rob Lourens 已提交
185 186 187
		const groups = collections.groupBy(segments,
			segment => isSearchPath(segment) ? 'searchPaths' : 'exprSegments');

188
		const expandedExprSegments = (groups.exprSegments || [])
R
Rob Lourens 已提交
189 190 191 192 193
			.map(p => {
				if (p[0] === '.') {
					p = '*' + p; // convert ".js" to "*.js"
				}

194
				return expandGlobalGlob(p);
R
Rob Lourens 已提交
195
			});
196
		const exprSegments = arrays.flatten(expandedExprSegments);
R
Rob Lourens 已提交
197

R
Rob Lourens 已提交
198 199 200 201 202 203 204 205
		const result: ISearchPathsResult = {};
		const searchPaths = this.expandSearchPathPatterns(groups.searchPaths);
		if (searchPaths && searchPaths.length) {
			result.searchPaths = searchPaths;
		}

		const includePattern = patternListToIExpression(exprSegments);
		if (includePattern) {
206
			result.pattern = includePattern;
R
Rob Lourens 已提交
207 208 209
		}

		return result;
R
Rob Lourens 已提交
210 211
	}

212
	/**
I
isidor 已提交
213
	 * Takes the input from the excludePattern as seen in the searchView. Runs the same algorithm as parseSearchPaths,
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
	 * but the result is a single IExpression that encapsulates all the exclude patterns.
	 */
	public parseExcludePattern(pattern: string): glob.IExpression | undefined {
		const result = this.parseSearchPaths(pattern);
		let excludeExpression = glob.getEmptyExpression();
		if (result.pattern) {
			excludeExpression = objects.mixin(excludeExpression, result.pattern);
		}

		if (result.searchPaths) {
			result.searchPaths.forEach(searchPath => {
				const excludeFsPath = searchPath.searchPath.fsPath;
				const excludePath = searchPath.pattern ?
					paths.join(excludeFsPath, searchPath.pattern) :
					excludeFsPath;

				excludeExpression[excludePath] = true;
			});
		}

		return Object.keys(excludeExpression).length ? excludeExpression : undefined;
	}

237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
	private mergeExcludesFromFolderQueries(folderQueries: IFolderQuery[]): glob.IExpression | undefined {
		const mergedExcludes = folderQueries.reduce((merged: glob.IExpression, fq: IFolderQuery) => {
			if (fq.excludePattern) {
				objects.mixin(merged, this.getAbsoluteIExpression(fq.excludePattern, fq.folder.fsPath));
			}

			return merged;
		}, Object.create(null));

		// Don't return an empty IExpression
		return Object.keys(mergedExcludes).length ? mergedExcludes : undefined;
	}

	private getAbsoluteIExpression(expr: glob.IExpression, root: string): glob.IExpression {
		return Object.keys(expr)
			.reduce((absExpr: glob.IExpression, key: string) => {
				if (expr[key] && !paths.isAbsolute(key)) {
					const absPattern = paths.join(root, key);
255
					absExpr[absPattern] = expr[key];
256 257 258 259 260 261
				}

				return absExpr;
			}, Object.create(null));
	}

262
	private getExcludesForFolder(folderConfig: ISearchConfiguration, options: ICommonQueryBuilderOptions): glob.IExpression | undefined {
R
Rob Lourens 已提交
263
		return options.disregardExcludeSettings ?
R
Rob Lourens 已提交
264
			undefined :
R
Rob Lourens 已提交
265
			getExcludes(folderConfig);
R
Rob Lourens 已提交
266
	}
267

R
Rob Lourens 已提交
268 269 270
	/**
	 * Split search paths (./ or absolute paths in the includePatterns) into absolute paths and globs applied to those paths
	 */
271
	private expandSearchPathPatterns(searchPaths: string[]): ISearchPathPattern[] {
272
		if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY || !searchPaths || !searchPaths.length) {
273
			// No workspace => ignore search paths
274
			return [];
275 276
		}

277
		const searchPathPatterns = arrays.flatten(searchPaths.map(searchPath => {
278 279
			// 1 open folder => just resolve the search paths to absolute paths
			const { pathPortion, globPortion } = splitGlobFromPath(searchPath);
280 281 282
			const pathPortions = this.expandAbsoluteSearchPaths(pathPortion);
			return pathPortions.map(searchPath => {
				return <ISearchPathPattern>{
R
Rob Lourens 已提交
283
					searchPath,
284
					pattern: globPortion
285 286 287
				};
			});
		}));
288 289

		return searchPathPatterns.filter(arrays.uniqueFilter(searchPathPattern => searchPathPattern.searchPath.toString()));
290 291
	}

292 293 294
	/**
	 * Takes a searchPath like `./a/foo` and expands it to absolute paths for all the workspaces it matches.
	 */
R
Rob Lourens 已提交
295
	private expandAbsoluteSearchPaths(searchPath: string): uri[] {
296
		if (paths.isAbsolute(searchPath)) {
R
Rob Lourens 已提交
297 298
			// Currently only local resources can be searched for with absolute search paths
			return [uri.file(paths.normalize(searchPath))];
299
		}
300

301
		if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.FOLDER) {
R
Rob Lourens 已提交
302
			const workspaceUri = this.workspaceContextService.getWorkspace().folders[0].uri;
303
			return [resources.joinPath(workspaceUri, searchPath)];
304
		} else if (searchPath === './') {
305
			return []; // ./ or ./**/foo makes sense for single-folder but not multi-folder workspaces
306
		} else {
307
			const relativeSearchPathMatch = searchPath.match(/\.[\/\\]([^\/\\]+)([\/\\].+)?/);
308 309
			if (relativeSearchPathMatch) {
				const searchPathRoot = relativeSearchPathMatch[1];
310
				const matchingRoots = this.workspaceContextService.getWorkspace().folders.filter(folder => folder.name === searchPathRoot);
311
				if (matchingRoots.length) {
R
Rob Lourens 已提交
312 313
					return matchingRoots.map(root => {
						return relativeSearchPathMatch[2] ?
314
							resources.joinPath(root.uri, relativeSearchPathMatch[2]) :
R
Rob Lourens 已提交
315
							root.uri;
R
Rob Lourens 已提交
316
					});
317
				} else {
318 319 320
					// No root folder with name
					const searchPathNotFoundError = nls.localize('search.noWorkspaceWithName', "No folder in workspace with name: {0}", searchPathRoot);
					throw new Error(searchPathNotFoundError);
321
				}
322
			} else {
323
				// Malformed ./ search path, ignore
324 325 326
			}
		}

327
		return [];
328
	}
329

330 331
	private getFolderQueryForSearchPath(searchPath: ISearchPathPattern): IFolderQuery {
		const folder = searchPath.searchPath;
332
		const folderConfig = this.configurationService.getValue<ISearchConfiguration>({ resource: folder });
333 334 335 336 337 338 339
		return <IFolderQuery>{
			folder,
			includePattern: searchPath.pattern && patternListToIExpression([searchPath.pattern]),
			fileEncoding: folderConfig.files && folderConfig.files.encoding
		};
	}

340
	private getFolderQueryForRoot(folder: uri, options: ICommonQueryBuilderOptions): IFolderQuery {
341
		const folderConfig = this.configurationService.getValue<ISearchConfiguration>({ resource: folder });
342 343 344
		return <IFolderQuery>{
			folder,
			excludePattern: this.getExcludesForFolder(folderConfig, options),
345
			fileEncoding: folderConfig.files && folderConfig.files.encoding,
346 347 348
			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,
349 350
		};
	}
351 352 353 354 355 356 357 358
}

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) {
359 360 361 362 363 364
			let pathPortion = searchPath.substr(0, lastSlashMatch.index);
			if (!pathPortion.match(/[/\\]/)) {
				// If the last slash was the only slash, then we now have '' or 'C:'. Append a slash.
				pathPortion += '/';
			}

365
			return {
366
				pathPortion,
367 368 369 370 371 372 373 374 375
				globPortion: searchPath.substr(lastSlashMatch.index + 1)
			};
		}
	}

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

function patternListToIExpression(patterns: string[]): glob.IExpression {
379 380
	return patterns.length ?
		patterns.reduce((glob, cur) => { glob[cur] = true; return glob; }, Object.create(null)) :
R
Rob Lourens 已提交
381
		undefined;
R
Rob Lourens 已提交
382 383 384 385 386 387 388
}

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

/**
391
 * Note - we used {} here previously but ripgrep can't handle nested {} patterns. See https://github.com/Microsoft/vscode/issues/32761
392
 */
393
function expandGlobalGlob(pattern: string): string[] {
394
	const patterns = [
395 396 397
		`**/${pattern}/**`,
		`**/${pattern}`
	];
398 399

	return patterns.map(p => p.replace(/\*\*\/\*\*/g, '**'));
400
}