Przeglądaj źródła

FilteredSolutionsExtension: fix hierachical view

Dalibor Votruba 1 rok temu
rodzic
commit
795eea4aa0

+ 132 - 0
FilteredSolutionsExtension/FilteredSolutionsExtension/FilteredSolutionGenerator.cs

@@ -14,12 +14,22 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
             RegexOptions.Compiled);
 
         public async Task GenerateFilteredSolutionAsync(string originalSolutionPath, string targetSolutionPath, List<ProjectInfo> selectedProjects)
+        {
+            await GenerateFilteredSolutionWithRemappingAsync(originalSolutionPath, targetSolutionPath, selectedProjects);
+        }
+
+        public async Task GenerateFilteredSolutionWithRemappingAsync(string originalSolutionPath, string targetSolutionPath, List<ProjectInfo> selectedProjects)
         {
             var originalContent = await FileUtils.ReadAllTextAsync(originalSolutionPath);
             var lines = originalContent.Split(new[] { '\r', '\n' }, System.StringSplitOptions.None);
             var filteredLines = new List<string>();
             var selectedGuids = new HashSet<string>(selectedProjects.Select(p => p.ProjectGuid), System.StringComparer.OrdinalIgnoreCase);
 
+            // Calculate path remapping
+            var originalSolutionDir = Path.GetDirectoryName(originalSolutionPath);
+            var targetSolutionDir = Path.GetDirectoryName(targetSolutionPath);
+            var pathRemapping = CalculatePathRemapping(selectedProjects, originalSolutionDir, targetSolutionDir);
+
             bool inProjectSection = false;
             bool currentProjectIncluded = false;
             bool inGlobalSection = false;
@@ -40,6 +50,14 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
                     {
                         currentProjectGuid = match.Groups[4].Value;
                         currentProjectIncluded = selectedGuids.Contains(currentProjectGuid);
+                        
+                        // If this project is included, remap its path
+                        if (currentProjectIncluded)
+                        {
+                            var remappedLine = RemapProjectPath(line, pathRemapping, originalSolutionDir, targetSolutionDir);
+                            filteredLines.Add(remappedLine);
+                        }
+                        continue;
                     }
                 }
 
@@ -134,7 +152,121 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
 
             // Write the filtered solution
             var filteredContent = string.Join(System.Environment.NewLine, filteredLines);
+            
+            // Ensure target directory exists
+            Directory.CreateDirectory(Path.GetDirectoryName(targetSolutionPath));
+            
             await FileUtils.WriteAllTextAsync(targetSolutionPath, filteredContent, Encoding.UTF8);
