queryBuilder.ts 13.9 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 84 85
			filePattern: options.filePattern
				? options.filePattern.trim()
				: options.filePattern,
R
Rob Lourens 已提交
86
			excludePattern,
87
			includePattern,
88
			maxResults: options.maxResults,
89 90
			sortByScore: options.sortByScore,
			cacheKey: options.cacheKey,
91
			contentPattern: contentPattern,
R
Rob Lourens 已提交
92
			useRipgrep,
93
			disregardIgnoreFiles: options.disregardIgnoreFiles || !useIgnoreFiles,
94 95
			disregardExcludeSettings: options.disregardExcludeSettings,
			ignoreSymlinks
96
		};
97 98 99 100 101 102

		// 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 已提交
103
	}
R
Rob Lourens 已提交
104

105 106 107 108 109 110 111
	/**
	 * 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
112
				if (strings.containsUppercaseCharacter(contentPattern.pattern, true)) {
113 114
					contentPattern.isCaseSensitive = true;
				}
115
			} else if (strings.containsUppercaseCharacter(contentPattern.pattern)) {
116 117 118 119 120
				contentPattern.isCaseSensitive = true;
			}
		}
	}

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

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

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

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

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

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

		return result;
R
Rob Lourens 已提交
160 161
	}

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

187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
	/**
	 * 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;
	}

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

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

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

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

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

		return searchPathPatterns.filter(arrays.uniqueFilter(searchPathPattern => searchPathPattern.searchPath.toString()));
262 263
	}

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

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

299
		return [];
300
	}
301

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

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

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

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

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

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

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

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

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