diff --git a/extensions/configuration-editing/src/settingsDocumentHelper.ts b/extensions/configuration-editing/src/settingsDocumentHelper.ts index 24215b25404..5a267b6093e 100644 --- a/extensions/configuration-editing/src/settingsDocumentHelper.ts +++ b/extensions/configuration-editing/src/settingsDocumentHelper.ts @@ -26,8 +26,8 @@ export class SettingsDocument { return this.provideFilesAssociationsCompletionItems(location, position); } - // files.exclude, search.exclude - if (location.path[0] === 'files.exclude' || location.path[0] === 'search.exclude') { + // files.exclude, search.exclude, explorer.autoRevealExclude + if (location.path[0] === 'files.exclude' || location.path[0] === 'search.exclude' || location.path[0] === 'explorer.autoRevealExclude') { return this.provideExcludeCompletionItems(location, position); } diff --git a/src/vs/workbench/common/resources.ts b/src/vs/workbench/common/resources.ts index bab45fa5187..12921b46433 100644 --- a/src/vs/workbench/common/resources.ts +++ b/src/vs/workbench/common/resources.ts @@ -87,7 +87,10 @@ export class ResourceGlobMatcher extends Disposable { } } - matches(resource: URI): boolean { + matches( + resource: URI, + hasSibling?: (name: string) => boolean + ): boolean { const folder = this.contextService.getWorkspaceFolder(resource); let expressionForRoot: ParsedExpression | undefined; @@ -108,6 +111,6 @@ export class ResourceGlobMatcher extends Disposable { resourcePathToMatch = resource.fsPath; // TODO@isidor: support non-file URIs } - return !!expressionForRoot && typeof resourcePathToMatch === 'string' && !!expressionForRoot(resourcePathToMatch); + return !!expressionForRoot && typeof resourcePathToMatch === 'string' && !!expressionForRoot(resourcePathToMatch, undefined, hasSibling); } } diff --git a/src/vs/workbench/contrib/files/browser/explorerService.ts b/src/vs/workbench/contrib/files/browser/explorerService.ts index aa7ed9293b5..01f528cddcf 100644 --- a/src/vs/workbench/contrib/files/browser/explorerService.ts +++ b/src/vs/workbench/contrib/files/browser/explorerService.ts @@ -23,6 +23,8 @@ import { IProgressService, ProgressLocation, IProgressNotificationOptions, IProg import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IExpression } from 'vs/base/common/glob'; +import { ResourceGlobMatcher } from 'vs/workbench/common/resources'; export const UNDO_REDO_SOURCE = new UndoRedoSource(); @@ -39,6 +41,7 @@ export class ExplorerService implements IExplorerService { private model: ExplorerModel; private onFileChangesScheduler: RunOnceScheduler; private fileChangeEvents: FileChangesEvent[] = []; + private revealExcludeMatcher: ResourceGlobMatcher; constructor( @IFileService private fileService: IFileService, @@ -130,6 +133,11 @@ export class ExplorerService implements IExplorerService { this.refresh(false); } })); + this.revealExcludeMatcher = new ResourceGlobMatcher( + (uri) => getRevealExcludes(configurationService.getValue({ resource: uri })), + (event) => event.affectsConfiguration('explorer.autoRevealExclude'), + contextService, configurationService); + this.disposables.add(this.revealExcludeMatcher); } get roots(): ExplorerItem[] { @@ -254,8 +262,14 @@ export class ExplorerService implements IExplorerService { return; } + // If file or parent matches exclude patterns, do not reveal unless reveal argument is 'force' + const ignoreRevealExcludes = reveal === 'force'; + const fileStat = this.findClosest(resource); if (fileStat) { + if (!this.shouldAutoRevealItem(fileStat, ignoreRevealExcludes)) { + return; + } await this.view.selectResource(fileStat.resource, reveal); return Promise.resolve(undefined); } @@ -277,7 +291,10 @@ export class ExplorerService implements IExplorerService { const item = root.find(resource); await this.view.refresh(true, root); - // Select and Reveal + // Once item is resolved, check again if folder should be expanded + if (item && !this.shouldAutoRevealItem(item, ignoreRevealExcludes)) { + return; + } await this.view.selectResource(item ? item.resource : undefined, reveal); } catch (error) { root.isError = true; @@ -395,6 +412,28 @@ export class ExplorerService implements IExplorerService { } } + // Check if an item matches a explorer.autoRevealExclude pattern + private shouldAutoRevealItem(item: ExplorerItem | undefined, ignore: boolean): boolean { + if (item === undefined || ignore) { + return true; + } + if (this.revealExcludeMatcher.matches(item.resource, name => !!(item.parent && item.parent.getChild(name)))) { + return false; + } + const root = item.root; + let currentItem = item.parent; + while (currentItem !== root) { + if (currentItem === undefined) { + return true; + } + if (this.revealExcludeMatcher.matches(currentItem.resource)) { + return false; + } + currentItem = currentItem.parent; + } + return true; + } + private async onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): Promise { let shouldRefresh = false; @@ -440,3 +479,13 @@ function doesFileEventAffect(item: ExplorerItem, view: IExplorerView, events: Fi return false; } + +function getRevealExcludes(configuration: IFilesConfiguration): IExpression { + const revealExcludes = configuration && configuration.explorer && configuration.explorer.autoRevealExclude; + + if (!revealExcludes) { + return {}; + } + + return revealExcludes; +} diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index 295d8bb15c0..e4372f87df0 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -330,7 +330,7 @@ CommandsRegistry.registerCommand({ const explorerView = viewlet.getExplorerView(); if (explorerView) { explorerView.setExpanded(true); - await explorerService.select(uri, true); + await explorerService.select(uri, 'force'); explorerView.focus(); } } else { diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index c8a43730d93..d7e78956197 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -371,6 +371,30 @@ configurationRegistry.registerConfiguration({ ], 'description': nls.localize('autoReveal', "Controls whether the explorer should automatically reveal and select files when opening them.") }, + 'explorer.autoRevealExclude': { + 'type': 'object', + 'markdownDescription': nls.localize('autoRevealExclude', "Configure glob patterns for excluding files and folders from being revealed and selected in the explorer when they are opened. Read more about glob patterns [here](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options)."), + 'default': { '**/node_modules': true, '**/bower_components': true }, + 'additionalProperties': { + 'anyOf': [ + { + 'type': 'boolean', + 'description': nls.localize('explorer.autoRevealExclude.boolean', "The glob pattern to match file paths against. Set to true or false to enable or disable the pattern."), + }, + { + type: 'object', + properties: { + when: { + type: 'string', // expression ({ "**/*.js": { "when": "$(basename).js" } }) + pattern: '\\w*\\$\\(basename\\)\\w*', + default: '$(basename).ext', + description: nls.localize('explorer.autoRevealExclude.when', 'Additional check on the siblings of a matching file. Use $(basename) as variable for the matching file name.') + } + } + } + ] + } + }, 'explorer.enableDragAndDrop': { 'type': 'boolean', 'description': nls.localize('enableDragAndDrop', "Controls whether the explorer should allow to move files and folders via drag and drop. This setting only effects drag and drop from inside the explorer."), diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 7b0e72e16f5..11931668b89 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -181,7 +181,7 @@ export class ExplorerView extends ViewPane implements IExplorerView { private horizontalScrolling: boolean | undefined; private dragHandler!: DelayedDragHandler; - private autoReveal: boolean | 'focusNoScroll' = false; + private autoReveal: boolean | 'force' | 'focusNoScroll' = false; private decorationsProvider: ExplorerDecorationsProvider | undefined; constructor( @@ -725,7 +725,7 @@ export class ExplorerView extends ViewPane implements IExplorerView { } public async selectResource(resource: URI | undefined, reveal = this.autoReveal, retry = 0): Promise { - // do no retry more than once to prevent inifinite loops in cases of inconsistent model + // do no retry more than once to prevent infinite loops in cases of inconsistent model if (retry === 2) { return; } @@ -766,7 +766,7 @@ export class ExplorerView extends ViewPane implements IExplorerView { await this.tree.expand(item.nestedParent); } - if (reveal === true && this.tree.getRelativeTop(item) === null) { + if ((reveal === true || reveal === 'force') && this.tree.getRelativeTop(item) === null) { // Don't scroll to the item if it's already visible, or if set not to. this.tree.reveal(item, 0.5); } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 1fe7a698f73..9cc0159b382 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -604,7 +604,7 @@ interface CachedParsedExpression { } /** - * Respectes files.exclude setting in filtering out content from the explorer. + * Respects files.exclude setting in filtering out content from the explorer. * Makes sure that visible editors are always shown in the explorer even if they are filtered out by settings. */ export class FilesFilter implements ITreeFilter { diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index bff6d425a37..c9141c6880f 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -21,6 +21,7 @@ import { once } from 'vs/base/common/functional'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { localize } from 'vs/nls'; +import { IExpression } from 'vs/base/common/glob'; /** * Explorer viewlet id. @@ -88,6 +89,7 @@ export interface IFilesConfiguration extends PlatformIFilesConfiguration, IWorkb sortOrder: 'editorOrder' | 'alphabetical' | 'fullPath'; }; autoReveal: boolean | 'focusNoScroll'; + autoRevealExclude: IExpression; enableDragAndDrop: boolean; confirmDelete: boolean; enableUndo: boolean; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index 308e79c7c31..1e134a6c408 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -739,6 +739,7 @@ export function isExcludeSetting(setting: ISetting): boolean { return setting.key === 'files.exclude' || setting.key === 'search.exclude' || setting.key === 'workbench.localHistory.exclude' || + setting.key === 'explorer.autoRevealExclude' || setting.key === 'files.watcherExclude'; }