+
+            // Log remapping information
+            ErrorHandler.LogInfo($"Created filtered solution with {pathRemapping.Count} remapped paths");
+            foreach (var remap in pathRemapping.Take(5)) // Log first 5 for debugging
+            {
+                ErrorHandler.LogInfo($"  {remap.Key} -> {remap.Value}");
+            }
+        }
+
+        private Dictionary<string, string> CalculatePathRemapping(List<ProjectInfo> selectedProjects, string originalSolutionDir, string targetSolutionDir)
+        {
+            var pathRemapping = new Dictionary<string, string>(System.StringComparer.OrdinalIgnoreCase);
+
+            foreach (var project in selectedProjects)
+            {
+                try
+                {
+                    // Calculate original relative path
+                    var originalRelativePath = GetRelativePath(originalSolutionDir, project.FullPath);
+                    
+                    // Calculate new relative path from target solution directory
+                    var newRelativePath = GetRelativePath(targetSolutionDir, project.FullPath);
+                    
+                    // Store the mapping if paths are different
+                    if (!string.Equals(originalRelativePath, newRelativePath, System.StringComparison.OrdinalIgnoreCase))
+                    {
+                        pathRemapping[originalRelativePath] = newRelativePath;
+                    }
+                }
+                catch (System.Exception ex)
+                {
+                    ErrorHandler.LogError($"Error calculating path remapping for {project.Name}", ex);
+                }
+            }
+
+            return pathRemapping;
+        }
+
+        private string RemapProjectPath(string projectLine, Dictionary<string, string> pathRemapping, string originalSolutionDir, string targetSolutionDir)
+        {
+            var match = ProjectRegex.Match(projectLine);
+            if (!match.Success)
+                return projectLine;
+
+            var typeGuid = match.Groups[1].Value;
+            var name = match.Groups[2].Value;
+            var originalPath = match.Groups[3].Value;
+            var projectGuid = match.Groups[4].Value;
+
+            // Check if we have a remapping for this path
+            var newPath = originalPath;
+            if (pathRemapping.ContainsKey(originalPath))
+            {
+                newPath = pathRemapping[originalPath];
+            }
+            else
+            {
+                // Fallback: calculate relative path if not in mapping
+                try
+                {
+                    var fullPath = Path.IsPathRooted(originalPath) 
+                        ? originalPath 
+                        : Path.GetFullPath(Path.Combine(originalSolutionDir, originalPath));
+                    
+                    newPath = GetRelativePath(targetSolutionDir, fullPath);
+                }
+                catch
+                {
+                    // Keep original path if calculation fails
+                    newPath = originalPath;
+                }
+            }
+
+            // Reconstruct the project line with new path
+            return $"Project(\"{{{typeGuid}}}\") = \"{name}\", \"{newPath}\", \"{{{projectGuid}}}\"";
+        }
+
+        private string GetRelativePath(string fromPath, string toPath)
+        {
+            try
+            {
+                if (string.IsNullOrEmpty(fromPath) || string.IsNullOrEmpty(toPath))
+                    return toPath;
+
+                var fromUri = new System.Uri(AppendDirectorySeparator(fromPath));
+                var toUri = new System.Uri(toPath);
+                
+                if (fromUri.Scheme != toUri.Scheme)
+                    return toPath; // Cannot make relative path across different schemes
+                
+                var relativeUri = fromUri.MakeRelativeUri(toUri);
+                var relativePath = System.Uri.UnescapeDataString(relativeUri.ToString());
+                
+                // Convert forward slashes to backslashes for Windows paths
+                return relativePath.Replace('/', Path.DirectorySeparatorChar);
+            }
+            catch
+            {
+                return toPath; // Fallback to absolute path
+            }
+        }
+
+        private string AppendDirectorySeparator(string path)
+        {
+            if (!path.EndsWith(Path.DirectorySeparatorChar.ToString()) && 
+                !path.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
+            {
+                return path + Path.DirectorySeparatorChar;
+            }
+            return path;
         }
 
         private bool ContainsSelectedProjectGuid(string line, HashSet<string> selectedGuids)

+ 66 - 26
FilteredSolutionsExtension/FilteredSolutionsExtension/ProjectFilterDialog.xaml

@@ -1,9 +1,15 @@
 <Window x:Class="Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension.ProjectFilterDialog"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-        Title="Select Projects for Solution" Height="800" Width="1100" 
+        xmlns:local="clr-namespace:Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension"
+        Title="Select Projects for Solution" Height="800" Width="1200" 
         WindowStartupLocation="CenterScreen" ResizeMode="CanResize"
-        MinHeight="600" MinWidth="900">
+        MinHeight="600" MinWidth="1000">
+    <Window.Resources>
+        <local:StringToVisibilityConverter x:Key="StringToVisibilityConverter"/>
+        <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
+    </Window.Resources>
+    
     <Grid Margin="15">
         <Grid.RowDefinitions>
             <RowDefinition Height="Auto"/>
@@ -31,7 +37,6 @@
                     <ColumnDefinition Width="Auto"/>
                     <ColumnDefinition Width="*"/>
                     <ColumnDefinition Width="Auto"/>
-                    <ColumnDefinition Width="Auto"/>
                 </Grid.ColumnDefinitions>
                 
                 <Label Grid.Column="0" Content="Filter pattern:" VerticalAlignment="Center"/>
@@ -39,9 +44,7 @@
                          VerticalAlignment="Center" Height="25"
                          ToolTip="Use * for wildcards (e.g., Test*, *Core*, *.Data) - filters as you type"
                          TextChanged="FilterTextBox_TextChanged"/>
-                <Button Grid.Column="2" x:Name="FilterButton" Content="Apply Filter" 
-                        Width="90" Click="FilterButton_Click" Margin="5,0,0,0"/>
-                <Button Grid.Column="3" x:Name="ClearFilterButton" Content="Clear" 
+                <Button Grid.Column="2" x:Name="ClearFilterButton" Content="Clear" 
                         Width="60" Click="ClearFilterButton_Click" Margin="5,0,0,0"/>
             </Grid>
         </GroupBox>
@@ -56,6 +59,13 @@
                     Click="SelectTestsButton_Click" Margin="0,0,8,0" 
                     ToolTip="Select projects with 'Test' in name and their dependencies"/>
             <Separator Width="1" Margin="8,0"/>
+            <Button x:Name="ExpandAllButton" Content="Expand All" Width="80" Height="32" 
+                    Click="ExpandAllButton_Click" Margin="8,0,8,0"
+                    ToolTip="Expand all project dependency trees"/>
+            <Button x:Name="CollapseAllButton" Content="Collapse All" Width="90" Height="32" 
+                    Click="CollapseAllButton_Click" Margin="0,0,8,0"
+                    ToolTip="Collapse all project dependency trees"/>
+            <Separator Width="1" Margin="8,0"/>
             <TextBlock x:Name="SelectionCountTextBlock" VerticalAlignment="Center" 
                        Margin="8,0,0,0" Foreground="DarkBlue" FontWeight="Medium"/>
         </StackPanel>
@@ -63,30 +73,56 @@
         <!-- Project Tree -->
         <Border Grid.Row="3" BorderBrush="LightGray" BorderThickness="1" Margin="0,0,0,15">
             <TreeView x:Name="ProjectTreeView" ScrollViewer.HorizontalScrollBarVisibility="Auto"
-                      ScrollViewer.VerticalScrollBarVisibility="Auto" Padding="5"
-                      VirtualizingPanel.IsVirtualizing="True"
-                      VirtualizingPanel.VirtualizationMode="Recycling">
-                <TreeView.ItemTemplate>
-                    <DataTemplate>
-                        <StackPanel Orientation="Horizontal" Margin="0,4">
-                            <CheckBox IsChecked="{Binding IsChecked, Mode=TwoWay}" 
-                                      Checked="ProjectTreeItem_Checked"
-                                      Unchecked="ProjectTreeItem_Unchecked"
-                                      Margin="0,0,8,0"
-                                      VerticalAlignment="Top"/>
-                            <Image Source="{Binding ProjectTypeIcon}" Width="16" Height="16" 
-                                   Margin="0,0,6,0" VerticalAlignment="Top"/>
-                            <StackPanel Orientation="Vertical" MinWidth="200">
+                      ScrollViewer.VerticalScrollBarVisibility="Auto" Padding="5">
+                <TreeView.Resources>
+                    <!-- Template for dependency items (children) -->
+                    <DataTemplate x:Key="DependencyTemplate" DataType="{x:Type local:ProjectTreeItem}">
+                        <StackPanel Orientation="Horizontal" Margin="0,1">
+                            <TextBlock Text="    " Width="24"/> <!-- Indentation -->
+                            <Image Source="{Binding ProjectTypeIcon}" Width="14" Height="14" 
+                                   Margin="0,0,4,0" VerticalAlignment="Center"/>
+                            <TextBlock Text="{Binding DisplayName}" 
+                                       Foreground="Gray" FontStyle="Italic" 
+                                       VerticalAlignment="Center"
+                                       ToolTip="{Binding ToolTipText}"/>
+                        </StackPanel>
+                    </DataTemplate>
+                    
+                    <!-- Template for main projects -->
+                    <HierarchicalDataTemplate DataType="{x:Type local:ProjectTreeItem}" ItemsSource="{Binding Children}"
+                                              ItemTemplate="{StaticResource DependencyTemplate}">
+                        <StackPanel Orientation="Vertical" Margin="0,2">
+                            <!-- Main project line -->
+                            <StackPanel Orientation="Horizontal">
+                                <CheckBox IsChecked="{Binding IsChecked, Mode=TwoWay}" 
+                                          Checked="ProjectTreeItem_Checked"
+                                          Unchecked="ProjectTreeItem_Unchecked"
+                                          Margin="0,0,8,0"
+                                          VerticalAlignment="Center"
+                                          IsEnabled="{Binding IsSelectable}"/>
+                                <Image Source="{Binding ProjectTypeIcon}" Width="16" Height="16" 
+                                       Margin="0,0,6,0" VerticalAlignment="Center"/>
                                 <TextBlock Text="{Binding DisplayName}" VerticalAlignment="Center"
-                                           FontWeight="Medium" 
+                                           FontWeight="{Binding FontWeight}" 
+                                           Foreground="{Binding TextBrush}"
                                            ToolTip="{Binding ToolTipText}"/>
-                                <TextBlock Text="{Binding DependencyText}" Margin="0,2,0,0" 
-                                           Foreground="Gray" FontStyle="Italic" VerticalAlignment="Center"
-                                           TextWrapping="Wrap" MaxWidth="600" FontSize="11"/>
+                            </StackPanel>
+                            
+                            <!-- Dependencies label (only for projects with dependencies) -->
+                            <StackPanel Orientation="Horizontal" Margin="24,2,0,0"
+                                        Visibility="{Binding HasDependencies, Converter={StaticResource BooleanToVisibilityConverter}}">
+                                <TextBlock Text="Dependencies:" 
+                                           FontWeight="Normal" Foreground="DarkBlue" FontSize="11"/>
                             </StackPanel>
                         </StackPanel>
-                    </DataTemplate>
-                </TreeView.ItemTemplate>
+                    </HierarchicalDataTemplate>
+                </TreeView.Resources>
+                <TreeView.ItemContainerStyle>
+                    <Style TargetType="{x:Type TreeViewItem}">
+                        <Setter Property="IsExpanded" Value="True"/>
+                        <Setter Property="Margin" Value="0,1"/>
+                    </Style>
+                </TreeView.ItemContainerStyle>
             </TreeView>
         </Border>
         
@@ -99,6 +135,10 @@
         
         <!-- Action Buttons -->
         <StackPanel Grid.Row="5" Orientation="Horizontal" HorizontalAlignment="Right">
+            <Button x:Name="SaveAsButton" Content="Save As..." Width="100" Height="35" 
+                    Click="SaveAsButton_Click" Margin="0,0,10,0"
+                    Background="Green" Foreground="White" FontWeight="Medium"
+                    ToolTip="Save filtered solution to a custom location"/>
             <Button x:Name="OkButton" Content="Create Filtered Solution" Width="160" Height="35" 
                     Click="OkButton_Click" IsDefault="True" Margin="0,0,10,0"
                     Background="DodgerBlue" Foreground="White" FontWeight="Medium"/>

+ 283 - 46
FilteredSolutionsExtension/FilteredSolutionsExtension/ProjectFilterDialog.xaml.cs

@@ -1,8 +1,10 @@
 using Microsoft.VisualStudio.Shell;
+using Microsoft.Win32;
 using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.ComponentModel;
+using System.IO;
 using System.Linq;
 using System.Runtime.CompilerServices;
 using System.Threading.Tasks;
@@ -20,6 +22,7 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
         private string _filterPattern = "";
         private bool _updatingSelection = false;
         private DispatcherTimer _filterTimer;
+        private string _lastSaveAsPath = "";
         
         public string FilteredSolutionPath { get; private set; }
         public ObservableCollection<ProjectTreeItem> Projects { get; set; }
@@ -63,6 +66,10 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
                 System.Windows.Application.Current.Dispatcher.Invoke(() =>
                 {
                     Projects.Clear();
+                    
+                    // First pass: create all project tree items
+                    var projectTreeItems = new Dictionary<string, ProjectTreeItem>();
+                    
                     foreach (var projectNode in _allProjectNodes)
                     {
                         var projectInfo = new ProjectInfo
@@ -73,12 +80,42 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
                             TypeGuid = projectNode.TypeGuid
                         };
                         
-                        var treeItem = new ProjectTreeItem(projectInfo);
-                        // Store reference to original ProjectNode for dependency tracking
-                        treeItem.Tag = projectNode;
+                        var treeItem = new ProjectTreeItem(projectInfo)
+                        {
+                            Tag = projectNode,
+                            IsSelectable = true
+                        };
+
+                        projectTreeItems[projectNode.ProjectGuid] = treeItem;
                         Projects.Add(treeItem);
                     }
                     
+                    // Second pass: build dependency trees
+                    int totalDependencies = 0;
+                    foreach (var projectNode in _allProjectNodes)
+                    {
+                        if (projectTreeItems.TryGetValue(projectNode.ProjectGuid, out var treeItem))
+                        {
+                            BuildDependencyTree(treeItem, projectNode);
+                            totalDependencies += treeItem.Children.Count;
+                        }
+                    }
+                    
+                    ErrorHandler.LogInfo($"Total dependencies added to tree: {totalDependencies}");
+                    
+                    // Add a test child to first project if no dependencies exist (for debugging)
+                    if (totalDependencies == 0 && Projects.Count > 0)
+                    {
+                        var testChild = new ProjectTreeItem(new ProjectInfo { Name = "Test Dependency Project" })
+                        {
+                            IsDependency = true,
+                            IsSelectable = false
+                        };
+                        Projects[0].Children.Add(testChild);
+                        Projects[0].DependencyCountText = "(references 1 test project)";
+                        ErrorHandler.LogInfo("Added test dependency for debugging");
+                    }
+                    
                     ProjectCountTextBlock.Text = $"{Projects.Count} projects found";
                     UpdateSelectionCount();
                     
@@ -89,7 +126,7 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
                     // Set the ItemsSource for the TreeView
                     ProjectTreeView.ItemsSource = Projects;
                     
-                    ErrorHandler.LogInfo($"Loaded {Projects.Count} projects in filter dialog");
+                    ErrorHandler.LogInfo($"Loaded {Projects.Count} projects in filter dialog with dependency trees");
                 });
             }
             catch (Exception ex)
