Browse Source

Merge pull request #43028 from neikeq/rework-csharp-build-panel

C#: Re-work solution build output panel
Rémi Verschelde 4 years ago
parent
commit
d5073c6b4c

+ 20 - 45
modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs

@@ -15,14 +15,14 @@ namespace GodotTools.BuildLogger
         public void Initialize(IEventSource eventSource)
         {
             if (null == Parameters)
-                throw new LoggerException("Log directory was not set.");
+                throw new LoggerException("Log directory parameter not specified.");
 
             var parameters = Parameters.Split(new[] { ';' });
 
             string logDir = parameters[0];
 
             if (string.IsNullOrEmpty(logDir))
-                throw new LoggerException("Log directory was not set.");
+                throw new LoggerException("Log directory parameter is empty.");
 
             if (parameters.Length > 1)
                 throw new LoggerException("Too many parameters passed.");
@@ -51,22 +51,31 @@ namespace GodotTools.BuildLogger
                 {
                     throw new LoggerException("Failed to create log file: " + ex.Message);
                 }
-                else
-                {
-                    // Unexpected failure
-                    throw;
-                }
+
+                // Unexpected failure
+                throw;
             }
 
             eventSource.ProjectStarted += eventSource_ProjectStarted;
-            eventSource.TaskStarted += eventSource_TaskStarted;
+            eventSource.ProjectFinished += eventSource_ProjectFinished;
             eventSource.MessageRaised += eventSource_MessageRaised;
             eventSource.WarningRaised += eventSource_WarningRaised;
             eventSource.ErrorRaised += eventSource_ErrorRaised;
-            eventSource.ProjectFinished += eventSource_ProjectFinished;
         }
 
-        void eventSource_ErrorRaised(object sender, BuildErrorEventArgs e)
+        private void eventSource_ProjectStarted(object sender, ProjectStartedEventArgs e)
+        {
+            WriteLine(e.Message);
+            indent++;
+        }
+
+        private void eventSource_ProjectFinished(object sender, ProjectFinishedEventArgs e)
+        {
+            indent--;
+            WriteLine(e.Message);
+        }
+
+        private void eventSource_ErrorRaised(object sender, BuildErrorEventArgs e)
         {
             string line = $"{e.File}({e.LineNumber},{e.ColumnNumber}): error {e.Code}: {e.Message}";
 
@@ -81,7 +90,7 @@ namespace GodotTools.BuildLogger
             issuesStreamWriter.WriteLine(errorLine);
         }
 
-        void eventSource_WarningRaised(object sender, BuildWarningEventArgs e)
+        private void eventSource_WarningRaised(object sender, BuildWarningEventArgs e)
         {
             string line = $"{e.File}({e.LineNumber},{e.ColumnNumber}): warning {e.Code}: {e.Message}";
 
@@ -108,40 +117,6 @@ namespace GodotTools.BuildLogger
             }
         }
 
-        private void eventSource_TaskStarted(object sender, TaskStartedEventArgs e)
-        {
-            // TaskStartedEventArgs adds ProjectFile, TaskFile, TaskName
-            // To keep this log clean, this logger will ignore these events.
-        }
-
-        private void eventSource_ProjectStarted(object sender, ProjectStartedEventArgs e)
-        {
-            WriteLine(e.Message);
-            indent++;
-        }
-
-        private void eventSource_ProjectFinished(object sender, ProjectFinishedEventArgs e)
-        {
-            indent--;
-            WriteLine(e.Message);
-        }
-
-        /// <summary>
-        /// Write a line to the log, adding the SenderName
-        /// </summary>
-        private void WriteLineWithSender(string line, BuildEventArgs e)
-        {
-            if (0 == string.Compare(e.SenderName, "MSBuild", StringComparison.OrdinalIgnoreCase))
-            {
-                // Well, if the sender name is MSBuild, let's leave it out for prettiness
-                WriteLine(line);
-            }
-            else
-            {
-                WriteLine(e.SenderName + ": " + line);
-            }
-        }
-
         /// <summary>
         /// Write a line to the log, adding the SenderName and Message
         /// (these parameters are on all MSBuild event argument objects)

+ 0 - 339
modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs

@@ -1,339 +0,0 @@
-using Godot;
-using System;
-using System.IO;
-using Godot.Collections;
-using GodotTools.Internals;
-using static GodotTools.Internals.Globals;
-using File = GodotTools.Utils.File;
-using Path = System.IO.Path;
-
-namespace GodotTools
-{
-    public class BottomPanel : VBoxContainer
-    {
-        private EditorInterface editorInterface;
-
-        private TabContainer panelTabs;
-
-        private VBoxContainer panelBuildsTab;
-
-        private ItemList buildTabsList;
-        private TabContainer buildTabs;
-
-        private Button warningsBtn;
-        private Button errorsBtn;
-        private Button viewLogBtn;
-
-        private void _UpdateBuildTab(int index, int? currentTab)
-        {
-            var tab = (BuildTab)buildTabs.GetChild(index);
-
-            string itemName = Path.GetFileNameWithoutExtension(tab.BuildInfo.Solution);
-            itemName += " [" + tab.BuildInfo.Configuration + "]";
-
-            buildTabsList.AddItem(itemName, tab.IconTexture);
-
-            string itemTooltip = "Solution: " + tab.BuildInfo.Solution;
-            itemTooltip += "\nConfiguration: " + tab.BuildInfo.Configuration;
-            itemTooltip += "\nStatus: ";
-
-            if (tab.BuildExited)
-                itemTooltip += tab.BuildResult == BuildTab.BuildResults.Success ? "Succeeded" : "Errored";
-            else
-                itemTooltip += "Running";
-
-            if (!tab.BuildExited || tab.BuildResult == BuildTab.BuildResults.Error)
-                itemTooltip += $"\nErrors: {tab.ErrorCount}";
-
-            itemTooltip += $"\nWarnings: {tab.WarningCount}";
-
-            buildTabsList.SetItemTooltip(index, itemTooltip);
-
-            // If this tab was already selected before the changes or if no tab was selected
-            if (currentTab == null || currentTab == index)
-            {
-                buildTabsList.Select(index);
-                _BuildTabsItemSelected(index);
-            }
-        }
-
-        private void _UpdateBuildTabsList()
-        {
-            buildTabsList.Clear();
-
-            int? currentTab = buildTabs.CurrentTab;
-
-            if (currentTab < 0 || currentTab >= buildTabs.GetTabCount())
-                currentTab = null;
-
-            for (int i = 0; i < buildTabs.GetChildCount(); i++)
-                _UpdateBuildTab(i, currentTab);
-        }
-
-        public BuildTab GetBuildTabFor(BuildInfo buildInfo)
-        {
-            foreach (var buildTab in new Array<BuildTab>(buildTabs.GetChildren()))
-            {
-                if (buildTab.BuildInfo.Equals(buildInfo))
-                    return buildTab;
-            }
-
-            var newBuildTab = new BuildTab(buildInfo);
-            AddBuildTab(newBuildTab);
-
-            return newBuildTab;
-        }
-
-        private void _BuildTabsItemSelected(int idx)
-        {
-            if (idx < 0 || idx >= buildTabs.GetTabCount())
-                throw new IndexOutOfRangeException();
-
-            buildTabs.CurrentTab = idx;
-            if (!buildTabs.Visible)
-                buildTabs.Visible = true;
-
-            warningsBtn.Visible = true;
-            errorsBtn.Visible = true;
-            viewLogBtn.Visible = true;
-        }
-
-        private void _BuildTabsNothingSelected()
-        {
-            if (buildTabs.GetTabCount() != 0)
-            {
-                // just in case
-                buildTabs.Visible = false;
-
-                // This callback is called when clicking on the empty space of the list.
-                // ItemList won't deselect the items automatically, so we must do it ourselves.
-                buildTabsList.UnselectAll();
-            }
-
-            warningsBtn.Visible = false;
-            errorsBtn.Visible = false;
-            viewLogBtn.Visible = false;
-        }
-
-        private void _WarningsToggled(bool pressed)
-        {
-            int currentTab = buildTabs.CurrentTab;
-
-            if (currentTab < 0 || currentTab >= buildTabs.GetTabCount())
-                throw new InvalidOperationException("No tab selected");
-
-            var buildTab = (BuildTab)buildTabs.GetChild(currentTab);
-            buildTab.WarningsVisible = pressed;
-            buildTab.UpdateIssuesList();
-        }
-
-        private void _ErrorsToggled(bool pressed)
-        {
-            int currentTab = buildTabs.CurrentTab;
-
-            if (currentTab < 0 || currentTab >= buildTabs.GetTabCount())
-                throw new InvalidOperationException("No tab selected");
-
-            var buildTab = (BuildTab)buildTabs.GetChild(currentTab);
-            buildTab.ErrorsVisible = pressed;
-            buildTab.UpdateIssuesList();
-        }
-
-        public void BuildProjectPressed()
-        {
-            if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
-                return; // No solution to build
-
-            string editorScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor");
-            string playerScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor_player");
-
-            CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, editorScriptsMetadataPath);
-
-            if (File.Exists(editorScriptsMetadataPath))
-            {
-                try
-                {
-                    File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath);
-                }
-                catch (IOException e)
-                {
-                    GD.PushError($"Failed to copy scripts metadata file. Exception message: {e.Message}");
-                    return;
-                }
-            }
-
-            bool buildSuccess = BuildManager.BuildProjectBlocking("Debug");
-
-            if (!buildSuccess)
-                return;
-
-            // Notify running game for hot-reload
-            Internal.EditorDebuggerNodeReloadScripts();
-
-            // Hot-reload in the editor
-            GodotSharpEditor.Instance.GetNode<HotReloadAssemblyWatcher>("HotReloadAssemblyWatcher").RestartTimer();
-
-            if (Internal.IsAssembliesReloadingNeeded())
-                Internal.ReloadAssemblies(softReload: false);
-        }
-
-        private void _ViewLogPressed()
-        {
-            if (!buildTabsList.IsAnythingSelected())
-                return;
-
-            var selectedItems = buildTabsList.GetSelectedItems();
-
-            if (selectedItems.Length != 1)
-                throw new InvalidOperationException($"Expected 1 selected item, got {selectedItems.Length}");
-
-            int selectedItem = selectedItems[0];
-
-            var buildTab = (BuildTab)buildTabs.GetTabControl(selectedItem);
-
-            OS.ShellOpen(Path.Combine(buildTab.BuildInfo.LogsDirPath, BuildManager.MsBuildLogFileName));
-        }
-
-        public override void _Notification(int what)
-        {
-            base._Notification(what);
-
-            if (what == EditorSettings.NotificationEditorSettingsChanged)
-            {
-                var editorBaseControl = editorInterface.GetBaseControl();
-                panelTabs.AddThemeStyleboxOverride("panel", editorBaseControl.GetThemeStylebox("DebuggerPanel", "EditorStyles"));
-                panelTabs.AddThemeStyleboxOverride("tab_fg", editorBaseControl.GetThemeStylebox("DebuggerTabFG", "EditorStyles"));
-                panelTabs.AddThemeStyleboxOverride("tab_bg", editorBaseControl.GetThemeStylebox("DebuggerTabBG", "EditorStyles"));
-            }
-        }
-
-        public void AddBuildTab(BuildTab buildTab)
-        {
-            buildTabs.AddChild(buildTab);
-            RaiseBuildTab(buildTab);
-        }
-
-        public void RaiseBuildTab(BuildTab buildTab)
-        {
-            if (buildTab.GetParent() != buildTabs)
-                throw new InvalidOperationException("Build tab is not in the tabs list");
-
-            buildTabs.MoveChild(buildTab, 0);
-            _UpdateBuildTabsList();
-        }
-
-        public void ShowBuildTab()
-        {
-            for (int i = 0; i < panelTabs.GetTabCount(); i++)
-            {
-                if (panelTabs.GetTabControl(i) == panelBuildsTab)
-                {
-                    panelTabs.CurrentTab = i;
-                    GodotSharpEditor.Instance.MakeBottomPanelItemVisible(this);
-                    return;
-                }
-            }
-
-            GD.PushError("Builds tab not found");
-        }
-
-        public override void _Ready()
-        {
-            base._Ready();
-
-            editorInterface = GodotSharpEditor.Instance.GetEditorInterface();
-
-            var editorBaseControl = editorInterface.GetBaseControl();
-
-            SizeFlagsVertical = (int)SizeFlags.ExpandFill;
-            SetAnchorsAndMarginsPreset(LayoutPreset.Wide);
-
-            panelTabs = new TabContainer
-            {
-                TabAlign = TabContainer.TabAlignEnum.Left,
-                RectMinSize = new Vector2(0, 228) * EditorScale,
-                SizeFlagsVertical = (int)SizeFlags.ExpandFill
-            };
-            panelTabs.AddThemeStyleboxOverride("panel", editorBaseControl.GetThemeStylebox("DebuggerPanel", "EditorStyles"));
-            panelTabs.AddThemeStyleboxOverride("tab_fg", editorBaseControl.GetThemeStylebox("DebuggerTabFG", "EditorStyles"));
-            panelTabs.AddThemeStyleboxOverride("tab_bg", editorBaseControl.GetThemeStylebox("DebuggerTabBG", "EditorStyles"));
-            AddChild(panelTabs);
-
-            {
-                // Builds tab
-                panelBuildsTab = new VBoxContainer
-                {
-                    Name = "Builds".TTR(),
-                    SizeFlagsHorizontal = (int)SizeFlags.ExpandFill
-                };
-                panelTabs.AddChild(panelBuildsTab);
-
-                var toolBarHBox = new HBoxContainer {SizeFlagsHorizontal = (int)SizeFlags.ExpandFill};
-                panelBuildsTab.AddChild(toolBarHBox);
-
-                var buildProjectBtn = new Button
-                {
-                    Text = "Build Project".TTR(),
-                    FocusMode = FocusModeEnum.None
-                };
-                buildProjectBtn.PressedSignal += BuildProjectPressed;
-                toolBarHBox.AddChild(buildProjectBtn);
-
-                toolBarHBox.AddSpacer(begin: false);
-
-                warningsBtn = new Button
-                {
-                    Text = "Warnings".TTR(),
-                    ToggleMode = true,
-                    Pressed = true,
-                    Visible = false,
-                    FocusMode = FocusModeEnum.None
-                };
-                warningsBtn.Toggled += _WarningsToggled;
-                toolBarHBox.AddChild(warningsBtn);
-
-                errorsBtn = new Button
-                {
-                    Text = "Errors".TTR(),
-                    ToggleMode = true,
-                    Pressed = true,
-                    Visible = false,
-                    FocusMode = FocusModeEnum.None
-                };
-                errorsBtn.Toggled += _ErrorsToggled;
-                toolBarHBox.AddChild(errorsBtn);
-
-                toolBarHBox.AddSpacer(begin: false);
-
-                viewLogBtn = new Button
-                {
-                    Text = "View log".TTR(),
-                    FocusMode = FocusModeEnum.None,
-                    Visible = false
-                };
-                viewLogBtn.PressedSignal += _ViewLogPressed;
-                toolBarHBox.AddChild(viewLogBtn);
-
-                var hsc = new HSplitContainer
-                {
-                    SizeFlagsHorizontal = (int)SizeFlags.ExpandFill,
-                    SizeFlagsVertical = (int)SizeFlags.ExpandFill
-                };
-                panelBuildsTab.AddChild(hsc);
-
-                buildTabsList = new ItemList {SizeFlagsHorizontal = (int)SizeFlags.ExpandFill};
-                buildTabsList.ItemSelected += _BuildTabsItemSelected;
-                buildTabsList.NothingSelected += _BuildTabsNothingSelected;
-                hsc.AddChild(buildTabsList);
-
-                buildTabs = new TabContainer
-                {
-                    TabAlign = TabContainer.TabAlignEnum.Left,
-                    SizeFlagsHorizontal = (int)SizeFlags.ExpandFill,
-                    TabsVisible = false
-                };
-                hsc.AddChild(buildTabs);
-            }
-        }
-    }
-}

