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:
Jackson Kearl 2021-01-21 16:59:07 -08:00 committed by GitHub
parent 9f9d1a76d9
commit 7e55fa0c54
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 304 additions and 71 deletions

View file

@ -71,6 +71,11 @@
"category": "Git",
"icon": "$(compare-changes)"
},
{
"command": "git.openAllChanges",
"title": "%command.openAllChanges%",
"category": "Git"
},
{
"command": "git.openFile",
"title": "%command.openFile%",

View file

@ -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",

View file

@ -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, {

View file

@ -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;

View file

@ -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.**")
}
}
});

View file

@ -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();

View file

@ -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 + '/**'

View file

@ -571,15 +571,42 @@ suite('QueryBuilder', () => {
].forEach(([includePattern, expectedPatterns]) => testSimpleIncludes(<string>includePattern, <string[]>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(<string>includePattern, <string[]>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)
@ -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)

View file

@ -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());
}
}
});

View file

@ -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); }
}

View file

@ -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) {

View file

@ -40,6 +40,7 @@ export type SearchConfiguration = {
isRegexp: boolean,
useExcludeSettingsAndIgnoreFiles: boolean,
showIncludesExcludes: boolean,
onlyOpenEditors: boolean,
};
export const SEARCH_EDITOR_EXT = '.code-search';

View file

@ -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;
}
}
}

View file

@ -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,21 +412,25 @@ 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 => {
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;

View file

@ -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('/'));
}