From f56d7fcd24c800fcf64b532a1427c1d44ccfdafe Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Tue, 29 Sep 2020 16:00:44 -0700 Subject: [PATCH] Initial work on "search in open editors" --- .../search/browser/patternInputWidget.ts | 67 +++++++++++++++++-- .../search/browser/search.contribution.ts | 5 ++ .../contrib/search/browser/searchView.ts | 11 ++- .../contrib/search/common/queryBuilder.ts | 20 ++++++ .../searchEditor/browser/searchEditor.ts | 11 +-- .../searchEditor/browser/searchEditorInput.ts | 1 + .../browser/searchEditorSerialization.ts | 4 ++ .../services/search/common/search.ts | 5 ++ 8 files changed, 113 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/search/browser/patternInputWidget.ts b/src/vs/workbench/contrib/search/browser/patternInputWidget.ts index 63bf2771a464d..fea2c182bb431 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..1a3a3705f747d 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, PatternInputWidget } 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 @@ -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: { diff --git a/src/vs/workbench/contrib/search/common/queryBuilder.ts b/src/vs/workbench/contrib/search/common/queryBuilder.ts index d8a1a911e4568..ffe88898502bf 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 ) { } @@ -178,9 +181,26 @@ export class QueryBuilder { excludePattern: excludeSearchPathsInfo.pattern, includePattern: includeSearchPathsInfo.pattern, + onlyOpenEditors: options.onlyOpenEditors, maxResults: options.maxResults }; + 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, expandPatterns: false, onlyOpenEditors: false, includePattern: openEditorIncludes.join(', ') }); + } + // 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; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 9a4f7b0e09e78..0927118a7c021 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 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..4ed641bb79fef 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; }