+ 8 - 2
modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs → modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs

@@ -4,7 +4,7 @@ using Godot.Collections;
 using GodotTools.Internals;
 using Path = System.IO.Path;
 
-namespace GodotTools
+namespace GodotTools.Build
 {
     [Serializable]
     public sealed class BuildInfo : Reference // TODO Remove Reference once we have proper serialization
@@ -20,7 +20,9 @@ namespace GodotTools
         public override bool Equals(object obj)
         {
             if (obj is BuildInfo other)
-                return other.Solution == Solution && other.Configuration == Configuration;
+                return other.Solution == Solution && other.Targets == Targets &&
+                       other.Configuration == Configuration && other.Restore == Restore &&
+                       other.CustomProperties == CustomProperties && other.LogsDirPath == LogsDirPath;
 
             return false;
         }
@@ -31,7 +33,11 @@ namespace GodotTools
             {
                 int hash = 17;
                 hash = hash * 29 + Solution.GetHashCode();
+                hash = hash * 29 + Targets.GetHashCode();
                 hash = hash * 29 + Configuration.GetHashCode();
+                hash = hash * 29 + Restore.GetHashCode();
+                hash = hash * 29 + CustomProperties.GetHashCode();
+                hash = hash * 29 + LogsDirPath.GetHashCode();
                 return hash;
             }
         }

+ 78 - 45
modules/mono/editor/GodotTools/GodotTools/BuildManager.cs → modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs

@@ -1,20 +1,19 @@
 using System;
-using System.Collections.Generic;
 using System.IO;
 using System.Threading.Tasks;
-using GodotTools.Build;
 using GodotTools.Ides.Rider;
 using GodotTools.Internals;
-using GodotTools.Utils;
 using JetBrains.Annotations;
 using static GodotTools.Internals.Globals;
 using File = GodotTools.Utils.File;
+using OS = GodotTools.Utils.OS;
+using Path = System.IO.Path;
 
