queryBuilder.ts 13.8 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

7
import * as nls from 'vs/nls';
8
import * as arrays from 'vs/base/common/arrays';
9
import * as objects from 'vs/base/common/objects';
R
Rob Lourens 已提交
10
import * as collections from 'vs/base/common/collections';
11
import * as strings from 'vs/base/common/strings';
R
Rob Lourens 已提交
12
import * as glob from 'vs/base/common/glob';
13
import * as paths from 'vs/base/common/paths';
R
Rob Lourens 已提交
14
import uri from 'vs/base/common/uri';
15
import { untildify } from 'vs/base/common/labels';
16
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
17
import { IPatternInfo, IQueryOptions, IFolderQuery, ISearchQuery, QueryType, ISearchConfiguration, getExcludes, pathIncludedInQuery } from 'vs/platform/search/common/search';
J
Johannes Rieken 已提交
18
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
19
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
E
Erich Gamma 已提交
20

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

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

E
Erich Gamma 已提交
31 32
export class QueryBuilder {

33 34
	constructor(
		@IConfigurationService private configurationService: IConfigurationService,
35 36 37
		@IWorkspaceContextService private workspaceContextService: IWorkspaceContextService,
		@IEnvironmentService private environmentService: IEnvironmentService
	) { }
E
Erich Gamma 已提交
38

R
Rob Lourens 已提交
39 40
	public text(contentPattern: IPatternInfo, folderResources?: uri[], options?: IQueryOptions): ISearchQuery {
		return this.query(QueryType.Text, contentPattern, folderResources, options);
E
Erich Gamma 已提交
41 42
	}

R
Rob Lourens 已提交
43 44
	public file(folderResources?: uri[], options?: IQueryOptions): ISearchQuery {
		return this.query(QueryType.File, null, folderResources, options);
E
Erich Gamma 已提交
45 46
	}

B
Benjamin Pasero 已提交
47
	private query(type: QueryType, contentPattern?: IPatternInfo, folderResources?: uri[], options: IQueryOptions = {}): ISearchQuery {
48 49
		let { searchPaths, pattern: includePattern } = this.parseSearchPaths(options.includePattern);
		let excludePattern = this.parseExcludePattern(options.excludePattern);
50

51
		// Build folderQueries from searchPaths, if given, otherwise folderResources
52
		let folderQueries = folderResources && folderResources.map(uri => this.getFolderQueryForRoot(uri, type === QueryType.File, options));
53 54 55 56 57
		if (searchPaths && searchPaths.length) {
			const allRootExcludes = folderQueries && this.mergeExcludesFromFolderQueries(folderQueries);
			folderQueries = searchPaths.map(searchPath => this.getFolderQueryForSearchPath(searchPath));
			if (allRootExcludes) {
				excludePattern = objects.mixin(excludePattern || Object.create(null), allRootExcludes);
58
			}
59
		}
E
Erich Gamma 已提交
60

61
		// TODO@rob - see #37998
R
Rob Lourens 已提交
62
		const useIgnoreFiles = !folderResources || folderResources.every(folder => {
63
			const folderConfig = this.configurationService.getValue<ISearchConfiguration>({ resource: folder });
64 65 66
			return folderConfig.search.useIgnoreFiles;
		});

67
		const useRipgrep = !folderResources || folderResources.every(folder => {
68
			const folderConfig = this.configurationService.getValue<ISearchConfiguration>({ resource: folder });
R
Rob Lourens 已提交
69 70
			return folderConfig.search.useRipgrep;
		});
71

72
		const ignoreSymlinks = !this.configurationService.getValue<ISearchConfiguration>().search.followSymlinks;
73

B
Benjamin Pasero 已提交
74 75 76
		if (contentPattern) {
			this.resolveSmartCaseToCaseSensitive(contentPattern);
		}
77

78
		const query = <ISearchQuery>{
R
Rob Lourens 已提交
79 80
			type,
			folderQueries,
81 82
			usingSearchPaths: !!(searchPaths && searchPaths.length),
			extraFileResources: options.extraFileResources,
83
			filePattern: options.filePattern,
R
Rob Lourens 已提交
84
			excludePattern,
85
			includePattern,
86
			maxResults: options.maxResults,
87 88
			sortByScore: options.sortByScore,
			cacheKey: options.cacheKey,
89
			contentPattern: contentPattern,
R
Rob Lourens 已提交
90
			useRipgrep,
91
			disregardIgnoreFiles: options.disregardIgnoreFiles || !useIgnoreFiles,
92 93
			disregardExcludeSettings: options.disregardExcludeSettings,
			ignoreSymlinks
94
		};
95 96 97 98 99 100

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

		return query;
E
Erich Gamma 已提交
101
	}
R
Rob Lourens 已提交
102

103 104 105 106 107 108 109
	/**
	 * Fix the isCaseSensitive flag based on the query and the isSmartCase flag, for search providers that don't support smart case natively.
	 */
	private resolveSmartCaseToCaseSensitive(contentPattern: IPatternInfo): void {
		if (contentPattern.isSmartCase) {
			if (contentPattern.isRegExp) {
				// Consider it case sensitive if it contains an unescaped capital letter
110
				if (strings.containsUppercaseCharacter(contentPattern.pattern, true)) {
111 112
					contentPattern.isCaseSensitive = true;
				}
113
			} else if (strings.containsUppercaseCharacter(contentPattern.pattern)) {
114 115 116 117 118
				contentPattern.isCaseSensitive = true;
			}
		}
	}

R
Rob Lourens 已提交
119 120
	/**
	 * Take the includePattern as seen in the search viewlet, and split into components that look like searchPaths, and
R
Rob Lourens 已提交
121 122 123
	 * glob patterns. Glob patterns are expanded from 'foo/bar' to '{foo/bar/**, **\/foo/bar}.
	 *
	 * Public for test.
R
Rob Lourens 已提交
124
	 */
R
Rob Lourens 已提交
125
	public parseSearchPaths(pattern: string): ISearchPathsResult {
R
Rob Lourens 已提交
126
		const isSearchPath = (segment: string) => {
127 128
			// 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 已提交
129
		};
R
Rob Lourens 已提交
130

131 132
		const segments = splitGlobPattern(pattern)
			.map(segment => untildify(segment, this.environmentService.userHome));
R
Rob Lourens 已提交
133 134 135
		const groups = collections.groupBy(segments,
			segment => isSearchPath(segment) ? 'searchPaths' : 'exprSegments');

136
		const expandedExprSegments = (groups.exprSegments || [])
R
Rob Lourens 已提交
137 138 139 140 141
			.map(p => {
				if (p[0] === '.') {
					p = '*' + p; // convert ".js" to "*.js"
				}

142
				return expandGlobalGlob(p);
R
Rob Lourens 已提交
143
			});
144
		const exprSegments = arrays.flatten(expandedExprSegments);
R
Rob Lourens 已提交
145

R
Rob Lourens 已提交
146 147 148 149 150 151 152 153
		const result: ISearchPathsResult = {};
		const searchPaths = this.expandSearchPathPatterns(groups.searchPaths);
		if (searchPaths && searchPaths.length) {
			result.searchPaths = searchPaths;
		}

		const includePattern = patternListToIExpression(exprSegments);
		if (includePattern) {
154
			result.pattern = includePattern;
R
Rob Lourens 已提交
155 156 157
		}

		return result;
R
Rob Lourens 已提交
158 159
	}

160
	/**
I
isidor 已提交
161
	 * Takes the input from the excludePattern as seen in the searchView. Runs the same algorithm as parseSearchPaths,
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
	 * 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;
	}

185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
	/**
	 * A helper that splits positive and negative patterns from a string that combines both.
	 */
	public parseIncludeExcludePattern(pattern: string): { includePattern?: string, excludePattern?: string } {
		const grouped = collections.groupBy(
			splitGlobPattern(pattern),
			s => strings.startsWith(s, '!') ? 'excludePattern' : 'includePattern');

		const result = {};
		if (grouped.includePattern) {
			result['includePattern'] = grouped.includePattern.join(', ');
		}

		if (grouped.excludePattern) {
			result['excludePattern'] = grouped.excludePattern
				.map(s => strings.ltrim(s, '!'))
				.join(', ');
		}

		return result;
	}

207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
	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);
