Use observable collections for solution model

This commit is contained in:
Matt Parker
2025-10-19 21:48:12 +10:00
parent b180f82b1f
commit 45df81afe6
6 changed files with 190 additions and 89 deletions

View File

@@ -1,5 +1,6 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using ObservableCollections;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
namespace SharpIDE.Application.Features.SolutionDiscovery; namespace SharpIDE.Application.Features.SolutionDiscovery;
@@ -9,8 +10,8 @@ public class SharpIdeFolder : ISharpIdeNode, IExpandableSharpIdeNode, IChildShar
public required IExpandableSharpIdeNode Parent { get; set; } public required IExpandableSharpIdeNode Parent { get; set; }
public required string Path { get; set; } public required string Path { get; set; }
public required string Name { get; set; } public required string Name { get; set; }
public required List<SharpIdeFile> Files { get; set; } public ObservableHashSet<SharpIdeFile> Files { get; init; }
public required List<SharpIdeFolder> Folders { get; set; } public ObservableHashSet<SharpIdeFolder> Folders { get; init; }
public bool Expanded { get; set; } public bool Expanded { get; set; }
[SetsRequiredMembers] [SetsRequiredMembers]
@@ -19,8 +20,8 @@ public class SharpIdeFolder : ISharpIdeNode, IExpandableSharpIdeNode, IChildShar
Parent = parent; Parent = parent;
Path = folderInfo.FullName; Path = folderInfo.FullName;
Name = folderInfo.Name; Name = folderInfo.Name;
Files = folderInfo.GetFiles(this, allFiles); Files = new ObservableHashSet<SharpIdeFile>(folderInfo.GetFiles(this, allFiles));
Folders = this.GetSubFolders(this, allFiles, allFolders); Folders = new ObservableHashSet<SharpIdeFolder>(this.GetSubFolders(this, allFiles, allFolders));
} }
public SharpIdeFolder() public SharpIdeFolder()

View File