-namespace GodotTools
+namespace GodotTools.Build
 {
     public static class BuildManager
     {
-        private static readonly List<BuildInfo> BuildsInProgress = new List<BuildInfo>();
+        private static BuildInfo _buildInProgress;
 
         public const string PropNameMSBuildMono = "MSBuild (Mono)";
         public const string PropNameMSBuildVs = "MSBuild (VS Build Tools)";
@@ -24,6 +23,14 @@ namespace GodotTools
         public const string MsBuildIssuesFileName = "msbuild_issues.csv";
         public const string MsBuildLogFileName = "msbuild_log.txt";
 
+        public delegate void BuildLaunchFailedEventHandler(BuildInfo buildInfo, string reason);
+
+        public static event BuildLaunchFailedEventHandler BuildLaunchFailed;
+        public static event Action<BuildInfo> BuildStarted;
+        public static event Action<BuildResult> BuildFinished;
+        public static event Action<string> StdOutputReceived;
+        public static event Action<string> StdErrorReceived;
+
         private static void RemoveOldIssuesFile(BuildInfo buildInfo)
         {
             var issuesFile = GetIssuesFilePath(buildInfo);
@@ -36,12 +43,13 @@ namespace GodotTools
 
         private static void ShowBuildErrorDialog(string message)
         {
-            GodotSharpEditor.Instance.ShowErrorDialog(message, "Build error");
-            GodotSharpEditor.Instance.BottomPanel.ShowBuildTab();
+            var plugin = GodotSharpEditor.Instance;
+            plugin.ShowErrorDialog(message, "Build error");
+            plugin.MakeBottomPanelItemVisible(plugin.MSBuildPanel);
         }
 
-        public static void RestartBuild(BuildTab buildTab) => throw new NotImplementedException();
-        public static void StopBuild(BuildTab buildTab) => throw new NotImplementedException();
+        public static void RestartBuild(BuildOutputView buildOutputView) => throw new NotImplementedException();
+        public static void StopBuild(BuildOutputView buildOutputView) => throw new NotImplementedException();
 
         private static string GetLogFilePath(BuildInfo buildInfo)
         {
@@ -61,15 +69,14 @@ namespace GodotTools
 
         public static bool Build(BuildInfo buildInfo)
         {
-            if (BuildsInProgress.Contains(buildInfo))
+            if (_buildInProgress != null)
                 throw new InvalidOperationException("A build is already in progress");
 
-            BuildsInProgress.Add(buildInfo);
+            _buildInProgress = buildInfo;
 
             try
             {
-                BuildTab buildTab = GodotSharpEditor.Instance.BottomPanel.GetBuildTabFor(buildInfo);
-                buildTab.OnBuildStart();
+                BuildStarted?.Invoke(buildInfo);
 
                 // Required in order to update the build tasks list
                 Internal.GodotMainIteration();
@@ -80,44 +87,44 @@ namespace GodotTools
                 }
                 catch (IOException e)
                 {
-                    buildTab.OnBuildExecFailed($"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
+                    BuildLaunchFailed?.Invoke(buildInfo, $"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
                     Console.Error.WriteLine(e);
                 }
 
                 try
                 {
-                    int exitCode = BuildSystem.Build(buildInfo);
+                    int exitCode = BuildSystem.Build(buildInfo, StdOutputReceived, StdErrorReceived);
 
                     if (exitCode != 0)
                         PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}");
 
-                    buildTab.OnBuildExit(exitCode == 0 ? BuildTab.BuildResults.Success : BuildTab.BuildResults.Error);
+                    BuildFinished?.Invoke(exitCode == 0 ? BuildResult.Success : BuildResult.Error);
 
                     return exitCode == 0;
                 }
                 catch (Exception e)
                 {
-                    buildTab.OnBuildExecFailed($"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}");
+                    BuildLaunchFailed?.Invoke(buildInfo, $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}");
                     Console.Error.WriteLine(e);
                     return false;
                 }
             }
             finally
             {
-                BuildsInProgress.Remove(buildInfo);
+                _buildInProgress = null;
             }
         }
 
         public static async Task<bool> BuildAsync(BuildInfo buildInfo)
         {
-            if (BuildsInProgress.Contains(buildInfo))
+            if (_buildInProgress != null)
                 throw new InvalidOperationException("A build is already in progress");
 
-            BuildsInProgress.Add(buildInfo);
+            _buildInProgress = buildInfo;
 
             try
             {
-                BuildTab buildTab = GodotSharpEditor.Instance.BottomPanel.GetBuildTabFor(buildInfo);
+                BuildStarted?.Invoke(buildInfo);
 
                 try
                 {
@@ -125,43 +132,57 @@ namespace GodotTools
                 }
                 catch (IOException e)
                 {
-                    buildTab.OnBuildExecFailed($"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
+                    BuildLaunchFailed?.Invoke(buildInfo, $"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
                     Console.Error.WriteLine(e);
                 }
 
                 try
                 {
-                    int exitCode = await BuildSystem.BuildAsync(buildInfo);
+                    int exitCode = await BuildSystem.BuildAsync(buildInfo, StdOutputReceived, StdErrorReceived);
 
                     if (exitCode != 0)
                         PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}");
 
-                    buildTab.OnBuildExit(exitCode == 0 ? BuildTab.BuildResults.Success : BuildTab.BuildResults.Error);
+                    BuildFinished?.Invoke(exitCode == 0 ? BuildResult.Success : BuildResult.Error);
 
                     return exitCode == 0;
                 }
                 catch (Exception e)
                 {
-                    buildTab.OnBuildExecFailed($"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}");
+                    BuildLaunchFailed?.Invoke(buildInfo, $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}");
                     Console.Error.WriteLine(e);
                     return false;
                 }
             }
             finally
             {
-                BuildsInProgress.Remove(buildInfo);
+                _buildInProgress = null;
             }
         }
 
-        public static bool BuildProjectBlocking(string config, [CanBeNull] string platform = null)
+        public static bool BuildProjectBlocking(string config, [CanBeNull] string[] targets = null, [CanBeNull] string platform = null)
         {
-            if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
+            var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, targets ?? new[] {"Build"}, config, restore: true);
+
+            // If a platform was not specified, try determining the current one. If that fails, let MSBuild auto-detect it.
+            if (platform != null || OS.PlatformNameMap.TryGetValue(Godot.OS.GetName(), out platform))
+                buildInfo.CustomProperties.Add($"GodotTargetPlatform={platform}");
+
+            if (Internal.GodotIsRealTDouble())
+                buildInfo.CustomProperties.Add("GodotRealTIsDouble=true");
+
+            return BuildProjectBlocking(buildInfo);
+        }
+
+        private static bool BuildProjectBlocking(BuildInfo buildInfo)
+        {
+            if (!File.Exists(buildInfo.Solution))
                 return true; // No solution to build
 
             // Make sure the API assemblies are up to date before building the project.
             // We may not have had the chance to update the release API assemblies, and the debug ones
             // may have been deleted by the user at some point after they were loaded by the Godot editor.
-            string apiAssembliesUpdateError = Internal.UpdateApiAssembliesFromPrebuilt(config == "ExportRelease" ? "Release" : "Debug");
+            string apiAssembliesUpdateError = Internal.UpdateApiAssembliesFromPrebuilt(buildInfo.Configuration == "ExportRelease" ? "Release" : "Debug");
 
             if (!string.IsNullOrEmpty(apiAssembliesUpdateError))
             {
@@ -173,15 +194,6 @@ namespace GodotTools
             {
                 pr.Step("Building project solution", 0);
 
-                var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, targets: new[] {"Build"}, config, restore: true);
-
-                // If a platform was not specified, try determining the current one. If that fails, let MSBuild auto-detect it.
-                if (platform != null || OS.PlatformNameMap.TryGetValue(Godot.OS.GetName(), out platform))
-                    buildInfo.CustomProperties.Add($"GodotTargetPlatform={platform}");
-
-                if (Internal.GodotIsRealTDouble())
-                    buildInfo.CustomProperties.Add("GodotRealTIsDouble=true");
-
                 if (!Build(buildInfo))
                 {
                     ShowBuildErrorDialog("Failed to build project solution");
@@ -197,18 +209,41 @@ namespace GodotTools
             if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
                 return true; // No solution to build
 
+            GenerateEditorScriptMetadata();
+
+            if (GodotSharpEditor.Instance.SkipBuildBeforePlaying)
+                return true; // Requested play from an external editor/IDE which already built the project
+
+            return BuildProjectBlocking("Debug");
+        }
+
+        // NOTE: This will be replaced with C# source generators in 4.0
+        public static void GenerateEditorScriptMetadata()
+        {
             string editorScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor");
             string playerScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor_player");
 
             CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, editorScriptsMetadataPath);
 
-            if (File.Exists(editorScriptsMetadataPath))
-                File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath);
+            if (!File.Exists(editorScriptsMetadataPath))
+                return;
 
-            if (GodotSharpEditor.Instance.SkipBuildBeforePlaying)
-                return true; // Requested play from an external editor/IDE which already built the project
+            try
+            {
+                File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath);
+            }
+            catch (IOException e)
+            {
+                throw new IOException("Failed to copy scripts metadata file.", innerException: e);
+            }
+        }
 
-            return BuildProjectBlocking("Debug");
+        // NOTE: This will be replaced with C# source generators in 4.0
+        public static string GenerateExportedGameScriptMetadata(bool isDebug)
+        {
+            string scriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, $"scripts_metadata.{(isDebug ? "debug" : "release")}");
+            CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, scriptsMetadataPath);
+            return scriptsMetadataPath;
         }
 
         public static void Initialize()
@@ -254,8 +289,6 @@ namespace GodotTools
                 ["hint"] = Godot.PropertyHint.Enum,
                 ["hint_string"] = hintString
             });
