populate sln explorer tree in background thread

This commit is contained in:
Matt Parker
2025-12-13 14:31:31 +10:00
parent a74553a4ca
commit d981013af3
5 changed files with 63 additions and 19 deletions

View File

@@ -119,7 +119,7 @@ public class SharpIdeProjectModel : ISharpIdeNode, IExpandableSharpIdeNode, IChi
DirectoryPath = Path.GetDirectoryName(projectModel.FullFilePath)!; DirectoryPath = Path.GetDirectoryName(projectModel.FullFilePath)!;
Files = new ObservableList<SharpIdeFile>(TreeMapperV2.GetFiles(projectModel.FullFilePath, this, allFiles)); Files = new ObservableList<SharpIdeFile>(TreeMapperV2.GetFiles(projectModel.FullFilePath, this, allFiles));
Folders = new ObservableList<SharpIdeFolder>(TreeMapperV2.GetSubFolders(projectModel.FullFilePath, this, allFiles, allFolders)); Folders = new ObservableList<SharpIdeFolder>(TreeMapperV2.GetSubFolders(projectModel.FullFilePath, this, allFiles, allFolders));
MsBuildEvaluationProjectTask = ProjectEvaluation.GetProject(projectModel.FullFilePath); MsBuildEvaluationProjectTask = Task.Run(() => ProjectEvaluation.GetProject(projectModel.FullFilePath));
allProjects.Add(this); allProjects.Add(this);
} }

View File

