diff --git a/.editorconfig b/.editorconfig
index e4eee44..6761551 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -253,3 +253,21 @@ dotnet_naming_style.begins_with__.capitalization = camel_case
dotnet_diagnostic.CA1014.severity = none
dotnet_diagnostic.CA2007.severity = none
dotnet_diagnostic.IDE0070.severity = none
+
+# IDE0028: Simplify collection initialization
+dotnet_diagnostic.IDE0028.severity = warning
+
+# IDE0290: Use primary constructor
+dotnet_diagnostic.IDE0290.severity = none
+
+# IDE0300: Simplify collection initialization
+dotnet_diagnostic.IDE0300.severity = warning
+
+# IDE0301: Simplify collection initialization
+dotnet_diagnostic.IDE0301.severity = warning
+
+# IDE0303: Simplify collection initialization
+dotnet_diagnostic.IDE0303.severity = warning
+
+# IDE0305: Simplify collection initialization
+dotnet_diagnostic.IDE0305.severity = warning
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a83c977..c6fd744 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -6,7 +6,7 @@ on:
jobs:
build:
- runs-on: windows-2022
+ runs-on: windows-2025-vs2026
env:
Configuration: Release
@@ -14,10 +14,10 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
- name: Setup .NET SDK
- uses: actions/setup-dotnet@v5
+ uses: actions/setup-dotnet@v6
- name: Build
run: dotnet build
diff --git a/.github/workflows/publish-manually.yml b/.github/workflows/publish-manually.yml
index 14f9440..3e789ba 100644
--- a/.github/workflows/publish-manually.yml
+++ b/.github/workflows/publish-manually.yml
@@ -9,7 +9,7 @@ on:
jobs:
publish:
- runs-on: windows-2022
+ runs-on: windows-2025-vs2026
env:
Configuration: Release
@@ -17,16 +17,16 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
with:
ref: ${{ github.event.inputs.version }}
- name: Setup MSBuild
- uses: microsoft/setup-msbuild@v2
+ uses: microsoft/setup-msbuild@v3
- name: Publish
run: msbuild source/ProjectFilter/ProjectFilter.csproj /t:PublishToMarketplace /Restore /v:Minimal /p:BuildBeforePublish=true /p:PersonalAccessToken=${{ secrets.PUBLISHER_TOKEN }}
env:
# This workflow should be used when the `VsixPublisher` from the `Microsoft.VSSDK.BuildTools`
# package fails, so we'll use the `VsixPublisher` that is installed with Visual Studio.
- VsixPublisher: "C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VSSDK\\VisualStudioIntegration\\Tools\\Bin\\VsixPublisher.exe"
+ VsixPublisher: "C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\VSSDK\\VisualStudioIntegration\\Tools\\Bin\\VsixPublisher.exe"
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index cf675f9..4dcd333 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -6,7 +6,7 @@ on:
jobs:
publish:
- runs-on: windows-2022
+ runs-on: windows-2025-vs2026
env:
Configuration: Release
@@ -14,10 +14,10 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v7
- name: Setup MSBuild
- uses: actions/setup-dotnet@v5
+ uses: actions/setup-dotnet@v6
- name: Publish
working-directory: source/ProjectFilter
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b37956a..8687131 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+# 2.7.0 (2026-09-06)
+
+- ✨ New option to hide folders that only contain unloaded projects and miscellaneous files.
+ -
# 2.6.1 (2025-08-19)
- 🔧 Dependency updates.
diff --git a/Directory.Build.props b/Directory.Build.props
index cc40040..a93d14d 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,11 +1,12 @@
- 2.6.1
+ 2.7.0
net48
enable
- 10.0
+ latest
AllEnabledByDefault
+ true
diff --git a/global.json b/global.json
index d20023b..5d13fe0 100644
--- a/global.json
+++ b/global.json
@@ -1,5 +1,5 @@
{
"sdk": {
- "version": "10.0.203"
+ "version": "10.0.400"
}
}
diff --git a/sample/Small/Small.sln b/sample/Small/Small.sln
index 2599d0a..e1fdae3 100644
--- a/sample/Small/Small.sln
+++ b/sample/Small/Small.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.1.31911.260
+# Visual Studio Version 18
+VisualStudioVersion = 18.10.12120.281
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "source", "source", "{8BB688F8-2B48-4692-9DFC-A199435AB13D}"
EndProject
@@ -17,6 +17,9 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Beta", "source\Beta\Beta.csproj", "{00020190-70EA-4CC3-B3C1-CBD9720BD7A7}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Gamma", "Gamma", "{2715BEFA-EFBC-4AF8-966F-2395B8035457}"
+ ProjectSection(SolutionItems) = preProject
+ source\Gamma\Directory.Build.props = source\Gamma\Directory.Build.props
+ EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gamma.Abstractions", "source\Gamma\Gamma.Abstractions\Gamma.Abstractions.csproj", "{3F39F3CF-2E4E-4CB0-AFF1-A11658E24EF1}"
EndProject
diff --git a/sample/Small/source/Gamma/Directory.Build.props b/sample/Small/source/Gamma/Directory.Build.props
new file mode 100644
index 0000000..f2514e0
--- /dev/null
+++ b/sample/Small/source/Gamma/Directory.Build.props
@@ -0,0 +1,4 @@
+
+
+
diff --git a/source/ProjectFilter/Commands/FilterProjectsCommand.cs b/source/ProjectFilter/Commands/FilterProjectsCommand.cs
index ef4f08e..aef685a 100644
--- a/source/ProjectFilter/Commands/FilterProjectsCommand.cs
+++ b/source/ProjectFilter/Commands/FilterProjectsCommand.cs
@@ -63,6 +63,7 @@ protected async override Task ExecuteAsync(OleMenuCmdEventArgs e) {
vm.LoadProjectDependencies = solutionSettings?.LoadProjectDependencies ?? globalSettings.LoadProjectDependencies;
vm.UseRegularExpressions = solutionSettings?.UseRegularExpressions ?? globalSettings.UseRegularExpressions;
vm.ExpandLoadedProjects = solutionSettings?.ExpandLoadedProjects ?? globalSettings.ExpandLoadedProjects;
+ vm.HideSolutionFoldersWithoutLoadedProjects = solutionSettings?.HideSolutionFoldersWithoutLoadedProjects ?? globalSettings.HideSolutionFoldersWithoutLoadedProjects;
dialog = new FilterDialog {
DataContext = vm
@@ -76,7 +77,8 @@ protected async override Task ExecuteAsync(OleMenuCmdEventArgs e) {
solutionSettings = new SolutionSettings {
LoadProjectDependencies = vm.LoadProjectDependencies,
UseRegularExpressions = vm.UseRegularExpressions,
- ExpandLoadedProjects = vm.ExpandLoadedProjects
+ ExpandLoadedProjects = vm.ExpandLoadedProjects,
+ HideSolutionFoldersWithoutLoadedProjects = vm.HideSolutionFoldersWithoutLoadedProjects
};
PopulateNodeSettings(vm.Items, solutionSettings.Nodes);
@@ -87,6 +89,7 @@ protected async override Task ExecuteAsync(OleMenuCmdEventArgs e) {
globalSettings.LoadProjectDependencies = vm.LoadProjectDependencies;
globalSettings.UseRegularExpressions = vm.UseRegularExpressions;
globalSettings.ExpandLoadedProjects = vm.ExpandLoadedProjects;
+ globalSettings.HideSolutionFoldersWithoutLoadedProjects = vm.HideSolutionFoldersWithoutLoadedProjects;
await globalSettings.SaveAsync();
if (result) {
diff --git a/source/ProjectFilter/Services/ExtensionSettings.cs b/source/ProjectFilter/Services/ExtensionSettings.cs
index a19990b..2477d2d 100644
--- a/source/ProjectFilter/Services/ExtensionSettings.cs
+++ b/source/ProjectFilter/Services/ExtensionSettings.cs
@@ -13,6 +13,7 @@ public ExtensionSettings() {
LoadProjectDependencies = true;
UseRegularExpressions = false;
ExpandLoadedProjects = true;
+ HideSolutionFoldersWithoutLoadedProjects = false;
}
@@ -24,4 +25,7 @@ public ExtensionSettings() {
public bool ExpandLoadedProjects { get; set; }
+
+ public bool HideSolutionFoldersWithoutLoadedProjects { get; set; }
+
}
diff --git a/source/ProjectFilter/Services/FilterOptions.cs b/source/ProjectFilter/Services/FilterOptions.cs
index 5af2e26..7a484c0 100644
--- a/source/ProjectFilter/Services/FilterOptions.cs
+++ b/source/ProjectFilter/Services/FilterOptions.cs
@@ -12,12 +12,14 @@ public FilterOptions(
IEnumerable projectsToLoad,
IEnumerable projectsToUnload,
bool loadProjectDependencies,
- bool expandLoadedProjects
+ bool expandLoadedProjects,
+ bool hideSolutionFoldersWithoutLoadedProjects
) {
- ProjectsToLoad = projectsToLoad.ToList();
- ProjectsToUnload = projectsToUnload.ToList();
+ ProjectsToLoad = [.. projectsToLoad];
+ ProjectsToUnload = [.. projectsToUnload];
LoadProjectDependencies = loadProjectDependencies;
ExpandLoadedProjects = expandLoadedProjects;
+ HideSolutionFoldersWithoutLoadedProjects = hideSolutionFoldersWithoutLoadedProjects;
}
@@ -32,4 +34,7 @@ bool expandLoadedProjects
public bool ExpandLoadedProjects{ get; }
+
+ public bool HideSolutionFoldersWithoutLoadedProjects { get; }
+
}
diff --git a/source/ProjectFilter/Services/FilterService.State.cs b/source/ProjectFilter/Services/FilterService.State.cs
index 7a48c3f..3a5fafc 100644
--- a/source/ProjectFilter/Services/FilterService.State.cs
+++ b/source/ProjectFilter/Services/FilterService.State.cs
@@ -24,13 +24,13 @@ public State(IWaitDialog waitDialog, ThreadedWaitDialogProgressData initialProgr
_currentProgress = initialProgress;
Solution = solution;
- _projectsToLoad = new HashSet();
- _loadedProjects = new HashSet();
+ _projectsToLoad = [];
+ _loadedProjects = [];
- _projectsToUnload = new HashSet();
- _unloadedProjects = new HashSet();
+ _projectsToUnload = [];
+ _unloadedProjects = [];
- ProjectsVisitedWhileLoading = new HashSet();
+ ProjectsVisitedWhileLoading = [];
RequiresProjectDependencyCalculation = true;
}
@@ -109,7 +109,7 @@ private void UpdateProgressSteps() {
public IEnumerable GetLoadedProjects() {
- return _loadedProjects.ToList();
+ return [.. _loadedProjects];
}
diff --git a/source/ProjectFilter/Services/FilterService.cs b/source/ProjectFilter/Services/FilterService.cs
index 9d88821..9d3bfc8 100644
--- a/source/ProjectFilter/Services/FilterService.cs
+++ b/source/ProjectFilter/Services/FilterService.cs
@@ -62,8 +62,8 @@ public async Task ApplyAsync(FilterOptions options) {
expandedFolders = new HashSet();
}
- projectsToUnload = options.ProjectsToUnload.ToList();
- projectsToLoad = options.ProjectsToLoad.ToList();
+ projectsToUnload = [.. options.ProjectsToUnload];
+ projectsToLoad = [.. options.ProjectsToLoad];
// Work out which projects actually need to be unloaded
// so that we can calculate an accurate progress. If a
@@ -121,6 +121,15 @@ public async Task ApplyAsync(FilterOptions options) {
// remain hidden because the user cancelled half way through.
await solutionExplorer.HideUnloadedProjectsAsync();
+ // Hiding unloaded projects will hide the solution folders that those hidden
+ // projects are in as long as there is nothing else in those folders. If all
+ // of the projects in a solution folder have been unloaded, but the folder contains
+ // other solution item (like a readme file or editorconfig file), the folder
+ // will not be hidden. We have an option that allows those folders to be hidden.
+ if (options.HideSolutionFoldersWithoutLoadedProjects) {
+ await solutionExplorer.HideSolutionFoldersWithoutLoadedProjectsAsync();
+ }
+
// Expand the projects if we are supposed to. For some reason, Visual Studio seems to expand
// the projects anyway, so if we are not supposed to expand them, then we will collapse them.
if (options.ExpandLoadedProjects) {
@@ -262,7 +271,7 @@ private static async Task> GetProjectDependenciesAsync(Guid id
solutionBuildManager = (IVsSolutionBuildManager2)await VS.Services.GetSolutionBuildManagerAsync();
- output = new List();
+ output = [];
if (state.Solution.TryGetHierarchy(identifier, out IVsHierarchy hierarchy)) {
IVsHierarchy[] dependencies;
@@ -282,7 +291,7 @@ private static async Task> GetProjectDependenciesAsync(Guid id
state.RequiresProjectDependencyCalculation = false;
}
- dependencies = Array.Empty();
+ dependencies = [];
// First we need to ask for the dependencies without specifying an
// array so that we can find out how many dependencies there are.
@@ -364,7 +373,7 @@ private static async Task> GetProjectsToCollapseAsync(ISolutio
// All of the projects that were loaded should be collapsed.
- projects = state.GetLoadedProjects().ToHashSet();
+ projects = [.. state.GetLoadedProjects()];
// Any solution folders that are now expanded
// and were not originally expanded should also be collapsed.
diff --git a/source/ProjectFilter/Services/HierarchyNode.cs b/source/ProjectFilter/Services/HierarchyNode.cs
index ec1db95..bbb5fe6 100644
--- a/source/ProjectFilter/Services/HierarchyNode.cs
+++ b/source/ProjectFilter/Services/HierarchyNode.cs
@@ -21,7 +21,7 @@ ImageMoniker expandedIcon
Name = name;
CollapsedIcon = collapsedIcon;
ExpandedIcon = expandedIcon;
- _children = new List();
+ _children = [];
IsFolder = false;
IsLoaded = false;
diff --git a/source/ProjectFilter/Services/HierarchyProvider.cs b/source/ProjectFilter/Services/HierarchyProvider.cs
index d61a190..817ebd8 100644
--- a/source/ProjectFilter/Services/HierarchyProvider.cs
+++ b/source/ProjectFilter/Services/HierarchyProvider.cs
@@ -23,7 +23,7 @@ public async Task> GetHierarchyAsync() {
nodes = await GetNodesAsync();
- mapping = new Dictionary();
+ mapping = [];
foreach (var item in nodes) {
mapping[item.Hierarchy.Identifier] = item.Hierarchy;
@@ -34,7 +34,7 @@ public async Task> GetHierarchyAsync() {
// to sort each collection of children individually.
nodes.Sort((x, y) => CompareNodes(x.Hierarchy, y.Hierarchy));
- roots = new List();
+ roots = [];
// Add child nodes to the parents and find the root nodes.
foreach (var node in nodes) {
@@ -64,7 +64,7 @@ public async Task> GetHierarchyAsync() {
imageService = (IVsImageService2)await VS.Services.GetImageServiceAsync();
hierarchyItemManager = await VS.GetMefServiceAsync();
- output = new List<(HierarchyNode Hierarchy, Guid Parent)>();
+ output = [];
foreach (var hierarchy in solution.GetAllProjectHierarchies(ProjectStateFilter.All)) {
if (!TryGetIdentifier(solution, hierarchy, out Guid identifier)) {
diff --git a/source/ProjectFilter/Services/IExtensionSettings.cs b/source/ProjectFilter/Services/IExtensionSettings.cs
index 97c984d..b3706c0 100644
--- a/source/ProjectFilter/Services/IExtensionSettings.cs
+++ b/source/ProjectFilter/Services/IExtensionSettings.cs
@@ -17,6 +17,9 @@ public interface IExtensionSettings {
bool ExpandLoadedProjects { get; set; }
+ bool HideSolutionFoldersWithoutLoadedProjects { get; set; }
+
+
Task LoadAsync();
diff --git a/source/ProjectFilter/Services/ISolutionExplorer.cs b/source/ProjectFilter/Services/ISolutionExplorer.cs
index f521ba0..aa6ce0c 100644
--- a/source/ProjectFilter/Services/ISolutionExplorer.cs
+++ b/source/ProjectFilter/Services/ISolutionExplorer.cs
@@ -16,6 +16,9 @@ public interface ISolutionExplorer {
Task HideUnloadedProjectsAsync();
+ Task HideSolutionFoldersWithoutLoadedProjectsAsync();
+
+
Task ExpandAsync(IEnumerable projects);
diff --git a/source/ProjectFilter/Services/ISolutionExplorerNode.cs b/source/ProjectFilter/Services/ISolutionExplorerNode.cs
new file mode 100644
index 0000000..9b25df3
--- /dev/null
+++ b/source/ProjectFilter/Services/ISolutionExplorerNode.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+
+
+namespace ProjectFilter.Services;
+
+
+public interface ISolutionExplorerNode {
+
+ bool IsFolder { get; }
+
+
+ bool IsProject { get; }
+
+
+ bool IsLoaded { get; }
+
+
+ IReadOnlyList Children { get; }
+
+}
diff --git a/source/ProjectFilter/Services/SolutionExplorer.cs b/source/ProjectFilter/Services/SolutionExplorer.cs
index 2947d3c..af279f5 100644
--- a/source/ProjectFilter/Services/SolutionExplorer.cs
+++ b/source/ProjectFilter/Services/SolutionExplorer.cs
@@ -8,6 +8,8 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
+using System.Diagnostics;
+using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Task = System.Threading.Tasks.Task;
@@ -21,6 +23,7 @@ public class SolutionExplorer : ISolutionExplorer {
private static readonly CommandID HideUnloadedProjectsCommand = new(VSConstants.CMDSETID.StandardCommandSet15_guid, 1654);
private static readonly CommandID ShowUnloadedProjectsCommand = new(VSConstants.CMDSETID.StandardCommandSet15_guid, 1653);
private static readonly CommandID UnhideFoldersCommand = KnownCommands.Project_UnhideFolders;
+ private static readonly CommandID HideSolutionFolderCommand = new(VSConstants.CMDSETID.StandardCommandSet2K_guid, 1608);
public async Task IsEmptyAsync() {
@@ -79,8 +82,9 @@ public async Task HideUnloadedProjectsAsync() {
selection = await solutionExplorer.GetSelectionAsync();
solutionExplorer.SetSelection(solution);
- // The commands that we need to execute only work when Solution Explorer
- // has the focus. Remember the current window, then focus Solution Explorer.
+ // The commands that we need to execute may only work when
+ // Solution Explorer has the focus. Play it safe and remember
+ // the current window, then focus Solution Explorer.
window = await VS.Windows.GetCurrentWindowAsync();
solutionExplorer.Frame.Show();
@@ -118,6 +122,127 @@ public async Task HideUnloadedProjectsAsync() {
}
+ public async Task HideSolutionFoldersWithoutLoadedProjectsAsync() {
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+
+ SolutionExplorerWindow? solutionExplorer;
+
+
+ solutionExplorer = await VS.Windows.GetSolutionExplorerWindowAsync();
+
+ if (solutionExplorer is not null) {
+ IVsSolution solution;
+ IHierarchyProvider provider;
+ IEnumerable nodes;
+ IEnumerable selection;
+ WindowFrame? window;
+ bool focused;
+
+
+ solution = await VS.Services.GetSolutionAsync();
+ provider = await VS.GetRequiredServiceAsync();
+ nodes = await provider.GetHierarchyAsync();
+
+ window = null;
+ focused = false;
+
+ // We need to execute some commands that operate on the solution folders,
+ // which means we may need to change the selected item solution explorer.
+ // Remember the current selection so that we can restore it.
+ selection = await solutionExplorer.GetSelectionAsync();
+
+ // We only want to hide folders that contain projects where all of the projects are
+ // unloaded. We don't want to hide folders that only contain miscellaneous files like
+ // the "Solution Items" folder. The nodes we get from the `IHierarchyProvider` only
+ // include nodes that contain projects either within them directly or somewhere within
+ // a descendant folder. That means if we encounter a folder that doesn't have loaded
+ // projects within it, then we know it must contain only unloaded projects.
+ foreach (IHierarchyNode node in GetSolutionFoldersWithoutLoadedProjects(nodes)) {
+ if (ErrorHandler.Succeeded(solution.GetProjectOfGuid(node.Identifier, out IVsHierarchy hierarchy))) {
+ SolutionItem? folder;
+
+
+ folder = await SolutionItem.FromHierarchyAsync(hierarchy, VSConstants.VSITEMID_ROOT);
+
+ if (folder is not null) {
+ folder.GetItemInfo(out IVsHierarchy vsHierarchy, out uint itemID, out var _);
+
+ if (vsHierarchy is IVsUIHierarchy uiHierarchy) {
+ // The commands that we need to execute may only work when
+ // Solution Explorer has the focus. Play it safe and remember
+ // the current window, then focus Solution Explorer.
+ if (!focused) {
+ window = await VS.Windows.GetCurrentWindowAsync();
+ solutionExplorer.Frame.Show();
+ focused = true;
+ }
+
+ solutionExplorer.SetSelection(folder);
+ ExecuteCommand(uiHierarchy, itemID, HideSolutionFolderCommand);
+ }
+ }
+ }
+ }
+
+ // Restore the original selection.
+ solutionExplorer.SetSelection(selection);
+
+ // Restore the previously-active window.
+ if (window is not null) {
+ await window.ShowAsync();
+ }
+ }
+ }
+
+
+ public static List GetSolutionFoldersWithoutLoadedProjects(IEnumerable nodes) {
+ List folders;
+
+
+ folders = [];
+
+ foreach (IHierarchyNode child in nodes) {
+ if (child.IsFolder) {
+ Visit(child, folders, out _);
+ }
+ }
+
+ return folders;
+
+ static void Visit(
+ IHierarchyNode folder,
+ List foldersWithoutLoadedProjects,
+ out bool hasLoadedProjectsWithin
+ ) {
+ bool hasLoadedProjectsDirectly;
+ bool hasLoadedProjectsInDescendants;
+
+
+ hasLoadedProjectsDirectly = false;
+ hasLoadedProjectsInDescendants = false;
+
+ foreach (IHierarchyNode child in folder.Children) {
+ if (child.IsFolder) {
+ Visit(child, foldersWithoutLoadedProjects, out bool hasLoadedProjectsWithinFolder);
+
+ if (hasLoadedProjectsWithinFolder) {
+ hasLoadedProjectsInDescendants = true;
+ }
+
+ } else if (child.IsLoaded) {
+ hasLoadedProjectsDirectly = true;
+ }
+ }
+
+ hasLoadedProjectsWithin = hasLoadedProjectsDirectly || hasLoadedProjectsInDescendants;
+
+ if (!hasLoadedProjectsWithin) {
+ foldersWithoutLoadedProjects.Add(folder);
+ }
+ }
+ }
+
+
private static void ExecuteCommand(IVsUIHierarchy hierarchy, uint itemID, CommandID command) {
ThreadHelper.ThrowIfNotOnUIThread();
@@ -160,7 +285,7 @@ private static async Task ExpandOrCollapseAsync(IEnumerable projects, Acti
solution = (IVsSolution4)await VS.Services.GetSolutionAsync();
- items = new List();
+ items = [];
foreach (var project in projects) {
if (solution.TryGetHierarchy(project, out IVsHierarchy hierarchy)) {
@@ -190,7 +315,7 @@ public async Task> GetExpandedFoldersAsync() {
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
- expanded = new HashSet();
+ expanded = [];
solutionExplorer = await VS.Windows.GetSolutionExplorerWindowAsync();
solution = await VS.Services.GetSolutionAsync() as IVsSolution4;
@@ -238,4 +363,49 @@ out uint state
return false;
}
+
+ [DebuggerDisplay("{SolutionItem.Name,nq}")]
+ private class Node : ISolutionExplorerNode {
+
+ private List? _children;
+
+
+ public Node(SolutionItem solutionItem) {
+ SolutionItem = solutionItem;
+ }
+
+
+ public SolutionItem SolutionItem { get; }
+
+
+ public bool IsFolder {
+ get {
+ return SolutionItem.Type == SolutionItemType.SolutionFolder;
+ }
+ }
+
+
+ public bool IsProject {
+ get {
+ return SolutionItem.Type == SolutionItemType.Project;
+ }
+ }
+
+
+ public bool IsLoaded {
+ get {
+ return SolutionItem is Project project && project.IsLoaded;
+ }
+ }
+
+
+ public IReadOnlyList Children {
+ get {
+ // TODO: The children do not include unloaded projects.
+ return _children ??= [.. SolutionItem.Children.Where((x) => x is not null).Select(x => new Node(x!))];
+ }
+ }
+
+ }
+
}
diff --git a/source/ProjectFilter/Services/SolutionNodeSettings.cs b/source/ProjectFilter/Services/SolutionNodeSettings.cs
index 67be3ee..7cf5d59 100644
--- a/source/ProjectFilter/Services/SolutionNodeSettings.cs
+++ b/source/ProjectFilter/Services/SolutionNodeSettings.cs
@@ -7,7 +7,7 @@ namespace ProjectFilter.Services;
public class SolutionNodeSettings {
public SolutionNodeSettings() {
- Children = new Dictionary();
+ Children = [];
}
diff --git a/source/ProjectFilter/Services/SolutionSettings.cs b/source/ProjectFilter/Services/SolutionSettings.cs
index b0c6104..be44f3c 100644
--- a/source/ProjectFilter/Services/SolutionSettings.cs
+++ b/source/ProjectFilter/Services/SolutionSettings.cs
@@ -10,7 +10,8 @@ public SolutionSettings() {
LoadProjectDependencies = true;
UseRegularExpressions = false;
ExpandLoadedProjects = true;
- Nodes = new Dictionary();
+ HideSolutionFoldersWithoutLoadedProjects = null;
+ Nodes = [];
}
@@ -23,6 +24,9 @@ public SolutionSettings() {
public bool ExpandLoadedProjects { get; set; }
+ public bool? HideSolutionFoldersWithoutLoadedProjects { get; set; }
+
+
public Dictionary Nodes { get; }
}
diff --git a/source/ProjectFilter/UI/FilterDialog.xaml b/source/ProjectFilter/UI/FilterDialog.xaml
index 0124f8b..23ef096 100644
--- a/source/ProjectFilter/UI/FilterDialog.xaml
+++ b/source/ProjectFilter/UI/FilterDialog.xaml
@@ -48,6 +48,8 @@
+
+
@@ -100,6 +102,17 @@
/>
+
+
+
+
+