@@ -35,8 +35,8 @@ public class SharpIdeSolutionModel : ISharpIdeNode, IExpandableSharpIdeNode
public required string Name { get; set; } public required string Name { get; set; }
public required string FilePath { get; set; } public required string FilePath { get; set; }
public required string DirectoryPath { get; set; } public required string DirectoryPath { get; set; }
public required List<SharpIdeProjectModel> Projects { get; set; } public required ObservableHashSet<SharpIdeProjectModel> Projects { get; set; }
public required List<SharpIdeSolutionFolder> SlnFolders { get; set; } public required ObservableHashSet<SharpIdeSolutionFolder> SlnFolders { get; set; }
public required HashSet<SharpIdeProjectModel> AllProjects { get; set; } public required HashSet<SharpIdeProjectModel> AllProjects { get; set; }
public required HashSet<SharpIdeFile> AllFiles { get; set; } public required HashSet<SharpIdeFile> AllFiles { get; set; }
public required HashSet<SharpIdeFolder> AllFolders { get; set; } public required HashSet<SharpIdeFolder> AllFolders { get; set; }
@@ -52,8 +52,8 @@ public class SharpIdeSolutionModel : ISharpIdeNode, IExpandableSharpIdeNode
Name = solutionName; Name = solutionName;
FilePath = solutionFilePath; FilePath = solutionFilePath;
DirectoryPath = Path.GetDirectoryName(solutionFilePath)!; DirectoryPath = Path.GetDirectoryName(solutionFilePath)!;
Projects = intermediateModel.Projects.Select(s => new SharpIdeProjectModel(s, allProjects, allFiles, allFolders, this)).ToList(); Projects = new ObservableHashSet<SharpIdeProjectModel>(intermediateModel.Projects.Select(s => new SharpIdeProjectModel(s, allProjects, allFiles, allFolders, this)));
SlnFolders = intermediateModel.SolutionFolders.Select(s => new SharpIdeSolutionFolder(s, allProjects, allFiles, allFolders, this)).ToList(); SlnFolders = new ObservableHashSet<SharpIdeSolutionFolder>(intermediateModel.SolutionFolders.Select(s => new SharpIdeSolutionFolder(s, allProjects, allFiles, allFolders, this)));
AllProjects = allProjects.ToHashSet(); AllProjects = allProjects.ToHashSet();
AllFiles = allFiles.ToHashSet(); AllFiles = allFiles.ToHashSet();
AllFolders = allFolders.ToHashSet(); AllFolders = allFolders.ToHashSet();
@@ -62,9 +62,9 @@ public class SharpIdeSolutionModel : ISharpIdeNode, IExpandableSharpIdeNode
public class SharpIdeSolutionFolder : ISharpIdeNode, IExpandableSharpIdeNode, IChildSharpIdeNode public class SharpIdeSolutionFolder : ISharpIdeNode, IExpandableSharpIdeNode, IChildSharpIdeNode
{ {
public required string Name { get; set; } public required string Name { get; set; }
public required List<SharpIdeSolutionFolder> Folders { get; set; } public required ObservableHashSet<SharpIdeSolutionFolder> Folders { get; set; }
public required List<SharpIdeProjectModel> Projects { get; set; } public required ObservableHashSet<SharpIdeProjectModel> Projects { get; set; }
public required List<SharpIdeFile> Files { get; set; } public required ObservableHashSet<SharpIdeFile> Files { get; set; }
public bool Expanded { get; set; } public bool Expanded { get; set; }
public required IExpandableSharpIdeNode Parent { get; set; } public required IExpandableSharpIdeNode Parent { get; set; }
@@ -73,17 +73,17 @@ public class SharpIdeSolutionFolder : ISharpIdeNode, IExpandableSharpIdeNode, IC
{ {
Name = intermediateModel.Model.Name; Name = intermediateModel.Model.Name;
Parent = parent; Parent = parent;
Files = intermediateModel.Files.Select(s => new SharpIdeFile(s.FullPath, s.Name, this, allFiles)).ToList(); Files = new ObservableHashSet<SharpIdeFile>(intermediateModel.Files.Select(s => new SharpIdeFile(s.FullPath, s.Name, this, allFiles)));
Folders = intermediateModel.Folders.Select(x => new SharpIdeSolutionFolder(x, allProjects, allFiles, allFolders, this)).ToList(); Folders = new ObservableHashSet<SharpIdeSolutionFolder>(intermediateModel.Folders.Select(x => new SharpIdeSolutionFolder(x, allProjects, allFiles, allFolders, this)));
Projects = intermediateModel.Projects.Select(x => new SharpIdeProjectModel(x, allProjects, allFiles, allFolders, this)).ToList(); Projects = new ObservableHashSet<SharpIdeProjectModel>(intermediateModel.Projects.Select(x => new SharpIdeProjectModel(x, allProjects, allFiles, allFolders, this)));
} }
} }
public class SharpIdeProjectModel : ISharpIdeNode, IExpandableSharpIdeNode, IChildSharpIdeNode public class SharpIdeProjectModel : ISharpIdeNode, IExpandableSharpIdeNode, IChildSharpIdeNode
{ {
public required string Name { get; set; } public required string Name { get; set; }
public required string FilePath { get; set; } public required string FilePath { get; set; }
public required List<SharpIdeFolder> Folders { get; set; } public required ObservableHashSet<SharpIdeFolder> Folders { get; set; }
public required List<SharpIdeFile> Files { get; set; } public required ObservableHashSet<SharpIdeFile> Files { get; set; }
public bool Expanded { get; set; } public bool Expanded { get; set; }
public required IExpandableSharpIdeNode Parent { get; set; } public required IExpandableSharpIdeNode Parent { get; set; }
public bool Running { get; set; } public bool Running { get; set; }
@@ -96,8 +96,8 @@ public class SharpIdeProjectModel : ISharpIdeNode, IExpandableSharpIdeNode, IChi
Parent = parent; Parent = parent;
Name = projectModel.Model.ActualDisplayName; Name = projectModel.Model.ActualDisplayName;
FilePath = projectModel.FullFilePath; FilePath = projectModel.FullFilePath;
Files = TreeMapperV2.GetFiles(projectModel.FullFilePath, this, allFiles); Files = new ObservableHashSet<SharpIdeFile>(TreeMapperV2.GetFiles(projectModel.FullFilePath, this, allFiles));
Folders = TreeMapperV2.GetSubFolders(projectModel.FullFilePath, this, allFiles, allFolders); Folders = new ObservableHashSet<SharpIdeFolder>(TreeMapperV2.GetSubFolders(projectModel.FullFilePath, this, allFiles, allFolders));
MsBuildEvaluationProjectTask = ProjectEvaluation.GetProject(projectModel.FullFilePath); MsBuildEvaluationProjectTask = ProjectEvaluation.GetProject(projectModel.FullFilePath);
allProjects.Add(this); allProjects.Add(this);
} }

