207 lines
9.1 KiB
C#
207 lines
9.1 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 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);
|
|
_solutionExplorerPanel.SolutionModel = solutionModel;
|
|
_codeEditorPanel.Solution = solutionModel;
|
|
_bottomPanelManager.Solution = solutionModel;
|
|
_searchWindow.Solution = solutionModel;
|
|
_searchAllFilesWindow.Solution = solutionModel;
|
|
_fileExternalChangeHandler.SolutionModel = solutionModel;
|
|
_fileChangedService.SolutionModel = solutionModel;
|
|
_sharpIdeSolutionModificationService.SolutionModel = solutionModel;
|
|
Callable.From(_solutionExplorerPanel.BindToSolution).CallDeferred();
|
|
_roslynAnalysis.StartSolutionAnalysis(solutionModel);
|
|
_fileWatcher.StartWatching(solutionModel);
|
|
|
|
var previousTabs = Singletons.AppState.RecentSlns.Single(s => s.FilePath == solutionModel.FilePath).IdeSolutionState.OpenTabs;
|
|
var filesToOpen = previousTabs
|
|
.Select(s => (solutionModel.AllFiles.Single(f => f.Path == s.FilePath), new SharpIdeFileLinePosition(s.CaretLine, s.CaretColumn), s.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.ClearHistory();
|
|
// Select the selected tab
|
|
var selectedFile = filesToOpen.SingleOrDefault(f => f.IsSelected);
|
|
if (selectedFile.Item1 is not null) await GodotGlobalEvents.Instance.FileExternallySelected.InvokeParallelAsync(selectedFile.Item1, selectedFile.Item2);
|
|
});
|
|
|
|
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();
|
|
}
|
|
}
|
|
} |