@@ -3,6 +3,7 @@ using Ardalis.GuardClauses;
using Godot; using Godot;
using ObservableCollections; using ObservableCollections;
using R3; using R3;
using SharpIDE.Application;
using SharpIDE.Application.Features.Analysis; using SharpIDE.Application.Features.Analysis;
using SharpIDE.Application.Features.NavigationHistory; using SharpIDE.Application.Features.NavigationHistory;
using SharpIDE.Application.Features.SolutionDiscovery; using SharpIDE.Application.Features.SolutionDiscovery;
@@ -27,6 +28,7 @@ public partial class SolutionExplorerPanel : MarginContainer
public Texture2D SlnIcon { get; set; } = null!; public Texture2D SlnIcon { get; set; } = null!;
public SharpIdeSolutionModel SolutionModel { get; set; } = null!; public SharpIdeSolutionModel SolutionModel { get; set; } = null!;
private PanelContainer _panelContainer = null!;
private Tree _tree = null!; private Tree _tree = null!;
private TreeItem _rootItem = null!; private TreeItem _rootItem = null!;
@@ -35,8 +37,11 @@ public partial class SolutionExplorerPanel : MarginContainer
private (List<IFileOrFolder>, ClipboardOperation)? _itemsOnClipboard; private (List<IFileOrFolder>, ClipboardOperation)? _itemsOnClipboard;
public override void _Ready() public override void _Ready()
{ {
_tree = GetNode<Tree>("Tree"); _panelContainer = GetNode<PanelContainer>("PanelContainer");
_tree = GetNode<Tree>("%Tree");
_tree.ItemMouseSelected += TreeOnItemMouseSelected; _tree.ItemMouseSelected += TreeOnItemMouseSelected;
// Remove the tree from the scene tree for now, we will add it back when we bind to a solution
_panelContainer.RemoveChild(_tree);
GodotGlobalEvents.Instance.FileExternallySelected.Subscribe(OnFileExternallySelected); GodotGlobalEvents.Instance.FileExternallySelected.Subscribe(OnFileExternallySelected);
} }
@@ -125,11 +130,16 @@ public partial class SolutionExplorerPanel : MarginContainer
return null; return null;
} }
public void BindToSolution() => BindToSolution(SolutionModel); public async Task BindToSolution() => await BindToSolution(SolutionModel);
[RequiresGodotUiThread] [RequiresGodotUiThread]
public void BindToSolution(SharpIdeSolutionModel solution) public async Task BindToSolution(SharpIdeSolutionModel solution)
{ {
_tree.Clear(); await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
using var _ = SharpIdeOtel.Source.StartActivity($"{nameof(SolutionExplorerPanel)}.{nameof(BindToSolution)}");
// Solutions with hundreds of thousands of files can cause the ui to freeze as the tree is populated
// the Tree has been removed from the scene tree in _Ready, so we can operate on it off the ui thread, then add it back
_tree.Clear();
// Root // Root
var rootItem = _tree.CreateItem(); var rootItem = _tree.CreateItem();
@@ -147,7 +157,7 @@ public partial class SolutionExplorerPanel : MarginContainer
NotifyCollectionChangedAction.Add => this.InvokeAsync(() => e.NewItem.View.Value = CreateProjectTreeItem(_tree, _rootItem, e.NewItem.Value)), NotifyCollectionChangedAction.Add => this.InvokeAsync(() => e.NewItem.View.Value = CreateProjectTreeItem(_tree, _rootItem, e.NewItem.Value)),
NotifyCollectionChangedAction.Remove => FreeTreeItem(e.OldItem.View.Value), NotifyCollectionChangedAction.Remove => FreeTreeItem(e.OldItem.View.Value),
_ => Task.CompletedTask _ => Task.CompletedTask
})).AddTo(this); })).AddToDeferred(this);
// Observe Solution Folders // Observe Solution Folders
var foldersView = solution.SlnFolders.CreateView(y => new TreeItemContainer()); var foldersView = solution.SlnFolders.CreateView(y => new TreeItemContainer());
@@ -158,10 +168,14 @@ public partial class SolutionExplorerPanel : MarginContainer
NotifyCollectionChangedAction.Add => this.InvokeAsync(() => e.NewItem.View.Value = CreateSlnFolderTreeItem(_tree, _rootItem, e.NewItem.Value)), NotifyCollectionChangedAction.Add => this.InvokeAsync(() => e.NewItem.View.Value = CreateSlnFolderTreeItem(_tree, _rootItem, e.NewItem.Value)),
NotifyCollectionChangedAction.Remove => FreeTreeItem(e.OldItem.View.Value), NotifyCollectionChangedAction.Remove => FreeTreeItem(e.OldItem.View.Value),
_ => Task.CompletedTask _ => Task.CompletedTask
})).AddTo(this); })).AddToDeferred(this);
rootItem.SetCollapsedRecursive(true); rootItem.SetCollapsedRecursive(true);
rootItem.Collapsed = false; rootItem.Collapsed = false;
await this.InvokeAsync(() =>
{
_panelContainer.AddChild(_tree);
});
} }
[RequiresGodotUiThread] [RequiresGodotUiThread]
@@ -182,7 +196,7 @@ public partial class SolutionExplorerPanel : MarginContainer
NotifyCollectionChangedAction.Add => this.InvokeAsync(() => innerEvent.NewItem.View.Value = CreateSlnFolderTreeItem(_tree, folderItem, innerEvent.NewItem.Value)), NotifyCollectionChangedAction.Add => this.InvokeAsync(() => innerEvent.NewItem.View.Value = CreateSlnFolderTreeItem(_tree, folderItem, innerEvent.NewItem.Value)),
NotifyCollectionChangedAction.Remove => FreeTreeItem(innerEvent.OldItem.View.Value), NotifyCollectionChangedAction.Remove => FreeTreeItem(innerEvent.OldItem.View.Value),
_ => Task.CompletedTask _ => Task.CompletedTask
})).AddTo(this); })).AddToDeferred(this);
var projectsView = slnFolder.Projects.CreateView(y => new TreeItemContainer()); var projectsView = slnFolder.Projects.CreateView(y => new TreeItemContainer());
projectsView.Unfiltered.ToList().ForEach(s => s.View.Value = CreateProjectTreeItem(_tree, folderItem, s.Value)); projectsView.Unfiltered.ToList().ForEach(s => s.View.Value = CreateProjectTreeItem(_tree, folderItem, s.Value));
@@ -192,7 +206,7 @@ public partial class SolutionExplorerPanel : MarginContainer
NotifyCollectionChangedAction.Add => this.InvokeAsync(() => innerEvent.NewItem.View.Value = CreateProjectTreeItem(_tree, folderItem, innerEvent.NewItem.Value)), NotifyCollectionChangedAction.Add => this.InvokeAsync(() => innerEvent.NewItem.View.Value = CreateProjectTreeItem(_tree, folderItem, innerEvent.NewItem.Value)),
NotifyCollectionChangedAction.Remove => FreeTreeItem(innerEvent.OldItem.View.Value), NotifyCollectionChangedAction.Remove => FreeTreeItem(innerEvent.OldItem.View.Value),
_ => Task.CompletedTask _ => Task.CompletedTask
})).AddTo(this); })).AddToDeferred(this);
var filesView = slnFolder.Files.CreateView(y => new TreeItemContainer()); var filesView = slnFolder.Files.CreateView(y => new TreeItemContainer());
filesView.Unfiltered.ToList().ForEach(s => s.View.Value = CreateFileTreeItem(_tree, folderItem, s.Value)); filesView.Unfiltered.ToList().ForEach(s => s.View.Value = CreateFileTreeItem(_tree, folderItem, s.Value));
@@ -202,7 +216,7 @@ public partial class SolutionExplorerPanel : MarginContainer
NotifyCollectionChangedAction.Add => this.InvokeAsync(() => innerEvent.NewItem.View.Value = CreateFileTreeItem(_tree, folderItem, innerEvent.NewItem.Value, innerEvent.NewStartingIndex)), NotifyCollectionChangedAction.Add => this.InvokeAsync(() => innerEvent.NewItem.View.Value = CreateFileTreeItem(_tree, folderItem, innerEvent.NewItem.Value, innerEvent.NewStartingIndex)),
NotifyCollectionChangedAction.Remove => FreeTreeItem(innerEvent.OldItem.View.Value), NotifyCollectionChangedAction.Remove => FreeTreeItem(innerEvent.OldItem.View.Value),
_ => Task.CompletedTask _ => Task.CompletedTask
})).AddTo(this); })).AddToDeferred(this);
return folderItem; return folderItem;
} }
@@ -225,7 +239,7 @@ public partial class SolutionExplorerPanel : MarginContainer
NotifyCollectionChangedAction.Move => MoveTreeItem(_tree, innerEvent.NewItem.View, innerEvent.NewItem.Value, innerEvent.OldStartingIndex, innerEvent.NewStartingIndex), NotifyCollectionChangedAction.Move => MoveTreeItem(_tree, innerEvent.NewItem.View, innerEvent.NewItem.Value, innerEvent.OldStartingIndex, innerEvent.NewStartingIndex),
NotifyCollectionChangedAction.Remove => FreeTreeItem(innerEvent.OldItem.View.Value), NotifyCollectionChangedAction.Remove => FreeTreeItem(innerEvent.OldItem.View.Value),
_ => Task.CompletedTask _ => Task.CompletedTask
})).AddTo(this); })).AddToDeferred(this);
// Observe project files // Observe project files
var filesView = projectModel.Files.CreateView(y => new TreeItemContainer()); var filesView = projectModel.Files.CreateView(y => new TreeItemContainer());
@@ -237,7 +251,7 @@ public partial class SolutionExplorerPanel : MarginContainer
NotifyCollectionChangedAction.Move => MoveTreeItem(_tree, innerEvent.NewItem.View, innerEvent.NewItem.Value, innerEvent.OldStartingIndex, innerEvent.NewStartingIndex), NotifyCollectionChangedAction.Move => MoveTreeItem(_tree, innerEvent.NewItem.View, innerEvent.NewItem.Value, innerEvent.OldStartingIndex, innerEvent.NewStartingIndex),
NotifyCollectionChangedAction.Remove => FreeTreeItem(innerEvent.OldItem.View.Value), NotifyCollectionChangedAction.Remove => FreeTreeItem(innerEvent.OldItem.View.Value),
_ => Task.CompletedTask _ => Task.CompletedTask
})).AddTo(this); })).AddToDeferred(this);
return projectItem; return projectItem;
} }
@@ -253,7 +267,7 @@ public partial class SolutionExplorerPanel : MarginContainer
.Skip(1).SubscribeOnThreadPool().ObserveOnThreadPool().SubscribeAwait(async (s, ct) => .Skip(1).SubscribeOnThreadPool().ObserveOnThreadPool().SubscribeAwait(async (s, ct) =>
{ {
await this.InvokeAsync(() => folderItem.SetText(0, s)); await this.InvokeAsync(() => folderItem.SetText(0, s));
}).AddTo(this); }).AddToDeferred(this);
// Observe subfolders // Observe subfolders
var subFoldersView = sharpIdeFolder.Folders.CreateView(y => new TreeItemContainer()); var subFoldersView = sharpIdeFolder.Folders.CreateView(y => new TreeItemContainer());
@@ -266,7 +280,7 @@ public partial class SolutionExplorerPanel : MarginContainer
NotifyCollectionChangedAction.Move => MoveTreeItem(_tree, innerEvent.NewItem.View, innerEvent.NewItem.Value, innerEvent.OldStartingIndex, innerEvent.NewStartingIndex), NotifyCollectionChangedAction.Move => MoveTreeItem(_tree, innerEvent.NewItem.View, innerEvent.NewItem.Value, innerEvent.OldStartingIndex, innerEvent.NewStartingIndex),
NotifyCollectionChangedAction.Remove => FreeTreeItem(innerEvent.OldItem.View.Value), NotifyCollectionChangedAction.Remove => FreeTreeItem(innerEvent.OldItem.View.Value),
_ => Task.CompletedTask _ => Task.CompletedTask
})).AddTo(this); })).AddToDeferred(this);
// Observe files // Observe files
var filesView = sharpIdeFolder.Files.CreateView(y => new TreeItemContainer()); var filesView = sharpIdeFolder.Files.CreateView(y => new TreeItemContainer());
@@ -278,7 +292,7 @@ public partial class SolutionExplorerPanel : MarginContainer
NotifyCollectionChangedAction.Move => MoveTreeItem(_tree, innerEvent.NewItem.View, innerEvent.NewItem.Value, innerEvent.OldStartingIndex, innerEvent.NewStartingIndex), NotifyCollectionChangedAction.Move => MoveTreeItem(_tree, innerEvent.NewItem.View, innerEvent.NewItem.Value, innerEvent.OldStartingIndex, innerEvent.NewStartingIndex),
NotifyCollectionChangedAction.Remove => FreeTreeItem(innerEvent.OldItem.View.Value), NotifyCollectionChangedAction.Remove => FreeTreeItem(innerEvent.OldItem.View.Value),
_ => Task.CompletedTask _ => Task.CompletedTask
})).AddTo(this); })).AddToDeferred(this);
return folderItem; return folderItem;
} }
@@ -308,7 +322,7 @@ public partial class SolutionExplorerPanel : MarginContainer
fileItem.SetText(0, s); fileItem.SetText(0, s);
fileItem.SetIconsForFileExtension(sharpIdeFile); fileItem.SetIconsForFileExtension(sharpIdeFile);
}); });
}).AddTo(this); }).AddToDeferred(this);
return fileItem; return fileItem;
} }