@@ -99,6 +136,54 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
             }
         }
 
+        private void BuildDependencyTree(ProjectTreeItem parentItem, ProjectNode projectNode)
+        {
+            // Clear existing children first
+            parentItem.Children.Clear();
+            
+            ErrorHandler.LogInfo($"Building dependency tree for {projectNode.Name} with {projectNode.Dependencies.Count} dependencies");
+            
+            // Add dependency children (projects that this project depends on)
+            foreach (var dependency in projectNode.Dependencies)
+            {
+                var depInfo = new ProjectInfo
+                {
+                    Name = dependency.Name,
+                    FullPath = dependency.Path,
+                    ProjectGuid = dependency.ProjectGuid,
+                    TypeGuid = dependency.TypeGuid
+                };
+
+                var depItem = new ProjectTreeItem(depInfo)
+                {
+                    Tag = dependency,
+                    IsSelectable = false, // Dependencies are not selectable in tree
+                    IsDependency = true,
+                    IsChecked = false // Dependencies don't have checkboxes
+                };
+
+                parentItem.Children.Add(depItem);
+                ErrorHandler.LogInfo($"  Added dependency: {dependency.Name}");
+            }
+
+            // Force property change notification after adding children
+            parentItem.OnPropertyChanged(nameof(parentItem.HasDependencies));
+            
+            // Verify HasDependencies is working
+            ErrorHandler.LogInfo($"Project {projectNode.Name}: HasDependencies = {parentItem.HasDependencies}, Children.Count = {parentItem.Children.Count}");
+
+            // Update dependency count text for parent - only show if has dependencies
+            if (projectNode.Dependencies.Count > 0)
+            {
+                parentItem.DependencyCountText = "";  // Remove count from main display
+                ErrorHandler.LogInfo($"Project {projectNode.Name} has {projectNode.Dependencies.Count} dependencies");
+            }
+            else
+            {
+                parentItem.DependencyCountText = "";
+            }
+        }
+
         private void SelectAllButton_Click(object sender, RoutedEventArgs e)
         {
             _updatingSelection = true;
@@ -106,7 +191,8 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
             {
                 foreach (var project in Projects)
                 {
-                    project.IsChecked = true;
+                    if (project.IsSelectable)
+                        project.IsChecked = true;
                 }
                 UpdateSelectionCount();
             }
@@ -123,7 +209,8 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
             {
                 foreach (var project in Projects)
                 {
-                    project.IsChecked = false;
+                    if (project.IsSelectable)
+                        project.IsChecked = false;
                 }
                 UpdateSelectionCount();
             }
@@ -144,7 +231,8 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
             {
                 foreach (var project in Projects)
                 {
-                    if (project.DisplayName.IndexOf("Test", StringComparison.OrdinalIgnoreCase) >= 0)
+                    if (project.IsSelectable && 
+                        project.DisplayName.IndexOf("Test", StringComparison.OrdinalIgnoreCase) >= 0)
                     {
                         project.IsChecked = true;
                         // Auto-select dependencies for test projects
@@ -162,9 +250,75 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
             }
         }
 
-        private void FilterButton_Click(object sender, RoutedEventArgs e)
+        private void ExpandAllButton_Click(object sender, RoutedEventArgs e)
         {
-            ApplyFilter();
+            SetAllTreeViewItemsExpanded(true);
+            StatusTextBlock.Text = "All project trees expanded";
+        }
+
+        private void CollapseAllButton_Click(object sender, RoutedEventArgs e)
+        {
+            SetAllTreeViewItemsExpanded(false);
+            StatusTextBlock.Text = "All project trees collapsed";
+        }
+
+        private void SetAllTreeViewItemsExpanded(bool isExpanded)
+        {
+            try
+            {
+                // First approach: try to access containers directly
+                foreach (var item in Projects)
+                {
+                    var container = ProjectTreeView.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
+                    if (container != null)
+                    {
+                        SetTreeViewItemExpanded(container, isExpanded);
+                    }
+                }
+
+                // Second approach: force container generation for any that weren't generated yet
+                ProjectTreeView.UpdateLayout();
+                
+                // Try again for any containers that weren't available the first time
+                foreach (var item in Projects)
+                {
+                    var container = ProjectTreeView.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
+                    if (container != null)
+                    {
+                        container.IsExpanded = isExpanded;
+                        // Also ensure child containers are handled
+                        for (int i = 0; i < container.Items.Count; i++)
+                        {
+                            var childContainer = container.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
+                            if (childContainer != null)
+                            {
+                                childContainer.IsExpanded = isExpanded;
+                            }
+                        }
+                    }
+                }
+            }
+            catch (Exception ex)
+            {
+                ErrorHandler.LogError($"Error {(isExpanded ? "expanding" : "collapsing")} tree items", ex);
+            }
+        }
+
+        private void SetTreeViewItemExpanded(TreeViewItem item, bool isExpanded)
+        {
+            if (item == null) return;
+
+            item.IsExpanded = isExpanded;
+
+            // Recursively expand/collapse child items
+            for (int i = 0; i < item.Items.Count; i++)
+            {
+                var childContainer = item.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
+                if (childContainer != null)
+                {
+                    SetTreeViewItemExpanded(childContainer, isExpanded);
+                }
+            }
         }
 
         private void ClearFilterButton_Click(object sender, RoutedEventArgs e)
@@ -176,7 +330,7 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
 
         private void FilterTextBox_TextChanged(object sender, TextChangedEventArgs e)
         {
-            // Debounce the filter to avoid too many updates
+            // Auto-filter while typing with debounce
             _filterTimer?.Stop();
             _filterTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(300) };
             _filterTimer.Tick += (s, args) =>
@@ -191,41 +345,41 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
         {
             var pattern = FilterTextBox.Text?.Trim() ?? "";
             
-            if (string.IsNullOrWhiteSpace(pattern))
+            foreach (var project in Projects)
             {
-                // Show all projects
-                foreach (var project in Projects)
-                {
-                    var container = ProjectTreeView.ItemContainerGenerator.ContainerFromItem(project) as FrameworkElement;
-                    if (container != null)
-                    {
-                        container.Visibility = Visibility.Visible;
-                    }
-                }
-                return;
+                ApplyFilterToItem(project, pattern);
             }
+        }
 
-            try
+        private bool ApplyFilterToItem(ProjectTreeItem item, string pattern)
+        {
+            bool isVisible = true;
+            
+            if (!string.IsNullOrWhiteSpace(pattern))
             {
-                var wildcardPattern = pattern.Replace("*", ".*");
-                var regex = new System.Text.RegularExpressions.Regex(wildcardPattern, 
-                    System.Text.RegularExpressions.RegexOptions.IgnoreCase);
+                try
+                {
+                    var wildcardPattern = pattern.Replace("*", ".*");
+                    var regex = new System.Text.RegularExpressions.Regex(wildcardPattern, 
+                        System.Text.RegularExpressions.RegexOptions.IgnoreCase);
 
-                foreach (var project in Projects)
+                    // For main projects, check if name matches
+                    // For dependencies, always show if parent is visible
+                    isVisible = item.IsDependency || regex.IsMatch(item.ProjectInfo.Name);
+                }
+                catch (Exception ex)
                 {
-                    var isMatch = regex.IsMatch(project.ProjectInfo.Name);
-                    var container = ProjectTreeView.ItemContainerGenerator.ContainerFromItem(project) as FrameworkElement;
-                    if (container != null)
-                    {
-                        container.Visibility = isMatch ? Visibility.Visible : Visibility.Collapsed;
-                    }
+                    ErrorHandler.LogError("Error applying filter pattern", ex);
+                    StatusTextBlock.Text = "Invalid filter pattern";
+                    isVisible = true; // Show all on error
                 }
             }
-            catch (Exception ex)
-            {
-                ErrorHandler.LogError("Error applying filter pattern", ex);
-                StatusTextBlock.Text = "Invalid filter pattern";
-            }
+
+            // For TreeView with hierarchical data, we need to use a different approach
+            // The filtering will be handled by the TreeView's visibility binding
+            item.IsVisible = isVisible;
+            
+            return isVisible;
         }
 
         private void ProjectTreeItem_Checked(object sender, RoutedEventArgs e)
@@ -234,7 +388,7 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
 
             var checkBox = sender as CheckBox;
             var treeItem = checkBox?.DataContext as ProjectTreeItem;
-            if (treeItem?.Tag is ProjectNode projectNode)
+            if (treeItem?.Tag is ProjectNode projectNode && treeItem.IsSelectable)
             {
                 _updatingSelection = true;
                 try
@@ -256,7 +410,7 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
 
             var checkBox = sender as CheckBox;
             var treeItem = checkBox?.DataContext as ProjectTreeItem;
-            if (treeItem?.Tag is ProjectNode projectNode)
+            if (treeItem?.Tag is ProjectNode projectNode && treeItem.IsSelectable)
             {
                 _updatingSelection = true;
                 try
@@ -278,7 +432,7 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
             var treeItem = Projects.FirstOrDefault(p => 
                 string.Equals(((ProjectNode)p.Tag)?.ProjectGuid, projectNode.ProjectGuid, StringComparison.OrdinalIgnoreCase));
             
-            if (treeItem != null && treeItem.IsChecked != isChecked)
+            if (treeItem != null && treeItem.IsChecked != isChecked && treeItem.IsSelectable)
             {
                 treeItem.IsChecked = isChecked;
             }
@@ -298,7 +452,7 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
                 var treeItem = Projects.FirstOrDefault(p => 
                     string.Equals(((ProjectNode)p.Tag)?.ProjectGuid, dependent.ProjectGuid, StringComparison.OrdinalIgnoreCase));
                 
-                if (treeItem != null && treeItem.IsChecked)
+                if (treeItem != null && treeItem.IsChecked && treeItem.IsSelectable)
                 {
                     treeItem.IsChecked = false;
                     DeselectDependents(dependent);
@@ -308,18 +462,99 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
 
         private void UpdateSelectionCount()
         {
-            var selectedCount = Projects.Count(p => p.IsChecked);
+            var selectedCount = Projects.Count(p => p.IsSelectable && p.IsChecked);
             SelectionCountTextBlock.Text = $"{selectedCount} of {Projects.Count} projects selected";
             
-            // Enable/disable OK button based on selection
+            // Enable/disable buttons based on selection
             OkButton.IsEnabled = selectedCount > 0;
+            SaveAsButton.IsEnabled = selectedCount > 0;
+        }
+
+        private async void SaveAsButton_Click(object sender, RoutedEventArgs e)
+        {
+            try
+            {
+                var selectedTreeItems = Projects.Where(p => p.IsSelectable && p.IsChecked).ToList();
+                if (!selectedTreeItems.Any())
+                {
+                    ErrorHandler.ShowUserInfo("Warning", "Please select at least one project.");
+                    return;
+                }
+
+                // Show save dialog
+                var saveDialog = new SaveFileDialog
+                {
+                    Title = "Save Filtered Solution As",
+                    Filter = "Solution files (*.sln)|*.sln|All files (*.*)|*.*",
+                    DefaultExt = ".sln",
+                    FileName = $"{_solutionInfo.Name}_filtered.sln",
+                    InitialDirectory = string.IsNullOrEmpty(_lastSaveAsPath) ? 
+                        Path.GetDirectoryName(_solutionInfo.FullPath) : 
+                        Path.GetDirectoryName(_lastSaveAsPath)
+                };
+
+                if (saveDialog.ShowDialog() == true)
+                {
+                    _lastSaveAsPath = saveDialog.FileName;
+                    
+                    StatusTextBlock.Text = "Creating filtered solution...";
+                    SaveAsButton.IsEnabled = false;
+                    OkButton.IsEnabled = false;
+                    CancelButton.IsEnabled = false;
+
+                    // Get corresponding ProjectNodes
+                    var selectedProjectNodes = selectedTreeItems
+                        .Select(ti => ti.Tag as ProjectNode)
+                        .Where(pn => pn != null)
+                        .ToList();
+
+                    ErrorHandler.LogInfo($"User saving {selectedProjectNodes.Count} projects to custom location");
+
+                    // The parser already includes dependencies
+                    var projectsWithDependencies = _solutionParser.GetProjectsWithDependencies(selectedProjectNodes);
+                    
+                    // Create filtered solution with path remapping
+                    FilteredSolutionPath = await _solutionParser.CreateFilteredSolutionWithRemappingAsync(
+                        _solutionInfo.FullPath, projectsWithDependencies, saveDialog.FileName);
+                    
+                    StatusTextBlock.Text = "Filtered solution saved successfully";
+                    
+                    // Ask if user wants to open the saved solution
+                    var result = MessageBox.Show(
+                        $"Filtered solution saved to:\n{saveDialog.FileName}\n\nWould you like to open it now?",
+                        "Solution Saved",
+                        MessageBoxButton.YesNo,
+                        MessageBoxImage.Question);
+                        
+                    if (result == MessageBoxResult.Yes)
+                    {
+                        DialogResult = true;
+                    }
+                    else
+                    {
+                        // Reset buttons for continued use
+                        SaveAsButton.IsEnabled = true;
+                        OkButton.IsEnabled = true;
+                        CancelButton.IsEnabled = true;
+                    }
+                }
+            }
+            catch (Exception ex)
+            {
+                await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+                ErrorHandler.ShowUserError("Error", "Error saving filtered solution", ex);
+                StatusTextBlock.Text = "Error saving filtered solution";
+                SaveAsButton.IsEnabled = true;
+                OkButton.IsEnabled = true;
+                CancelButton.IsEnabled = true;
+            }
         }
 
         private async void OkButton_Click(object sender, RoutedEventArgs e)
         {
             try
             {
-                var selectedTreeItems = Projects.Where(p => p.IsChecked).ToList();
+                var selectedTreeItems = Projects.Where(p => p.IsSelectable && p.IsChecked).ToList();
                 if (!selectedTreeItems.Any())
                 {
                     ErrorHandler.ShowUserInfo("Warning", "Please select at least one project.");
@@ -328,6 +563,7 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
 
                 StatusTextBlock.Text = "Creating filtered solution...";
                 OkButton.IsEnabled = false;
+                SaveAsButton.IsEnabled = false;
                 CancelButton.IsEnabled = false;
 
                 // Get corresponding ProjectNodes
@@ -338,10 +574,10 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
 
                 ErrorHandler.LogInfo($"User selected {selectedProjectNodes.Count} projects for filtering");
 
-                // The parser already includes dependencies, but double-check
+                // The parser already includes dependencies
                 var projectsWithDependencies = _solutionParser.GetProjectsWithDependencies(selectedProjectNodes);
                 
-                // Create filtered solution
+                // Create filtered solution in temp directory with path remapping
                 FilteredSolutionPath = await _solutionParser.CreateFilteredSolutionAsync(
                     _solutionInfo.FullPath, projectsWithDependencies);
                 
@@ -354,6 +590,7 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
                 ErrorHandler.ShowUserError("Error", "Error creating filtered solution", ex);
                 StatusTextBlock.Text = "Error creating filtered solution";
                 OkButton.IsEnabled = true;
+                SaveAsButton.IsEnabled = true;
                 CancelButton.IsEnabled = true;
             }
         }

+ 129 - 52
FilteredSolutionsExtension/FilteredSolutionsExtension/ProjectTreeItem.cs

@@ -1,7 +1,9 @@
-using System;
+using System;
+using System.Collections.ObjectModel;
 using System.ComponentModel;
 using System.Linq;
 using System.Runtime.CompilerServices;
+using System.Windows;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
 
@@ -10,9 +12,14 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
     public class ProjectTreeItem : INotifyPropertyChanged
     {
         private bool _isChecked = true;
+        private bool _isSelectable = true;
+        private bool _isDependency = false;
+        private bool _isVisible = true;
+        private string _dependencyCountText = "";
 
         public ProjectInfo ProjectInfo { get; }
         public object Tag { get; set; } // Reference to ProjectNode for dependency tracking
+        public ObservableCollection<ProjectTreeItem> Children { get; set; }
 
         public bool IsChecked
         {
@@ -28,44 +35,91 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
             }
         }
 
+        public bool IsSelectable
+        {
+            get => _isSelectable;
+            set
+            {
+                if (_isSelectable != value)
+                {
+                    _isSelectable = value;
+                    OnPropertyChanged();
+                    OnPropertyChanged(nameof(FontWeight));
+                    OnPropertyChanged(nameof(TextBrush));
+                }
+            }
+        }
+
+        public bool IsDependency
+        {
+            get => _isDependency;
+            set
+            {
+                if (_isDependency != value)
+                {
+                    _isDependency = value;
+                    OnPropertyChanged();
+                    OnPropertyChanged(nameof(FontWeight));
+                    OnPropertyChanged(nameof(TextBrush));
+                }
+            }
+        }
+
+        public bool IsVisible
+        {
+            get => _isVisible;
+            set
+            {
+                if (_isVisible != value)
+                {
+                    _isVisible = value;
+                    OnPropertyChanged();
+                    OnPropertyChanged(nameof(TreeItemVisibility));
+                }
+            }
+        }
+
+        public Visibility TreeItemVisibility => IsVisible ? Visibility.Visible : Visibility.Collapsed;
+
         public string DisplayName => ProjectInfo.Name;
 
-        public string DependencyText
+        public bool HasDependencies
         {
             get
             {
-                if (Tag is ProjectNode projectNode)
+                if (IsDependency) return false;
+                return Children != null && Children.Count > 0;
+            }
+        }
+
+        public string DependencyCountText
+        {
+            get => _dependencyCountText;
+            set
+            {
+                if (_dependencyCountText != value)
                 {
-                    var depCount = projectNode.Dependencies.Count;
-                    var dependentCount = projectNode.Dependents.Count;
-                    
-                    if (depCount == 0 && dependentCount == 0)
-                        return "";
+                    _dependencyCountText = value;
+                    OnPropertyChanged();
+                }
+            }
+        }
 
-                    var parts = new System.Collections.Generic.List<string>();
-                    
-                    if (depCount > 0)
-                    {
-                        var depNames = projectNode.Dependencies.Take(2).Select(d => d.Name);
-                        if (depCount > 2)
-                        {
-                            depNames = depNames.Concat(new[] { $"+{depCount - 2} more" });
-                        }
-                        parts.Add($"depends on: {string.Join(", ", depNames)}");
-                    }
-                    
-                    if (dependentCount > 0)
-                    {
-                        var dependentNames = projectNode.Dependents.Take(2).Select(d => d.Name);
-                        if (dependentCount > 2)
-                        {
-                            dependentNames = dependentNames.Concat(new[] { $"+{dependentCount - 2} more" });
-                        }
-                        parts.Add($"used by: {string.Join(", ", dependentNames)}");
-                    }
-                    
-                    return string.Join(" | ", parts);
+        public FontWeight FontWeight => IsDependency ? FontWeights.Normal : FontWeights.Medium;
+
+        public Brush TextBrush => IsDependency ? Brushes.Gray : Brushes.Black;
+
+        public string DependencyText
+        {
+            get
+            {
+                // For dependencies, don't show extra text - just the name is enough
+                if (IsDependency)
+                {
+                    return "";
                 }
+
+                // For main projects, we can show summary info in tooltip but not in main display
                 return "";
             }
         }
@@ -76,7 +130,7 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
             {
                 try
                 {
-                    // Get project type icon based on TypeGuid or file extension
+                    // For dependency nodes, use a different icon or modifier
                     var iconName = GetProjectTypeIconName();
                     if (!string.IsNullOrEmpty(iconName))
                     {
@@ -97,32 +151,42 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
         private string GetProjectTypeIconName()
         {
             if (ProjectInfo?.TypeGuid == null)
-                return "project.png"; // Default project icon
+                return IsDependency ? "dependency.png" : "project.png"; // Different icon for dependencies
 
             // Map common project type GUIDs to icon names
+            string baseIcon;
             switch (ProjectInfo.TypeGuid.ToUpperInvariant())
             {
                 case "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}": // C# Project
-                    return IsTestProject() ? "test.png" : "csharp.png";
-                    
+                    baseIcon = IsTestProject() ? "test.png" : "csharp.png";
+                    break;
                 case "{F184B08F-C81C-45F6-A57F-5ABD9991F28F}": // VB.NET Project
-                    return "vbnet.png";
-                    
+                    baseIcon = "vbnet.png";
+                    break;
                 case "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}": // C++ Project
-                    return "cpp.png";
-                    
+                    baseIcon = "cpp.png";
+                    break;
                 case "{349C5851-65DF-11DA-9384-00065B846F21}": // Web Application
-                    return "web.png";
-                    
+                    baseIcon = "web.png";
+                    break;
                 case "{E24C65DC-7377-472B-9ABA-BC803B73C61A}": // Web Site
-                    return "website.png";
-                    
+                    baseIcon = "website.png";
+                    break;
                 case "{3AC096D0-A1C2-E12C-1390-A8335801FDAB}": // Test Project
-                    return "test.png";
-                    
+                    baseIcon = "test.png";
+                    break;
                 default:
-                    return IsTestProject() ? "test.png" : "project.png";
+                    baseIcon = IsTestProject() ? "test.png" : "project.png";
+                    break;
             }
+
+            // Modify icon name for dependencies
+            if (IsDependency)
+            {
+                return "dep_" + baseIcon; // Use prefixed dependency icons if available
+            }
+
+            return baseIcon;
         }
 
         private bool IsTestProject()
@@ -172,6 +236,11 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
             {
                 var lines = new System.Collections.Generic.List<string>();
                 
+                if (IsDependency)
+                {
+                    lines.Add("This project is referenced/used by the parent project.");
+                }
+                
                 if (!string.IsNullOrEmpty(ProjectPath))
                 {
                     lines.Add($"Path: {ProjectPath}");
@@ -188,14 +257,14 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
                     lines.Add($"Size: {size}");
                 }
                 
-                if (Tag is ProjectNode projectNode)
+                if (Tag is ProjectNode projectNode && !IsDependency)
                 {
                     if (projectNode.Dependencies.Count > 0)
                     {
-                        lines.Add($"Dependencies ({projectNode.Dependencies.Count}):");
+                        lines.Add($"This project depends on ({projectNode.Dependencies.Count}):");
                         foreach (var dep in projectNode.Dependencies.Take(5))
                         {
-                            lines.Add($"   {dep.Name}");
+                            lines.Add($"  • {dep.Name}");
                         }
                         if (projectNode.Dependencies.Count > 5)
                         {
@@ -205,10 +274,10 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
                     
                     if (projectNode.Dependents.Count > 0)
                     {
-                        lines.Add($"Used by ({projectNode.Dependents.Count}):");
+                        lines.Add($"Referenced by ({projectNode.Dependents.Count}):");
                         foreach (var dependent in projectNode.Dependents.Take(5))
                         {
-                            lines.Add($"   {dependent.Name}");
+                            lines.Add($"  • {dependent.Name}");
                         }
                         if (projectNode.Dependents.Count > 5)
                         {
@@ -224,11 +293,19 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
         public ProjectTreeItem(ProjectInfo projectInfo)
         {
             ProjectInfo = projectInfo ?? throw new ArgumentNullException(nameof(projectInfo));
+            Children = new ObservableCollection<ProjectTreeItem>();
+            
+            // Subscribe to Children collection changes to trigger UI updates
+            Children.CollectionChanged += (s, e) => 
+            {
+                OnPropertyChanged(nameof(Children));
+                OnPropertyChanged(nameof(HasDependencies));
+            };
         }
 
         public event PropertyChangedEventHandler PropertyChanged;
 
-        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+        public virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
         {
             PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
         }

+ 216 - 78
FilteredSolutionsExtension/FilteredSolutionsExtension/SolutionEventsHandler.cs

@@ -5,6 +5,7 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using System.Threading;
 using System.Threading.Tasks;
 
 namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
@@ -14,12 +15,39 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
         private readonly FilteredSolutionsExtensionPackage _package;
         private bool _isProcessingSolution = false;
         private readonly HashSet<string> _processedSolutions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+        private string _pendingSolutionPath = null;
+        private CancellationTokenSource _loadWaitCancellation = null;
 
         public SolutionEventsHandler(FilteredSolutionsExtensionPackage package)
         {
             _package = package ?? throw new ArgumentNullException(nameof(package));
         }
 
+        #region Solution Loading Detection - Primary Method
+
+        public int OnAfterBackgroundSolutionLoadComplete()
+        {
+            ThreadHelper.ThrowIfNotOnUIThread();
+            
+            try
+            {
+                ErrorHandler.LogInfo("Background solution load completed");
+                
+                // This is the most reliable event for knowing when solution is fully loaded
+                ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
+                {
+                    await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+                    await HandleSolutionFullyLoadedAsync();
+                });
+            }
+            catch (Exception ex)
+            {
+                ErrorHandler.LogError("Error in OnAfterBackgroundSolutionLoadComplete", ex);
+            }
+
+            return VSConstants.S_OK;
+        }
+
         public int OnAfterOpenSolution(object pUnkReserved, int fNewSolution)
         {
             ThreadHelper.ThrowIfNotOnUIThread();
@@ -28,7 +56,7 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
 
             try
             {
-                // Use async pattern with JoinableTaskFactory to avoid blocking the UI thread
+                // Use async pattern to get DTE service
                 ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
                 {
                     await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
@@ -36,57 +64,182 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
                     var dte = await _package.GetServiceAsync(typeof(EnvDTE.DTE)) as EnvDTE.DTE;
                     if (dte?.Solution != null && !string.IsNullOrEmpty(dte.Solution.FullName))
                     {
-                        var solutionPath = dte.Solution.FullName;
+                        _pendingSolutionPath = dte.Solution.FullName;
+                        ErrorHandler.LogInfo($"Solution opened, waiting for full load: {Path.GetFileName(_pendingSolutionPath)}");
+                        
+                        // Start a timeout mechanism in case OnAfterBackgroundSolutionLoadComplete doesn't fire
+                        StartLoadWaitTimeout();
+                    }
+                });
+            }
+            catch (Exception ex)
+            {
+                ErrorHandler.LogError("Error in OnAfterOpenSolution", ex);
+            }
+
+            return VSConstants.S_OK;
+        }
+
+        #endregion
+
+        #region Load Completion Handling
+
+        private async Task HandleSolutionFullyLoadedAsync()
+        {
+            if (_isProcessingSolution || string.IsNullOrEmpty(_pendingSolutionPath)) 
+                return;
+
+            try
+            {
+                await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+
+                // Cancel any pending timeout
+                _loadWaitCancellation?.Cancel();
+                _loadWaitCancellation = null;
+
+                var solutionPath = _pendingSolutionPath;
+                _pendingSolutionPath = null;
+
+                // 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;
+                }
+
+                var dte = await _package.GetServiceAsync(typeof(EnvDTE.DTE)) as EnvDTE.DTE;
+                if (dte?.Solution != null)
+                {
+                    // Verify solution is actually fully loaded by checking project count
+                    var projectCount = await GetRealProjectCountAsync(dte.Solution);
+                    ErrorHandler.LogInfo($"Solution fully loaded: {Path.GetFileName(solutionPath)} with {projectCount} projects");
+
+                    if (projectCount > 1)
+                    {
+                        _isProcessingSolution = true;
+                        _processedSolutions.Add(solutionPath);
 
-                        // Check if already processed or is a filtered solution
-                        if (_processedSolutions.Contains(solutionPath) || 
-                            IsFilteredSolution(solutionPath))
+                        try
+                        {
+                            await ShowFilterDialogAsync(solutionPath, dte);
+                        }
+                        finally
                         {
-                            ErrorHandler.LogInfo($"Skipping solution dialog - already processed or filtered: {Path.GetFileName(solutionPath)}");
-                            return;
+                            _isProcessingSolution = false;
                         }
+                    }
+                    else
+                    {
+                        ErrorHandler.LogInfo("Skipping solution dialog - only one project found");
+                    }
+                }
+            }
+            catch (Exception ex)
+            {
+                ErrorHandler.LogError("Error in HandleSolutionFullyLoadedAsync", ex);
+                _isProcessingSolution = false;
+                _pendingSolutionPath = null;
+            }
+        }
+
+        private void StartLoadWaitTimeout()
+        {
+            _loadWaitCancellation?.Cancel();
+            _loadWaitCancellation = new CancellationTokenSource();
+            
+            ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
+            {
+                try
+                {
+                    // Wait up to 10 seconds for background load to complete
+                    await Task.Delay(10000, _loadWaitCancellation.Token);
+                    
+                    if (!_loadWaitCancellation.Token.IsCancellationRequested)
+                    {
+                        ErrorHandler.LogInfo("Background load timeout reached, proceeding with dialog");
+                        await HandleSolutionFullyLoadedAsync();
+                    }
+                }
+                catch (OperationCanceledException)
+                {
+                    // Expected when background load completes normally
+                }
+                catch (Exception ex)
+                {
+                    ErrorHandler.LogError("Error in load wait timeout", ex);
+                }
+            });
+        }
 
-                        // 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");
+        #endregion
 
-                        if (projectCount > 1)
+        #region Alternative Load Detection Methods
+
+        private async Task<int> GetRealProjectCountAsync(EnvDTE.Solution solution)
+        {
+            await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+            
+            if (solution?.Projects == null)
+                return 0;
+
+            int count = 0;
+            try
+            {
+                // Use a more robust counting method that waits for projects to be available
+                const int maxRetries = 20; // 2 seconds with 100ms intervals
+                const int retryDelayMs = 100;
+
+                for (int retry = 0; retry < maxRetries; retry++)
+                {
+                    try
+                    {
+                        count = 0;
+                        foreach (EnvDTE.Project project in solution.Projects)
                         {
-                            // 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)
+                            try
                             {
-                                _isProcessingSolution = true;
-                                _processedSolutions.Add(solutionPath);
-
-                                try
-                                {
-                                    await ShowFilterDialogAsync(solutionPath, dte);
-                                }
-                                finally
+                                // 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
                                 {
-                                    _isProcessingSolution = false;
+                                    count++;
                                 }
                             }
+                            catch
+                            {
+                                // Skip projects that can't be accessed yet
+                            }
                         }
-                        else
-                        {
-                            ErrorHandler.LogInfo("Skipping solution dialog - only one project found");
-                        }
+
+                        // If we got a reasonable count, break out of retry loop
+                        if (count > 0 || retry == maxRetries - 1)
+                            break;
+
+                        // Wait a bit and retry
+                        await Task.Delay(retryDelayMs);
                     }
-                });
+                    catch (Exception ex)
+                    {
+                        ErrorHandler.LogError($"Error counting projects (retry {retry + 1})", ex);
+                        if (retry == maxRetries - 1)
+                            break;
+                        await Task.Delay(retryDelayMs);
+                    }
+                }
             }
             catch (Exception ex)
             {
-                ErrorHandler.LogError("Error in OnAfterOpenSolution", ex);
-                _isProcessingSolution = false;
+                ErrorHandler.LogError("Error in GetRealProjectCountAsync", ex);
             }
 
-            return VSConstants.S_OK;
+            return count;
         }
 
+        #endregion
+
+        #region Dialog Management
+
         private async Task ShowFilterDialogAsync(string solutionPath, EnvDTE.DTE dte)
         {
             await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
@@ -123,6 +276,10 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
             }
         }
 
+        #endregion
+
+        #region Utility Methods
+
         private bool IsFilteredSolution(string solutionPath)
         {
             try
@@ -147,39 +304,9 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
             }
         }
 
-        private int CountRealProjects(EnvDTE.Solution solution)
-        {
-            if (solution?.Projects == null)
-                return 0;
-
-            int count = 0;
-            try
-            {
-                foreach (EnvDTE.Project project in solution.Projects)
-                {
-                    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);
-            }
+        #endregion
 
-            return count;
-        }
+        #region Solution Events - Standard Implementation
 
         public int OnBeforeOpenSolution(string pszSolutionFilename)
         {
@@ -192,9 +319,14 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
             
             try
             {
+                // Cancel any pending operations
+                _loadWaitCancellation?.Cancel();
+                _loadWaitCancellation = null;
+                
                 // Clear processed solutions when closing to allow fresh dialog on next open
                 _processedSolutions.Clear();
                 _isProcessingSolution = false;
+                _pendingSolutionPath = null;
                 ErrorHandler.LogInfo("Solution closed - cleared processed solutions cache");
             }
             catch (Exception ex)
@@ -211,8 +343,13 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
             
             try
             {
+                // Cancel any pending load operations
+                _loadWaitCancellation?.Cancel();
+                _loadWaitCancellation = null;
+                
                 // Reset processing flag when solution is about to close
                 _isProcessingSolution = false;
+                _pendingSolutionPath = null;
             }
             catch (Exception ex)
             {
@@ -222,7 +359,10 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
             return VSConstants.S_OK;
         }
 
-        // IVsSolutionLoadEvents implementation for more granular control
+        #endregion
+
+        #region IVsSolutionLoadEvents Implementation
+
         public int OnBeforeOpenProject(ref Guid guidProjectID, ref Guid guidProjectType, string pszFileName, IVsSolutionLoadEvents pSolutionLoadEvents)
         {
             return VSConstants.S_OK;
@@ -230,6 +370,7 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
 
         public int OnBeforeBackgroundSolutionLoadBegins()
         {
+            ErrorHandler.LogInfo("Background solution load beginning");
             return VSConstants.S_OK;
         }
 
@@ -249,18 +390,10 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
             return VSConstants.S_OK;
         }
 
-        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;
-        }
+        #endregion
+
+        #region Standard IVsSolutionEvents Methods - Minimal Implementation
 
-        // Standard IVsSolutionEvents methods - minimal implementation
         public int OnAfterOpenProject(IVsHierarchy pHierarchy, int fAdded)
         {
             return VSConstants.S_OK;
@@ -301,7 +434,10 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
             return VSConstants.S_OK;
         }
 
-        // IVsSolutionEvents3 additional methods
+        #endregion
+
+        #region IVsSolutionEvents3 Additional Methods
+
         public int OnBeforeOpeningChildren(IVsHierarchy pHierarchy)
         {
             return VSConstants.S_OK;
@@ -321,5 +457,7 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
         {
             return VSConstants.S_OK;
         }
+
+        #endregion
     }
 }

+ 87 - 26
FilteredSolutionsExtension/FilteredSolutionsExtension/SolutionParser.cs

@@ -18,9 +18,13 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
             RegexOptions.Compiled | RegexOptions.IgnoreCase);
 
         private static readonly Regex ProjectReferenceRegex = new Regex(
-            @"<ProjectReference\s+Include=""([^""]+)""\s*>.*?(?:<Project>{([^}]+)}</Project>)?",
+            @"<ProjectReference\s+Include\s*=\s*[""']([^""']+)[""'][^>]*>(?:.*?<Project\s*>\s*\{([^}]+)\}\s*</Project>)?",
             RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline);
 
+        private static readonly Regex SimpleProjectReferenceRegex = new Regex(
+            @"<ProjectReference\s+Include\s*=\s*[""']([^""']+)[""']",
+            RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
         // Known project type GUIDs
         private static readonly HashSet<string> SolutionFolderTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
         {
@@ -139,7 +143,12 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
             
             var filteredSolutionPath = Path.Combine(tempDir, $"{solutionName}_filtered.sln");
             
-            ErrorHandler.LogInfo($"Creating filtered solution: {Path.GetFileName(filteredSolutionPath)} with {selectedProjects.Count} projects");
+            return await CreateFilteredSolutionWithRemappingAsync(originalSolutionPath, selectedProjects, filteredSolutionPath);
+        }
+
+        public async Task<string> CreateFilteredSolutionWithRemappingAsync(string originalSolutionPath, List<ProjectNode> selectedProjects, string targetSolutionPath)
+        {
+            ErrorHandler.LogInfo($"Creating filtered solution: {Path.GetFileName(targetSolutionPath)} with {selectedProjects.Count} projects");
             
             // Convert ProjectNode to ProjectInfo for generator
             var projectInfos = selectedProjects.Select(pn => new ProjectInfo
@@ -150,11 +159,12 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
                 TypeGuid = pn.TypeGuid
             }).ToList();
 
+            // Generate solution with path remapping
             var generator = new FilteredSolutionGenerator();
-            await generator.GenerateFilteredSolutionAsync(originalSolutionPath, filteredSolutionPath, projectInfos);
+            await generator.GenerateFilteredSolutionWithRemappingAsync(originalSolutionPath, targetSolutionPath, projectInfos);
             
-            ErrorHandler.LogInfo($"Successfully created filtered solution: {filteredSolutionPath}");
-            return filteredSolutionPath;
+            ErrorHandler.LogInfo($"Successfully created filtered solution: {targetSolutionPath}");
+            return targetSolutionPath;
         }
 
         private void ParseProjects(string content, List<ProjectNode> projects, string solutionPath)
@@ -262,48 +272,99 @@ namespace Quadarax.Application.Tools.Vsix.FilteredSolutionsExtension
                 {
                     if (File.Exists(project.Path))
                     {
+                        ErrorHandler.LogInfo($"Parsing project file: {project.Path}");
                         var projectContent = await FileUtils.ReadAllTextAsync(project.Path);
+                        
+                        // Log a sample of the project content for debugging
+                        var contentPreview = projectContent.Length > 500 ? projectContent.Substring(0, 500) + "..." : projectContent;
+                        ErrorHandler.LogInfo($"Project content preview: {contentPreview}");
+                        
                         var referenceMatches = ProjectReferenceRegex.Matches(projectContent);
+                        ErrorHandler.LogInfo($"Found {referenceMatches.Count} ProjectReference matches in {project.Name}");
 
-                        foreach (Match match in referenceMatches)
+                        // If no matches with complex regex, try simple one
+                        if (referenceMatches.Count == 0)
                         {
-                            var referencedProjectPath = match.Groups[1].Value;
-                            var referencedProjectGuid = match.Groups[2].Success ? match.Groups[2].Value : null;
-
-                            // If no GUID in reference, try to find it by path
-                            if (string.IsNullOrEmpty(referencedProjectGuid))
+                            var simpleMatches = SimpleProjectReferenceRegex.Matches(projectContent);
+                            ErrorHandler.LogInfo($"Found {simpleMatches.Count} simple ProjectReference matches in {project.Name}");
+                            
+                            foreach (Match match in simpleMatches)
                             {
-                                var fullRefPath = Path.IsPathRooted(referencedProjectPath) 
-                                    ? referencedProjectPath 
-                                    : Path.GetFullPath(Path.Combine(Path.GetDirectoryName(project.Path), referencedProjectPath));
-
-                                var referencedProject = projects.FirstOrDefault(p => 
-                                    string.Equals(p.Path, fullRefPath, StringComparison.OrdinalIgnoreCase));
+                                var referencedProjectPath = match.Groups[1].Value;
+                                ErrorHandler.LogInfo($"  Simple ProjectReference: {referencedProjectPath}");
                                 
-                                if (referencedProject != null)
-                                {
-                                    referencedProjectGuid = referencedProject.ProjectGuid;
-                                }
+                                // Process simple matches
+                                ProcessProjectReference(project, referencedProjectPath, null, projects, projectDependencies, ref projectRefCount);
                             }
-
-                            if (!string.IsNullOrEmpty(referencedProjectGuid))
+                        }
+                        else
+                        {
+                            foreach (Match match in referenceMatches)
                             {
-                                AddDependency(projectDependencies, project.ProjectGuid, referencedProjectGuid);
-                                projectRefCount++;
+                                var referencedProjectPath = match.Groups[1].Value;
+                                var referencedProjectGuid = match.Groups[2].Success ? match.Groups[2].Value : null;
+                                
+                                ErrorHandler.LogInfo($"  ProjectReference: {referencedProjectPath}, GUID: {referencedProjectGuid ?? "not found"}");
+                                
+                                ProcessProjectReference(project, referencedProjectPath, referencedProjectGuid, projects, projectDependencies, ref projectRefCount);
                             }
                         }
                         processedProjects++;
                     }
+                    else
+                    {
+                        ErrorHandler.LogError($"Project file not found: {project.Path}", null);
+                    }
                 }
                 catch (Exception ex)
                 {
-                    ErrorHandler.LogError($"Warning: Could not parse project file {project.Path}", ex);
+                    ErrorHandler.LogError($"Error parsing project file {project.Path}", ex);
                 }
             }
 
             ErrorHandler.LogInfo($"Parsed {projectRefCount} project reference dependencies from {processedProjects} project files");
         }
 
+        private void ProcessProjectReference(ProjectNode project, string referencedProjectPath, string referencedProjectGuid, 
+            List<ProjectNode> projects, Dictionary<string, List<string>> projectDependencies, ref int projectRefCount)
+        {
+            // If no GUID in reference, try to find it by path
+            if (string.IsNullOrEmpty(referencedProjectGuid))
+            {
+                var fullRefPath = Path.IsPathRooted(referencedProjectPath) 
+                    ? referencedProjectPath 
+                    : Path.GetFullPath(Path.Combine(Path.GetDirectoryName(project.Path), referencedProjectPath));
+
+                ErrorHandler.LogInfo($"    Resolving path: {referencedProjectPath} -> {fullRefPath}");
+
+                var referencedProject = projects.FirstOrDefault(p => 
+                    string.Equals(p.Path, fullRefPath, StringComparison.OrdinalIgnoreCase));
+                
+                if (referencedProject != null)
+                {
+                    referencedProjectGuid = referencedProject.ProjectGuid;
+                    ErrorHandler.LogInfo($"    Found matching project: {referencedProject.Name} with GUID: {referencedProjectGuid}");
+                }
+                else
+                {
+                    ErrorHandler.LogInfo($"    No matching project found for path: {fullRefPath}");
+                    // List all available project paths for debugging
+                    ErrorHandler.LogInfo($"    Available projects:");
+                    foreach (var p in projects)
+                    {
+                        ErrorHandler.LogInfo($"      - {p.Name}: {p.Path}");
+                    }
+                }
+            }
+
+            if (!string.IsNullOrEmpty(referencedProjectGuid))
+            {
+                AddDependency(projectDependencies, project.ProjectGuid, referencedProjectGuid);
+                projectRefCount++;
+                ErrorHandler.LogInfo($"    Added dependency: {project.Name} -> {referencedProjectGuid}");
+            }
+        }
+
         private void AddDependency(Dictionary<string, List<string>> projectDependencies, string projectGuid, string dependentGuid)
         {
             if (string.IsNullOrEmpty(projectGuid) || string.IsNullOrEmpty(dependentGuid))