queryBuilder.ts 11.6 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 nls = require('vs/nls');
8
import * as arrays from 'vs/base/common/arrays';
9
import * as objects from 'vs/base/common/objects';
R
Rob Lourens 已提交
10 11
import * as collections from 'vs/base/common/collections';
import * as glob from 'vs/base/common/glob';
12
import * as paths from 'vs/base/common/paths';
R
Rob Lourens 已提交
13
import * as strings from 'vs/base/common/strings';
R
Rob Lourens 已提交
14
import uri from 'vs/base/common/uri';
15
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
16
import { IPatternInfo, IQueryOptions, IFolderQuery, ISearchQuery, QueryType, ISearchConfiguration, getExcludes, pathIncludedInQuery } from 'vs/platform/search/common/search';
J
Johannes Rieken 已提交
17
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
E
Erich Gamma 已提交
18

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

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

E
Erich Gamma 已提交
29 30
export class QueryBuilder {

31 32 33
	constructor(
		@IConfigurationService private configurationService: IConfigurationService,
		@IWorkspaceContextService private workspaceContextService: IWorkspaceContextService) {
E
Erich Gamma 已提交
34 35
	}

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

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

R
Rob Lourens 已提交
44
	private query(type: QueryType, contentPattern: IPatternInfo, folderResources?: uri[], options: IQueryOptions = {}): ISearchQuery {
45 46
		let { searchPaths, pattern: includePattern } = this.parseSearchPaths(options.includePattern);
		let excludePattern = this.parseExcludePattern(options.excludePattern);
47

48
		// Build folderQueries from searchPaths, if given, otherwise folderResources
49 50 51 52 53 54
		let folderQueries = folderResources && folderResources.map(uri => this.getFolderQueryForRoot(uri, options));
		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);
55
			}
56
		}
E
Erich Gamma 已提交
57

58
		const useRipgrep = type === QueryType.File ? options.useRipgrep : !folderResources || folderResources.every(folder => {
R
Rob Lourens 已提交
59 60 61
			const folderConfig = this.configurationService.getConfiguration<ISearchConfiguration>(undefined, { resource: folder });
			return folderConfig.search.useRipgrep;
		});
62

63
		const query = <ISearchQuery>{
R
Rob Lourens 已提交
64 65
			type,
			folderQueries,
66 67
			usingSearchPaths: !!(searchPaths && searchPaths.length),
			extraFileResources: options.extraFileResources,
68
			filePattern: options.filePattern,
R
Rob Lourens 已提交
69
			excludePattern,
70
			includePattern,
71
			maxResults: options.maxResults,
72 73
			sortByScore: options.sortByScore,
			cacheKey: options.cacheKey,
74
			contentPattern: contentPattern,
R
Rob Lourens 已提交
75
			useRipgrep,
76
			disregardIgnoreFiles: options.disregardIgnoreFiles,
77
			disregardExcludeSettings: options.disregardExcludeSettings
78
		};
79 80 81 82 83 84

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

R
Rob Lourens 已提交
87 88
	/**
	 * Take the includePattern as seen in the search viewlet, and split into components that look like searchPaths, and
R
Rob Lourens 已提交
89 90 91
	 * glob patterns. Glob patterns are expanded from 'foo/bar' to '{foo/bar/**, **\/foo/bar}.
	 *
	 * Public for test.
R
Rob Lourens 已提交
92
	 */
R
Rob Lourens 已提交
93
	public parseSearchPaths(pattern: string): ISearchPathsResult {
R
Rob Lourens 已提交
94 95
		const isSearchPath = (segment: string) => {
			// A segment is a search path if it is an absolute path or starts with ./
96
			return paths.isAbsolute(segment) || strings.startsWith(segment, './') || strings.startsWith(segment, '.\\');
R
Rob Lourens 已提交
97
		};
R
Rob Lourens 已提交
98

R
Rob Lourens 已提交
99 100 101 102
		const segments = splitGlobPattern(pattern);
		const groups = collections.groupBy(segments,
			segment => isSearchPath(segment) ? 'searchPaths' : 'exprSegments');

103
		const expandedExprSegments = (groups.exprSegments || [])
R
Rob Lourens 已提交
104 105 106 107 108
			.map(p => {
				if (p[0] === '.') {
					p = '*' + p; // convert ".js" to "*.js"
				}

109
				return expandGlobalGlob(p);
R
Rob Lourens 已提交
110
			});
111
		const exprSegments = arrays.flatten(expandedExprSegments);
R
Rob Lourens 已提交
112

R
Rob Lourens 已提交
113 114 115 116 117 118 119 120
		const result: ISearchPathsResult = {};
		const searchPaths = this.expandSearchPathPatterns(groups.searchPaths);
		if (searchPaths && searchPaths.length) {
			result.searchPaths = searchPaths;
		}

		const includePattern = patternListToIExpression(exprSegments);
		if (includePattern) {
121
			result.pattern = includePattern;
R
Rob Lourens 已提交
122 123 124
		}

		return result;
R
Rob Lourens 已提交
125 126
	}

