From 1db934227b97eb1fca14637c9b739552306a879f Mon Sep 17 00:00:00 2001 From: andreamah Date: Thu, 22 Dec 2022 15:58:23 -0600 Subject: [PATCH] `FindTextInFiles` doesn't support comma-separated globs with {} Fixes #169422 --- .../services/search/common/queryBuilder.ts | 87 ++++++++++++++++++- .../search/test/browser/queryBuilder.test.ts | 21 +++++ 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/services/search/common/queryBuilder.ts b/src/vs/workbench/services/search/common/queryBuilder.ts index d02db90040f3c..3d95ddfc31724 100644 --- a/src/vs/workbench/services/search/common/queryBuilder.ts +++ b/src/vs/workbench/services/search/common/queryBuilder.ts @@ -22,7 +22,6 @@ import { IWorkspaceContextService, IWorkspaceFolderData, toWorkspaceFolder, Work import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { getExcludes, ICommonQueryProps, IFileQuery, IFolderQuery, IPatternInfo, ISearchConfiguration, ITextQuery, ITextSearchPreviewOptions, pathIncludedInQuery, QueryType } from 'vs/workbench/services/search/common/search'; - /** * One folder to search and a glob expression that should be applied. */ @@ -164,7 +163,7 @@ export class QueryBuilder { pattern = Array.isArray(pattern) ? pattern.map(normalizeSlashes) : normalizeSlashes(pattern); return expandPatterns ? this.parseSearchPaths(pattern) - : { pattern: patternListToIExpression(...(Array.isArray(pattern) ? pattern : [pattern])) }; + : this.parseSearchPathWithBraceExpansion(pattern); } private commonQuery(folderResources: (IWorkspaceFolderData | URI)[] = [], options: ICommonQueryBuilderOptions = {}): ICommonQueryProps { @@ -279,6 +278,90 @@ export class QueryBuilder { return !!contentPattern.isMultiline; } + /** + * get first `{` and `}` curly braces that aren't escaped + * - if the brace is prepended by a \ character, then the next character is escaped + */ + private getStartEndInfo(pattern: string): { start: number; end: number; escapedString: string } { + let start = -1; + let inBraces = false; + let escaped = false; + let escapedString = ''; + for (let i = 0; i < pattern.length; i++) { + const char = pattern[i]; + if (escaped) { + escaped = false; + // if we escaped the brace, discard of it, otherwise, keep the escape character + escapedString += (char === '{' || char === '}') ? char : ('\\' + char); + continue; + } + switch (char) { + case '\\': + escaped = true; + break; + case '{': + escapedString += char; + inBraces = true; + start = escapedString.length - 1; + break; + case '}': + escapedString += char; + if (inBraces) { + return { start, end: escapedString.length - 1, escapedString: escapedString + pattern.substring(i + 1) }; + } + break; + default: + escapedString += char; + break; + } + } + + return { start, end: -1, escapedString }; + } + /** + * parses out curly braces and returns equivalent globs + * @param pattern + * @returns + */ + private parseOutBraces(pattern: string): string[] { + const { start, end, escapedString } = this.getStartEndInfo(pattern); + if (start === -1 || end === -1) { + return [escapedString]; + } + + const strInBraces = escapedString.substring(start + 1, end); + const fixedStart = escapedString.substring(0, start); + const fixedEnd = escapedString.substring(end + 1); + + const arr = glob.splitGlobAware(strInBraces, ','); + + const ends = this.parseOutBraces(fixedEnd); + + return arr.flatMap((elem) => { + const start = fixedStart + elem; + return ends.map((end) => { + return start + end; + }); + }); + } + + /** + * parse out the FIRST-LEVEL curly braces from glob + * is aware of escaping, but no other nuances + * @param pattern + * @returns + */ + parseSearchPathWithBraceExpansion(pattern: string | string[]): ISearchPathsInfo { + if (!Array.isArray(pattern)) { + pattern = [pattern]; + } + const patterns = pattern.flatMap((currentPattern) => { + return this.parseOutBraces(currentPattern); + }); + + return { pattern: patternListToIExpression(...(patterns)) }; + } + /** * Take the includePattern as seen in the search viewlet, and split into components that look like searchPaths, and * glob patterns. Glob patterns are expanded from 'foo/bar' to '{foo/bar/**, **\/foo/bar}. diff --git a/src/vs/workbench/services/search/test/browser/queryBuilder.test.ts b/src/vs/workbench/services/search/test/browser/queryBuilder.test.ts index 18d8a3f359f34..02591e61bee83 100644 --- a/src/vs/workbench/services/search/test/browser/queryBuilder.test.ts +++ b/src/vs/workbench/services/search/test/browser/queryBuilder.test.ts @@ -551,6 +551,27 @@ suite('QueryBuilder', () => { }); }); + suite('parseSearchPathWithBraceExpansion', () => { + test('simple includes', () => { + function testSimpleIncludes(includePattern: string, expectedPatterns: string[]): void { + const result = queryBuilder.parseSearchPathWithBraceExpansion(includePattern); + assert.deepStrictEqual( + { ...result.pattern }, + patternsToIExpression(...expectedPatterns), + includePattern); + assert.strictEqual(result.searchPaths, undefined); + } + + [ + ['eep/{a,b}/test', ['eep/a/test', 'eep/b/test']], + ['eep/{a,b}/{c,d,e}', ['eep/a/c', 'eep/a/d', 'eep/a/e', 'eep/b/c', 'eep/b/d', 'eep/b/e']], + ['eep/{a,b}/\\{c,d,e}', ['eep/a/{c,d,e}', 'eep/b/{c,d,e}']], + ['eep/{a,b\\}/test', ['eep/{a,b}/test']], + ['eep/{a,\\b}/test', ['eep/\\b/test', 'eep/a/test']], + ].forEach(([includePattern, expectedPatterns]) => testSimpleIncludes(includePattern, expectedPatterns)); + }); + }); + suite('parseSearchPaths', () => { test('simple includes', () => { function testSimpleIncludes(includePattern: string, expectedPatterns: string[]): void {