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)!;
Files = new ObservableList<SharpIdeFile>(TreeMapperV2.GetFiles(projectModel.FullFilePath, this, allFiles));
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);
}

View File

@@ -3,6 +3,7 @@ using Ardalis.GuardClauses;
using Godot;
using ObservableCollections;
using R3;
using SharpIDE.Application;
using SharpIDE.Application.Features.Analysis;
using SharpIDE.Application.Features.NavigationHistory;
using SharpIDE.Application.Features.SolutionDiscovery;
@@ -27,6 +28,7 @@ public partial class SolutionExplorerPanel : MarginContainer
public Texture2D SlnIcon { get; set; } = null!;
public SharpIdeSolutionModel SolutionModel { get; set; } = null!;
private PanelContainer _panelContainer = null!;
private Tree _tree = null!;
private TreeItem _rootItem = null!;
@@ -35,8 +37,11 @@ public partial class SolutionExplorerPanel : MarginContainer
private (List<IFileOrFolder>, ClipboardOperation)? _itemsOnClipboard;
public override void _Ready()
{
_tree = GetNode<Tree>("Tree");
_panelContainer = GetNode<PanelContainer>("PanelContainer");
_tree = GetNode<Tree>("%Tree");
_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);
}
@@ -125,11 +130,16 @@ public partial class SolutionExplorerPanel : MarginContainer
return null;
}
public void BindToSolution() => BindToSolution(SolutionModel);
public async Task BindToSolution() => await BindToSolution(SolutionModel);
[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
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.Remove => FreeTreeItem(e.OldItem.View.Value),
_ => Task.CompletedTask
})).AddTo(this);
})).AddToDeferred(this);
// Observe Solution Folders
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.Remove => FreeTreeItem(e.OldItem.View.Value),
_ => Task.CompletedTask
})).AddTo(this);
})).AddToDeferred(this);
rootItem.SetCollapsedRecursive(true);
rootItem.Collapsed = false;
await this.InvokeAsync(() =>
{
_panelContainer.AddChild(_tree);
});
}
[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.Remove => FreeTreeItem(innerEvent.OldItem.View.Value),
_ => Task.CompletedTask
})).AddTo(this);
})).AddToDeferred(this);
var projectsView = slnFolder.Projects.CreateView(y => new TreeItemContainer());
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.Remove => FreeTreeItem(innerEvent.OldItem.View.Value),
_ => Task.CompletedTask
})).AddTo(this);
})).AddToDeferred(this);
var filesView = slnFolder.Files.CreateView(y => new TreeItemContainer());
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.Remove => FreeTreeItem(innerEvent.OldItem.View.Value),
_ => Task.CompletedTask
})).AddTo(this);
})).AddToDeferred(this);
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.Remove => FreeTreeItem(innerEvent.OldItem.View.Value),
_ => Task.CompletedTask
})).AddTo(this);
})).AddToDeferred(this);
// Observe project files
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.Remove => FreeTreeItem(innerEvent.OldItem.View.Value),
_ => Task.CompletedTask
})).AddTo(this);
})).AddToDeferred(this);
return projectItem;
}
@@ -253,7 +267,7 @@ public partial class SolutionExplorerPanel : MarginContainer
.Skip(1).SubscribeOnThreadPool().ObserveOnThreadPool().SubscribeAwait(async (s, ct) =>
{
await this.InvokeAsync(() => folderItem.SetText(0, s));
}).AddTo(this);
}).AddToDeferred(this);
// Observe subfolders
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.Remove => FreeTreeItem(innerEvent.OldItem.View.Value),
_ => Task.CompletedTask
})).AddTo(this);
})).AddToDeferred(this);
// Observe files
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.Remove => FreeTreeItem(innerEvent.OldItem.View.Value),
_ => Task.CompletedTask
})).AddTo(this);
})).AddToDeferred(this);
return folderItem;
}
@@ -308,7 +322,7 @@ public partial class SolutionExplorerPanel : MarginContainer
fileItem.SetText(0, s);
fileItem.SetIconsForFileExtension(sharpIdeFile);
});
}).AddTo(this);
}).AddToDeferred(this);
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="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://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"]
content_margin_left = 4.0
content_margin_top = 4.0
@@ -41,12 +47,17 @@ SlnFolderIcon = ExtResource("4_8ymw0")
CsprojIcon = ExtResource("5_r1qfc")
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
theme_override_colors/font_color = Color(0.830335, 0.830335, 0.830335, 1)
theme_override_constants/v_separation = 1
theme_override_constants/inner_item_margin_left = 2
theme_override_constants/draw_guides = 0
theme_override_styles/panel = SubResource("StyleBoxEmpty_idvpu")
theme_override_styles/cursor = SubResource("StyleBoxFlat_idvpu")
theme_override_styles/cursor_unfocused = SubResource("StyleBoxFlat_idvpu")
allow_rmb_select = true

View File

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

View File

@@ -29,4 +29,23 @@ public static class GodotNodeExtensions
node.TreeExited += () => disposable.Dispose();
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();
}
}