|
|
@@ -2,20 +2,22 @@ using Microsoft.VisualStudio;
|
|
|
using Microsoft.VisualStudio.Shell;
|
|
|
using Microsoft.VisualStudio.Shell.Interop;
|
|
|
using System;
|
|
|
+using System.Collections.Generic;
|
|
|
using System.IO;
|
|
|
using System.Linq;
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
|
|
|
{
|
|
|
- public class SolutionEventsHandler : IVsSolutionEvents3, IVsSolutionLoadEvents
|
|
|
+ public class SolutionEventsHandler : IVsSolutionEvents3, IVsSolutionLoadEvents
|
|
|
{
|
|
|
private readonly FilteredSolutionsExtensionPackage _package;
|
|
|
private bool _isProcessingSolution = false;
|
|
|
+ private readonly HashSet<string> _processedSolutions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
|
public SolutionEventsHandler(FilteredSolutionsExtensionPackage package)
|
|
|
{
|
|
|
- _package = package;
|
|
|
+ _package = package ?? throw new ArgumentNullException(nameof(package));
|
|
|
}
|
|
|
|
|
|
public int OnAfterOpenSolution(object pUnkReserved, int fNewSolution)
|
|
|
@@ -26,7 +28,7 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
|
|
|
|
|
|
try
|
|
|
{
|
|
|
- // Use async pattern with JoinableTaskFactory
|
|
|
+ // Use async pattern with JoinableTaskFactory to avoid blocking the UI thread
|
|
|
ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
|
|
|
{
|
|
|
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
|
|
|
@@ -36,27 +38,43 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
|
|
|
{
|
|
|
var solutionPath = dte.Solution.FullName;
|
|
|
|
|
|
- // Check if this is already a filtered solution (temp file)
|
|
|
- if (solutionPath.Contains("_filtered.sln") || Path.GetTempPath().Contains(Path.GetDirectoryName(solutionPath)))
|
|
|
+ // Check if already processed or is a filtered solution
|
|
|
+ if (_processedSolutions.Contains(solutionPath) ||
|
|
|
+ IsFilteredSolution(solutionPath))
|
|
|
{
|
|
|
+ ErrorHandler.LogInfo($"Skipping solution dialog - already processed or filtered: {Path.GetFileName(solutionPath)}");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // Count projects to see if we should show the dialog
|
|
|
- var projectCount = dte.Solution.Projects?.Count ?? 0;
|
|
|
+ // Count real projects - only show dialog for multi-project solutions
|
|
|
+ var projectCount = CountRealProjects(dte.Solution);
|
|
|
+ ErrorHandler.LogInfo($"Solution opened: {Path.GetFileName(solutionPath)} with {projectCount} projects");
|
|
|
+
|
|
|
if (projectCount > 1)
|
|
|
{
|
|
|
- // Delay to ensure solution is fully loaded
|
|
|
- await Task.Delay(500);
|
|
|
+ // Small delay to ensure solution is fully loaded
|
|
|
+ await Task.Delay(750);
|
|
|
|
|
|
+ // Check again if we're not already processing (race condition protection)
|
|
|
if (!_isProcessingSolution)
|
|
|
{
|
|
|
_isProcessingSolution = true;
|
|
|
- var solutionInfo = new SolutionInfo(solutionPath);
|
|
|
- _package.ShowFilterDialog(solutionInfo);
|
|
|
- _isProcessingSolution = false;
|
|
|
+ _processedSolutions.Add(solutionPath);
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ await ShowFilterDialogAsync(solutionPath, dte);
|
|
|
+ }
|
|
|
+ finally
|
|
|
+ {
|
|
|
+ _isProcessingSolution = false;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
+ else
|
|
|
+ {
|
|
|
+ ErrorHandler.LogInfo("Skipping solution dialog - only one project found");
|
|
|
+ }
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
@@ -69,68 +87,142 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
|
|
|
return VSConstants.S_OK;
|
|
|
}
|
|
|
|
|
|
- // Alternative approach - try to catch before solution fully opens
|
|
|
- public int OnBeforeOpenSolution(string pszSolutionFilename)
|
|
|
+ private async Task ShowFilterDialogAsync(string solutionPath, EnvDTE.DTE dte)
|
|
|
{
|
|
|
- ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
-
|
|
|
- if (_isProcessingSolution) return VSConstants.S_OK;
|
|
|
-
|
|
|
- if (!string.IsNullOrEmpty(pszSolutionFilename) && File.Exists(pszSolutionFilename))
|
|
|
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
|
|
|
+
|
|
|
+ try
|
|
|
{
|
|
|
- // Check if this is already a filtered solution
|
|
|
- if (pszSolutionFilename.Contains("_filtered.sln") || Path.GetTempPath().Contains(Path.GetDirectoryName(pszSolutionFilename)))
|
|
|
+ ErrorHandler.LogInfo($"Showing filter dialog for: {Path.GetFileName(solutionPath)}");
|
|
|
+
|
|
|
+ var solutionInfo = new SolutionInfo(solutionPath);
|
|
|
+ var dialog = new ProjectFilterDialog(solutionInfo);
|
|
|
+ var result = dialog.ShowDialog();
|
|
|
+
|
|
|
+ if (result == true && !string.IsNullOrEmpty(dialog.FilteredSolutionPath))
|
|
|
{
|
|
|
- return VSConstants.S_OK;
|
|
|
+ ErrorHandler.LogInfo($"User selected filtered solution: {Path.GetFileName(dialog.FilteredSolutionPath)}");
|
|
|
+
|
|
|
+ // Mark filtered solution as processed to prevent dialog on next open
|
|
|
+ _processedSolutions.Add(dialog.FilteredSolutionPath);
|
|
|
+
|
|
|
+ // Close current solution and open filtered one
|
|
|
+ dte.Solution.Close(false);
|
|
|
+ dte.Solution.Open(dialog.FilteredSolutionPath);
|
|
|
}
|
|
|
-
|
|
|
- try
|
|
|
+ else
|
|
|
{
|
|
|
- // Quick check of project count by parsing solution file
|
|
|
- var content = File.ReadAllText(pszSolutionFilename);
|
|
|
- var projectLines = content.Split('\n').Where(line => line.Trim().StartsWith("Project(")).ToList();
|
|
|
+ ErrorHandler.LogInfo("User chose to open all projects or cancelled dialog");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ ErrorHandler.LogError("Error showing filter dialog", ex);
|
|
|
+ ErrorHandler.ShowUserError("Filter Dialog Error",
|
|
|
+ "Failed to show project filter dialog. Opening solution with all projects.", ex);
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- if (projectLines.Count > 1)
|
|
|
- {
|
|
|
- _isProcessingSolution = true;
|
|
|
+ private bool IsFilteredSolution(string solutionPath)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ if (string.IsNullOrEmpty(solutionPath))
|
|
|
+ return false;
|
|
|
|
|
|
- // Show dialog immediately
|
|
|
- var solutionInfo = new SolutionInfo(pszSolutionFilename);
|
|
|
- var dialog = new ProjectFilterDialog(solutionInfo);
|
|
|
- var result = dialog.ShowDialog();
|
|
|
+ // Check if it's a filtered solution by name
|
|
|
+ if (solutionPath.IndexOf("_filtered.sln", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
|
+ return true;
|
|
|
|
|
|
- if (result == true && !string.IsNullOrEmpty(dialog.FilteredSolutionPath))
|
|
|
- {
|
|
|
- // We need to cancel the current opening and open the filtered one instead
|
|
|
- Task.Delay(100).ContinueWith(_ =>
|
|
|
- {
|
|
|
- ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
|
|
|
- {
|
|
|
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
|
|
|
- var dte = await _package.GetServiceAsync(typeof(EnvDTE.DTE)) as EnvDTE.DTE;
|
|
|
- dte?.Solution?.Close();
|
|
|
- dte?.Solution?.Open(dialog.FilteredSolutionPath);
|
|
|
- _isProcessingSolution = false;
|
|
|
- });
|
|
|
- });
|
|
|
+ // Check if it's in the temp directory
|
|
|
+ var tempPath = Path.GetTempPath();
|
|
|
+ var solutionDir = Path.GetDirectoryName(solutionPath);
|
|
|
+
|
|
|
+ return !string.IsNullOrEmpty(solutionDir) &&
|
|
|
+ solutionDir.StartsWith(tempPath, StringComparison.OrdinalIgnoreCase);
|
|
|
+ }
|
|
|
+ catch
|
|
|
+ {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- return VSConstants.S_FALSE; // Cancel original opening
|
|
|
- }
|
|
|
+ private int CountRealProjects(EnvDTE.Solution solution)
|
|
|
+ {
|
|
|
+ if (solution?.Projects == null)
|
|
|
+ return 0;
|
|
|
|
|
|
- _isProcessingSolution = false;
|
|
|
- }
|
|
|
- }
|
|
|
- catch (Exception ex)
|
|
|
+ int count = 0;
|
|
|
+ try
|
|
|
+ {
|
|
|
+ foreach (EnvDTE.Project project in solution.Projects)
|
|
|
{
|
|
|
- ErrorHandler.LogError("Error in OnBeforeOpenSolution", ex);
|
|
|
- _isProcessingSolution = false;
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // Skip solution folders and other non-project items
|
|
|
+ if (!string.IsNullOrEmpty(project.FullName) &&
|
|
|
+ !string.Equals(project.Kind, EnvDTE.Constants.vsProjectKindSolutionItems, StringComparison.OrdinalIgnoreCase) &&
|
|
|
+ !string.Equals(project.Kind, "{2150E333-8FDC-42A3-9474-1A3956D46DE8}", StringComparison.OrdinalIgnoreCase)) // Solution Folder GUID
|
|
|
+ {
|
|
|
+ count++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch
|
|
|
+ {
|
|
|
+ // Skip projects that can't be accessed (unloaded, etc.)
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ ErrorHandler.LogError("Error counting projects", ex);
|
|
|
+ }
|
|
|
+
|
|
|
+ return count;
|
|
|
+ }
|
|
|
+
|
|
|
+ public int OnBeforeOpenSolution(string pszSolutionFilename)
|
|
|
+ {
|
|
|
+ return VSConstants.S_OK;
|
|
|
+ }
|
|
|
+
|
|
|
+ public int OnAfterCloseSolution(object pUnkReserved)
|
|
|
+ {
|
|
|
+ ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // Clear processed solutions when closing to allow fresh dialog on next open
|
|
|
+ _processedSolutions.Clear();
|
|
|
+ _isProcessingSolution = false;
|
|
|
+ ErrorHandler.LogInfo("Solution closed - cleared processed solutions cache");
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ ErrorHandler.LogError("Error in OnAfterCloseSolution", ex);
|
|
|
+ }
|
|
|
+
|
|
|
+ return VSConstants.S_OK;
|
|
|
+ }
|
|
|
+
|
|
|
+ public int OnBeforeCloseSolution(object pUnkReserved)
|
|
|
+ {
|
|
|
+ ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // Reset processing flag when solution is about to close
|
|
|
+ _isProcessingSolution = false;
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ ErrorHandler.LogError("Error in OnBeforeCloseSolution", ex);
|
|
|
+ }
|
|
|
|
|
|
return VSConstants.S_OK;
|
|
|
}
|
|
|
|
|
|
- // IVsSolutionLoadEvents implementation
|
|
|
+ // IVsSolutionLoadEvents implementation for more granular control
|
|
|
public int OnBeforeOpenProject(ref Guid guidProjectID, ref Guid guidProjectType, string pszFileName, IVsSolutionLoadEvents pSolutionLoadEvents)
|
|
|
{
|
|
|
return VSConstants.S_OK;
|
|
|
@@ -158,24 +250,76 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
|
|
|
}
|
|
|
|
|
|
public int OnAfterBackgroundSolutionLoadComplete()
|
|
|
+ {
|
|
|
+ ThreadHelper.ThrowIfNotOnUIThread();
|
|
|
+
|
|
|
+ // This is called when background loading is complete
|
|
|
+ // We could potentially move our dialog logic here for better timing
|
|
|
+ // but OnAfterOpenSolution should be sufficient for most cases
|
|
|
+
|
|
|
+ return VSConstants.S_OK;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Standard IVsSolutionEvents methods - minimal implementation
|
|
|
+ public int OnAfterOpenProject(IVsHierarchy pHierarchy, int fAdded)
|
|
|
+ {
|
|
|
+ return VSConstants.S_OK;
|
|
|
+ }
|
|
|
+
|
|
|
+ public int OnQueryCloseProject(IVsHierarchy pHierarchy, int fRemoving, ref int pfCancel)
|
|
|
+ {
|
|
|
+ return VSConstants.S_OK;
|
|
|
+ }
|
|
|
+
|
|
|
+ public int OnBeforeCloseProject(IVsHierarchy pHierarchy, int fRemoved)
|
|
|
+ {
|
|
|
+ return VSConstants.S_OK;
|
|
|
+ }
|
|
|
+
|
|
|
+ public int OnAfterLoadProject(IVsHierarchy pStubHierarchy, IVsHierarchy pRealHierarchy)
|
|
|
+ {
|
|
|
+ return VSConstants.S_OK;
|
|
|
+ }
|
|
|
+
|
|
|
+ public int OnQueryUnloadProject(IVsHierarchy pRealHierarchy, ref int pfCancel)
|
|
|
+ {
|
|
|
+ return VSConstants.S_OK;
|
|
|
+ }
|
|
|
+
|
|
|
+ public int OnBeforeUnloadProject(IVsHierarchy pRealHierarchy, IVsHierarchy pStubHierarchy)
|
|
|
+ {
|
|
|
+ return VSConstants.S_OK;
|
|
|
+ }
|
|
|
+
|
|
|
+ public int OnQueryCloseSolution(object pUnkReserved, ref int pfCancel)
|
|
|
+ {
|
|
|
+ return VSConstants.S_OK;
|
|
|
+ }
|
|
|
+
|
|
|
+ public int OnAfterMergeSolution(object pUnkReserved)
|
|
|
+ {
|
|
|
+ return VSConstants.S_OK;
|
|
|
+ }
|
|
|
+
|
|
|
+ // IVsSolutionEvents3 additional methods
|
|
|
+ public int OnBeforeOpeningChildren(IVsHierarchy pHierarchy)
|
|
|
+ {
|
|
|
+ return VSConstants.S_OK;
|
|
|
+ }
|
|
|
+
|
|
|
+ public int OnAfterOpeningChildren(IVsHierarchy pHierarchy)
|
|
|
+ {
|
|
|
+ return VSConstants.S_OK;
|
|
|
+ }
|
|
|
+
|
|
|
+ public int OnBeforeClosingChildren(IVsHierarchy pHierarchy)
|
|
|
{
|
|
|
return VSConstants.S_OK;
|
|
|
}
|
|
|
|
|
|
- // Other IVsSolutionEvents methods
|
|
|
- public int OnAfterOpenProject(IVsHierarchy pHierarchy, int fAdded) => VSConstants.S_OK;
|
|
|
- public int OnQueryCloseProject(IVsHierarchy pHierarchy, int fRemoving, ref int pfCancel) => VSConstants.S_OK;
|
|
|
- public int OnBeforeCloseProject(IVsHierarchy pHierarchy, int fRemoved) => VSConstants.S_OK;
|
|
|
- public int OnAfterLoadProject(IVsHierarchy pStubHierarchy, IVsHierarchy pRealHierarchy) => VSConstants.S_OK;
|
|
|
- public int OnQueryUnloadProject(IVsHierarchy pRealHierarchy, ref int pfCancel) => VSConstants.S_OK;
|
|
|
- public int OnBeforeUnloadProject(IVsHierarchy pRealHierarchy, IVsHierarchy pStubHierarchy) => VSConstants.S_OK;
|
|
|
- public int OnQueryCloseSolution(object pUnkReserved, ref int pfCancel) => VSConstants.S_OK;
|
|
|
- public int OnBeforeCloseSolution(object pUnkReserved) => VSConstants.S_OK;
|
|
|
- public int OnAfterCloseSolution(object pUnkReserved) => VSConstants.S_OK;
|
|
|
- public int OnAfterMergeSolution(object pUnkReserved) => VSConstants.S_OK;
|
|
|
- public int OnBeforeOpeningChildren(IVsHierarchy pHierarchy) => VSConstants.S_OK;
|
|
|
- public int OnAfterOpeningChildren(IVsHierarchy pHierarchy) => VSConstants.S_OK;
|
|
|
- public int OnBeforeClosingChildren(IVsHierarchy pHierarchy) => VSConstants.S_OK;
|
|
|
- public int OnAfterClosingChildren(IVsHierarchy pHierarchy) => VSConstants.S_OK;
|
|
|
+ public int OnAfterClosingChildren(IVsHierarchy pHierarchy)
|
|
|
+ {
|
|
|
+ return VSConstants.S_OK;
|
|
|
+ }
|
|
|
}
|
|
|
}
|