From d285ce1bf2f72097b6be3033f300d2b47a19aceb Mon Sep 17 00:00:00 2001 From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> Date: Sat, 18 Oct 2025 14:20:04 +1000 Subject: [PATCH] use DI in Godot Nodes --- .../FileWatching/IdeFileSavedToDiskHandler.cs | 8 ++- src/SharpIDE.Godot/DiAutoload.cs | 66 +++++++++++++++++++ src/SharpIDE.Godot/DiAutoload.cs.uid | 1 + .../Features/Build/BuildPanel.cs | 7 +- .../Features/CodeEditor/CodeEditorPanel.cs | 4 +- .../Features/CodeEditor/SharpIdeCodeEdit.cs | 22 ++++--- .../Tab/SubTabs/ThreadsVariablesSubTab.cs | 5 +- .../Features/Run/RunMenuItem.cs | 9 ++- .../ContextMenus/ProjectContextMenu.cs | 12 +++- src/SharpIDE.Godot/IdeRoot.cs | 33 +++++----- src/SharpIDE.Godot/IdeWindow.cs | 6 +- src/SharpIDE.Godot/Singletons.cs | 6 -- src/SharpIDE.Godot/project.godot | 1 + 13 files changed, 136 insertions(+), 44 deletions(-) create mode 100644 src/SharpIDE.Godot/DiAutoload.cs create mode 100644 src/SharpIDE.Godot/DiAutoload.cs.uid diff --git a/src/SharpIDE.Application/Features/FileWatching/IdeFileSavedToDiskHandler.cs b/src/SharpIDE.Application/Features/FileWatching/IdeFileSavedToDiskHandler.cs index 8d8dfbc..b457cbf 100644 --- a/src/SharpIDE.Application/Features/FileWatching/IdeFileSavedToDiskHandler.cs +++ b/src/SharpIDE.Application/Features/FileWatching/IdeFileSavedToDiskHandler.cs @@ -9,10 +9,12 @@ namespace SharpIDE.Application.Features.FileWatching; public class IdeFileSavedToDiskHandler { + private readonly IdeOpenTabsFileManager _openTabsFileManager; public SharpIdeSolutionModel SolutionModel { get; set; } = null!; - public IdeFileSavedToDiskHandler() + public IdeFileSavedToDiskHandler(IdeOpenTabsFileManager openTabsFileManager) { + _openTabsFileManager = openTabsFileManager; GlobalEvents.Instance.IdeFileSavedToDisk.Subscribe(HandleIdeFileChanged); } @@ -40,11 +42,11 @@ public class IdeFileSavedToDiskHandler private async Task HandleWorkspaceFileChanged(SharpIdeFile file) { // TODO: Don't reload from disk if we raised the change event ourselves (e.g. save from IDE). Cleanup this whole disaster - var wasOpenAndUpdated = await IdeOpenTabsFileManager.Instance.ReloadFileFromDiskIfOpenInEditor(file); + var wasOpenAndUpdated = await _openTabsFileManager.ReloadFileFromDiskIfOpenInEditor(file); if (file.IsRoslynWorkspaceFile) { var fileText = wasOpenAndUpdated ? - await IdeOpenTabsFileManager.Instance.GetFileTextAsync(file) : + await _openTabsFileManager.GetFileTextAsync(file) : await File.ReadAllTextAsync(file.Path); await RoslynAnalysis.UpdateDocument(file, fileText); GlobalEvents.Instance.SolutionAltered.InvokeParallelFireAndForget(); diff --git a/src/SharpIDE.Godot/DiAutoload.cs b/src/SharpIDE.Godot/DiAutoload.cs new file mode 100644 index 0000000..688fa7f --- /dev/null +++ b/src/SharpIDE.Godot/DiAutoload.cs @@ -0,0 +1,66 @@ +using Godot; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using SharpIDE.Application.Features.Build; +using SharpIDE.Application.Features.FilePersistence; +using SharpIDE.Application.Features.FileWatching; +using SharpIDE.Application.Features.Run; + +namespace SharpIDE.Godot; + +[AttributeUsage(AttributeTargets.Field)] +public class InjectAttribute : Attribute; + +public partial class DiAutoload : Node +{ + private ServiceProvider? _serviceProvider; + + public override void _EnterTree() + { + GD.Print("[Injector] _EnterTree called"); + var services = new ServiceCollection(); + // Register services here + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + _serviceProvider = services.BuildServiceProvider(); + GetTree().NodeAdded += OnNodeAdded; + GD.Print("[Injector] Service provider built and NodeAdded event subscribed"); + } + + public override void _Ready() + { + + } + + private void OnNodeAdded(Node node) + { + // Inject dependencies into every new node + InjectDependencies(node); + } + + private void InjectDependencies(object target) + { + var type = target.GetType(); + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + + foreach (var field in type.GetFields(flags)) + { + if (Attribute.IsDefined(field, typeof(InjectAttribute))) + { + var service = _serviceProvider!.GetService(field.FieldType); + if (service is null) + { + GD.PrintErr($"[Injector] No service registered for {field.FieldType}"); + GetTree().Quit(); + } + + field.SetValue(target, service); + } + } + } +} \ No newline at end of file diff --git a/src/SharpIDE.Godot/DiAutoload.cs.uid b/src/SharpIDE.Godot/DiAutoload.cs.uid new file mode 100644 index 0000000..d88fc1e --- /dev/null +++ b/src/SharpIDE.Godot/DiAutoload.cs.uid @@ -0,0 +1 @@ +uid://c1ong07uc2rx4 diff --git a/src/SharpIDE.Godot/Features/Build/BuildPanel.cs b/src/SharpIDE.Godot/Features/Build/BuildPanel.cs index 830fad4..ba1aca8 100644 --- a/src/SharpIDE.Godot/Features/Build/BuildPanel.cs +++ b/src/SharpIDE.Godot/Features/Build/BuildPanel.cs @@ -1,6 +1,7 @@ using System.Threading.Channels; using GDExtensionBindgen; using Godot; +using SharpIDE.Application.Features.Build; namespace SharpIDE.Godot.Features.Build; @@ -8,10 +9,12 @@ public partial class BuildPanel : Control { private Terminal _terminal = null!; private ChannelReader? _buildOutputChannelReader; + + [Inject] private readonly BuildService _buildService = null!; public override void _Ready() { _terminal = new Terminal(GetNode("%Terminal")); - Singletons.BuildService.BuildStarted += OnBuildStarted; + _buildService.BuildStarted += OnBuildStarted; } public override void _Process(double delta) @@ -27,6 +30,6 @@ public partial class BuildPanel : Control private async Task OnBuildStarted() { await this.InvokeAsync(() => _terminal.Clear()); - _buildOutputChannelReader ??= Singletons.BuildService.BuildTextWriter.ConsoleChannel.Reader; + _buildOutputChannelReader ??= _buildService.BuildTextWriter.ConsoleChannel.Reader; } } \ No newline at end of file diff --git a/src/SharpIDE.Godot/Features/CodeEditor/CodeEditorPanel.cs b/src/SharpIDE.Godot/Features/CodeEditor/CodeEditorPanel.cs index 6962ff9..086ce58 100644 --- a/src/SharpIDE.Godot/Features/CodeEditor/CodeEditorPanel.cs +++ b/src/SharpIDE.Godot/Features/CodeEditor/CodeEditorPanel.cs @@ -4,6 +4,7 @@ using R3; using SharpIDE.Application.Features.Analysis; using SharpIDE.Application.Features.Debugging; using SharpIDE.Application.Features.Events; +using SharpIDE.Application.Features.Run; using SharpIDE.Application.Features.SolutionDiscovery; using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; @@ -18,6 +19,7 @@ public partial class CodeEditorPanel : MarginContainer private TabContainer _tabContainer = null!; private ExecutionStopInfo? _debuggerExecutionStopInfo; + [Inject] private readonly RunService _runService = null!; public override void _Ready() { _tabContainer = GetNode("TabContainer"); @@ -125,7 +127,7 @@ public partial class CodeEditorPanel : MarginContainer _debuggerExecutionStopInfo = null; _ = Task.GodotRun(async () => { - await Singletons.RunService.SendDebuggerStepOver(threadId); + await _runService.SendDebuggerStepOver(threadId); }); } } \ No newline at end of file diff --git a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs index 734a50f..e34218d 100644 --- a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs +++ b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs @@ -9,6 +9,8 @@ using SharpIDE.Application; using SharpIDE.Application.Features.Analysis; using SharpIDE.Application.Features.Debugging; using SharpIDE.Application.Features.Events; +using SharpIDE.Application.Features.FilePersistence; +using SharpIDE.Application.Features.Run; using SharpIDE.Application.Features.SolutionDiscovery; using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; using SharpIDE.RazorAccess; @@ -39,6 +41,10 @@ public partial class SharpIdeCodeEdit : CodeEdit private ImmutableArray _currentCodeActionsInPopup = []; private bool _fileChangingSuppressBreakpointToggleEvent; + [Inject] private readonly IdeOpenTabsFileManager _openTabsFileManager = null!; + [Inject] private readonly RunService _runService = null!; + + public override void _Ready() { SyntaxHighlighter = _syntaxHighlighter; @@ -112,7 +118,7 @@ public partial class SharpIdeCodeEdit : CodeEdit var lineInt = (int)line; var breakpointAdded = IsLineBreakpointed(lineInt); var lineForDebugger = lineInt + 1; // Godot is 0-indexed, Debugging is 1-indexed - var breakpoints = Singletons.RunService.Breakpoints.GetOrAdd(_currentFile, []); + var breakpoints = _runService.Breakpoints.GetOrAdd(_currentFile, []); if (breakpointAdded) { breakpoints.Add(new Breakpoint { Line = lineForDebugger } ); @@ -265,7 +271,7 @@ public partial class SharpIdeCodeEdit : CodeEdit _ = Task.GodotRun(async () => { _currentFile.IsDirty.Value = true; - await Singletons.OpenTabsFileManager.UpdateFileTextInMemory(_currentFile, Text); + await _openTabsFileManager.UpdateFileTextInMemory(_currentFile, Text); await _textChangedCts.CancelAsync(); // Currently the below methods throw, TODO Fix with suppress throwing, and handle _textChangedCts.Dispose(); _textChangedCts = new CancellationTokenSource(); @@ -298,7 +304,7 @@ public partial class SharpIdeCodeEdit : CodeEdit // TODO: This can be more efficient - we can just update in memory and proceed with highlighting etc. Save to disk in background. foreach (var (affectedFile, updatedText) in affectedFiles) { - await Singletons.OpenTabsFileManager.UpdateInMemoryIfOpenAndSaveAsync(affectedFile, updatedText); + await _openTabsFileManager.UpdateInMemoryIfOpenAndSaveAsync(affectedFile, updatedText); affectedFile.FileContentsChangedExternally.InvokeParallelFireAndForget(); } }); @@ -306,7 +312,7 @@ public partial class SharpIdeCodeEdit : CodeEdit private async Task OnFileChangedExternallyInMemory() { - var fileContents = await Singletons.OpenTabsFileManager.GetFileTextAsync(_currentFile); + var fileContents = await _openTabsFileManager.GetFileTextAsync(_currentFile); var syntaxHighlighting = RoslynAnalysis.GetDocumentSyntaxHighlighting(_currentFile); var razorSyntaxHighlighting = RoslynAnalysis.GetRazorDocumentSyntaxHighlighting(_currentFile); var diagnostics = RoslynAnalysis.GetDocumentDiagnostics(_currentFile); @@ -341,7 +347,7 @@ public partial class SharpIdeCodeEdit : CodeEdit { await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); // get off the UI thread _currentFile = file; - var readFileTask = Singletons.OpenTabsFileManager.GetFileTextAsync(file); + var readFileTask = _openTabsFileManager.GetFileTextAsync(file); _currentFile.FileContentsChangedExternally.Subscribe(OnFileChangedExternallyInMemory); _currentFile.FileContentsChangedExternallyFromDisk.Subscribe(OnFileChangedExternallyFromDisk); @@ -365,7 +371,7 @@ public partial class SharpIdeCodeEdit : CodeEdit private async Task OnFileChangedExternallyFromDisk() { - await Singletons.OpenTabsFileManager.ReloadFileFromDisk(_currentFile); + await _openTabsFileManager.ReloadFileFromDisk(_currentFile); await OnFileChangedExternallyInMemory(); } @@ -432,14 +438,14 @@ public partial class SharpIdeCodeEdit : CodeEdit { _ = Task.GodotRun(async () => { - await Singletons.OpenTabsFileManager.SaveAllOpenFilesAsync(); + await _openTabsFileManager.SaveAllOpenFilesAsync(); }); } else if (@event.IsActionPressed(InputStringNames.SaveFile)) { _ = Task.GodotRun(async () => { - await Singletons.OpenTabsFileManager.SaveFileAsync(_currentFile); + await _openTabsFileManager.SaveFileAsync(_currentFile); }); } } diff --git a/src/SharpIDE.Godot/Features/Debug_/Tab/SubTabs/ThreadsVariablesSubTab.cs b/src/SharpIDE.Godot/Features/Debug_/Tab/SubTabs/ThreadsVariablesSubTab.cs index c5ca4c1..22004f7 100644 --- a/src/SharpIDE.Godot/Features/Debug_/Tab/SubTabs/ThreadsVariablesSubTab.cs +++ b/src/SharpIDE.Godot/Features/Debug_/Tab/SubTabs/ThreadsVariablesSubTab.cs @@ -2,6 +2,7 @@ using Ardalis.GuardClauses; using Godot; using SharpIDE.Application.Features.Debugging; using SharpIDE.Application.Features.Events; +using SharpIDE.Application.Features.Run; using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; namespace SharpIDE.Godot.Features.Debug_.Tab.SubTabs; @@ -14,6 +15,8 @@ public partial class ThreadsVariablesSubTab : Control private VBoxContainer _variablesVboxContainer = null!; public SharpIdeProjectModel Project { get; set; } = null!; // private ThreadModel? _selectedThread = null!; // null when not at a stop point + + [Inject] private readonly RunService _runService = null!; public override void _Ready() { @@ -26,7 +29,7 @@ public partial class ThreadsVariablesSubTab : Control private async Task OnDebuggerExecutionStopped(ExecutionStopInfo stopInfo) { - var result = await Singletons.RunService.GetInfoAtStopPoint(); + var result = await _runService.GetInfoAtStopPoint(); var threadScenes = result.Threads.Select(s => { var threadListItem = _threadListItemScene.Instantiate(); diff --git a/src/SharpIDE.Godot/Features/Run/RunMenuItem.cs b/src/SharpIDE.Godot/Features/Run/RunMenuItem.cs index 4ae28b4..5e800df 100644 --- a/src/SharpIDE.Godot/Features/Run/RunMenuItem.cs +++ b/src/SharpIDE.Godot/Features/Run/RunMenuItem.cs @@ -1,4 +1,5 @@ using Godot; +using SharpIDE.Application.Features.Run; using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; using SharpIDE.Godot.Features.BottomPanel; @@ -11,6 +12,8 @@ public partial class RunMenuItem : HBoxContainer private Button _runButton = null!; private Button _debugButton = null!; private Button _stopButton = null!; + + [Inject] private readonly RunService _runService = null!; public override void _Ready() { _label = GetNode