-
-            EditorDef("mono/builds/print_build_output", false);
         }
     }
 }

+ 169 - 43
modules/mono/editor/GodotTools/GodotTools/BuildTab.cs → modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs

@@ -5,16 +5,10 @@ using GodotTools.Internals;
 using File = GodotTools.Utils.File;
 using Path = System.IO.Path;
 
-namespace GodotTools
+namespace GodotTools.Build
 {
-    public class BuildTab : VBoxContainer
+    public class BuildOutputView : VBoxContainer, ISerializationListener
     {
-        public enum BuildResults
-        {
-            Error,
-            Success
-        }
-
         [Serializable]
         private class BuildIssue : Reference // TODO Remove Reference once we have proper serialization
         {
@@ -29,10 +23,14 @@ namespace GodotTools
 
         private readonly Array<BuildIssue> issues = new Array<BuildIssue>(); // TODO Use List once we have proper serialization
         private ItemList issuesList;
+        private TextEdit buildLog;
+        private PopupMenu issuesListContextMenu;
 
-        public bool BuildExited { get; private set; } = false;
+        [Signal] public event Action BuildStateChanged;
 
-        public BuildResults? BuildResult { get; private set; } = null;
+        public bool HasBuildExited { get; private set; } = false;
+
+        public BuildResult? BuildResult { get; private set; } = null;
 
         public int ErrorCount { get; private set; } = 0;
 
@@ -41,23 +39,31 @@ namespace GodotTools
         public bool ErrorsVisible { get; set; } = true;
         public bool WarningsVisible { get; set; } = true;
 
-        public Texture2D IconTexture
+        public Texture2D BuildStateIcon
         {
             get
             {
-                if (!BuildExited)
+                if (!HasBuildExited)
                     return GetThemeIcon("Stop", "EditorIcons");
 
-                if (BuildResult == BuildResults.Error)
-                    return GetThemeIcon("StatusError", "EditorIcons");
+                if (BuildResult == Build.BuildResult.Error)
+                    return GetThemeIcon("Error", "EditorIcons");
+
+                if (WarningCount > 1)
+                    return GetThemeIcon("Warning", "EditorIcons");
 
-                return GetThemeIcon("StatusSuccess", "EditorIcons");
+                return null;
             }
         }
 
-        public BuildInfo BuildInfo { get; private set; }
+        private BuildInfo BuildInfo { get; set; }
 
-        private void _LoadIssuesFromFile(string csvFile)
+        public bool LogVisible
+        {
+            set => buildLog.Visible = value;
+        }
+
+        private void LoadIssuesFromFile(string csvFile)
         {
             using (var file = new Godot.File())
             {
@@ -107,7 +113,7 @@ namespace GodotTools
             }
         }
 
-        private void _IssueActivated(int idx)
+        private void IssueActivated(int idx)
         {
             if (idx < 0 || idx >= issuesList.GetItemCount())
                 throw new IndexOutOfRangeException("Item list index out of range");
@@ -190,49 +196,79 @@ namespace GodotTools
             }
         }
 
-        public void OnBuildStart()
+        private void BuildLaunchFailed(BuildInfo buildInfo, string cause)
+        {
+            HasBuildExited = true;
+            BuildResult = Build.BuildResult.Error;
+
+            issuesList.Clear();
+
+            var issue = new BuildIssue {Message = cause, Warning = false};
+
+            ErrorCount += 1;
+            issues.Add(issue);
+
+            UpdateIssuesList();
+
+            EmitSignal(nameof(BuildStateChanged));
+        }
+
+        private void BuildStarted(BuildInfo buildInfo)
         {
-            BuildExited = false;
+            BuildInfo = buildInfo;
+            HasBuildExited = false;
 
             issues.Clear();
             WarningCount = 0;
             ErrorCount = 0;
+            buildLog.Text = string.Empty;
+
             UpdateIssuesList();
 
-            GodotSharpEditor.Instance.BottomPanel.RaiseBuildTab(this);
+            EmitSignal(nameof(BuildStateChanged));
         }
 
-        public void OnBuildExit(BuildResults result)
+        private void BuildFinished(BuildResult result)
         {
-            BuildExited = true;
+            HasBuildExited = true;
             BuildResult = result;
 
-            _LoadIssuesFromFile(Path.Combine(BuildInfo.LogsDirPath, BuildManager.MsBuildIssuesFileName));
+            LoadIssuesFromFile(Path.Combine(BuildInfo.LogsDirPath, BuildManager.MsBuildIssuesFileName));
+
             UpdateIssuesList();
 
-            GodotSharpEditor.Instance.BottomPanel.RaiseBuildTab(this);
+            EmitSignal(nameof(BuildStateChanged));
         }
 
-        public void OnBuildExecFailed(string cause)
+        private void StdOutputReceived(string text)
         {
-            BuildExited = true;
-            BuildResult = BuildResults.Error;
-
-            issuesList.Clear();
+            buildLog.Text += text + "\n";
+            ScrollToLastNonEmptyLogLine();
+        }
 
-            var issue = new BuildIssue { Message = cause, Warning = false };
+        private void StdErrorReceived(string text)
+        {
+            buildLog.Text += text + "\n";
+            ScrollToLastNonEmptyLogLine();
+        }
 
-            ErrorCount += 1;
-            issues.Add(issue);
+        private void ScrollToLastNonEmptyLogLine()
+        {
+            int line;
+            for (line = buildLog.GetLineCount(); line > 0; line--)
+            {
+                string lineText = buildLog.GetLine(line);
 
-            UpdateIssuesList();
+                if (!string.IsNullOrEmpty(lineText) || !string.IsNullOrEmpty(lineText?.Trim()))
+                    break;
+            }
 
-            GodotSharpEditor.Instance.BottomPanel.RaiseBuildTab(this);
+            buildLog.CursorSetLine(line);
         }
 
         public void RestartBuild()
         {
-            if (!BuildExited)
+            if (!HasBuildExited)
                 throw new InvalidOperationException("Build already started");
 
             BuildManager.RestartBuild(this);
@@ -240,28 +276,118 @@ namespace GodotTools
 
         public void StopBuild()
         {
-            if (!BuildExited)
+            if (!HasBuildExited)
                 throw new InvalidOperationException("Build is not in progress");
 
             BuildManager.StopBuild(this);
         }
 
+        private enum IssuesContextMenuOption
+        {
+            Copy
+        }
+
+        private void IssuesListContextOptionPressed(int id)
+        {
+            switch ((IssuesContextMenuOption)id)
+            {
+                case IssuesContextMenuOption.Copy:
+                {
+                    // We don't allow multi-selection but just in case that changes later...
+                    string text = null;
+
+                    foreach (int issueIndex in issuesList.GetSelectedItems())
+                    {
+                        if (text != null)
+                            text += "\n";
+                        text += issuesList.GetItemText(issueIndex);
+                    }
+
+                    if (text != null)
+                        DisplayServer.ClipboardSet(text);
+                    break;
+                }
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(id), id, "Invalid issue context menu option");
+            }
+        }
+
+        private void IssuesListRmbSelected(int index, Vector2 atPosition)
+        {
+            _ = index; // Unused
+
+            issuesListContextMenu.Clear();
+            issuesListContextMenu.Size = new Vector2i(1, 1);
+
+            if (issuesList.IsAnythingSelected())
+            {
+                // Add menu entries for the selected item
+                issuesListContextMenu.AddIconItem(GetThemeIcon("ActionCopy", "EditorIcons"),
+                    label: "Copy Error".TTR(), (int)IssuesContextMenuOption.Copy);
+            }
+
+            if (issuesListContextMenu.GetItemCount() > 0)
+            {
+                issuesListContextMenu.Position = (Vector2i)(issuesList.RectGlobalPosition + atPosition);
+                issuesListContextMenu.Popup();
+            }
+        }
+
         public override void _Ready()
         {
             base._Ready();
 
-            issuesList = new ItemList { SizeFlagsVertical = (int)SizeFlags.ExpandFill };
-            issuesList.ItemActivated += _IssueActivated;
-            AddChild(issuesList);
+            SizeFlagsVertical = (int)SizeFlags.ExpandFill;
+
+            var hsc = new HSplitContainer
+            {
+                SizeFlagsHorizontal = (int)SizeFlags.ExpandFill,
+                SizeFlagsVertical = (int)SizeFlags.ExpandFill
+            };
+            AddChild(hsc);
+
+            issuesList = new ItemList
+            {
+                SizeFlagsVertical = (int)SizeFlags.ExpandFill,
+                SizeFlagsHorizontal = (int)SizeFlags.ExpandFill // Avoid being squashed by the build log
+            };
+            issuesList.ItemActivated += IssueActivated;
+            issuesList.AllowRmbSelect = true;
+            issuesList.ItemRmbSelected += IssuesListRmbSelected;
+            hsc.AddChild(issuesList);
+
+            issuesListContextMenu = new PopupMenu();
+            issuesListContextMenu.IdPressed += IssuesListContextOptionPressed;
+            issuesList.AddChild(issuesListContextMenu);
+
+            buildLog = new TextEdit
+            {
+                Readonly = true,
+                SizeFlagsVertical = (int)SizeFlags.ExpandFill,
+                SizeFlagsHorizontal = (int)SizeFlags.ExpandFill // Avoid being squashed by the issues list
+            };
+            hsc.AddChild(buildLog);
+
+            AddBuildEventListeners();
         }
 
-        private BuildTab()
+        private void AddBuildEventListeners()
         {
+            BuildManager.BuildLaunchFailed += BuildLaunchFailed;
+            BuildManager.BuildStarted += BuildStarted;
+            BuildManager.BuildFinished += BuildFinished;
+            // StdOutput/Error can be received from different threads, so we need to use CallDeferred
+            BuildManager.StdOutputReceived += line => CallDeferred(nameof(StdOutputReceived), line);
+            BuildManager.StdErrorReceived += line => CallDeferred(nameof(StdErrorReceived), line);
         }
 
-        public BuildTab(BuildInfo buildInfo)
+        public void OnBeforeSerialize()
         {
-            BuildInfo = buildInfo;
+        }
+
+        public void OnAfterDeserialize()
+        {
+            AddBuildEventListeners(); // Re-add them
         }
     }
 }