View File

@@ -1,4 +1,4 @@
[gd_scene load_steps=8 format=3 uid="uid://cy1bb32g7j7dr"] [gd_scene load_steps=9 format=3 uid="uid://cy1bb32g7j7dr"]
[ext_resource type="Script" uid="uid://bai53k7ongbxw" path="res://Features/SolutionExplorer/SolutionExplorerPanel.cs" id="1_gjy0r"] [ext_resource type="Script" uid="uid://bai53k7ongbxw" path="res://Features/SolutionExplorer/SolutionExplorerPanel.cs" id="1_gjy0r"]
[ext_resource type="Texture2D" uid="uid://do0edciarrnp0" path="res://Features/SolutionExplorer/Resources/CsharpFile.svg" id="2_8ymw0"] [ext_resource type="Texture2D" uid="uid://do0edciarrnp0" path="res://Features/SolutionExplorer/Resources/CsharpFile.svg" id="2_8ymw0"]
@@ -7,6 +7,12 @@
[ext_resource type="Texture2D" uid="uid://cqt30ma6xgder" path="res://Features/SolutionExplorer/Resources/Csproj.svg" id="5_r1qfc"] [ext_resource type="Texture2D" uid="uid://cqt30ma6xgder" path="res://Features/SolutionExplorer/Resources/Csproj.svg" id="5_r1qfc"]
[ext_resource type="Texture2D" uid="uid://btlxqx3c08fjj" path="res://Features/SolutionExplorer/Resources/SlnIcon.svg" id="6_idvpu"] [ext_resource type="Texture2D" uid="uid://btlxqx3c08fjj" path="res://Features/SolutionExplorer/Resources/SlnIcon.svg" id="6_idvpu"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_idvpu"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 5.0
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_idvpu"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_idvpu"]
content_margin_left = 4.0 content_margin_left = 4.0
content_margin_top = 4.0 content_margin_top = 4.0
@@ -41,12 +47,17 @@ SlnFolderIcon = ExtResource("4_8ymw0")
CsprojIcon = ExtResource("5_r1qfc") CsprojIcon = ExtResource("5_r1qfc")
SlnIcon = ExtResource("6_idvpu") SlnIcon = ExtResource("6_idvpu")
[node name="Tree" type="Tree" parent="."] [node name="PanelContainer" type="PanelContainer" parent="."]
layout_mode = 2
[node name="Tree" type="Tree" parent="PanelContainer"]
unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
theme_override_colors/font_color = Color(0.830335, 0.830335, 0.830335, 1) theme_override_colors/font_color = Color(0.830335, 0.830335, 0.830335, 1)
theme_override_constants/v_separation = 1 theme_override_constants/v_separation = 1
theme_override_constants/inner_item_margin_left = 2 theme_override_constants/inner_item_margin_left = 2
theme_override_constants/draw_guides = 0 theme_override_constants/draw_guides = 0
theme_override_styles/panel = SubResource("StyleBoxEmpty_idvpu")
theme_override_styles/cursor = SubResource("StyleBoxFlat_idvpu") theme_override_styles/cursor = SubResource("StyleBoxFlat_idvpu")
theme_override_styles/cursor_unfocused = SubResource("StyleBoxFlat_idvpu") theme_override_styles/cursor_unfocused = SubResource("StyleBoxFlat_idvpu")
allow_rmb_select = true allow_rmb_select = true