View File

@@ -0,0 +1,24 @@
using System.Collections.Specialized;
using Godot;
using ObservableCollections;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
namespace SharpIDE.Godot.Features.Common;
public class TreeItemContainer
{
public TreeItem? Value { get; set; }
}
public static class ObservableTreeExtensions
{
public static ObservableHashSet<T> WithInitialPopulation<T>(this ObservableHashSet<T> hashSet, Action<ViewChangedEvent<T, TreeItemContainer>> func) where T : class
{
foreach (var existing in hashSet)
{
var viewChangedEvent = new ViewChangedEvent<T, TreeItemContainer>(NotifyCollectionChangedAction.Add, (existing, new TreeItemContainer()), (null!, null!), 0, 0, new SortOperation<T>());
func(viewChangedEvent);
}
return hashSet;
}
}

View File

@@ -5,6 +5,7 @@ using ObservableCollections;
using R3; using R3;
using SharpIDE.Application.Features.SolutionDiscovery; using SharpIDE.Application.Features.SolutionDiscovery;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
using SharpIDE.Godot.Features.Common;
namespace SharpIDE.Godot.Features.Problems; namespace SharpIDE.Godot.Features.Problems;
@@ -41,10 +42,7 @@ public partial class ProblemsPanel : Control
BindToTree(_projects); BindToTree(_projects);
} }
private class TreeItemContainer
{
public TreeItem? Value { get; set; }
}
public void BindToTree(ObservableHashSet<SharpIdeProjectModel> list) public void BindToTree(ObservableHashSet<SharpIdeProjectModel> list)
{ {
var view = list.CreateView(y => new TreeItemContainer()); var view = list.CreateView(y => new TreeItemContainer());

View File

@@ -1,8 +1,12 @@
using System.Collections.Specialized;
using Ardalis.GuardClauses; using Ardalis.GuardClauses;
using Godot; using Godot;
using ObservableCollections;
using R3;
using SharpIDE.Application.Features.Analysis; using SharpIDE.Application.Features.Analysis;
using SharpIDE.Application.Features.SolutionDiscovery; using SharpIDE.Application.Features.SolutionDiscovery;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
using SharpIDE.Godot.Features.Common;
using SharpIDE.Godot.Features.Problems; using SharpIDE.Godot.Features.Problems;
namespace SharpIDE.Godot.Features.SolutionExplorer; namespace SharpIDE.Godot.Features.SolutionExplorer;
@@ -22,6 +26,7 @@ public partial class SolutionExplorerPanel : MarginContainer
public SharpIdeSolutionModel SolutionModel { get; set; } = null!; public SharpIdeSolutionModel SolutionModel { get; set; } = null!;
private Tree _tree = null!; private Tree _tree = null!;
private TreeItem _rootItem = null!;
public override void _Ready() public override void _Ready()
{ {
_tree = GetNode<Tree>("Tree"); _tree = GetNode<Tree>("Tree");
@@ -86,97 +91,170 @@ public partial class SolutionExplorerPanel : MarginContainer
return null; return null;
} }
public void RepopulateTree() public void BindToSolution() => BindToSolution(SolutionModel);
[RequiresGodotUiThread]
public void BindToSolution(SharpIdeSolutionModel solution)
{ {
_tree.Clear(); _tree.Clear();
var rootItem = _tree.CreateItem(); // Root
rootItem.SetText(0, SolutionModel.Name); var rootItem = _tree.CreateItem();
rootItem.SetIcon(0, SlnIcon); rootItem.SetText(0, solution.Name);
rootItem.SetIcon(0, SlnIcon);
_rootItem = rootItem;
// Add projects directly under solution // Observe Projects
foreach (var project in SolutionModel.Projects) var projectsView = solution.Projects
{ .WithInitialPopulation(s => CreateProjectTreeItem(_tree, _rootItem, s))
AddProjectToTree(rootItem, project); .CreateView(y => new TreeItemContainer());
} projectsView.ObserveChanged()
.SubscribeAwait(async (e, ct) => await (e.Action switch
{
NotifyCollectionChangedAction.Add => this.InvokeAsync(() => CreateProjectTreeItem(_tree, _rootItem, e)),
NotifyCollectionChangedAction.Remove => FreeTreeItem(e.OldItem.View.Value),
_ => Task.CompletedTask
})).AddTo(this);
// Add folders under solution // Observe Solution Folders
foreach (var folder in SolutionModel.SlnFolders) var foldersView = solution.SlnFolders
{ .WithInitialPopulation(s => CreateSlnFolderTreeItem(_tree, _rootItem, s))
AddSlnFolderToTree(rootItem, folder); .CreateView(y => new TreeItemContainer());
} foldersView.ObserveChanged()
rootItem.SetCollapsedRecursive(true); .SubscribeAwait(async (e, ct) => await (e.Action switch
rootItem.Collapsed = false; {
NotifyCollectionChangedAction.Add => this.InvokeAsync(() => CreateSlnFolderTreeItem(_tree, _rootItem, e)),
NotifyCollectionChangedAction.Remove => FreeTreeItem(e.OldItem.View.Value),
_ => Task.CompletedTask
})).AddTo(this);
rootItem.SetCollapsedRecursive(true);
rootItem.Collapsed = false;
} }
private void AddSlnFolderToTree(TreeItem parent, SharpIdeSolutionFolder folder) [RequiresGodotUiThread]
private void CreateSlnFolderTreeItem(Tree tree, TreeItem parent, ViewChangedEvent<SharpIdeSolutionFolder, TreeItemContainer> e)
{ {
var folderItem = _tree.CreateItem(parent); var folderItem = tree.CreateItem(parent);
folderItem.SetText(0, folder.Name); folderItem.SetText(0, e.NewItem.Value.Name);
folderItem.SetIcon(0, SlnFolderIcon); folderItem.SetIcon(0, SlnFolderIcon);
var container = new RefCountedContainer<SharpIdeSolutionFolder>(folder); folderItem.SetMetadata(0, new RefCountedContainer<SharpIdeSolutionFolder>(e.NewItem.Value));
folderItem.SetMetadata(0, container); e.NewItem.View.Value = folderItem;
foreach (var project in folder.Projects) // Observe folder sub-collections
{ var subFoldersView = e.NewItem.Value.Folders
AddProjectToTree(folderItem, project); .WithInitialPopulation(s => CreateSlnFolderTreeItem(_tree, folderItem, s))
} .CreateView(y => new TreeItemContainer());
subFoldersView.ObserveChanged()
.SubscribeAwait(async (innerEvent, ct) => await (innerEvent.Action switch
{
NotifyCollectionChangedAction.Add => this.InvokeAsync(() => CreateSlnFolderTreeItem(_tree, folderItem, innerEvent)),
NotifyCollectionChangedAction.Remove => FreeTreeItem(innerEvent.OldItem.View.Value),
_ => Task.CompletedTask
})).AddTo(this);
foreach (var subFolder in folder.Folders) var projectsView = e.NewItem.Value.Projects
{ .WithInitialPopulation(s => CreateProjectTreeItem(_tree, folderItem, s))
AddSlnFolderToTree(folderItem, subFolder); // recursion .CreateView(y => new TreeItemContainer());
} projectsView.ObserveChanged()
.SubscribeAwait(async (innerEvent, ct) => await (innerEvent.Action switch
{
NotifyCollectionChangedAction.Add => this.InvokeAsync(() => CreateProjectTreeItem(_tree, folderItem, innerEvent)),
NotifyCollectionChangedAction.Remove => FreeTreeItem(innerEvent.OldItem.View.Value),
_ => Task.CompletedTask
})).AddTo(this);
foreach (var sharpIdeFile in folder.Files) var filesView = e.NewItem.Value.Files
{ .WithInitialPopulation(s => CreateFileTreeItem(_tree, folderItem, s))
AddFileToTree(folderItem, sharpIdeFile); .CreateView(y => new TreeItemContainer());
} filesView.ObserveChanged()
.SubscribeAwait(async (innerEvent, ct) => await (innerEvent.Action switch
{
NotifyCollectionChangedAction.Add => this.InvokeAsync(() => CreateFileTreeItem(_tree, folderItem, innerEvent)),
NotifyCollectionChangedAction.Remove => FreeTreeItem(innerEvent.OldItem.View.Value),
_ => Task.CompletedTask
})).AddTo(this);
} }
private void AddProjectToTree(TreeItem parent, SharpIdeProjectModel project) [RequiresGodotUiThread]
private void CreateProjectTreeItem(Tree tree, TreeItem parent, ViewChangedEvent<SharpIdeProjectModel, TreeItemContainer> e)
{ {
var projectItem = _tree.CreateItem(parent); var projectItem = tree.CreateItem(parent);
projectItem.SetText(0, project.Name); projectItem.SetText(0, e.NewItem.Value.Name);
projectItem.SetIcon(0, CsprojIcon); projectItem.SetIcon(0, CsprojIcon);
var container = new RefCountedContainer<SharpIdeProjectModel>(project); projectItem.SetMetadata(0, new RefCountedContainer<SharpIdeProjectModel>(e.NewItem.Value));
projectItem.SetMetadata(0, container); e.NewItem.View.Value = projectItem;
foreach (var sharpIdeFolder in project.Folders) // Observe project folders
{ var foldersView = e.NewItem.Value.Folders
AddFolderToTree(projectItem, sharpIdeFolder); .WithInitialPopulation(s => CreateFolderTreeItem(_tree, projectItem, s))
} .CreateView(y => new TreeItemContainer());
foldersView.ObserveChanged()
.SubscribeAwait(async (innerEvent, ct) => await (innerEvent.Action switch
{
NotifyCollectionChangedAction.Add => this.InvokeAsync(() => CreateFolderTreeItem(_tree, projectItem, innerEvent)),
NotifyCollectionChangedAction.Remove => FreeTreeItem(innerEvent.OldItem.View.Value),
_ => Task.CompletedTask
})).AddTo(this);
foreach (var file in project.Files) // Observe project files
{ var filesView = e.NewItem.Value.Files
AddFileToTree(projectItem, file); .WithInitialPopulation(s => CreateFileTreeItem(_tree, projectItem, s))
} .CreateView(y => new TreeItemContainer());
filesView.ObserveChanged()
.SubscribeAwait(async (innerEvent, ct) => await (innerEvent.Action switch
{
NotifyCollectionChangedAction.Add => this.InvokeAsync(() => CreateFileTreeItem(_tree, projectItem, innerEvent)),
NotifyCollectionChangedAction.Remove => FreeTreeItem(innerEvent.OldItem.View.Value),
_ => Task.CompletedTask
})).AddTo(this);
} }
private void AddFolderToTree(TreeItem projectItem, SharpIdeFolder sharpIdeFolder) [RequiresGodotUiThread]
private void CreateFolderTreeItem(Tree tree, TreeItem parent, ViewChangedEvent<SharpIdeFolder, TreeItemContainer> e)
{ {
var folderItem = _tree.CreateItem(projectItem); var folderItem = tree.CreateItem(parent);
folderItem.SetText(0, sharpIdeFolder.Name); folderItem.SetText(0, e.NewItem.Value.Name);
folderItem.SetIcon(0, FolderIcon); folderItem.SetIcon(0, FolderIcon);
var container = new RefCountedContainer<SharpIdeFolder>(sharpIdeFolder); folderItem.SetMetadata(0, new RefCountedContainer<SharpIdeFolder>(e.NewItem.Value));
folderItem.SetMetadata(0, container); e.NewItem.View.Value = folderItem;
foreach (var subFolder in sharpIdeFolder.Folders) // Observe subfolders
{ var subFoldersView = e.NewItem.Value.Folders
AddFolderToTree(folderItem, subFolder); // recursion .WithInitialPopulation(s => CreateFolderTreeItem(_tree, folderItem, s))
} .CreateView(y => new TreeItemContainer());
subFoldersView.ObserveChanged()
.SubscribeAwait(async (innerEvent, ct) => await (innerEvent.Action switch
{
NotifyCollectionChangedAction.Add => this.InvokeAsync(() => CreateFolderTreeItem(_tree, folderItem, innerEvent)),
NotifyCollectionChangedAction.Remove => FreeTreeItem(innerEvent.OldItem.View.Value),
_ => Task.CompletedTask
})).AddTo(this);
foreach (var file in sharpIdeFolder.Files) // Observe files
{ var filesView = e.NewItem.Value.Files
AddFileToTree(folderItem, file); .WithInitialPopulation(s => CreateFileTreeItem(_tree, folderItem, s))
} .CreateView(y => new TreeItemContainer());
filesView.ObserveChanged()
.SubscribeAwait(async (innerEvent, ct) => await (innerEvent.Action switch
{
NotifyCollectionChangedAction.Add => this.InvokeAsync(() => CreateFileTreeItem(_tree, folderItem, innerEvent)),
NotifyCollectionChangedAction.Remove => FreeTreeItem(innerEvent.OldItem.View.Value),
_ => Task.CompletedTask
})).AddTo(this);
} }
private void AddFileToTree(TreeItem parent, SharpIdeFile file) [RequiresGodotUiThread]
private void CreateFileTreeItem(Tree tree, TreeItem parent, ViewChangedEvent<SharpIdeFile, TreeItemContainer> e)
{ {
var fileItem = _tree.CreateItem(parent); var fileItem = tree.CreateItem(parent);
fileItem.SetText(0, file.Name); fileItem.SetText(0, e.NewItem.Value.Name);
fileItem.SetIcon(0, CsharpFileIcon); fileItem.SetIcon(0, CsharpFileIcon);
var container = new RefCountedContainer<SharpIdeFile>(file); fileItem.SetMetadata(0, new RefCountedContainer<SharpIdeFile>(e.NewItem.Value));
fileItem.SetMetadata(0, container); e.NewItem.View.Value = fileItem;
}
private async Task FreeTreeItem(TreeItem? item)
{
await this.InvokeAsync(() => item?.Free());
} }
} }

View File

@@ -141,7 +141,7 @@ public partial class IdeRoot : Control
_searchAllFilesWindow.Solution = solutionModel; _searchAllFilesWindow.Solution = solutionModel;
_fileExternalChangeHandler.SolutionModel = solutionModel; _fileExternalChangeHandler.SolutionModel = solutionModel;
_fileChangedService.SolutionModel = solutionModel; _fileChangedService.SolutionModel = solutionModel;
Callable.From(_solutionExplorerPanel.RepopulateTree).CallDeferred(); Callable.From(_solutionExplorerPanel.BindToSolution).CallDeferred();
_roslynAnalysis.StartSolutionAnalysis(solutionModel); _roslynAnalysis.StartSolutionAnalysis(solutionModel);
_fileWatcher.StartWatching(solutionModel); _fileWatcher.StartWatching(solutionModel);