+ 8 - 0
modules/mono/editor/GodotTools/GodotTools/Build/BuildResult.cs

@@ -0,0 +1,8 @@
+namespace GodotTools.Build
+{
+    public enum BuildResult
+    {
+        Error,
+        Success
+    }
+}

+ 18 - 24
modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs

@@ -44,10 +44,7 @@ namespace GodotTools.Build
             }
         }
 
-        private static bool PrintBuildOutput =>
-            (bool)EditorSettings.GetSetting("mono/builds/print_build_output");
-
-        private static Process LaunchBuild(BuildInfo buildInfo)
+        private static Process LaunchBuild(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler)
         {
             (string msbuildPath, BuildTool buildTool) = MsBuildFinder.FindMsBuild();
 
@@ -58,13 +55,13 @@ namespace GodotTools.Build
 
             var startInfo = new ProcessStartInfo(msbuildPath, compilerArgs);
 
-            bool redirectOutput = !IsDebugMsBuildRequested() && !PrintBuildOutput;
-
-            if (!redirectOutput || Godot.OS.IsStdoutVerbose())
-                Console.WriteLine($"Running: \"{startInfo.FileName}\" {startInfo.Arguments}");
+            string launchMessage = $"Running: \"{startInfo.FileName}\" {startInfo.Arguments}";
+            stdOutHandler?.Invoke(launchMessage);
+            if (Godot.OS.IsStdoutVerbose())
+                Console.WriteLine(launchMessage);
 
-            startInfo.RedirectStandardOutput = redirectOutput;
-            startInfo.RedirectStandardError = redirectOutput;
+            startInfo.RedirectStandardOutput = true;
+            startInfo.RedirectStandardError = true;
             startInfo.UseShellExecute = false;
 
             if (UsingMonoMsBuildOnWindows)
@@ -82,20 +79,22 @@ namespace GodotTools.Build
 
             var process = new Process {StartInfo = startInfo};
 
+            if (stdOutHandler != null)
+                process.OutputDataReceived += (s, e) => stdOutHandler.Invoke(e.Data);
+            if (stdErrHandler != null)
+                process.ErrorDataReceived += (s, e) => stdErrHandler.Invoke(e.Data);
+
             process.Start();
 
-            if (redirectOutput)
-            {
-                process.BeginOutputReadLine();
-                process.BeginErrorReadLine();
-            }
+            process.BeginOutputReadLine();
+            process.BeginErrorReadLine();
 
             return process;
         }
 
-        public static int Build(BuildInfo buildInfo)
+        public static int Build(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler)
         {
-            using (var process = LaunchBuild(buildInfo))
+            using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler))
             {
                 process.WaitForExit();
 
@@ -103,9 +102,9 @@ namespace GodotTools.Build
             }
         }
 