225
					absExpr[absPattern] = expr[key];
226 227 228 229 230 231 232
				}

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

	private getExcludesForFolder(folderConfig: ISearchConfiguration, options: IQueryOptions): glob.IExpression | undefined {
R
Rob Lourens 已提交
233
		return options.disregardExcludeSettings ?
R
Rob Lourens 已提交
234
			undefined :
R
Rob Lourens 已提交
235
			getExcludes(folderConfig);
R
Rob Lourens 已提交
236
	}
237

R
Rob Lourens 已提交
238 239 240
	/**
	 * Split search paths (./ or absolute paths in the includePatterns) into absolute paths and globs applied to those paths
	 */
241
	private expandSearchPathPatterns(searchPaths: string[]): ISearchPathPattern[] {
242
		if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY || !searchPaths || !searchPaths.length) {
243
			// No workspace => ignore search paths
244
			return [];
245 246
		}

247
		const searchPathPatterns = arrays.flatten(searchPaths.map(searchPath => {
248 249
			// 1 open folder => just resolve the search paths to absolute paths
			const { pathPortion, globPortion } = splitGlobFromPath(searchPath);
250 251 252
			const pathPortions = this.expandAbsoluteSearchPaths(pathPortion);
			return pathPortions.map(searchPath => {
				return <ISearchPathPattern>{
R
Rob Lourens 已提交
253
					searchPath,
254
					pattern: globPortion
255 256 257
				};
			});
		}));