127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
	/**
	 * Takes the input from the excludePattern as seen in the searchViewlet. Runs the same algorithm as parseSearchPaths,
	 * 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;
	}

152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
	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);
					absExpr[absPattern] = true;
				}

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

	private getExcludesForFolder(folderConfig: ISearchConfiguration, options: IQueryOptions): glob.IExpression | undefined {
R
Rob Lourens 已提交
178
		return options.disregardExcludeSettings ?
R
Rob Lourens 已提交
179
			undefined :
R
Rob Lourens 已提交
180
			getExcludes(folderConfig);
R
Rob Lourens 已提交
181
	}
182

R
Rob Lourens 已提交
183 184 185
	/**
	 * Split search paths (./ or absolute paths in the includePatterns) into absolute paths and globs applied to those paths
	 */
186
	private expandSearchPathPatterns(searchPaths: string[]): ISearchPathPattern[] {
187
		if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY || !searchPaths || !searchPaths.length) {
188
			// No workspace => ignore search paths
189
			return [];
190 191
		}

192
		const searchPathPatterns = arrays.flatten(searchPaths.map(searchPath => {
193 194
			// 1 open folder => just resolve the search paths to absolute paths
			const { pathPortion, globPortion } = splitGlobFromPath(searchPath);
195 196 197
			const pathPortions = this.expandAbsoluteSearchPaths(pathPortion);
			return pathPortions.map(searchPath => {
				return <ISearchPathPattern>{
198
					searchPath: uri.file(searchPath),
199
					pattern: globPortion
200 201 202
				};
			});
		}));
203 204

		return searchPathPatterns.filter(arrays.uniqueFilter(searchPathPattern => searchPathPattern.searchPath.toString()));
205 206
	}

207 208 209
	/**
	 * Takes a searchPath like `./a/foo` and expands it to absolute paths for all the workspaces it matches.
	 */
210 211
	private expandAbsoluteSearchPaths(searchPath: string): string[] {
		if (paths.isAbsolute(searchPath)) {
212
			return [paths.normalize(searchPath)];
213
		}
214

215
		if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.FOLDER) { // TODO: @Sandy Try checking workspace folders length instead.
216
			return [paths.normalize(
S
Sandeep Somavarapu 已提交
217
				paths.join(this.workspaceContextService.getWorkspace().folders[0].fsPath, searchPath))];
218
		} else if (searchPath === './') {
219
			return []; // ./ or ./**/foo makes sense for single-folder but not multi-folder workspaces
220
		} else {
221
			const relativeSearchPathMatch = searchPath.match(/\.[\/\\]([^\/\\]+)([\/\\].+)?/);
222 223
			if (relativeSearchPathMatch) {
				const searchPathRoot = relativeSearchPathMatch[1];
S
Sandeep Somavarapu 已提交
224
				const matchingRoots = this.workspaceContextService.getWorkspace().folders.filter(folder => paths.basename(folder.fsPath) === searchPathRoot);
225
				if (matchingRoots.length) {
R
Rob Lourens 已提交
226 227
					return matchingRoots.map(root => {
						return relativeSearchPathMatch[2] ?
228
							paths.normalize(paths.join(root.fsPath, relativeSearchPathMatch[2])) :
R
Rob Lourens 已提交
229 230
							root.fsPath;
					});
231
				} else {
232 233 234
					// No root folder with name
					const searchPathNotFoundError = nls.localize('search.noWorkspaceWithName', "No folder in workspace with name: {0}", searchPathRoot);
					throw new Error(searchPathNotFoundError);
235
				}
236
			} else {
237
				// Malformed ./ search path, ignore
238 239 240
			}
		}

241
		return [];
242
	}
243

244 245 246 247 248 249 250 251 252 253 254
	private getFolderQueryForSearchPath(searchPath: ISearchPathPattern): IFolderQuery {
		const folder = searchPath.searchPath;
		const folderConfig = this.configurationService.getConfiguration<ISearchConfiguration>(undefined, { resource: folder });
		return <IFolderQuery>{
			folder,
			includePattern: searchPath.pattern && patternListToIExpression([searchPath.pattern]),
			fileEncoding: folderConfig.files && folderConfig.files.encoding
		};
	}

	private getFolderQueryForRoot(folder: uri, options?: IQueryOptions): IFolderQuery {
255 256 257 258
		const folderConfig = this.configurationService.getConfiguration<ISearchConfiguration>(undefined, { resource: folder });
		return <IFolderQuery>{
			folder,
			excludePattern: this.getExcludesForFolder(folderConfig, options),
R
Rob Lourens 已提交
259
			fileEncoding: folderConfig.files && folderConfig.files.encoding
260 261
		};
	}
262 263 264 265 266 267 268 269
}

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) {
270 271 272 273 274 275
			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 += '/';
			}

276
			return {
277
				pathPortion,
278 279 280 281 282 283 284 285 286
				globPortion: searchPath.substr(lastSlashMatch.index + 1)
			};
		}
	}

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

function patternListToIExpression(patterns: string[]): glob.IExpression {
290 291
	return patterns.length ?
		patterns.reduce((glob, cur) => { glob[cur] = true; return glob; }, Object.create(null)) :
R
Rob Lourens 已提交
292
		undefined;
R
Rob Lourens 已提交
293 294 295 296 297 298 299
}

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

/**
302
 * Note - we used {} here previously but ripgrep can't handle nested {} patterns. See https://github.com/Microsoft/vscode/issues/32761
303
 */
304 305 306 307 308
function expandGlobalGlob(pattern: string): string[] {
	return [
		`**/${pattern}/**`,
		`**/${pattern}`
	];
309
}