diff --git a/src/SharpIDE.Application/Features/SolutionDiscovery/SharpIdeFolder.cs b/src/SharpIDE.Application/Features/SolutionDiscovery/SharpIdeFolder.cs index c37a640..bea82a8 100644 --- a/src/SharpIDE.Application/Features/SolutionDiscovery/SharpIdeFolder.cs +++ b/src/SharpIDE.Application/Features/SolutionDiscovery/SharpIdeFolder.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; +using ObservableCollections; using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; namespace SharpIDE.Application.Features.SolutionDiscovery; @@ -9,8 +10,8 @@ public class SharpIdeFolder : ISharpIdeNode, IExpandableSharpIdeNode, IChildShar public required IExpandableSharpIdeNode Parent { get; set; } public required string Path { get; set; } public required string Name { get; set; } - public required List Files { get; set; } - public required List Folders { get; set; } + public ObservableHashSet Files { get; init; } + public ObservableHashSet Folders { get; init; } public bool Expanded { get; set; } [SetsRequiredMembers] @@ -19,8 +20,8 @@ public class SharpIdeFolder : ISharpIdeNode, IExpandableSharpIdeNode, IChildShar Parent = parent; Path = folderInfo.FullName; Name = folderInfo.Name; - Files = folderInfo.GetFiles(this, allFiles); - Folders = this.GetSubFolders(this, allFiles, allFolders); + Files = new ObservableHashSet(folderInfo.GetFiles(this, allFiles)); + Folders = new ObservableHashSet(this.GetSubFolders(this, allFiles, allFolders)); } public SharpIdeFolder() diff --git a/src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/SharpIdeModels.cs b/src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/SharpIdeModels.cs index 1e79dcd..5207188 100644 --- a/src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/SharpIdeModels.cs +++ b/src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/SharpIdeModels.cs @@ -35,8 +35,8 @@ public class SharpIdeSolutionModel : ISharpIdeNode, IExpandableSharpIdeNode public required string Name { get; set; } public required string FilePath { get; set; } public required string DirectoryPath { get; set; } - public required List Projects { get; set; } - public required List SlnFolders { get; set; } + public required ObservableHashSet Projects { get; set; } + public required ObservableHashSet SlnFolders { get; set; } public required HashSet AllProjects { get; set; } public required HashSet AllFiles { get; set; } public required HashSet AllFolders { get; set; } @@ -52,8 +52,8 @@ public class SharpIdeSolutionModel : ISharpIdeNode, IExpandableSharpIdeNode Name = solutionName; FilePath = solutionFilePath; DirectoryPath = Path.GetDirectoryName(solutionFilePath)!; - Projects = intermediateModel.Projects.Select(s => new SharpIdeProjectModel(s, allProjects, allFiles, allFolders, this)).ToList(); - SlnFolders = intermediateModel.SolutionFolders.Select(s => new SharpIdeSolutionFolder(s, allProjects, allFiles, allFolders, this)).ToList(); + Projects = new ObservableHashSet(intermediateModel.Projects.Select(s => new SharpIdeProjectModel(s, allProjects, allFiles, allFolders, this))); + SlnFolders = new ObservableHashSet(intermediateModel.SolutionFolders.Select(s => new SharpIdeSolutionFolder(s, allProjects, allFiles, allFolders, this))); AllProjects = allProjects.ToHashSet(); AllFiles = allFiles.ToHashSet(); AllFolders = allFolders.ToHashSet(); @@ -62,9 +62,9 @@ public class SharpIdeSolutionModel : ISharpIdeNode, IExpandableSharpIdeNode public class SharpIdeSolutionFolder : ISharpIdeNode, IExpandableSharpIdeNode, IChildSharpIdeNode { public required string Name { get; set; } - public required List Folders { get; set; } - public required List Projects { get; set; } - public required List Files { get; set; } + public required ObservableHashSet Folders { get; set; } + public required ObservableHashSet Projects { get; set; } + public required ObservableHashSet Files { get; set; } public bool Expanded { get; set; } public required IExpandableSharpIdeNode Parent { get; set; } @@ -73,17 +73,17 @@ public class SharpIdeSolutionFolder : ISharpIdeNode, IExpandableSharpIdeNode, IC { Name = intermediateModel.Model.Name; Parent = parent; - Files = intermediateModel.Files.Select(s => new SharpIdeFile(s.FullPath, s.Name, this, allFiles)).ToList(); - Folders = intermediateModel.Folders.Select(x => new SharpIdeSolutionFolder(x, allProjects, allFiles, allFolders, this)).ToList(); - Projects = intermediateModel.Projects.Select(x => new SharpIdeProjectModel(x, allProjects, allFiles, allFolders, this)).ToList(); + Files = new ObservableHashSet(intermediateModel.Files.Select(s => new SharpIdeFile(s.FullPath, s.Name, this, allFiles))); + Folders = new ObservableHashSet(intermediateModel.Folders.Select(x => new SharpIdeSolutionFolder(x, allProjects, allFiles, allFolders, this))); + Projects = new ObservableHashSet(intermediateModel.Projects.Select(x => new SharpIdeProjectModel(x, allProjects, allFiles, allFolders, this))); } } public class SharpIdeProjectModel : ISharpIdeNode, IExpandableSharpIdeNode, IChildSharpIdeNode { public required string Name { get; set; } public required string FilePath { get; set; } - public required List Folders { get; set; } - public required List Files { get; set; } + public required ObservableHashSet Folders { get; set; } + public required ObservableHashSet Files { get; set; } public bool Expanded { get; set; } public required IExpandableSharpIdeNode Parent { get; set; } public bool Running { get; set; } @@ -96,8 +96,8 @@ public class SharpIdeProjectModel : ISharpIdeNode, IExpandableSharpIdeNode, IChi Parent = parent; Name = projectModel.Model.ActualDisplayName; FilePath = projectModel.FullFilePath; - Files = TreeMapperV2.GetFiles(projectModel.FullFilePath, this, allFiles); - Folders = TreeMapperV2.GetSubFolders(projectModel.FullFilePath, this, allFiles, allFolders); + Files = new ObservableHashSet(TreeMapperV2.GetFiles(projectModel.FullFilePath, this, allFiles)); + Folders = new ObservableHashSet(TreeMapperV2.GetSubFolders(projectModel.FullFilePath, this, allFiles, allFolders)); MsBuildEvaluationProjectTask = ProjectEvaluation.GetProject(projectModel.FullFilePath); allProjects.Add(this); } diff --git a/src/SharpIDE.Godot/Features/Common/TreeItemContainer.cs b/src/SharpIDE.Godot/Features/Common/TreeItemContainer.cs new file mode 100644 index 0000000..5e77335 --- /dev/null +++ b/src/SharpIDE.Godot/Features/Common/TreeItemContainer.cs @@ -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 WithInitialPopulation(this ObservableHashSet hashSet, Action> func) where T : class + { + foreach (var existing in hashSet) + { + var viewChangedEvent = new ViewChangedEvent(NotifyCollectionChangedAction.Add, (existing, new TreeItemContainer()), (null!, null!), 0, 0, new SortOperation()); + func(viewChangedEvent); + } + return hashSet; + } +} \ No newline at end of file diff --git a/src/SharpIDE.Godot/Features/Problems/ProblemsPanel.cs b/src/SharpIDE.Godot/Features/Problems/ProblemsPanel.cs index a68c022..ee93d5b 100644 --- a/src/SharpIDE.Godot/Features/Problems/ProblemsPanel.cs +++ b/src/SharpIDE.Godot/Features/Problems/ProblemsPanel.cs @@ -5,6 +5,7 @@ using ObservableCollections; using R3; using SharpIDE.Application.Features.SolutionDiscovery; using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; +using SharpIDE.Godot.Features.Common; namespace SharpIDE.Godot.Features.Problems; @@ -41,10 +42,7 @@ public partial class ProblemsPanel : Control BindToTree(_projects); } - private class TreeItemContainer - { - public TreeItem? Value { get; set; } - } + public void BindToTree(ObservableHashSet list) { var view = list.CreateView(y => new TreeItemContainer()); diff --git a/src/SharpIDE.Godot/Features/SolutionExplorer/SolutionExplorerPanel.cs b/src/SharpIDE.Godot/Features/SolutionExplorer/SolutionExplorerPanel.cs index b0a004f..4745d95 100644 --- a/src/SharpIDE.Godot/Features/SolutionExplorer/SolutionExplorerPanel.cs +++ b/src/SharpIDE.Godot/Features/SolutionExplorer/SolutionExplorerPanel.cs @@ -1,8 +1,12 @@ +using System.Collections.Specialized; using Ardalis.GuardClauses; using Godot; +using ObservableCollections; +using R3; using SharpIDE.Application.Features.Analysis; using SharpIDE.Application.Features.SolutionDiscovery; using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; +using SharpIDE.Godot.Features.Common; using SharpIDE.Godot.Features.Problems; namespace SharpIDE.Godot.Features.SolutionExplorer; @@ -22,6 +26,7 @@ public partial class SolutionExplorerPanel : MarginContainer public SharpIdeSolutionModel SolutionModel { get; set; } = null!; private Tree _tree = null!; + private TreeItem _rootItem = null!; public override void _Ready() { _tree = GetNode("Tree"); @@ -86,97 +91,170 @@ public partial class SolutionExplorerPanel : MarginContainer return null; } - public void RepopulateTree() + public void BindToSolution() => BindToSolution(SolutionModel); + [RequiresGodotUiThread] + public void BindToSolution(SharpIdeSolutionModel solution) { - _tree.Clear(); + _tree.Clear(); - var rootItem = _tree.CreateItem(); - rootItem.SetText(0, SolutionModel.Name); - rootItem.SetIcon(0, SlnIcon); + // Root + var rootItem = _tree.CreateItem(); + rootItem.SetText(0, solution.Name); + rootItem.SetIcon(0, SlnIcon); + _rootItem = rootItem; - // Add projects directly under solution - foreach (var project in SolutionModel.Projects) - { - AddProjectToTree(rootItem, project); - } + // Observe Projects + var projectsView = solution.Projects + .WithInitialPopulation(s => CreateProjectTreeItem(_tree, _rootItem, s)) + .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 - foreach (var folder in SolutionModel.SlnFolders) - { - AddSlnFolderToTree(rootItem, folder); - } - rootItem.SetCollapsedRecursive(true); - rootItem.Collapsed = false; + // Observe Solution Folders + var foldersView = solution.SlnFolders + .WithInitialPopulation(s => CreateSlnFolderTreeItem(_tree, _rootItem, s)) + .CreateView(y => new TreeItemContainer()); + foldersView.ObserveChanged() + .SubscribeAwait(async (e, ct) => await (e.Action switch + { + 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 e) { - var folderItem = _tree.CreateItem(parent); - folderItem.SetText(0, folder.Name); - folderItem.SetIcon(0, SlnFolderIcon); - var container = new RefCountedContainer(folder); - folderItem.SetMetadata(0, container); + var folderItem = tree.CreateItem(parent); + folderItem.SetText(0, e.NewItem.Value.Name); + folderItem.SetIcon(0, SlnFolderIcon); + folderItem.SetMetadata(0, new RefCountedContainer(e.NewItem.Value)); + e.NewItem.View.Value = folderItem; - foreach (var project in folder.Projects) - { - AddProjectToTree(folderItem, project); - } + // Observe folder sub-collections + var subFoldersView = e.NewItem.Value.Folders + .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) - { - AddSlnFolderToTree(folderItem, subFolder); // recursion - } + var projectsView = e.NewItem.Value.Projects + .WithInitialPopulation(s => CreateProjectTreeItem(_tree, folderItem, s)) + .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) - { - AddFileToTree(folderItem, sharpIdeFile); - } + var filesView = e.NewItem.Value.Files + .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 AddProjectToTree(TreeItem parent, SharpIdeProjectModel project) + [RequiresGodotUiThread] + private void CreateProjectTreeItem(Tree tree, TreeItem parent, ViewChangedEvent e) { - var projectItem = _tree.CreateItem(parent); - projectItem.SetText(0, project.Name); + var projectItem = tree.CreateItem(parent); + projectItem.SetText(0, e.NewItem.Value.Name); projectItem.SetIcon(0, CsprojIcon); - var container = new RefCountedContainer(project); - projectItem.SetMetadata(0, container); + projectItem.SetMetadata(0, new RefCountedContainer(e.NewItem.Value)); + e.NewItem.View.Value = projectItem; - foreach (var sharpIdeFolder in project.Folders) - { - AddFolderToTree(projectItem, sharpIdeFolder); - } + // Observe project folders + var foldersView = e.NewItem.Value.Folders + .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) - { - AddFileToTree(projectItem, file); - } + // Observe project files + var filesView = e.NewItem.Value.Files + .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 e) { - var folderItem = _tree.CreateItem(projectItem); - folderItem.SetText(0, sharpIdeFolder.Name); + var folderItem = tree.CreateItem(parent); + folderItem.SetText(0, e.NewItem.Value.Name); folderItem.SetIcon(0, FolderIcon); - var container = new RefCountedContainer(sharpIdeFolder); - folderItem.SetMetadata(0, container); + folderItem.SetMetadata(0, new RefCountedContainer(e.NewItem.Value)); + e.NewItem.View.Value = folderItem; - foreach (var subFolder in sharpIdeFolder.Folders) - { - AddFolderToTree(folderItem, subFolder); // recursion - } + // Observe subfolders + var subFoldersView = e.NewItem.Value.Folders + .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) - { - AddFileToTree(folderItem, file); - } + // Observe files + var filesView = e.NewItem.Value.Files + .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 e) { - var fileItem = _tree.CreateItem(parent); - fileItem.SetText(0, file.Name); + var fileItem = tree.CreateItem(parent); + fileItem.SetText(0, e.NewItem.Value.Name); fileItem.SetIcon(0, CsharpFileIcon); - var container = new RefCountedContainer(file); - fileItem.SetMetadata(0, container); + fileItem.SetMetadata(0, new RefCountedContainer(e.NewItem.Value)); + e.NewItem.View.Value = fileItem; + } + + private async Task FreeTreeItem(TreeItem? item) + { + await this.InvokeAsync(() => item?.Free()); } } diff --git a/src/SharpIDE.Godot/IdeRoot.cs b/src/SharpIDE.Godot/IdeRoot.cs index 5239330..fce0a74 100644 --- a/src/SharpIDE.Godot/IdeRoot.cs +++ b/src/SharpIDE.Godot/IdeRoot.cs @@ -141,7 +141,7 @@ public partial class IdeRoot : Control _searchAllFilesWindow.Solution = solutionModel; _fileExternalChangeHandler.SolutionModel = solutionModel; _fileChangedService.SolutionModel = solutionModel; - Callable.From(_solutionExplorerPanel.RepopulateTree).CallDeferred(); + Callable.From(_solutionExplorerPanel.BindToSolution).CallDeferred(); _roslynAnalysis.StartSolutionAnalysis(solutionModel); _fileWatcher.StartWatching(solutionModel);