258 259

		return searchPathPatterns.filter(arrays.uniqueFilter(searchPathPattern => searchPathPattern.searchPath.toString()));
260 261
	}

262 263 264
	/**
	 * Takes a searchPath like `./a/foo` and expands it to absolute paths for all the workspaces it matches.
	 */
R
Rob Lourens 已提交
265
	private expandAbsoluteSearchPaths(searchPath: string): uri[] {
266
		if (paths.isAbsolute(searchPath)) {
R
Rob Lourens 已提交
267 268
			// Currently only local resources can be searched for with absolute search paths
			return [uri.file(paths.normalize(searchPath))];
269
		}
270

271
		if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.FOLDER) { // TODO: @Sandy Try checking workspace folders length instead.
R
Rob Lourens 已提交
272 273
			const workspaceUri = this.workspaceContextService.getWorkspace().folders[0].uri;
			return [workspaceUri.with({ path: paths.normalize(paths.join(workspaceUri.path, searchPath)) })];
274
		} else if (searchPath === './') {
275
			return []; // ./ or ./**/foo makes sense for single-folder but not multi-folder workspaces
276
		} else {
277
			const relativeSearchPathMatch = searchPath.match(/\.[\/\\]([^\/\\]+)([\/\\].+)?/);
278 279
			if (relativeSearchPathMatch) {
				const searchPathRoot = relativeSearchPathMatch[1];
280
				const matchingRoots = this.workspaceContextService.getWorkspace().folders.filter(folder => paths.basename(folder.uri.fsPath) === searchPathRoot);
281
				if (matchingRoots.length) {
R
Rob Lourens 已提交
282 283
					return matchingRoots.map(root => {
						return relativeSearchPathMatch[2] ?
284
							root.uri.with({ path: paths.normalize(paths.join(root.uri.path, relativeSearchPathMatch[2])) }) :
R
Rob Lourens 已提交
285
							root.uri;
R
Rob Lourens 已提交
286
					});
287
				} else {
288 289 290
					// No root folder with name
					const searchPathNotFoundError = nls.localize('search.noWorkspaceWithName', "No folder in workspace with name: {0}", searchPathRoot);
					throw new Error(searchPathNotFoundError);
291
				}
292
			} else {
293
				// Malformed ./ search path, ignore
294 295 296
			}
		}

297
		return [];
298
	}
299

300 301
	private getFolderQueryForSearchPath(searchPath: ISearchPathPattern): IFolderQuery {
		const folder = searchPath.searchPath;
302
		const folderConfig = this.configurationService.getValue<ISearchConfiguration>({ resource: folder });
303 304 305 306 307 308 309
		return <IFolderQuery>{
			folder,
			includePattern: searchPath.pattern && patternListToIExpression([searchPath.pattern]),
			fileEncoding: folderConfig.files && folderConfig.files.encoding
		};
	}

310
	private getFolderQueryForRoot(folder: uri, perFolderUseIgnoreFiles: boolean, options?: IQueryOptions): IFolderQuery {
311
		const folderConfig = this.configurationService.getValue<ISearchConfiguration>({ resource: folder });
312 313 314
		return <IFolderQuery>{
			folder,
			excludePattern: this.getExcludesForFolder(folderConfig, options),
315 316
			fileEncoding: folderConfig.files && folderConfig.files.encoding,
			disregardIgnoreFiles: perFolderUseIgnoreFiles ? !folderConfig.search.useIgnoreFiles : undefined
317 318
		};
	}
319 320 321 322 323 324 325 326
}

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) {
327 328 329 330 331 332
			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 += '/';
			}

333
			return {
334
				pathPortion,
335 336 337 338 339 340 341 342 343
				globPortion: searchPath.substr(lastSlashMatch.index + 1)
			};
		}
	}

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

function patternListToIExpression(patterns: string[]): glob.IExpression {
347 348
	return patterns.length ?
		patterns.reduce((glob, cur) => { glob[cur] = true; return glob; }, Object.create(null)) :
R
Rob Lourens 已提交
349
		undefined;
R
Rob Lourens 已提交
350 351 352 353 354 355 356
}

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

/**
359
 * Note - we used {} here previously but ripgrep can't handle nested {} patterns. See https://github.com/Microsoft/vscode/issues/32761
360
 */
361 362 363 364 365
function expandGlobalGlob(pattern: string): string[] {
	return [
		`**/${pattern}/**`,
		`**/${pattern}`
	];
366
}