From fc9e6ef3ec34beccd536155d59e6da75c2c818a8 Mon Sep 17 00:00:00 2001 From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> Date: Sun, 18 Jan 2026 16:49:03 +1000 Subject: [PATCH] Build project before running --- .../Features/Analysis/RoslynAnalysis.cs | 6 +++--- src/SharpIDE.Application/Features/Build/BuildService.cs | 7 ++++--- src/SharpIDE.Application/Features/Run/RunService.cs | 8 ++++++-- src/SharpIDE.Godot/Features/Build/BuildPanel.cs | 2 +- .../Features/TestExplorer/TestExplorerPanel.cs | 4 ++-- src/SharpIDE.Godot/IdeRoot.cs | 5 +++-- src/SharpIDE.Photino/Components/BuildOutputDisplay.razor | 2 +- src/SharpIDE.Photino/Layout/MainLayout.razor | 2 +- 8 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs index 8f55966..1aa05e8 100644 --- a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs +++ b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs @@ -120,7 +120,7 @@ public partial class RoslynAnalysis(ILogger logger, BuildService using (var ___ = SharpIdeOtel.Source.StartActivity("RestoreSolution")) { // MsBuildProjectLoader doesn't do a restore which is absolutely required for resolving PackageReferences, if they have changed. I am guessing it just reads from project.assets.json - await _buildService.MsBuildAsync(_sharpIdeSolutionModel.FilePath, BuildType.Restore, cancellationToken); + await _buildService.MsBuildAsync(_sharpIdeSolutionModel.FilePath, BuildType.Restore, BuildStartedFlags.UserFacing, cancellationToken); } using (var ___ = SharpIdeOtel.Source.StartActivity("OpenSolution")) { @@ -187,7 +187,7 @@ public partial class RoslynAnalysis(ILogger logger, BuildService Guard.Against.Null(_msBuildProjectLoader, nameof(_msBuildProjectLoader)); // It is important to note that a Workspace has no concept of MSBuild, nuget packages etc. It is just told about project references and "metadata" references, which are dlls. This is the what MSBuild does - it reads the csproj, and most importantly resolves nuget package references to dlls - await _buildService.MsBuildAsync(_sharpIdeSolutionModel!.FilePath, BuildType.Restore, cancellationToken); + await _buildService.MsBuildAsync(_sharpIdeSolutionModel!.FilePath, BuildType.Restore, BuildStartedFlags.UserFacing, cancellationToken); var __ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.MSBuildProjectLoader.LoadSolutionInfoAsync"); // This call is the expensive part - MSBuild is slow. There doesn't seem to be any incrementalism for solutions. // The best we could do to speed it up is do .LoadProjectInfoAsync for the single project, and somehow munge that into the existing solution @@ -214,7 +214,7 @@ public partial class RoslynAnalysis(ILogger logger, BuildService Guard.Against.Null(_workspace, nameof(_workspace)); Guard.Against.Null(_msBuildProjectLoader, nameof(_msBuildProjectLoader)); - await _buildService.MsBuildAsync(_sharpIdeSolutionModel!.FilePath, BuildType.Restore, cancellationToken); + await _buildService.MsBuildAsync(_sharpIdeSolutionModel!.FilePath, BuildType.Restore, BuildStartedFlags.Internal, cancellationToken); var __ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.{nameof(CustomMsBuildProjectLoader)}.{nameof(CustomMsBuildProjectLoader.LoadProjectInfosAsync)}"); var thisProject = GetProjectForSharpIdeProjectModel(projectModel); diff --git a/src/SharpIDE.Application/Features/Build/BuildService.cs b/src/SharpIDE.Application/Features/Build/BuildService.cs index b36cb1e..5099ed9 100644 --- a/src/SharpIDE.Application/Features/Build/BuildService.cs +++ b/src/SharpIDE.Application/Features/Build/BuildService.cs @@ -16,15 +16,16 @@ public enum BuildType Clean, Restore } +public enum BuildStartedFlags { UserFacing = 0, Internal } public class BuildService(ILogger logger) { private readonly ILogger _logger = logger; - public EventWrapper BuildStarted { get; } = new(() => Task.CompletedTask); + public EventWrapper BuildStarted { get; } = new(_ => Task.CompletedTask); public EventWrapper BuildFinished { get; } = new(() => Task.CompletedTask); public ChannelTextWriter BuildTextWriter { get; } = new ChannelTextWriter(); private CancellationTokenSource? _cancellationTokenSource; - public async Task MsBuildAsync(string solutionOrProjectFilePath, BuildType buildType = BuildType.Build, CancellationToken cancellationToken = default) + public async Task MsBuildAsync(string solutionOrProjectFilePath, BuildType buildType = BuildType.Build, BuildStartedFlags buildStartedFlags = BuildStartedFlags.UserFacing, CancellationToken cancellationToken = default) { if (_cancellationTokenSource is not null) throw new InvalidOperationException("A build is already in progress."); _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); @@ -55,7 +56,7 @@ public class BuildService(ILogger logger) hostServices: null, flags: BuildRequestDataFlags.None); - BuildStarted.InvokeParallelFireAndForget(); + BuildStarted.InvokeParallelFireAndForget(buildStartedFlags); var timer = Stopwatch.StartNew(); var buildResult = await BuildManager.DefaultBuildManager.BuildAsync(buildParameters, buildRequest, _cancellationTokenSource.Token).ConfigureAwait(false); timer.Stop(); diff --git a/src/SharpIDE.Application/Features/Run/RunService.cs b/src/SharpIDE.Application/Features/Run/RunService.cs index 9a01de7..ee71f39 100644 --- a/src/SharpIDE.Application/Features/Run/RunService.cs +++ b/src/SharpIDE.Application/Features/Run/RunService.cs @@ -5,6 +5,7 @@ using AsyncReadProcess; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.Shared.VSCodeDebugProtocol.Messages; using SharpIDE.Application.Features.Analysis; +using SharpIDE.Application.Features.Build; using SharpIDE.Application.Features.Debugging; using SharpIDE.Application.Features.Evaluation; using SharpIDE.Application.Features.Events; @@ -14,13 +15,14 @@ using Breakpoint = SharpIDE.Application.Features.Debugging.Breakpoint; namespace SharpIDE.Application.Features.Run; -public partial class RunService(ILogger logger, RoslynAnalysis roslynAnalysis) +public partial class RunService(ILogger logger, RoslynAnalysis roslynAnalysis, BuildService buildService) { private readonly ConcurrentDictionary _projectLocks = []; private Debugger? _debugger; // TODO: Support multiple debuggers for multiple running projects private readonly ILogger _logger = logger; private readonly RoslynAnalysis _roslynAnalysis = roslynAnalysis; + private readonly BuildService _buildService = buildService; public async Task RunProject(SharpIdeProjectModel project, bool isDebug = false, DebuggerExecutableInfo? debuggerExecutableInfo = null) { @@ -32,8 +34,10 @@ public partial class RunService(ILogger logger, RoslynAnalysis rosly var waitResult = await semaphoreSlim.WaitAsync(0).ConfigureAwait(false); if (waitResult is false) throw new InvalidOperationException($"Project {project.Name} is already running."); if (project.RunningCancellationTokenSource is not null) throw new InvalidOperationException($"Project {project.Name} is already running with a cancellation token source."); - project.RunningCancellationTokenSource = new CancellationTokenSource(); + + await _buildService.MsBuildAsync(project.FilePath); + var launchProfiles = await LaunchSettingsParser.GetLaunchSettingsProfiles(project); var launchProfile = launchProfiles.FirstOrDefault(); try diff --git a/src/SharpIDE.Godot/Features/Build/BuildPanel.cs b/src/SharpIDE.Godot/Features/Build/BuildPanel.cs index 56ef6f9..5650400 100644 --- a/src/SharpIDE.Godot/Features/Build/BuildPanel.cs +++ b/src/SharpIDE.Godot/Features/Build/BuildPanel.cs @@ -27,7 +27,7 @@ public partial class BuildPanel : Control } } - private async Task OnBuildStarted() + private async Task OnBuildStarted(BuildStartedFlags _) { await this.InvokeAsync(() => _terminal.Clear()); _buildOutputChannelReader ??= _buildService.BuildTextWriter.ConsoleChannel.Reader; diff --git a/src/SharpIDE.Godot/Features/TestExplorer/TestExplorerPanel.cs b/src/SharpIDE.Godot/Features/TestExplorer/TestExplorerPanel.cs index 86187ce..d441359 100644 --- a/src/SharpIDE.Godot/Features/TestExplorer/TestExplorerPanel.cs +++ b/src/SharpIDE.Godot/Features/TestExplorer/TestExplorerPanel.cs @@ -44,7 +44,7 @@ public partial class TestExplorerPanel : Control var solution = _solutionAccessor.SolutionModel!; if (withBuild) { - await _buildService.MsBuildAsync(solution.FilePath); + await _buildService.MsBuildAsync(solution.FilePath, buildStartedFlags: BuildStartedFlags.Internal); } var testNodes = await _testRunnerService.DiscoverTests(solution); var scenes = testNodes.Select(s => @@ -70,7 +70,7 @@ public partial class TestExplorerPanel : Control { await _solutionAccessor.SolutionReadyTcs.Task; var solution = _solutionAccessor.SolutionModel!; - await _buildService.MsBuildAsync(solution.FilePath); + await _buildService.MsBuildAsync(solution.FilePath, buildStartedFlags: BuildStartedFlags.Internal); await this.InvokeAsync(() => _testNodesVBoxContainer.QueueFreeChildren()); _testNodeEntryNodes.Clear(); await _testRunnerService.RunTestsAsync(solution, HandleTestNodeUpdates); diff --git a/src/SharpIDE.Godot/IdeRoot.cs b/src/SharpIDE.Godot/IdeRoot.cs index 746fd8a..f90b8a6 100644 --- a/src/SharpIDE.Godot/IdeRoot.cs +++ b/src/SharpIDE.Godot/IdeRoot.cs @@ -98,10 +98,11 @@ public partial class IdeRoot : Control _nodeReadyTcs.SetResult(); } - private async Task OnBuildStarted() => await OnBuildRunningStateChanged(true); + private async Task OnBuildStarted(BuildStartedFlags flags) => await OnBuildRunningStateChanged(true, flags); private async Task OnBuildFinished() => await OnBuildRunningStateChanged(false); - private async Task OnBuildRunningStateChanged(bool running) + private async Task OnBuildRunningStateChanged(bool running, BuildStartedFlags? flags = null) { + if (running && flags is BuildStartedFlags.UserFacing) GodotGlobalEvents.Instance.BottomPanelTabExternallySelected.InvokeParallelFireAndForget(BottomPanelType.Build); await this.InvokeAsync(() => { _cancelMsBuildActionButton.Disabled = !running; diff --git a/src/SharpIDE.Photino/Components/BuildOutputDisplay.razor b/src/SharpIDE.Photino/Components/BuildOutputDisplay.razor index 761bf3f..a2d0850 100644 --- a/src/SharpIDE.Photino/Components/BuildOutputDisplay.razor +++ b/src/SharpIDE.Photino/Components/BuildOutputDisplay.razor @@ -28,7 +28,7 @@ }); } - private async Task ClearPreviousOutput() + private async Task ClearPreviousOutput(BuildStartedFlags _) { await _terminalDisplayRef.Clear(); await InvokeAsync(StateHasChanged); diff --git a/src/SharpIDE.Photino/Layout/MainLayout.razor b/src/SharpIDE.Photino/Layout/MainLayout.razor index e88688c..93facac 100644 --- a/src/SharpIDE.Photino/Layout/MainLayout.razor +++ b/src/SharpIDE.Photino/Layout/MainLayout.razor @@ -197,7 +197,7 @@ { if (AppState.IdeSettings.OpenTerminalOnBuildRebuildRestore) SelectBottomPanel(BottomPanelType.Build); _cancellationTokenSource = new CancellationTokenSource(); - await BuildService.MsBuildAsync(_solutionFilePath!, buildType, _cancellationTokenSource.Token); + await BuildService.MsBuildAsync(_solutionFilePath!, buildType, BuildStartedFlags.UserFacing, _cancellationTokenSource.Token); _cancellationTokenSource = null; } private async Task CancelBuild() => await _cancellationTokenSource!.CancelAsync();