-        public static async Task<int> BuildAsync(BuildInfo buildInfo)
+        public static async Task<int> BuildAsync(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler)
         {
-            using (var process = LaunchBuild(buildInfo))
+            using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler))
             {
                 await process.WaitForExitAsync();
 
@@ -152,10 +151,5 @@ namespace GodotTools.Build
             foreach (string env in platformEnvironmentVariables)
                 environmentVariables.Remove(env);
         }
-
-        private static bool IsDebugMsBuildRequested()
-        {
-            return Environment.GetEnvironmentVariable("GODOT_DEBUG_MSBUILD")?.Trim() == "1";
-        }
     }
 }

+ 165 - 0
modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs

@@ -0,0 +1,165 @@
+using System;
+using Godot;
+using GodotTools.Internals;
+using JetBrains.Annotations;
+using static GodotTools.Internals.Globals;
+using File = GodotTools.Utils.File;
+
+namespace GodotTools.Build
+{
+    public class MSBuildPanel : VBoxContainer
+    {
+        public BuildOutputView BuildOutputView { get; private set; }
+
+        private Button errorsBtn;
+        private Button warningsBtn;
+        private Button viewLogBtn;
+
+        private void WarningsToggled(bool pressed)
+        {
+            BuildOutputView.WarningsVisible = pressed;
+            BuildOutputView.UpdateIssuesList();
+        }
+
+        private void ErrorsToggled(bool pressed)
+        {
+            BuildOutputView.ErrorsVisible = pressed;
+            BuildOutputView.UpdateIssuesList();
+        }
+
+        [UsedImplicitly]
+        public void BuildSolution()
+        {
+            if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
+                return; // No solution to build
+
+            BuildManager.GenerateEditorScriptMetadata();
+
+            if (!BuildManager.BuildProjectBlocking("Debug"))
+                return; // Build failed
+
+            // Notify running game for hot-reload
+            Internal.EditorDebuggerNodeReloadScripts();
+
+            // Hot-reload in the editor
+            GodotSharpEditor.Instance.GetNode<HotReloadAssemblyWatcher>("HotReloadAssemblyWatcher").RestartTimer();
+
+            if (Internal.IsAssembliesReloadingNeeded())
+                Internal.ReloadAssemblies(softReload: false);
+        }
+
+        [UsedImplicitly]
+        private void RebuildSolution()
+        {
+            if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
+                return; // No solution to build
+
+            BuildManager.GenerateEditorScriptMetadata();
+
+            if (!BuildManager.BuildProjectBlocking("Debug", targets: new[] {"Rebuild"}))
+                return; // Build failed
+
+            // Notify running game for hot-reload
+            Internal.EditorDebuggerNodeReloadScripts();
+
+            // Hot-reload in the editor
+            GodotSharpEditor.Instance.GetNode<HotReloadAssemblyWatcher>("HotReloadAssemblyWatcher").RestartTimer();
+
+            if (Internal.IsAssembliesReloadingNeeded())
+                Internal.ReloadAssemblies(softReload: false);
+        }
+
+        [UsedImplicitly]
+        private void CleanSolution()
+        {
+            if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
+                return; // No solution to build
+
+            BuildManager.BuildProjectBlocking("Debug", targets: new[] {"Clean"});
+        }
+
+        private void ViewLogToggled(bool pressed) => BuildOutputView.LogVisible = pressed;
+
+        private void BuildMenuOptionPressed(int id)
+        {
+            switch ((BuildMenuOptions)id)
+            {
+                case BuildMenuOptions.BuildSolution:
+                    BuildSolution();
+                    break;
+                case BuildMenuOptions.RebuildSolution:
+                    RebuildSolution();
+                    break;
+                case BuildMenuOptions.CleanSolution:
+                    CleanSolution();
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(id), id, "Invalid build menu option");
+            }
+        }
+
+        private enum BuildMenuOptions
+        {
+            BuildSolution,
+            RebuildSolution,
+            CleanSolution
+        }
+
+        public override void _Ready()
+        {
+            base._Ready();
+
+            RectMinSize = new Vector2(0, 228) * EditorScale;
+            SizeFlagsVertical = (int)SizeFlags.ExpandFill;
+
+            var toolBarHBox = new HBoxContainer {SizeFlagsHorizontal = (int)SizeFlags.ExpandFill};
+            AddChild(toolBarHBox);
+
+            var buildMenuBtn = new MenuButton {Text = "Build", Icon = GetThemeIcon("Play", "EditorIcons")};
+            toolBarHBox.AddChild(buildMenuBtn);
+
+            var buildMenu = buildMenuBtn.GetPopup();
+            buildMenu.AddItem("Build Solution".TTR(), (int)BuildMenuOptions.BuildSolution);
+            buildMenu.AddItem("Rebuild Solution".TTR(), (int)BuildMenuOptions.RebuildSolution);
+            buildMenu.AddItem("Clean Solution".TTR(), (int)BuildMenuOptions.CleanSolution);
+            buildMenu.IdPressed += BuildMenuOptionPressed;
+
+            errorsBtn = new Button
+            {
+                HintTooltip = "Show Errors".TTR(),
+                Icon = GetThemeIcon("StatusError", "EditorIcons"),
+                ExpandIcon = false,
+                ToggleMode = true,
+                Pressed = true,
+                FocusMode = FocusModeEnum.None
+            };
+            errorsBtn.Toggled += ErrorsToggled;
+            toolBarHBox.AddChild(errorsBtn);
+
+            warningsBtn = new Button
+            {
+                HintTooltip = "Show Warnings".TTR(),
+                Icon = GetThemeIcon("NodeWarning", "EditorIcons"),
+                ExpandIcon = false,
+                ToggleMode = true,
+                Pressed = true,
+                FocusMode = FocusModeEnum.None
+            };
+            warningsBtn.Toggled += WarningsToggled;
+            toolBarHBox.AddChild(warningsBtn);
+
+            viewLogBtn = new Button
+            {
+                Text = "Show Output".TTR(),
+                ToggleMode = true,
+                Pressed = true,
+                FocusMode = FocusModeEnum.None
+            };
+            viewLogBtn.Toggled += ViewLogToggled;
+            toolBarHBox.AddChild(viewLogBtn);
+
+            BuildOutputView = new BuildOutputView();
+            AddChild(BuildOutputView);
+        }
+    }
+}

+ 3 - 3
modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs

@@ -31,7 +31,7 @@ namespace GodotTools.Build
                         string dotnetCliPath = OS.PathWhich("dotnet");
                         if (!string.IsNullOrEmpty(dotnetCliPath))
                             return (dotnetCliPath, BuildTool.DotnetCli);
-                        GD.PushError("Cannot find dotnet CLI executable. Fallback to MSBuild from Visual Studio.");
+                        GD.PushError($"Cannot find executable for '{BuildManager.PropNameDotnetCli}'. Fallback to MSBuild from Visual Studio.");
                         goto case BuildTool.MsBuildVs;
                     }
                     case BuildTool.MsBuildVs:
