Search In Open Editors (#107756)
* Initial work on "search in open editors" * Update wording * Update messaging for open editors config * Add command to open all git changes (in association with searching in all open editors) * Add strict parsing mode to search using providers for specific files * Clean * Remove log * Naming * Transfer open editors config to search editor * Pass in more places * Add some testing
This commit is contained in:
parent
9f9d1a76d9
commit
7e55fa0c54
15 changed files with 304 additions and 71 deletions
|
@ -71,6 +71,11 @@
|
|||
"category": "Git",
|
||||
"icon": "$(compare-changes)"
|
||||
},
|
||||
{
|
||||
"command": "git.openAllChanges",
|
||||
"title": "%command.openAllChanges%",
|
||||
"category": "Git"
|
||||
},
|
||||
{
|
||||
"command": "git.openFile",
|
||||
"title": "%command.openFile%",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -372,6 +372,20 @@ export class CommandCenter {
|
|||
await resource.open();
|
||||
}
|
||||
|
||||
@command('git.openAllChanges', { repository: true })
|
||||
async openChanges(repository: Repository): Promise<void> {
|
||||
[
|
||||
...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<void> {
|
||||
if (!url || typeof url !== 'string') {
|
||||
url = await pickRemoteSource(this.model, {
|
||||
|
|
|
@ -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<void>());
|
||||
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<ISearchConfiguration>().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<ISearchConfiguration>().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<void>());
|
||||
|
@ -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;
|
||||
|
|
|
@ -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.**")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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,7 +1448,18 @@ export class SearchView extends ViewPane {
|
|||
|
||||
if (!completed) {
|
||||
message = SEARCH_CANCELLED_MESSAGE;
|
||||
} else if (hasIncludes && hasExcludes) {
|
||||
} 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 {
|
||||
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);
|
||||
|
@ -1452,6 +1468,7 @@ export class SearchView extends ViewPane {
|
|||
} else {
|
||||
message = nls.localize('noResultsFound', "No results found. Review your settings for configured exclusions and check your gitignore files - ");
|
||||
}
|
||||
}
|
||||
|
||||
// Indicate as status to ARIA
|
||||
aria.status(message);
|
||||
|
@ -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();
|
||||
|
|
|
@ -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<uri> {
|
||||
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<uri> {
|
||||
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<string, ISearchPathPattern>();
|
||||
|
@ -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 + '/**'
|
||||
|
|
|
@ -571,15 +571,42 @@ suite('QueryBuilder', () => {
|
|||
].forEach(([includePattern, expectedPatterns]) => testSimpleIncludes(<string>includePattern, <string[]>expectedPatterns));
|
||||
});
|
||||
|
||||
function testIncludes(includePattern: string, expectedResult: ISearchPathsInfo): void {
|
||||
assertEqualSearchPathResults(
|
||||
queryBuilder.parseSearchPaths(includePattern),
|
||||
expectedResult,
|
||||
test('strict includes', () => {
|
||||
function testSimpleIncludes(includePattern: string, expectedPatterns: string[]): void {
|
||||
assert.deepEqual(
|
||||
queryBuilder.parseSearchPaths(includePattern, true),
|
||||
{
|
||||
pattern: patternsToIExpression(...expectedPatterns)
|
||||
},
|
||||
includePattern);
|
||||
}
|
||||
|
||||
function testIncludesDataItem([includePattern, expectedResult]: [string, ISearchPathsInfo]): void {
|
||||
testIncludes(includePattern, expectedResult);
|
||||
[
|
||||
['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(<string>includePattern, <string[]>expectedPatterns));
|
||||
});
|
||||
|
||||
function testIncludes(includePattern: string, expectedResultLoose: ISearchPathsInfo, expectedResultStrict?: ISearchPathsInfo): void {
|
||||
assertEqualSearchPathResults(
|
||||
queryBuilder.parseSearchPaths(includePattern),
|
||||
expectedResultLoose,
|
||||
includePattern);
|
||||
|
||||
if (expectedResultStrict) {
|
||||
assertEqualSearchPathResults(
|
||||
queryBuilder.parseSearchPaths(includePattern, true),
|
||||
expectedResultStrict,
|
||||
includePattern);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -750,6 +800,36 @@ suite('QueryBuilder', () => {
|
|||
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')
|
||||
}]
|
||||
}
|
||||
],
|
||||
[
|
||||
|
@ -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)
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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); }
|
||||
}
|
||||
|
|
|
@ -82,6 +82,7 @@ export async function openSearchEditor(accessor: ServicesAccessor): Promise<void
|
|||
if (searchView) {
|
||||
await instantiationService.invokeFunction(openNewSearchEditor, {
|
||||
filesToInclude: searchView.searchIncludePattern.getValue(),
|
||||
onlyOpenEditors: searchView.searchIncludePattern.onlySearchInOpenEditors(),
|
||||
filesToExclude: searchView.searchExcludePattern.getValue(),
|
||||
isRegexp: searchView.searchAndReplaceWidget.searchInput.getRegex(),
|
||||
isCaseSensitive: searchView.searchAndReplaceWidget.searchInput.getCaseSensitive(),
|
||||
|
@ -161,7 +162,7 @@ export const openNewSearchEditor =
|
|||
};
|
||||
|
||||
export const createEditorFromSearchResult =
|
||||
async (accessor: ServicesAccessor, searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string) => {
|
||||
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<ISearchConfigurationProperties>('search').searchEditor.defaultNumberOfContextLines;
|
||||
|
||||
if (searchResult.isDirty || contextLines === 0 || contextLines === null) {
|
||||
|
|
|
@ -40,6 +40,7 @@ export type SearchConfiguration = {
|
|||
isRegexp: boolean,
|
||||
useExcludeSettingsAndIgnoreFiles: boolean,
|
||||
showIncludesExcludes: boolean,
|
||||
onlyOpenEditors: boolean,
|
||||
};
|
||||
|
||||
export const SEARCH_EDITOR_EXT = '.code-search';
|
||||
|
|
|
@ -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<SearchConfiguration
|
|||
config.isCaseSensitive && 'CaseSensitive',
|
||||
config.matchWholeWord && 'WordMatch',
|
||||
config.isRegexp && 'RegExp',
|
||||
config.onlyOpenEditors && 'OpenEditors',
|
||||
(config.useExcludeSettingsAndIgnoreFiles === false) && 'IgnoreExcludeSettings'
|
||||
]).join(' ')}`,
|
||||
config.filesToInclude ? `# Including: ${config.filesToInclude}` : undefined,
|
||||
|
@ -153,6 +155,7 @@ export const defaultSearchConfig = (): SearchConfiguration => ({
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,6 +77,8 @@ export interface ICommonQueryProps<U extends UriComponents> {
|
|||
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,13 +412,14 @@ export function pathIncludedInQuery(queryProps: ICommonQueryProps<URI>, 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 => {
|
||||
return !!queryProps.folderQueries && queryProps.folderQueries.some(fq => {
|
||||
const searchPath = fq.folder.fsPath;
|
||||
if (extpath.isEqualOrParent(fsPath, searchPath)) {
|
||||
const relPath = relative(searchPath, fsPath);
|
||||
|
@ -424,6 +430,9 @@ export function pathIncludedInQuery(queryProps: ICommonQueryProps<URI>, fsPath:
|
|||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -385,13 +385,7 @@ function getRgArgs(query: TextSearchQuery, options: TextSearchOptions): string[]
|
|||
|
||||
if (otherIncludes && otherIncludes.length) {
|
||||
const uniqueOthers = new Set<string>();
|
||||
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('/'));
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue