diff --git a/extensions/git/package.json b/extensions/git/package.json index 80526a43d271a..0bbf3ae575773 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -71,6 +71,11 @@ "category": "Git", "icon": "$(compare-changes)" }, + { + "command": "git.openAllChanges", + "title": "%command.openAllChanges%", + "category": "Git" + }, { "command": "git.openFile", "title": "%command.openFile%", diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 8d18564961ea1..f76c42dbb550e 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -9,6 +9,7 @@ "command.close": "Close Repository", "command.refresh": "Refresh", "command.openChange": "Open Changes", + "command.openAllChanges": "Open All Changes", "command.openFile": "Open File", "command.openHEADFile": "Open File (HEAD)", "command.stage": "Stage Changes", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index bdd13a197bf0f..ac9249871fcaa 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -372,6 +372,20 @@ export class CommandCenter { await resource.open(); } + @command('git.openAllChanges', { repository: true }) + async openChanges(repository: Repository): Promise { + [ + ...repository.workingTreeGroup.resourceStates, + ...repository.untrackedGroup.resourceStates, + ].forEach(resource => { + commands.executeCommand( + 'vscode.open', + resource.resourceUri, + { preview: false, } + ); + }); + } + async cloneRepository(url?: string, parentPath?: string, options: { recursive?: boolean } = {}): Promise { if (!url || typeof url !== 'string') { url = await pickRemoteSource(this.model, { diff --git a/src/vs/workbench/contrib/search/browser/patternInputWidget.ts b/src/vs/workbench/contrib/search/browser/patternInputWidget.ts index 63bf2771a464d..d59e779686d19 100644 --- a/src/vs/workbench/contrib/search/browser/patternInputWidget.ts +++ b/src/vs/workbench/contrib/search/browser/patternInputWidget.ts @@ -18,7 +18,8 @@ import { ContextScopedHistoryInputBox } from 'vs/platform/browser/contextScopedH import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import type { IThemable } from 'vs/base/common/styler'; import { Codicon } from 'vs/base/common/codicons'; - +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ISearchConfiguration } from 'vs/workbench/services/search/common/search'; export interface IOptions { placeholder?: string; width?: number; @@ -50,7 +51,8 @@ export class PatternInputWidget extends Widget implements IThemable { constructor(parent: HTMLElement, private contextViewProvider: IContextViewProvider, options: IOptions = Object.create(null), @IThemeService protected themeService: IThemeService, - @IContextKeyService private readonly contextKeyService: IContextKeyService + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IConfigurationService protected readonly configurationService: IConfigurationService ) { super(); this.width = options.width || 100; @@ -178,6 +180,62 @@ export class PatternInputWidget extends Widget implements IThemable { } } +export class IncludePatternInputWidget extends PatternInputWidget { + + private _onChangeSearchInEditorsBoxEmitter = this._register(new Emitter()); + onChangeSearchInEditorsBox = this._onChangeSearchInEditorsBoxEmitter.event; + + constructor(parent: HTMLElement, contextViewProvider: IContextViewProvider, options: IOptions = Object.create(null), + @IThemeService themeService: IThemeService, + @IContextKeyService contextKeyService: IContextKeyService, + @IConfigurationService configurationService: IConfigurationService, + ) { + super(parent, contextViewProvider, options, themeService, contextKeyService, configurationService); + } + + private useSearchInEditorsBox!: Checkbox; + + dispose(): void { + super.dispose(); + this.useSearchInEditorsBox.dispose(); + } + + onlySearchInOpenEditors(): boolean { + return this.useSearchInEditorsBox.checked; + } + + setOnlySearchInOpenEditors(value: boolean) { + this.useSearchInEditorsBox.checked = value; + } + + protected getSubcontrolsWidth(): number { + if (this.configurationService.getValue().search?.experimental?.searchInOpenEditors) { + return super.getSubcontrolsWidth() + this.useSearchInEditorsBox.width(); + } + return super.getSubcontrolsWidth(); + } + + protected renderSubcontrols(controlsDiv: HTMLDivElement): void { + this.useSearchInEditorsBox = this._register(new Checkbox({ + icon: Codicon.book, + title: nls.localize('onlySearchInOpenEditors', "Search only in Open Editors"), + isChecked: false, + })); + if (!this.configurationService.getValue().search?.experimental?.searchInOpenEditors) { + return; + } + this._register(this.useSearchInEditorsBox.onChange(viaKeyboard => { + this._onChangeSearchInEditorsBoxEmitter.fire(); + if (!viaKeyboard) { + this.inputBox.focus(); + } + })); + this._register(attachCheckboxStyler(this.useSearchInEditorsBox, this.themeService)); + controlsDiv.appendChild(this.useSearchInEditorsBox.domNode); + super.renderSubcontrols(controlsDiv); + } +} + export class ExcludePatternInputWidget extends PatternInputWidget { private _onChangeIgnoreBoxEmitter = this._register(new Emitter()); @@ -185,9 +243,10 @@ export class ExcludePatternInputWidget extends PatternInputWidget { constructor(parent: HTMLElement, contextViewProvider: IContextViewProvider, options: IOptions = Object.create(null), @IThemeService themeService: IThemeService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IConfigurationService configurationService: IConfigurationService, ) { - super(parent, contextViewProvider, options, themeService, contextKeyService); + super(parent, contextViewProvider, options, themeService, contextKeyService, configurationService); } private useExcludesAndIgnoreFilesBox!: Checkbox; diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index ebda880f06653..c1f04f5e8bddd 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -998,6 +998,11 @@ configurationRegistry.registerConfiguration({ ], 'description': nls.localize('search.sortOrder', "Controls sorting order of search results.") }, + 'search.experimental.searchInOpenEditors': { + type: 'boolean', + default: false, + markdownDescription: nls.localize('search.experimental.searchInOpenEditors', "Experimental. When enabled, an option is provided to make workspace search only search files that have been opened. **Requires restart to take effect.**") + } } }); diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 232a61580e707..a466e6d107ebc 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -54,7 +54,7 @@ import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/vie import { IEditorPane } from 'vs/workbench/common/editor'; import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { IViewDescriptorService } from 'vs/workbench/common/views'; -import { ExcludePatternInputWidget, PatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget'; +import { ExcludePatternInputWidget, IncludePatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget'; import { appendKeyBindingLabel, IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActions'; import { searchDetailsIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; import { FileMatchRenderer, FolderMatchRenderer, MatchRenderer, SearchAccessibilityProvider, SearchDelegate, SearchDND } from 'vs/workbench/contrib/search/browser/searchResultsView'; @@ -125,7 +125,7 @@ export class SearchView extends ViewPane { private queryDetails!: HTMLElement; private toggleQueryDetailsButton!: HTMLElement; private inputPatternExcludes!: ExcludePatternInputWidget; - private inputPatternIncludes!: PatternInputWidget; + private inputPatternIncludes!: IncludePatternInputWidget; private resultsElement!: HTMLElement; private currentSelectedFileMatch: FileMatch | undefined; @@ -309,14 +309,17 @@ export class SearchView extends ViewPane { const filesToIncludeTitle = nls.localize('searchScope.includes', "files to include"); dom.append(folderIncludesList, $('h4', undefined, filesToIncludeTitle)); - this.inputPatternIncludes = this._register(this.instantiationService.createInstance(PatternInputWidget, folderIncludesList, this.contextViewService, { + this.inputPatternIncludes = this._register(this.instantiationService.createInstance(IncludePatternInputWidget, folderIncludesList, this.contextViewService, { ariaLabel: nls.localize('label.includes', 'Search Include Patterns'), history: patternIncludesHistory, })); this.inputPatternIncludes.setValue(patternIncludes); + this._register(this.inputPatternIncludes.onSubmit(triggeredOnType => this.triggerQueryChange({ triggeredOnType, delay: this.searchConfig.searchOnTypeDebouncePeriod }))); this._register(this.inputPatternIncludes.onCancel(() => this.cancelSearch(false))); + this._register(this.inputPatternIncludes.onChangeSearchInEditorsBox(() => this.triggerQueryChange())); + this.trackInputBox(this.inputPatternIncludes.inputFocusTracker, this.inputPatternIncludesFocused); // excludes list @@ -385,7 +388,7 @@ export class SearchView extends ViewPane { return this.searchWidget; } - get searchIncludePattern(): PatternInputWidget { + get searchIncludePattern(): IncludePatternInputWidget { return this.inputPatternIncludes; } @@ -1293,6 +1296,7 @@ export class SearchView extends ViewPane { const excludePatternText = this.inputPatternExcludes.getValue().trim(); const includePatternText = this.inputPatternIncludes.getValue().trim(); const useExcludesAndIgnoreFiles = this.inputPatternExcludes.useExcludesAndIgnoreFiles(); + const onlySearchInOpenEditors = this.inputPatternIncludes.onlySearchInOpenEditors(); if (contentPattern.length === 0) { this.clearSearchResults(false); @@ -1321,6 +1325,7 @@ export class SearchView extends ViewPane { maxResults: SearchView.MAX_TEXT_RESULTS, disregardIgnoreFiles: !useExcludesAndIgnoreFiles || undefined, disregardExcludeSettings: !useExcludesAndIgnoreFiles || undefined, + onlyOpenEditors: onlySearchInOpenEditors, excludePattern, includePattern, previewOptions: { @@ -1443,14 +1448,26 @@ export class SearchView extends ViewPane { if (!completed) { message = SEARCH_CANCELLED_MESSAGE; - } else if (hasIncludes && hasExcludes) { - message = nls.localize('noResultsIncludesExcludes', "No results found in '{0}' excluding '{1}' - ", includePatternText, excludePatternText); - } else if (hasIncludes) { - message = nls.localize('noResultsIncludes', "No results found in '{0}' - ", includePatternText); - } else if (hasExcludes) { - message = nls.localize('noResultsExcludes', "No results found excluding '{0}' - ", excludePatternText); + } else if (this.inputPatternIncludes.onlySearchInOpenEditors()) { + if (hasIncludes && hasExcludes) { + message = nls.localize('noOpenEditorResultsIncludesExcludes', "No results found in open editors matching '{0}' excluding '{1}' - ", includePatternText, excludePatternText); + } else if (hasIncludes) { + message = nls.localize('noOpenEditorResultsIncludes', "No results found in open editors matching '{0}' - ", includePatternText); + } else if (hasExcludes) { + message = nls.localize('noOpenEditorResultsExcludes', "No results found in open editors excluding '{0}' - ", excludePatternText); + } else { + message = nls.localize('noOpenEditorResultsFound', "No results found in open editors. Review your settings for configured exclusions and check your gitignore files - "); + } } else { - message = nls.localize('noResultsFound', "No results found. Review your settings for configured exclusions and check your gitignore files - "); + if (hasIncludes && hasExcludes) { + message = nls.localize('noResultsIncludesExcludes', "No results found in '{0}' excluding '{1}' - ", includePatternText, excludePatternText); + } else if (hasIncludes) { + message = nls.localize('noResultsIncludes', "No results found in '{0}' - ", includePatternText); + } else if (hasExcludes) { + message = nls.localize('noResultsExcludes', "No results found excluding '{0}' - ", excludePatternText); + } else { + message = nls.localize('noResultsFound', "No results found. Review your settings for configured exclusions and check your gitignore files - "); + } } // Indicate as status to ARIA @@ -1472,6 +1489,7 @@ export class SearchView extends ViewPane { this.inputPatternExcludes.setValue(''); this.inputPatternIncludes.setValue(''); + this.inputPatternIncludes.setOnlySearchInOpenEditors(false); this.triggerQueryChange({ preserveFocus: false }); })); @@ -1599,7 +1617,7 @@ export class SearchView extends ViewPane { this.messageDisposables.push(dom.addDisposableListener(openInEditorLink, dom.EventType.CLICK, (e: MouseEvent) => { dom.EventHelper.stop(e, false); - this.instantiationService.invokeFunction(createEditorFromSearchResult, this.searchResult, this.searchIncludePattern.getValue(), this.searchExcludePattern.getValue()); + this.instantiationService.invokeFunction(createEditorFromSearchResult, this.searchResult, this.searchIncludePattern.getValue(), this.searchExcludePattern.getValue(), this.searchIncludePattern.onlySearchInOpenEditors()); })); this.reLayout(); diff --git a/src/vs/workbench/contrib/search/common/queryBuilder.ts b/src/vs/workbench/contrib/search/common/queryBuilder.ts index d8a1a911e4568..7bb48f50172d7 100644 --- a/src/vs/workbench/contrib/search/common/queryBuilder.ts +++ b/src/vs/workbench/contrib/search/common/queryBuilder.ts @@ -16,6 +16,7 @@ import { isMultilineRegexSource } from 'vs/editor/common/model/textModelSearch'; import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkspaceContextService, IWorkspaceFolderData, toWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +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'; @@ -59,6 +60,7 @@ export interface ICommonQueryBuilderOptions { disregardExcludeSettings?: boolean; disregardSearchExcludeSettings?: boolean; ignoreSymlinks?: boolean; + onlyOpenEditors?: boolean; } export interface IFileQueryBuilderOptions extends ICommonQueryBuilderOptions { @@ -81,6 +83,7 @@ export class QueryBuilder { constructor( @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, @IPathService private readonly pathService: IPathService ) { } @@ -148,20 +151,21 @@ export class QueryBuilder { }; } - private handleIncludeExclude(pattern: string | string[] | undefined, expandPatterns: boolean | undefined): ISearchPathsInfo { + private handleIncludeExclude(pattern: string | string[] | undefined, expandPatterns: 'strict' | 'loose' | 'none'): ISearchPathsInfo { if (!pattern) { return {}; } pattern = Array.isArray(pattern) ? pattern.map(normalizeSlashes) : normalizeSlashes(pattern); - return expandPatterns ? - this.parseSearchPaths(pattern) : - { pattern: patternListToIExpression(...(Array.isArray(pattern) ? pattern : [pattern])) }; + return expandPatterns === 'none' ? + { pattern: patternListToIExpression(...(Array.isArray(pattern) ? pattern : [pattern])) } : + this.parseSearchPaths(pattern, expandPatterns === 'strict'); } - private commonQuery(folderResources: (IWorkspaceFolderData | URI)[] = [], options: ICommonQueryBuilderOptions = {}): ICommonQueryProps { - const includeSearchPathsInfo: ISearchPathsInfo = this.handleIncludeExclude(options.includePattern, options.expandPatterns); - const excludeSearchPathsInfo: ISearchPathsInfo = this.handleIncludeExclude(options.excludePattern, options.expandPatterns); + private commonQuery(folderResources: (IWorkspaceFolderData | URI)[] = [], options: ICommonQueryBuilderOptions = {}, strictPatterns?: boolean): ICommonQueryProps { + const patternExpansionMode = strictPatterns ? 'strict' : options.expandPatterns ? 'loose' : 'none'; + const includeSearchPathsInfo: ISearchPathsInfo = this.handleIncludeExclude(options.includePattern, patternExpansionMode); + const excludeSearchPathsInfo: ISearchPathsInfo = this.handleIncludeExclude(options.excludePattern, patternExpansionMode); // Build folderQueries from searchPaths, if given, otherwise folderResources const includeFolderName = folderResources.length > 1; @@ -178,9 +182,35 @@ export class QueryBuilder { excludePattern: excludeSearchPathsInfo.pattern, includePattern: includeSearchPathsInfo.pattern, + onlyOpenEditors: options.onlyOpenEditors, maxResults: options.maxResults }; + // When "onlyOpenEditors" is enabled, filter all opened editors by the existing include/exclude patterns, + // then rerun the query build setting the includes to those remaining editors + if (options.onlyOpenEditors) { + const openEditors = arrays.coalesce(arrays.flatten(this.editorGroupsService.groups.map(group => group.editors.map(editor => editor.resource)))); + const openEditorsInQuery = openEditors.filter(editor => pathIncludedInQuery(queryProps, editor.fsPath)); + const openEditorIncludes = openEditorsInQuery.map(editor => { + const workspace = this.workspaceContextService.getWorkspaceFolder(editor); + if (workspace) { + const relPath = path.relative(workspace?.uri.fsPath, editor.fsPath); + return includeFolderName ? `./${workspace.name}/${relPath}` : `${relPath}`; + } + else { + return editor.fsPath.replace(/^\//, ''); + } + }); + return this.commonQuery(folderResources, { + ...options, + onlyOpenEditors: false, + includePattern: openEditorIncludes, + excludePattern: openEditorIncludes.length + ? options.excludePattern + : '**/*' // when there are no included editors, explicitly exclude all other files + }, true); + } + // Filter extraFileResources against global include/exclude patterns - they are already expected to not belong to a workspace const extraFileResources = options.extraFileResources && options.extraFileResources.filter(extraFile => pathIncludedInQuery(queryProps, extraFile.fsPath)); queryProps.extraFileResources = extraFileResources && extraFileResources.length ? extraFileResources : undefined; @@ -224,11 +254,11 @@ export class QueryBuilder { /** * 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}. + * glob patterns. When `strictPatterns` is false, patterns are expanded from 'foo/bar' to '{foo/bar/**, **\/foo/bar}. * * Public for test. */ - parseSearchPaths(pattern: string | string[]): ISearchPathsInfo { + parseSearchPaths(pattern: string | string[], strictPatterns = false): ISearchPathsInfo { const isSearchPath = (segment: string) => { // A segment is a search path if it is an absolute path or starts with ./, ../, .\, or ..\ return path.isAbsolute(segment) || /^\.\.?([\/\\]|$)/.test(segment); @@ -251,15 +281,15 @@ export class QueryBuilder { .map(s => strings.rtrim(s, '/')) .map(s => strings.rtrim(s, '\\')) .map(p => { - if (p[0] === '.') { + if (!strictPatterns && p[0] === '.') { p = '*' + p; // convert ".js" to "*.js" } - return expandGlobalGlob(p); + return strictPatterns ? [p] : expandGlobalGlob(p); }); const result: ISearchPathsInfo = {}; - const searchPaths = this.expandSearchPathPatterns(groups.searchPaths || []); + const searchPaths = this.expandSearchPathPatterns(groups.searchPaths || [], strictPatterns); if (searchPaths && searchPaths.length) { result.searchPaths = searchPaths; } @@ -282,7 +312,7 @@ export class QueryBuilder { /** * Split search paths (./ or ../ or absolute paths in the includePatterns) into absolute paths and globs applied to those paths */ - private expandSearchPathPatterns(searchPaths: string[]): ISearchPathPattern[] { + private expandSearchPathPatterns(searchPaths: string[], strictPatterns: boolean): ISearchPathPattern[] { if (!searchPaths || !searchPaths.length) { // No workspace => ignore search paths return []; @@ -302,7 +332,7 @@ export class QueryBuilder { // Expanded search paths to multiple resolved patterns (with ** and without) return arrays.flatten( - oneExpanded.map(oneExpandedResult => this.resolveOneSearchPathPattern(oneExpandedResult, globPortion))); + oneExpanded.map(oneExpandedResult => this.resolveOneSearchPathPattern(oneExpandedResult, globPortion, strictPatterns))); })); const searchPathPatternMap = new Map(); @@ -388,7 +418,7 @@ export class QueryBuilder { return []; } - private resolveOneSearchPathPattern(oneExpandedResult: IOneSearchPathPattern, globPortion?: string): IOneSearchPathPattern[] { + private resolveOneSearchPathPattern(oneExpandedResult: IOneSearchPathPattern, globPortion: string | undefined, strictPatterns: boolean): IOneSearchPathPattern[] { const pattern = oneExpandedResult.pattern && globPortion ? `${oneExpandedResult.pattern}/${globPortion}` : oneExpandedResult.pattern || globPortion; @@ -399,7 +429,7 @@ export class QueryBuilder { pattern }]; - if (pattern && !pattern.endsWith('**')) { + if (!strictPatterns && pattern && !pattern.endsWith('**')) { results.push({ searchPath: oneExpandedResult.searchPath, pattern: pattern + '/**' diff --git a/src/vs/workbench/contrib/search/test/browser/queryBuilder.test.ts b/src/vs/workbench/contrib/search/test/browser/queryBuilder.test.ts index 6aae8a9309b83..f1ac5432f4d33 100644 --- a/src/vs/workbench/contrib/search/test/browser/queryBuilder.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/queryBuilder.test.ts @@ -571,15 +571,42 @@ suite('QueryBuilder', () => { ].forEach(([includePattern, expectedPatterns]) => testSimpleIncludes(includePattern, expectedPatterns)); }); - function testIncludes(includePattern: string, expectedResult: ISearchPathsInfo): void { + test('strict includes', () => { + function testSimpleIncludes(includePattern: string, expectedPatterns: string[]): void { + assert.deepEqual( + queryBuilder.parseSearchPaths(includePattern, true), + { + pattern: patternsToIExpression(...expectedPatterns) + }, + includePattern); + } + + [ + ['a', ['a']], + ['a/b', ['a/b']], + ['a/b, c', ['a/b', 'c']], + ['a,.txt', ['a', '.txt']], + ['a,,,b', ['a', 'b']], + ['**/a,b/**', ['**/a', 'b/**']] + ].forEach(([includePattern, expectedPatterns]) => testSimpleIncludes(includePattern, expectedPatterns)); + }); + + function testIncludes(includePattern: string, expectedResultLoose: ISearchPathsInfo, expectedResultStrict?: ISearchPathsInfo): void { assertEqualSearchPathResults( queryBuilder.parseSearchPaths(includePattern), - expectedResult, + expectedResultLoose, includePattern); + + if (expectedResultStrict) { + assertEqualSearchPathResults( + queryBuilder.parseSearchPaths(includePattern, true), + expectedResultStrict, + includePattern); + } } - function testIncludesDataItem([includePattern, expectedResult]: [string, ISearchPathsInfo]): void { - testIncludes(includePattern, expectedResult); + function testIncludesDataItem([includePattern, expectedResultLoose, expectedResultStrict]: [string, ISearchPathsInfo, ISearchPathsInfo] | [string, ISearchPathsInfo]): void { + testIncludes(includePattern, expectedResultLoose, expectedResultStrict); } test('absolute includes', () => { @@ -652,7 +679,7 @@ suite('QueryBuilder', () => { }); test('relative includes w/single root folder', () => { - const cases: [string, ISearchPathsInfo][] = [ + const cases: ([string, ISearchPathsInfo] | [string, ISearchPathsInfo, ISearchPathsInfo])[] = [ [ './a', { @@ -660,6 +687,12 @@ suite('QueryBuilder', () => { searchPath: ROOT_1_URI, pattern: patternsToIExpression('a', 'a/**') }] + }, + { + searchPaths: [{ + searchPath: ROOT_1_URI, + pattern: patternsToIExpression('a') + }] } ], [ @@ -669,6 +702,12 @@ suite('QueryBuilder', () => { searchPath: ROOT_1_URI, pattern: patternsToIExpression('a', 'a/**') }] + }, + { + searchPaths: [{ + searchPath: ROOT_1_URI, + pattern: patternsToIExpression('a') + }] } ], [ @@ -700,6 +739,12 @@ suite('QueryBuilder', () => { searchPath: ROOT_1_URI, pattern: patternsToIExpression('a/b', 'a/b/**', 'c/d', 'c/d/**') }] + }, + { + searchPaths: [{ + searchPath: ROOT_1_URI, + pattern: patternsToIExpression('a/b', 'c/d',) + }] } ], [ @@ -735,9 +780,14 @@ suite('QueryBuilder', () => { mockWorkspace.folders = toWorkspaceFolders([{ path: ROOT_1_URI.fsPath }, { path: getUri(ROOT_2).fsPath }], WS_CONFIG_PATH, extUriBiasedIgnorePathCase); mockWorkspace.configuration = uri.file(fixPath('config')); - const cases: [string, ISearchPathsInfo][] = [ + const cases: ([string, ISearchPathsInfo] | [string, ISearchPathsInfo, ISearchPathsInfo])[] = [ [ './root1', + { + searchPaths: [{ + searchPath: getUri(ROOT_1) + }] + }, { searchPaths: [{ searchPath: getUri(ROOT_1) @@ -746,12 +796,42 @@ suite('QueryBuilder', () => { ], [ './root2', + { + searchPaths: [{ + searchPath: getUri(ROOT_2), + }] + }, { searchPaths: [{ searchPath: getUri(ROOT_2), }] } ], + [ + './root1/a/b, ./root2/a.txt', + { + searchPaths: [ + { + searchPath: ROOT_1_URI, + pattern: patternsToIExpression('a/b', 'a/b/**') + }, + { + searchPath: getUri(ROOT_2), + pattern: patternsToIExpression('a.txt', 'a.txt/**') + }] + }, + { + searchPaths: [ + { + searchPath: ROOT_1_URI, + pattern: patternsToIExpression('a/b') + }, + { + searchPath: getUri(ROOT_2), + pattern: patternsToIExpression('a.txt') + }] + } + ], [ './root1/a/**/b, ./root2/**/*.txt', { @@ -776,7 +856,7 @@ suite('QueryBuilder', () => { mockWorkspace.folders = toWorkspaceFolders([{ path: ROOT_1_URI.fsPath, name: ROOT_1_FOLDERNAME }, { path: getUri(ROOT_2).fsPath }], WS_CONFIG_PATH, extUriBiasedIgnorePathCase); mockWorkspace.configuration = uri.file(fixPath('config')); - const cases: [string, ISearchPathsInfo][] = [ + const cases: ([string, ISearchPathsInfo] | [string, ISearchPathsInfo, ISearchPathsInfo])[] = [ [ './foldername', { @@ -792,6 +872,12 @@ suite('QueryBuilder', () => { searchPath: ROOT_1_URI, pattern: patternsToIExpression('foo', 'foo/**') }] + }, + { + searchPaths: [{ + searchPath: ROOT_1_URI, + pattern: patternsToIExpression('foo', 'foo') + }] } ] ]; @@ -804,7 +890,7 @@ suite('QueryBuilder', () => { mockWorkspace.folders = toWorkspaceFolders([{ path: ROOT_1_URI.fsPath }, { path: getUri(ROOT_2).fsPath }, { path: getUri(ROOT_3).fsPath }], WS_CONFIG_PATH, extUriBiasedIgnorePathCase); mockWorkspace.configuration = uri.file(fixPath('/config')); - const cases: [string, ISearchPathsInfo][] = [ + const cases: ([string, ISearchPathsInfo] | [string, ISearchPathsInfo, ISearchPathsInfo])[] = [ [ '', { @@ -819,6 +905,11 @@ suite('QueryBuilder', () => { ], [ './root1', + { + searchPaths: [{ + searchPath: getUri(ROOT_1) + }] + }, { searchPaths: [{ searchPath: getUri(ROOT_1) diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index 047af4bc3cc84..b094c494f1e35 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts @@ -318,7 +318,7 @@ registerAction2(class extends Action2 { const instantiationService = accessor.get(IInstantiationService); const searchView = getSearchView(viewsService); if (searchView) { - await instantiationService.invokeFunction(createEditorFromSearchResult, searchView.searchResult, searchView.searchIncludePattern.getValue(), searchView.searchExcludePattern.getValue()); + await instantiationService.invokeFunction(createEditorFromSearchResult, searchView.searchResult, searchView.searchIncludePattern.getValue(), searchView.searchExcludePattern.getValue(), searchView.searchIncludePattern.onlySearchInOpenEditors()); } } }); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 9a4f7b0e09e78..b4282997499db 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -38,7 +38,7 @@ import { IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platfor import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; -import { ExcludePatternInputWidget, PatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget'; +import { ExcludePatternInputWidget, IncludePatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget'; import { SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget'; import { InputBoxFocusedKey } from 'vs/workbench/contrib/search/common/constants'; import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; @@ -67,7 +67,7 @@ export class SearchEditor extends BaseTextEditor { private searchResultEditor!: CodeEditorWidget; private queryEditorContainer!: HTMLElement; private dimension?: DOM.Dimension; - private inputPatternIncludes!: PatternInputWidget; + private inputPatternIncludes!: IncludePatternInputWidget; private inputPatternExcludes!: ExcludePatternInputWidget; private includesExcludesContainer!: HTMLElement; private toggleQueryDetailsButton!: HTMLElement; @@ -168,10 +168,11 @@ export class SearchEditor extends BaseTextEditor { const folderIncludesList = DOM.append(this.includesExcludesContainer, DOM.$('.file-types.includes')); const filesToIncludeTitle = localize('searchScope.includes', "files to include"); DOM.append(folderIncludesList, DOM.$('h4', undefined, filesToIncludeTitle)); - this.inputPatternIncludes = this._register(this.instantiationService.createInstance(PatternInputWidget, folderIncludesList, this.contextViewService, { + this.inputPatternIncludes = this._register(this.instantiationService.createInstance(IncludePatternInputWidget, folderIncludesList, this.contextViewService, { ariaLabel: localize('label.includes', 'Search Include Patterns'), })); this.inputPatternIncludes.onSubmit(triggeredOnType => this.triggerSearch({ resetCursor: false, delay: triggeredOnType ? this.searchConfig.searchOnTypeDebouncePeriod : 0 })); + this._register(this.inputPatternIncludes.onChangeSearchInEditorsBox(() => this.triggerSearch())); // // Excludes const excludesList = DOM.append(this.includesExcludesContainer, DOM.$('.file-types.excludes')); @@ -181,7 +182,7 @@ export class SearchEditor extends BaseTextEditor { ariaLabel: localize('label.excludes', 'Search Exclude Patterns'), })); this.inputPatternExcludes.onSubmit(triggeredOnType => this.triggerSearch({ resetCursor: false, delay: triggeredOnType ? this.searchConfig.searchOnTypeDebouncePeriod : 0 })); - this.inputPatternExcludes.onChangeIgnoreBox(() => this.triggerSearch()); + this._register(this.inputPatternExcludes.onChangeIgnoreBox(() => this.triggerSearch())); [this.queryEditorWidget.searchInput, this.inputPatternIncludes, this.inputPatternExcludes].map(input => this._register(attachInputBoxStyler(input, this.themeService, { inputBorder: searchEditorTextInputBorder }))); @@ -449,6 +450,7 @@ export class SearchEditor extends BaseTextEditor { isRegexp: this.queryEditorWidget.searchInput.getRegex(), matchWholeWord: this.queryEditorWidget.searchInput.getWholeWords(), useExcludeSettingsAndIgnoreFiles: this.inputPatternExcludes.useExcludesAndIgnoreFiles(), + onlyOpenEditors: this.inputPatternIncludes.onlySearchInOpenEditors(), showIncludesExcludes: this.showingIncludesExcludes }; } @@ -483,6 +485,7 @@ export class SearchEditor extends BaseTextEditor { disregardExcludeSettings: !config.useExcludeSettingsAndIgnoreFiles || undefined, excludePattern: config.filesToExclude, includePattern: config.filesToInclude, + onlyOpenEditors: config.onlyOpenEditors, previewOptions: { matchLines: 1, charsPerLine: 1000 @@ -575,6 +578,7 @@ export class SearchEditor extends BaseTextEditor { if (config.contextLines !== undefined) { this.queryEditorWidget.setContextLines(config.contextLines); } if (config.filesToExclude !== undefined) { this.inputPatternExcludes.setValue(config.filesToExclude); } if (config.filesToInclude !== undefined) { this.inputPatternIncludes.setValue(config.filesToInclude); } + if (config.onlyOpenEditors !== undefined) { this.inputPatternIncludes.setOnlySearchInOpenEditors(config.onlyOpenEditors); } if (config.useExcludeSettingsAndIgnoreFiles !== undefined) { this.inputPatternExcludes.setUseExcludesAndIgnoreFiles(config.useExcludeSettingsAndIgnoreFiles); } if (config.showIncludesExcludes !== undefined) { this.toggleIncludesExcludes(config.showIncludesExcludes); } } diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts index 7508c1e938fe7..f8aa2378e4fb4 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts @@ -82,6 +82,7 @@ export async function openSearchEditor(accessor: ServicesAccessor): Promise { + async (accessor: ServicesAccessor, searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, onlySearchInOpenEditors: boolean) => { if (!searchResult.query) { console.error('Expected searchResult.query to be defined. Got', searchResult); return; @@ -180,6 +181,7 @@ export const createEditorFromSearchResult = const labelFormatter = (uri: URI): string => labelService.getUriLabel(uri, { relative: true }); const { text, matchRanges, config } = serializeSearchResultForEditor(searchResult, rawIncludePattern, rawExcludePattern, 0, labelFormatter, sortOrder); + config.onlyOpenEditors = onlySearchInOpenEditors; const contextLines = configurationService.getValue('search').searchEditor.defaultNumberOfContextLines; if (searchResult.isDirty || contextLines === 0 || contextLines === null) { diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts index afb8c5aa4878f..9d02af8f6d715 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts @@ -40,6 +40,7 @@ export type SearchConfiguration = { isRegexp: boolean, useExcludeSettingsAndIgnoreFiles: boolean, showIncludesExcludes: boolean, + onlyOpenEditors: boolean, }; export const SEARCH_EDITOR_EXT = '.code-search'; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts index 951be05422f0a..51ccafc40aacd 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts @@ -115,6 +115,7 @@ const contentPatternToSearchConfiguration = (pattern: ITextQuery, includes: stri showIncludesExcludes: !!(includes || excludes || pattern?.userDisabledExcludesAndIgnoreFiles), useExcludeSettingsAndIgnoreFiles: (pattern?.userDisabledExcludesAndIgnoreFiles === undefined ? true : !pattern.userDisabledExcludesAndIgnoreFiles), contextLines, + onlyOpenEditors: !!pattern.onlyOpenEditors, }; }; @@ -131,6 +132,7 @@ export const serializeSearchConfiguration = (config: Partial ({ matchWholeWord: false, contextLines: 0, showIncludesExcludes: false, + onlyOpenEditors: false, }); export const extractSearchQueryFromLines = (lines: string[]): SearchConfiguration => { @@ -197,6 +200,7 @@ export const extractSearchQueryFromLines = (lines: string[]): SearchConfiguratio query.isCaseSensitive = value.indexOf('CaseSensitive') !== -1; query.useExcludeSettingsAndIgnoreFiles = value.indexOf('IgnoreExcludeSettings') === -1; query.matchWholeWord = value.indexOf('WordMatch') !== -1; + query.onlyOpenEditors = value.indexOf('OpenEditors') !== -1; } } } diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index f6af1abf865c1..ad4ddf97f6776 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -77,6 +77,8 @@ export interface ICommonQueryProps { excludePattern?: glob.IExpression; extraFileResources?: U[]; + onlyOpenEditors?: boolean; + maxResults?: number; usingSearchPaths?: boolean; } @@ -372,6 +374,9 @@ export interface ISearchConfigurationProperties { defaultNumberOfContextLines: number | null, experimental: {} }; + experimental: { + searchInOpenEditors: boolean + } sortOrder: SearchSortOrder; } @@ -407,21 +412,25 @@ export function pathIncludedInQuery(queryProps: ICommonQueryProps, fsPath: return false; } - if (queryProps.includePattern && !glob.match(queryProps.includePattern, fsPath)) { - return false; - } + if (queryProps.includePattern || queryProps.usingSearchPaths) { + if (queryProps.includePattern && glob.match(queryProps.includePattern, fsPath)) { + return true; + } - // If searchPaths are being used, the extra file must be in a subfolder and match the pattern, if present - if (queryProps.usingSearchPaths) { - return !!queryProps.folderQueries && queryProps.folderQueries.every(fq => { - const searchPath = fq.folder.fsPath; - if (extpath.isEqualOrParent(fsPath, searchPath)) { - const relPath = relative(searchPath, fsPath); - return !fq.includePattern || !!glob.match(fq.includePattern, relPath); - } else { - return false; - } - }); + // If searchPaths are being used, the extra file must be in a subfolder and match the pattern, if present + if (queryProps.usingSearchPaths) { + return !!queryProps.folderQueries && queryProps.folderQueries.some(fq => { + const searchPath = fq.folder.fsPath; + if (extpath.isEqualOrParent(fsPath, searchPath)) { + const relPath = relative(searchPath, fsPath); + return !fq.includePattern || !!glob.match(fq.includePattern, relPath); + } else { + return false; + } + }); + } + + return false; } return true; diff --git a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts index 074d3528f2947..da0483de317d2 100644 --- a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts +++ b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts @@ -385,13 +385,7 @@ function getRgArgs(query: TextSearchQuery, options: TextSearchOptions): string[] if (otherIncludes && otherIncludes.length) { const uniqueOthers = new Set(); - otherIncludes.forEach(other => { - if (!other.endsWith('/**')) { - other += '/**'; - } - - uniqueOthers.add(other); - }); + otherIncludes.forEach(other => { uniqueOthers.add(other); }); args.push('-g', '!*'); uniqueOthers @@ -508,10 +502,6 @@ function getRgArgs(query: TextSearchQuery, options: TextSearchOptions): string[] */ export function spreadGlobComponents(globArg: string): string[] { const components = splitGlobAware(globArg, '/'); - if (components[components.length - 1] !== '**') { - components.push('**'); - } - return components.map((_, i) => components.slice(0, i + 1).join('/')); }