View File

@@ -157,7 +157,7 @@ public partial class IdeRoot : Control
_fileExternalChangeHandler.SolutionModel = solutionModel; _fileExternalChangeHandler.SolutionModel = solutionModel;
_fileChangedService.SolutionModel = solutionModel; _fileChangedService.SolutionModel = solutionModel;
_sharpIdeSolutionModificationService.SolutionModel = solutionModel; _sharpIdeSolutionModificationService.SolutionModel = solutionModel;
Callable.From(_solutionExplorerPanel.BindToSolution).CallDeferred(); _ = Task.GodotRun(_solutionExplorerPanel.BindToSolution);
_roslynAnalysis.StartLoadingSolutionInWorkspace(solutionModel); _roslynAnalysis.StartLoadingSolutionInWorkspace(solutionModel);
_fileWatcher.StartWatching(solutionModel); _fileWatcher.StartWatching(solutionModel);

View File

@@ -29,4 +29,23 @@ public static class GodotNodeExtensions
node.TreeExited += () => disposable.Dispose(); node.TreeExited += () => disposable.Dispose();
return disposable; return disposable;
} }
public static void AddToDeferred<T>(this T disposable, Node node) where T : IDisposable
{
Callable.From(() =>
{
// Note: Dispose when tree exited, so if node is not inside tree, dispose immediately.
if (!node.IsInsideTree())
{
if (!node.IsNodeReady()) // Before enter tree
{
GD.PrintErr("AddTo does not support to use before enter tree.");
}
disposable.Dispose();
}
node.TreeExited += () => disposable.Dispose();
}).CallDeferred();
}
} }