@@ -89,7 +89,7 @@ namespace GodotTools.Build
                         string dotnetCliPath = OS.PathWhich("dotnet");
                         if (!string.IsNullOrEmpty(dotnetCliPath))
                             return (dotnetCliPath, BuildTool.DotnetCli);
-                        GD.PushError("Cannot find dotnet CLI executable. Fallback to MSBuild from Mono.");
+                        GD.PushError($"Cannot find executable for '{BuildManager.PropNameDotnetCli}'. Fallback to MSBuild from Mono.");
                         goto case BuildTool.MsBuildMono;
                     }
                     case BuildTool.MsBuildMono:
@@ -161,7 +161,7 @@ namespace GodotTools.Build
 
             // Try to find 15.0 with vswhere
 
-            var envNames = Internal.GodotIs32Bits() ? new[] { "ProgramFiles", "ProgramW6432" } : new[] { "ProgramFiles(x86)", "ProgramFiles" };
+            var envNames = Internal.GodotIs32Bits() ? new[] {"ProgramFiles", "ProgramW6432"} : new[] {"ProgramFiles(x86)", "ProgramFiles"};
 
             string vsWherePath = null;
             foreach (var envName in envNames)

+ 5 - 4
modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs

@@ -5,6 +5,7 @@ using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Runtime.CompilerServices;
+using GodotTools.Build;
 using GodotTools.Core;
 using GodotTools.Internals;
 using JetBrains.Annotations;
@@ -143,6 +144,8 @@ namespace GodotTools.Export
 
         private void _ExportBeginImpl(string[] features, bool isDebug, string path, int flags)
         {
+            _ = flags; // Unused
+
             if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
                 return;
 
@@ -154,12 +157,10 @@ namespace GodotTools.Export
 
             string buildConfig = isDebug ? "ExportDebug" : "ExportRelease";
 
-            string scriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, $"scripts_metadata.{(isDebug ? "debug" : "release")}");
-            CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, scriptsMetadataPath);
-
+            string scriptsMetadataPath = BuildManager.GenerateExportedGameScriptMetadata(isDebug);
             AddFile(scriptsMetadataPath, scriptsMetadataPath);
 
-            if (!BuildManager.BuildProjectBlocking(buildConfig, platform))
+            if (!BuildManager.BuildProjectBlocking(buildConfig, platform: platform))
                 throw new Exception("Failed to build project");
 
             // Add dependency assemblies

+ 25 - 21
modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs

@@ -4,9 +4,9 @@ using GodotTools.Export;
 using GodotTools.Utils;
 using System;
 using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.Linq;
+using GodotTools.Build;
 using GodotTools.Ides;
 using GodotTools.Ides.Rider;
 using GodotTools.Internals;
@@ -19,7 +19,6 @@ using Path = System.IO.Path;
 
 namespace GodotTools
 {
-    [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
     public class GodotSharpEditor : EditorPlugin, ISerializationListener
     {
         private EditorSettings editorSettings;
@@ -37,7 +36,7 @@ namespace GodotTools
 
         private WeakRef exportPluginWeak; // TODO Use WeakReference once we have proper serialization
 
-        public BottomPanel BottomPanel { get; private set; }
+        public MSBuildPanel MSBuildPanel { get; private set; }
 
         public bool SkipBuildBeforePlaying { get; set; } = false;
 
@@ -153,7 +152,7 @@ namespace GodotTools
             }
         }
 
-        private void _BuildSolutionPressed()
+        private void BuildSolutionPressed()
         {
             if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
             {
@@ -161,23 +160,22 @@ namespace GodotTools
                     return; // Failed to create solution
             }
 
-            Instance.BottomPanel.BuildProjectPressed();
+            Instance.MSBuildPanel.BuildSolution();
         }
 
-        public override void _Notification(int what)
+        public override void _Ready()
         {
-            base._Notification(what);
+            base._Ready();
 
-            if (what == NotificationReady)
+            MSBuildPanel.BuildOutputView.BuildStateChanged += BuildStateChanged;
+
+            bool showInfoDialog = (bool)editorSettings.GetSetting("mono/editor/show_info_on_start");
+            if (showInfoDialog)
             {
-                bool showInfoDialog = (bool)editorSettings.GetSetting("mono/editor/show_info_on_start");
-                if (showInfoDialog)
-                {
-                    aboutDialog.Exclusive = true;
-                    _ShowAboutDialog();
-                    // Once shown a first time, it can be seen again via the Mono menu - it doesn't have to be exclusive from that time on.
-                    aboutDialog.Exclusive = false;
-                }
+                aboutDialog.Exclusive = true;
+                _ShowAboutDialog();
+                // Once shown a first time, it can be seen again via the Mono menu - it doesn't have to be exclusive from that time on.
+                aboutDialog.Exclusive = false;
             }
         }
 
@@ -393,6 +391,12 @@ namespace GodotTools
             }
         }
 
+        private void BuildStateChanged()
+        {
+            if (bottomPanelBtn != null)
+                bottomPanelBtn.Icon = MSBuildPanel.BuildOutputView.BuildStateIcon;
+        }
+
         public override void EnablePlugin()
         {
             base.EnablePlugin();
@@ -409,16 +413,15 @@ namespace GodotTools
             errorDialog = new AcceptDialog();
             editorBaseControl.AddChild(errorDialog);
 
-            BottomPanel = new BottomPanel();
-
-            bottomPanelBtn = AddControlToBottomPanel(BottomPanel, "Mono".TTR());
+            MSBuildPanel = new MSBuildPanel();
+            bottomPanelBtn = AddControlToBottomPanel(MSBuildPanel, "MSBuild".TTR());
 
             AddChild(new HotReloadAssemblyWatcher {Name = "HotReloadAssemblyWatcher"});
 
             menuPopup = new PopupMenu();
             menuPopup.Hide();
 
-            AddToolSubmenuItem("Mono", menuPopup);
+            AddToolSubmenuItem("C#", menuPopup);
 
             // TODO: Remove or edit this info dialog once Mono support is no longer in alpha
             {
@@ -476,7 +479,7 @@ namespace GodotTools
                 HintTooltip = "Build solution",
                 FocusMode = Control.FocusModeEnum.None
             };
-            toolBarBuildButton.PressedSignal += _BuildSolutionPressed;
+            toolBarBuildButton.PressedSignal += BuildSolutionPressed;
             AddControlToContainer(CustomControlContainer.Toolbar, toolBarBuildButton);
 
             if (File.Exists(GodotSharpDirs.ProjectSlnPath) && File.Exists(GodotSharpDirs.ProjectCsProjPath))
@@ -570,6 +573,7 @@ namespace GodotTools
 
         public static GodotSharpEditor Instance { get; private set; }
 
+        [UsedImplicitly]
         private GodotSharpEditor()
         {
         }