Files
SharpIDE/src/SharpIDE.Godot/IdeRoot.cs
2025-12-13 14:31:31 +10:00

211 lines
9.4 KiB
C#

using System.Diagnostics;
using Godot;
using Microsoft.Extensions.Logging;
using SharpIDE.Application.Features.Analysis;
using SharpIDE.Application.Features.Build;
using SharpIDE.Application.Features.Events;
using SharpIDE.Application.Features.FilePersistence;
using SharpIDE.Application.Features.FileWatching;
using SharpIDE.Application.Features.NavigationHistory;
using SharpIDE.Application.Features.Run;
using SharpIDE.Application.Features.SolutionDiscovery;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
using SharpIDE.Godot.Features.BottomPanel;
using SharpIDE.Godot.Features.CodeEditor;
using SharpIDE.Godot.Features.CustomControls;
using SharpIDE.Godot.Features.Run;
using SharpIDE.Godot.Features.Search;
using SharpIDE.Godot.Features.Search.SearchAllFiles;
using SharpIDE.Godot.Features.SolutionExplorer;
namespace SharpIDE.Godot;
public partial class IdeRoot : Control
{
public IdeWindow IdeWindow { get; set; } = null!;
private Button _openSlnButton = null!;
private Button _buildSlnButton = null!;
private Button _rebuildSlnButton = null!;
private Button _cleanSlnButton = null!;
private Button _restoreSlnButton = null!;
private SearchWindow _searchWindow = null!;
private SearchAllFilesWindow _searchAllFilesWindow = null!;
private CodeEditorPanel _codeEditorPanel = null!;
private SolutionExplorerPanel _solutionExplorerPanel = null!;
private InvertedVSplitContainer _invertedVSplitContainer = null!;
private RunPanel _runPanel = null!;
private Button _runMenuButton = null!;
private Popup _runMenuPopup = null!;
private BottomPanelManager _bottomPanelManager = null!;
private readonly PackedScene _runMenuItemScene = ResourceLoader.Load<PackedScene>("res://Features/Run/RunMenuItem.tscn");
private TaskCompletionSource _nodeReadyTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
[Inject] private readonly FileChangedService _fileChangedService = null!;
[Inject] private readonly IdeFileExternalChangeHandler _fileExternalChangeHandler = null!;
[Inject] private readonly IdeFileWatcher _fileWatcher = null!;
[Inject] private readonly BuildService _buildService = null!;
[Inject] private readonly IdeOpenTabsFileManager _openTabsFileManager = null!;
[Inject] private readonly RoslynAnalysis _roslynAnalysis = null!;
[Inject] private readonly SharpIdeSolutionModificationService _sharpIdeSolutionModificationService = null!;
[Inject] private readonly SharpIdeSolutionAccessor _sharpIdeSolutionAccessor = null!;
[Inject] private readonly IdeNavigationHistoryService _navigationHistoryService = null!;
[Inject] private readonly ILogger<IdeRoot> _logger = null!;
public override void _EnterTree()
{
GodotGlobalEvents.Instance = new GodotGlobalEvents();
GlobalEvents.Instance = new GlobalEvents();
}
public override void _ExitTree()
{
_fileWatcher?.Dispose();
GetTree().GetRoot().FocusExited -= OnFocusExited;
}
public override void _Ready()
{
_openSlnButton = GetNode<Button>("%OpenSlnButton");
_buildSlnButton = GetNode<Button>("%BuildSlnButton");
_rebuildSlnButton = GetNode<Button>("%RebuildSlnButton");
_cleanSlnButton = GetNode<Button>("%CleanSlnButton");
_restoreSlnButton = GetNode<Button>("%RestoreSlnButton");
_runMenuPopup = GetNode<Popup>("%RunMenuPopup");
_runMenuButton = GetNode<Button>("%RunMenuButton");
_codeEditorPanel = GetNode<CodeEditorPanel>("%CodeEditorPanel");
_searchWindow = GetNode<SearchWindow>("%SearchWindow");
_searchAllFilesWindow = GetNode<SearchAllFilesWindow>("%SearchAllFilesWindow");
_solutionExplorerPanel = GetNode<SolutionExplorerPanel>("%SolutionExplorerPanel");
_runPanel = GetNode<RunPanel>("%RunPanel");
_invertedVSplitContainer = GetNode<InvertedVSplitContainer>("%InvertedVSplitContainer");
_bottomPanelManager = GetNode<BottomPanelManager>("%BottomPanel");
_runMenuButton.Pressed += OnRunMenuButtonPressed;
GodotGlobalEvents.Instance.FileSelected.Subscribe(OnSolutionExplorerPanelOnFileSelected);
_openSlnButton.Pressed += () => IdeWindow.PickSolution();
_buildSlnButton.Pressed += OnBuildSlnButtonPressed;
_rebuildSlnButton.Pressed += OnRebuildSlnButtonPressed;
_cleanSlnButton.Pressed += OnCleanSlnButtonPressed;
_restoreSlnButton.Pressed += OnRestoreSlnButtonPressed;
GodotGlobalEvents.Instance.BottomPanelVisibilityChangeRequested.Subscribe(async show => await this.InvokeAsync(() => _invertedVSplitContainer.InvertedSetCollapsed(!show)));
GetTree().GetRoot().FocusExited += OnFocusExited;
_nodeReadyTcs.SetResult();
}
// TODO: Problematic, as this is called even when the focus shifts to an embedded subwindow, such as a popup
private void OnFocusExited()
{
if (Debugger.IsAttached is false)
{
_ = Task.GodotRun(async () => await _openTabsFileManager.SaveAllOpenFilesAsync());
}
}
private void OnRunMenuButtonPressed()
{
var popupMenuPosition = _runMenuButton.GlobalPosition;
const int buttonHeight = 37;
_runMenuPopup.Position = new Vector2I((int)popupMenuPosition.X, (int)popupMenuPosition.Y + buttonHeight);
_runMenuPopup.Popup();
}
private async void OnBuildSlnButtonPressed()
{
GodotGlobalEvents.Instance.BottomPanelTabExternallySelected.InvokeParallelFireAndForget(BottomPanelType.Build);
await _buildService.MsBuildAsync(_solutionExplorerPanel.SolutionModel.FilePath);
}
private async void OnRebuildSlnButtonPressed()
{
GodotGlobalEvents.Instance.BottomPanelTabExternallySelected.InvokeParallelFireAndForget(BottomPanelType.Build);
await _buildService.MsBuildAsync(_solutionExplorerPanel.SolutionModel.FilePath, BuildType.Rebuild);
}
private async void OnCleanSlnButtonPressed()
{
GodotGlobalEvents.Instance.BottomPanelTabExternallySelected.InvokeParallelFireAndForget(BottomPanelType.Build);
await _buildService.MsBuildAsync(_solutionExplorerPanel.SolutionModel.FilePath, BuildType.Clean);
}
private async void OnRestoreSlnButtonPressed()
{
GodotGlobalEvents.Instance.BottomPanelTabExternallySelected.InvokeParallelFireAndForget(BottomPanelType.Build);
await _buildService.MsBuildAsync(_solutionExplorerPanel.SolutionModel.FilePath, BuildType.Restore);
}
private async Task OnSolutionExplorerPanelOnFileSelected(SharpIdeFile file, SharpIdeFileLinePosition? fileLinePosition)
{
await _codeEditorPanel.SetSharpIdeFile(file, fileLinePosition);
_navigationHistoryService.RecordNavigation(file, fileLinePosition ?? new SharpIdeFileLinePosition(0, 0));
}
public void SetSlnFilePath(string path)
{
_ = Task.GodotRun(async () =>
{
GD.Print($"Selected: {path}");
var timer = Stopwatch.StartNew();
var solutionModel = await VsPersistenceMapper.GetSolutionModel(path); // TODO: Probably refactor into a DI Service
timer.Stop();
await _nodeReadyTcs.Task;
// Do not use injected services until after _nodeReadyTcs - Services aren't injected until _Ready
_logger.LogInformation("Solution model fully created in {ElapsedMilliseconds} ms", timer.ElapsedMilliseconds);
_sharpIdeSolutionAccessor.SolutionModel = solutionModel;
_sharpIdeSolutionAccessor.SolutionReadyTcs.SetResult();
_solutionExplorerPanel.SolutionModel = solutionModel;
_codeEditorPanel.Solution = solutionModel;
_searchWindow.Solution = solutionModel;
_searchAllFilesWindow.Solution = solutionModel;
_fileExternalChangeHandler.SolutionModel = solutionModel;
_fileChangedService.SolutionModel = solutionModel;
_sharpIdeSolutionModificationService.SolutionModel = solutionModel;
_ = Task.GodotRun(_solutionExplorerPanel.BindToSolution);
_roslynAnalysis.StartLoadingSolutionInWorkspace(solutionModel);
_fileWatcher.StartWatching(solutionModel);
var previousTabs = Singletons.AppState.RecentSlns.Single(s => s.FilePath == solutionModel.FilePath).IdeSolutionState.OpenTabs;
var filesToOpen = previousTabs
.Select(s => (solutionModel.AllFiles.GetValueOrDefault(s.FilePath), new SharpIdeFileLinePosition(s.CaretLine, s.CaretColumn), s.IsSelected))
.Where(s => s.Item1 is not null)
.OfType<(SharpIdeFile file, SharpIdeFileLinePosition linePosition, bool isSelected)>()
.ToList();
await this.InvokeDeferredAsync(async () =>
{
// Preserves order of tabs
foreach (var (file, linePosition, isSelected) in filesToOpen)
{
await GodotGlobalEvents.Instance.FileExternallySelected.InvokeParallelAsync(file, linePosition);
}
_navigationHistoryService.StartRecording();
// Select the selected tab
var selectedFile = filesToOpen.SingleOrDefault(f => f.isSelected);
if (selectedFile.file is not null) await GodotGlobalEvents.Instance.FileExternallySelected.InvokeParallelAsync(selectedFile.file, selectedFile.linePosition);
});
var tasks = solutionModel.AllProjects.Select(p => p.MsBuildEvaluationProjectTask).ToList();
await Task.WhenAll(tasks).ConfigureAwait(false);
var runnableProjects = solutionModel.AllProjects.Where(p => p.IsRunnable).ToList();
await this.InvokeAsync(() =>
{
var runMenuPopupVbox = _runMenuPopup.GetNode<VBoxContainer>("MarginContainer/VBoxContainer");
foreach (var project in runnableProjects)
{
var runMenuItem = _runMenuItemScene.Instantiate<RunMenuItem>();
runMenuItem.Project = project;
runMenuPopupVbox.AddChild(runMenuItem);
}
_runMenuButton.Disabled = false;
});
});
}
public override void _UnhandledKeyInput(InputEvent @event)
{
if (@event.IsActionPressed(InputStringNames.FindInFiles))
{
_searchWindow.Popup();
}
else if (@event.IsActionPressed(InputStringNames.FindFiles))
{
_searchAllFilesWindow.Popup();
}
}
}