From c0cdbb793822961e68727925d0f3f4e00f780306 Mon Sep 17 00:00:00 2001 From: Ignacio Etcheverry Date: Wed, 24 Jul 2019 23:18:55 +0200 Subject: Mono: Fix regression: external editors not working on Windows --- .../GodotTools/GodotTools/GodotSharpEditor.cs | 11 ++++++++++- .../GodotTools/GodotTools/MonoDevelopInstance.cs | 22 +++++++++++++--------- .../GodotTools/Utils/CollectionExtensions.cs | 5 +++-- 3 files changed, 26 insertions(+), 12 deletions(-) (limited to 'modules/mono/editor') diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index 90dec43412..9b5afb94a3 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -298,7 +298,16 @@ namespace GodotTools if (line >= 0) scriptPath += $";{line + 1};{col}"; - GetMonoDevelopInstance(GodotSharpDirs.ProjectSlnPath).Execute(scriptPath); + try + { + GetMonoDevelopInstance(GodotSharpDirs.ProjectSlnPath).Execute(scriptPath); + } + catch (FileNotFoundException) + { + string editorName = editor == ExternalEditor.VisualStudioForMac ? "Visual Studio" : "MonoDevelop"; + GD.PushError($"Cannot find code editor: {editorName}"); + return Error.FileNotFound; + } break; } diff --git a/modules/mono/editor/GodotTools/GodotTools/MonoDevelopInstance.cs b/modules/mono/editor/GodotTools/GodotTools/MonoDevelopInstance.cs index 0c8d86e799..61a0a992ce 100644 --- a/modules/mono/editor/GodotTools/GodotTools/MonoDevelopInstance.cs +++ b/modules/mono/editor/GodotTools/GodotTools/MonoDevelopInstance.cs @@ -4,6 +4,7 @@ using System.IO; using System.Collections.Generic; using System.Diagnostics; using GodotTools.Internals; +using GodotTools.Utils; namespace GodotTools { @@ -30,7 +31,7 @@ namespace GodotTools if (Utils.OS.IsOSX()) { - string bundleId = CodeEditorBundleIds[editorId]; + string bundleId = BundleIds[editorId]; if (Internal.IsOsxAppBundleInstalled(bundleId)) { @@ -47,12 +48,12 @@ namespace GodotTools } else { - command = CodeEditorPaths[editorId]; + command = OS.PathWhich(ExecutableNames[editorId]); } } else { - command = CodeEditorPaths[editorId]; + command = OS.PathWhich(ExecutableNames[editorId]); } args.Add("--ipc-tcp"); @@ -70,6 +71,9 @@ namespace GodotTools args.Add("\"" + Path.GetFullPath(filePath.NormalizePath()) + cursor + "\""); } + if (command == null) + throw new FileNotFoundException(); + if (newWindow) { process = Process.Start(new ProcessStartInfo @@ -99,20 +103,20 @@ namespace GodotTools this.editorId = editorId; } - private static readonly IReadOnlyDictionary CodeEditorPaths; - private static readonly IReadOnlyDictionary CodeEditorBundleIds; + private static readonly IReadOnlyDictionary ExecutableNames; + private static readonly IReadOnlyDictionary BundleIds; static MonoDevelopInstance() { if (Utils.OS.IsOSX()) { - CodeEditorPaths = new Dictionary + ExecutableNames = new Dictionary { // Rely on PATH {EditorId.MonoDevelop, "monodevelop"}, {EditorId.VisualStudioForMac, "VisualStudio"} }; - CodeEditorBundleIds = new Dictionary + BundleIds = new Dictionary { // TODO EditorId.MonoDevelop {EditorId.VisualStudioForMac, "com.microsoft.visual-studio"} @@ -120,7 +124,7 @@ namespace GodotTools } else if (Utils.OS.IsWindows()) { - CodeEditorPaths = new Dictionary + ExecutableNames = new Dictionary { // XamarinStudio is no longer a thing, and the latest version is quite old // MonoDevelop is available from source only on Windows. The recommendation @@ -131,7 +135,7 @@ namespace GodotTools } else if (Utils.OS.IsUnix()) { - CodeEditorPaths = new Dictionary + ExecutableNames = new Dictionary { // Rely on PATH {EditorId.MonoDevelop, "monodevelop"} diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/CollectionExtensions.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/CollectionExtensions.cs index 3ae6c10bbf..288c65de74 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/CollectionExtensions.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/CollectionExtensions.cs @@ -10,8 +10,9 @@ namespace GodotTools.Utils { foreach (T elem in enumerable) { - if (predicate(elem) != null) - return elem; + T result = predicate(elem); + if (result != null) + return result; } return orElse; -- cgit v1.2.3 From 4e84478f4e4176a945c0a986524e019db18541c3 Mon Sep 17 00:00:00 2001 From: Ignacio Etcheverry Date: Thu, 25 Jul 2019 21:06:40 +0200 Subject: Fix incorrectly updating csproj and not closing build issues file --- .../GodotTools.ProjectEditor/ProjectUtils.cs | 2 +- .../GodotTools/GodotTools/GodotSharpBuilds.cs | 2 +- .../editor/GodotTools/GodotTools/MonoBuildTab.cs | 65 ++++++++++++---------- 3 files changed, 38 insertions(+), 31 deletions(-) (limited to 'modules/mono/editor') diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs index 1edc426e00..233aab45b3 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs @@ -85,7 +85,7 @@ namespace GodotTools.ProjectEditor void AddPropertyIfNotPresent(string name, string condition, string value) { if (root.PropertyGroups - .Any(g => g.Condition == string.Empty || g.Condition == condition && + .Any(g => (g.Condition == string.Empty || g.Condition == condition) && g.Properties .Any(p => p.Name == name && p.Value == value && diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpBuilds.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpBuilds.cs index a884b0ead0..7d767260e3 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpBuilds.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpBuilds.cs @@ -64,7 +64,7 @@ namespace GodotTools private static string GetIssuesFilePath(MonoBuildInfo buildInfo) { - return Path.Combine(Godot.ProjectSettings.LocalizePath(buildInfo.LogsDirPath), MsBuildIssuesFileName); + return Path.Combine(buildInfo.LogsDirPath, MsBuildIssuesFileName); } private static void PrintVerbose(string text) diff --git a/modules/mono/editor/GodotTools/GodotTools/MonoBuildTab.cs b/modules/mono/editor/GodotTools/GodotTools/MonoBuildTab.cs index 75fdacc0da..3a74fa2f66 100644 --- a/modules/mono/editor/GodotTools/GodotTools/MonoBuildTab.cs +++ b/modules/mono/editor/GodotTools/GodotTools/MonoBuildTab.cs @@ -61,41 +61,48 @@ namespace GodotTools { using (var file = new Godot.File()) { - Error openError = file.Open(csvFile, Godot.File.ModeFlags.Read); - - if (openError != Error.Ok) - return; - - while (!file.EofReached()) + try { - string[] csvColumns = file.GetCsvLine(); + Error openError = file.Open(csvFile, Godot.File.ModeFlags.Read); - if (csvColumns.Length == 1 && csvColumns[0].Empty()) + if (openError != Error.Ok) return; - if (csvColumns.Length != 7) + while (!file.EofReached()) { - GD.PushError($"Expected 7 columns, got {csvColumns.Length}"); - continue; + string[] csvColumns = file.GetCsvLine(); + + if (csvColumns.Length == 1 && csvColumns[0].Empty()) + return; + + if (csvColumns.Length != 7) + { + GD.PushError($"Expected 7 columns, got {csvColumns.Length}"); + continue; + } + + var issue = new BuildIssue + { + Warning = csvColumns[0] == "warning", + File = csvColumns[1], + Line = int.Parse(csvColumns[2]), + Column = int.Parse(csvColumns[3]), + Code = csvColumns[4], + Message = csvColumns[5], + ProjectFile = csvColumns[6] + }; + + if (issue.Warning) + WarningCount += 1; + else + ErrorCount += 1; + + issues.Add(issue); } - - var issue = new BuildIssue - { - Warning = csvColumns[0] == "warning", - File = csvColumns[1], - Line = int.Parse(csvColumns[2]), - Column = int.Parse(csvColumns[3]), - Code = csvColumns[4], - Message = csvColumns[5], - ProjectFile = csvColumns[6] - }; - - if (issue.Warning) - WarningCount += 1; - else - ErrorCount += 1; - - issues.Add(issue); + } + finally + { + file.Close(); // Disposing it is not enough. We need to call Close() } } } -- cgit v1.2.3 From 8821569689030c21b541b14e4aa7c1ee513dd03f Mon Sep 17 00:00:00 2001 From: Ignacio Etcheverry Date: Fri, 26 Jul 2019 16:16:53 +0200 Subject: Mono: Fix custom defines for Mono's MSBuild; remove xbuild Mono's MSBuild and System/VisualStudio's MSBuild expect a different format for surrounding property values with quotes on the command line. xbuild does not seem to support semicolons in property values from the command line: https://xamarin.github.io/bugzilla-archives/16/16465/bug.html It's a good time to just remove xbuild support entirely. --- .../GodotTools/GodotTools/Build/BuildSystem.cs | 2 +- .../GodotTools/GodotTools/Build/MsBuildFinder.cs | 38 ++++------------------ .../GodotTools/GodotTools/GodotSharpBuilds.cs | 15 +++++---- 3 files changed, 16 insertions(+), 39 deletions(-) (limited to 'modules/mono/editor') diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs index f849356919..d8cb9024c3 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs @@ -137,7 +137,7 @@ namespace GodotTools.Build private static string BuildArguments(string solution, string config, string loggerOutputDir, List customProperties) { - string arguments = $@"""{solution}"" /v:normal /t:Rebuild ""/p:{"Configuration=" + config}"" " + + string arguments = $@"""{solution}"" /v:normal /t:Build ""/p:{"Configuration=" + config}"" " + $@"""/l:{typeof(GodotBuildLogger).FullName},{GodotBuildLogger.AssemblyPath};{loggerOutputDir}"""; foreach (string customProperty in customProperties) diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs b/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs index f0068385f4..926aabdf89 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs @@ -15,7 +15,6 @@ namespace GodotTools.Build { private static string _msbuildToolsPath = string.Empty; private static string _msbuildUnixPath = string.Empty; - private static string _xbuildUnixPath = string.Empty; public static string FindMsBuild() { @@ -44,7 +43,6 @@ namespace GodotTools.Build return Path.Combine(_msbuildToolsPath, "MSBuild.exe"); } - case GodotSharpBuilds.BuildTool.MsBuildMono: { string msbuildPath = Path.Combine(Internal.MonoWindowsInstallRoot, "bin", "msbuild.bat"); @@ -56,19 +54,6 @@ namespace GodotTools.Build return msbuildPath; } - - case GodotSharpBuilds.BuildTool.XBuild: - { - string xbuildPath = Path.Combine(Internal.MonoWindowsInstallRoot, "bin", "xbuild.bat"); - - if (!File.Exists(xbuildPath)) - { - throw new FileNotFoundException($"Cannot find executable for '{GodotSharpBuilds.PropNameXbuild}'. Tried with path: {xbuildPath}"); - } - - return xbuildPath; - } - default: throw new IndexOutOfRangeException("Invalid build tool in editor settings"); } @@ -76,20 +61,7 @@ namespace GodotTools.Build if (OS.IsUnix()) { - if (buildTool == GodotSharpBuilds.BuildTool.XBuild) - { - if (_xbuildUnixPath.Empty() || !File.Exists(_xbuildUnixPath)) - { - // Try to search it again if it wasn't found last time or if it was removed from its location - _xbuildUnixPath = FindBuildEngineOnUnix("msbuild"); - } - - if (_xbuildUnixPath.Empty()) - { - throw new FileNotFoundException($"Cannot find binary for '{GodotSharpBuilds.PropNameXbuild}'"); - } - } - else + if (buildTool == GodotSharpBuilds.BuildTool.MsBuildMono) { if (_msbuildUnixPath.Empty() || !File.Exists(_msbuildUnixPath)) { @@ -101,9 +73,13 @@ namespace GodotTools.Build { throw new FileNotFoundException($"Cannot find binary for '{GodotSharpBuilds.PropNameMsbuildMono}'"); } - } - return buildTool != GodotSharpBuilds.BuildTool.XBuild ? _msbuildUnixPath : _xbuildUnixPath; + return _msbuildUnixPath; + } + else + { + throw new IndexOutOfRangeException("Invalid build tool in editor settings"); + } } throw new PlatformNotSupportedException(); diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpBuilds.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpBuilds.cs index a884b0ead0..f578f5fc2c 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpBuilds.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpBuilds.cs @@ -18,7 +18,6 @@ namespace GodotTools public const string PropNameMsbuildMono = "MSBuild (Mono)"; public const string PropNameMsbuildVs = "MSBuild (VS Build Tools)"; - public const string PropNameXbuild = "xbuild (Deprecated)"; public const string MsBuildIssuesFileName = "msbuild_issues.csv"; public const string MsBuildLogFileName = "msbuild_log.txt"; @@ -26,8 +25,7 @@ namespace GodotTools public enum BuildTool { MsBuildMono, - MsBuildVs, - XBuild // Deprecated + MsBuildVs } private static void RemoveOldIssuesFile(MonoBuildInfo buildInfo) @@ -202,6 +200,9 @@ namespace GodotTools // case the user decided to delete them at some point after they were loaded. Internal.UpdateApiAssembliesFromPrebuilt(); + var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); + var buildTool = (BuildTool)editorSettings.GetSetting("mono/builds/build_tool"); + using (var pr = new EditorProgress("mono_project_debug_build", "Building project solution...", 1)) { pr.Step("Building project solution", 0); @@ -209,7 +210,7 @@ namespace GodotTools var buildInfo = new MonoBuildInfo(GodotSharpDirs.ProjectSlnPath, config); // Add Godot defines - string constants = OS.IsWindows() ? "GodotDefineConstants=\"" : "GodotDefineConstants=\\\""; + string constants = buildTool == BuildTool.MsBuildVs ? "GodotDefineConstants=\"" : "GodotDefineConstants=\\\""; foreach (var godotDefine in godotDefines) constants += $"GODOT_{godotDefine.ToUpper().Replace("-", "_").Replace(" ", "_").Replace(";", "_")};"; @@ -217,7 +218,7 @@ namespace GodotTools if (Internal.GodotIsRealTDouble()) constants += "GODOT_REAL_T_IS_DOUBLE;"; - constants += OS.IsWindows() ? "\"" : "\\\""; + constants += buildTool == BuildTool.MsBuildVs ? "\"" : "\\\""; buildInfo.CustomProperties.Add(constants); @@ -267,8 +268,8 @@ namespace GodotTools ["name"] = "mono/builds/build_tool", ["hint"] = Godot.PropertyHint.Enum, ["hint_string"] = OS.IsWindows() ? - $"{PropNameMsbuildMono},{PropNameMsbuildVs},{PropNameXbuild}" : - $"{PropNameMsbuildMono},{PropNameXbuild}" + $"{PropNameMsbuildMono},{PropNameMsbuildVs}" : + $"{PropNameMsbuildMono}" }); EditorDef("mono/builds/print_build_output", false); -- cgit v1.2.3 From 0b94203a79d3261d4cc3bbcdb3438a5a45c8c572 Mon Sep 17 00:00:00 2001 From: Ignacio Etcheverry Date: Thu, 18 Jul 2019 04:08:24 +0200 Subject: C#: Add Ide Connection library and server for the editor This will be used for communicating between the Godot editor and external IDEs/editors, for things like opening files, triggering hot-reload and running the game with a debugger attached. --- .../GodotTools.IdeConnection/ConsoleLogger.cs | 33 ++ .../GodotTools.IdeConnection/GodotIdeBase.cs | 94 ++++++ .../GodotTools.IdeConnection/GodotIdeClient.cs | 219 +++++++++++++ .../GodotTools.IdeConnection/GodotIdeConnection.cs | 207 +++++++++++++ .../GodotIdeConnectionClient.cs | 24 ++ .../GodotIdeConnectionServer.cs | 24 ++ .../GodotTools.IdeConnection/GodotIdeMetadata.cs | 45 +++ .../GodotTools.IdeConnection.csproj | 52 ++++ .../GodotTools/GodotTools.IdeConnection/ILogger.cs | 13 + .../GodotTools/GodotTools.IdeConnection/Message.cs | 21 ++ .../GodotTools.IdeConnection/MessageComposer.cs | 46 +++ .../GodotTools.IdeConnection/MessageParser.cs | 88 ++++++ .../Properties/AssemblyInfo.cs | 35 +++ modules/mono/editor/GodotTools/GodotTools.sln | 6 + .../editor/GodotTools/GodotTools/BottomPanel.cs | 343 +++++++++++++++++++++ .../GodotTools/GodotTools/Build/BuildSystem.cs | 16 +- .../GodotTools/GodotTools/Build/MsBuildFinder.cs | 14 +- .../mono/editor/GodotTools/GodotTools/BuildInfo.cs | 47 +++ .../editor/GodotTools/GodotTools/BuildManager.cs | 259 ++++++++++++++++ .../mono/editor/GodotTools/GodotTools/BuildTab.cs | 267 ++++++++++++++++ .../editor/GodotTools/GodotTools/CSharpProject.cs | 127 -------- .../GodotTools/GodotTools/CsProjOperations.cs | 127 ++++++++ .../GodotTools/GodotTools/ExternalEditorId.cs | 11 + .../GodotTools/GodotTools/GodotSharpBuilds.cs | 278 ----------------- .../GodotTools/GodotTools/GodotSharpEditor.cs | 121 +++----- .../GodotTools/GodotTools/GodotSharpExport.cs | 4 +- .../editor/GodotTools/GodotTools/GodotTools.csproj | 24 +- .../GodotTools/GodotTools/Ides/GodotIdeManager.cs | 166 ++++++++++ .../GodotTools/GodotTools/Ides/GodotIdeServer.cs | 208 +++++++++++++ .../GodotTools/Ides/MonoDevelop/EditorId.cs | 8 + .../GodotTools/Ides/MonoDevelop/Instance.cs | 131 ++++++++ .../GodotTools/GodotTools/Internals/Internal.cs | 15 + .../GodotTools/GodotTools/MonoBottomPanel.cs | 343 --------------------- .../editor/GodotTools/GodotTools/MonoBuildInfo.cs | 47 --- .../editor/GodotTools/GodotTools/MonoBuildTab.cs | 267 ---------------- .../GodotTools/GodotTools/MonoDevelopInstance.cs | 146 --------- .../GodotTools/Utils/CollectionExtensions.cs | 8 + .../GodotTools/GodotTools/Utils/NotifyAwaiter.cs | 64 ++++ modules/mono/editor/editor_internal_calls.cpp | 19 +- 39 files changed, 2659 insertions(+), 1308 deletions(-) create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools/BuildManager.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools/BuildTab.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools/CSharpProject.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools/ExternalEditorId.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools/GodotSharpBuilds.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/EditorId.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools/MonoBottomPanel.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools/MonoBuildInfo.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools/MonoBuildTab.cs delete mode 100644 modules/mono/editor/GodotTools/GodotTools/MonoDevelopInstance.cs create mode 100644 modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs (limited to 'modules/mono/editor') diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs new file mode 100644 index 0000000000..7a2ff2ca56 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs @@ -0,0 +1,33 @@ +using System; + +namespace GodotTools.IdeConnection +{ + public class ConsoleLogger : ILogger + { + public void LogDebug(string message) + { + Console.WriteLine("DEBUG: " + message); + } + + public void LogInfo(string message) + { + Console.WriteLine("INFO: " + message); + } + + public void LogWarning(string message) + { + Console.WriteLine("WARN: " + message); + } + + public void LogError(string message) + { + Console.WriteLine("ERROR: " + message); + } + + public void LogError(string message, Exception e) + { + Console.WriteLine("EXCEPTION: " + message); + Console.WriteLine(e); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs new file mode 100644 index 0000000000..be89638241 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs @@ -0,0 +1,94 @@ +using System; +using Path = System.IO.Path; + +namespace GodotTools.IdeConnection +{ + public class GodotIdeBase : IDisposable + { + private ILogger logger; + + public ILogger Logger + { + get => logger ?? (logger = new ConsoleLogger()); + set => logger = value; + } + + private readonly string projectMetadataDir; + + protected const string MetaFileName = "ide_server_meta.txt"; + protected string MetaFilePath => Path.Combine(projectMetadataDir, MetaFileName); + + private GodotIdeConnection connection; + protected readonly object ConnectionLock = new object(); + + public bool IsDisposed { get; private set; } = false; + + public bool IsConnected => connection != null && !connection.IsDisposed && connection.IsConnected; + + public event Action Connected + { + add + { + if (connection != null && !connection.IsDisposed) + connection.Connected += value; + } + remove + { + if (connection != null && !connection.IsDisposed) + connection.Connected -= value; + } + } + + protected GodotIdeConnection Connection + { + get => connection; + set + { + connection?.Dispose(); + connection = value; + } + } + + protected GodotIdeBase(string projectMetadataDir) + { + this.projectMetadataDir = projectMetadataDir; + } + + protected void DisposeConnection() + { + lock (ConnectionLock) + { + connection?.Dispose(); + } + } + + ~GodotIdeBase() + { + Dispose(disposing: false); + } + + public void Dispose() + { + if (IsDisposed) + return; + + lock (ConnectionLock) + { + if (IsDisposed) // lock may not be fair + return; + IsDisposed = true; + } + + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + connection?.Dispose(); + } + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs new file mode 100644 index 0000000000..4f56a8d71b --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs @@ -0,0 +1,219 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace GodotTools.IdeConnection +{ + public abstract class GodotIdeClient : GodotIdeBase + { + protected GodotIdeMetadata GodotIdeMetadata; + + private readonly FileSystemWatcher fsWatcher; + + protected GodotIdeClient(string projectMetadataDir) : base(projectMetadataDir) + { + messageHandlers = InitializeMessageHandlers(); + + // FileSystemWatcher requires an existing directory + if (!File.Exists(projectMetadataDir)) + Directory.CreateDirectory(projectMetadataDir); + + fsWatcher = new FileSystemWatcher(projectMetadataDir, MetaFileName); + } + + private void OnMetaFileChanged(object sender, FileSystemEventArgs e) + { + if (IsDisposed) + return; + + lock (ConnectionLock) + { + if (IsDisposed) + return; + + if (!File.Exists(MetaFilePath)) + return; + + var metadata = ReadMetadataFile(); + + if (metadata != null && metadata != GodotIdeMetadata) + { + GodotIdeMetadata = metadata.Value; + ConnectToServer(); + } + } + } + + private void OnMetaFileDeleted(object sender, FileSystemEventArgs e) + { + if (IsDisposed) + return; + + if (IsConnected) + DisposeConnection(); + + // The file may have been re-created + + lock (ConnectionLock) + { + if (IsDisposed) + return; + + if (IsConnected || !File.Exists(MetaFilePath)) + return; + + var metadata = ReadMetadataFile(); + + if (metadata != null) + { + GodotIdeMetadata = metadata.Value; + ConnectToServer(); + } + } + } + + private GodotIdeMetadata? ReadMetadataFile() + { + using (var reader = File.OpenText(MetaFilePath)) + { + string portStr = reader.ReadLine(); + + if (portStr == null) + return null; + + string editorExecutablePath = reader.ReadLine(); + + if (editorExecutablePath == null) + return null; + + if (!int.TryParse(portStr, out int port)) + return null; + + return new GodotIdeMetadata(port, editorExecutablePath); + } + } + + private void ConnectToServer() + { + var tcpClient = new TcpClient(); + + Connection = new GodotIdeConnectionClient(tcpClient, HandleMessage); + Connection.Logger = Logger; + + try + { + Logger.LogInfo("Connecting to Godot Ide Server"); + + tcpClient.Connect(IPAddress.Loopback, GodotIdeMetadata.Port); + + Logger.LogInfo("Connection open with Godot Ide Server"); + + var clientThread = new Thread(Connection.Start) + { + IsBackground = true, + Name = "Godot Ide Connection Client" + }; + clientThread.Start(); + } + catch (SocketException e) + { + if (e.SocketErrorCode == SocketError.ConnectionRefused) + Logger.LogError("The connection to the Godot Ide Server was refused"); + else + throw; + } + } + + public void Start() + { + Logger.LogInfo("Starting Godot Ide Client"); + + fsWatcher.Changed += OnMetaFileChanged; + fsWatcher.Deleted += OnMetaFileDeleted; + fsWatcher.EnableRaisingEvents = true; + + lock (ConnectionLock) + { + if (IsDisposed) + return; + + if (!File.Exists(MetaFilePath)) + { + Logger.LogInfo("There is no Godot Ide Server running"); + return; + } + + var metadata = ReadMetadataFile(); + + if (metadata != null) + { + GodotIdeMetadata = metadata.Value; + ConnectToServer(); + } + else + { + Logger.LogError("Failed to read Godot Ide metadata file"); + } + } + } + + public bool WriteMessage(Message message) + { + return Connection.WriteMessage(message); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + fsWatcher?.Dispose(); + } + } + + protected virtual bool HandleMessage(Message message) + { + if (messageHandlers.TryGetValue(message.Id, out var action)) + { + action(message.Arguments); + return true; + } + + return false; + } + + private readonly Dictionary> messageHandlers; + + private Dictionary> InitializeMessageHandlers() + { + return new Dictionary> + { + ["OpenFile"] = args => + { + switch (args.Length) + { + case 1: + OpenFile(file: args[0]); + return; + case 2: + OpenFile(file: args[0], line: int.Parse(args[1])); + return; + case 3: + OpenFile(file: args[0], line: int.Parse(args[1]), column: int.Parse(args[2])); + return; + default: + throw new ArgumentException(); + } + } + }; + } + + protected abstract void OpenFile(string file); + protected abstract void OpenFile(string file, int line); + protected abstract void OpenFile(string file, int line, int column); + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs new file mode 100644 index 0000000000..e7e81f175e --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs @@ -0,0 +1,207 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Net.Sockets; +using System.Text; + +namespace GodotTools.IdeConnection +{ + public abstract class GodotIdeConnection : IDisposable + { + protected const string Version = "1.0"; + + protected static readonly string ClientHandshake = $"Godot Ide Client Version {Version}"; + protected static readonly string ServerHandshake = $"Godot Ide Server Version {Version}"; + + private const int ClientWriteTimeout = 8000; + private readonly TcpClient tcpClient; + + private TextReader clientReader; + private TextWriter clientWriter; + + private readonly object writeLock = new object(); + + private readonly Func messageHandler; + + public event Action Connected; + + private ILogger logger; + + public ILogger Logger + { + get => logger ?? (logger = new ConsoleLogger()); + set => logger = value; + } + + public bool IsDisposed { get; private set; } = false; + + public bool IsConnected => tcpClient.Client != null && tcpClient.Client.Connected; + + protected GodotIdeConnection(TcpClient tcpClient, Func messageHandler) + { + this.tcpClient = tcpClient; + this.messageHandler = messageHandler; + } + + public void Start() + { + try + { + if (!StartConnection()) + return; + + string messageLine; + while ((messageLine = ReadLine()) != null) + { + if (!MessageParser.TryParse(messageLine, out Message msg)) + { + Logger.LogError($"Received message with invalid format: {messageLine}"); + continue; + } + + Logger.LogDebug($"Received message: {msg}"); + + if (msg.Id == "close") + { + Logger.LogInfo("Closing connection"); + return; + } + + try + { + try + { + Debug.Assert(messageHandler != null); + + if (!messageHandler(msg)) + Logger.LogError($"Received unknown message: {msg}"); + } + catch (Exception e) + { + Logger.LogError($"Message handler for '{msg}' failed with exception", e); + } + } + catch (Exception e) + { + Logger.LogError($"Exception thrown from message handler. Message: {msg}", e); + } + } + } + catch (Exception e) + { + Logger.LogError($"Unhandled exception in the Godot Ide Connection thread", e); + } + finally + { + Dispose(); + } + } + + private bool StartConnection() + { + NetworkStream clientStream = tcpClient.GetStream(); + + clientReader = new StreamReader(clientStream, Encoding.UTF8); + + lock (writeLock) + clientWriter = new StreamWriter(clientStream, Encoding.UTF8); + + clientStream.WriteTimeout = ClientWriteTimeout; + + if (!WriteHandshake()) + { + Logger.LogError("Could not write handshake"); + return false; + } + + if (!IsValidResponseHandshake(ReadLine())) + { + Logger.LogError("Received invalid handshake"); + return false; + } + + Connected?.Invoke(); + + Logger.LogInfo("Godot Ide connection started"); + + return true; + } + + private string ReadLine() + { + try + { + return clientReader?.ReadLine(); + } + catch (Exception e) + { + if (IsDisposed) + { + var se = e as SocketException ?? e.InnerException as SocketException; + if (se != null && se.SocketErrorCode == SocketError.Interrupted) + return null; + } + + throw; + } + } + + public bool WriteMessage(Message message) + { + Logger.LogDebug($"Sending message {message}"); + + var messageComposer = new MessageComposer(); + + messageComposer.AddArgument(message.Id); + foreach (string argument in message.Arguments) + messageComposer.AddArgument(argument); + + return WriteLine(messageComposer.ToString()); + } + + protected bool WriteLine(string text) + { + if (clientWriter == null || IsDisposed || !IsConnected) + return false; + + lock (writeLock) + { + try + { + clientWriter.WriteLine(text); + clientWriter.Flush(); + } + catch (Exception e) + { + if (!IsDisposed) + { + var se = e as SocketException ?? e.InnerException as SocketException; + if (se != null && se.SocketErrorCode == SocketError.Shutdown) + Logger.LogInfo("Client disconnected ungracefully"); + else + Logger.LogError("Exception thrown when trying to write to client", e); + + Dispose(); + } + } + } + + return true; + } + + protected abstract bool WriteHandshake(); + protected abstract bool IsValidResponseHandshake(string handshakeLine); + + public void Dispose() + { + if (IsDisposed) + return; + + IsDisposed = true; + + clientReader?.Dispose(); + clientWriter?.Dispose(); + ((IDisposable) tcpClient)?.Dispose(); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs new file mode 100644 index 0000000000..1b11a14358 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs @@ -0,0 +1,24 @@ +using System; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace GodotTools.IdeConnection +{ + public class GodotIdeConnectionClient : GodotIdeConnection + { + public GodotIdeConnectionClient(TcpClient tcpClient, Func messageHandler) + : base(tcpClient, messageHandler) + { + } + + protected override bool WriteHandshake() + { + return WriteLine(ClientHandshake); + } + + protected override bool IsValidResponseHandshake(string handshakeLine) + { + return handshakeLine == ServerHandshake; + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs new file mode 100644 index 0000000000..aa98dc7ca3 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs @@ -0,0 +1,24 @@ +using System; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace GodotTools.IdeConnection +{ + public class GodotIdeConnectionServer : GodotIdeConnection + { + public GodotIdeConnectionServer(TcpClient tcpClient, Func messageHandler) + : base(tcpClient, messageHandler) + { + } + + protected override bool WriteHandshake() + { + return WriteLine(ServerHandshake); + } + + protected override bool IsValidResponseHandshake(string handshakeLine) + { + return handshakeLine == ClientHandshake; + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs new file mode 100644 index 0000000000..d16daba0e2 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs @@ -0,0 +1,45 @@ +namespace GodotTools.IdeConnection +{ + public struct GodotIdeMetadata + { + public int Port { get; } + public string EditorExecutablePath { get; } + + public GodotIdeMetadata(int port, string editorExecutablePath) + { + Port = port; + EditorExecutablePath = editorExecutablePath; + } + + public static bool operator ==(GodotIdeMetadata a, GodotIdeMetadata b) + { + return a.Port == b.Port && a.EditorExecutablePath == b.EditorExecutablePath; + } + + public static bool operator !=(GodotIdeMetadata a, GodotIdeMetadata b) + { + return !(a == b); + } + + public override bool Equals(object obj) + { + if (obj is GodotIdeMetadata metadata) + return metadata == this; + + return false; + } + + public bool Equals(GodotIdeMetadata other) + { + return Port == other.Port && EditorExecutablePath == other.EditorExecutablePath; + } + + public override int GetHashCode() + { + unchecked + { + return (Port * 397) ^ (EditorExecutablePath != null ? EditorExecutablePath.GetHashCode() : 0); + } + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj new file mode 100644 index 0000000000..84c08251ab --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj @@ -0,0 +1,52 @@ + + + + + Debug + AnyCPU + {92600954-25F0-4291-8E11-1FEE9FC4BE20} + Library + Properties + GodotTools.IdeConnection + GodotTools.IdeConnection + v4.5 + 512 + + + AnyCPU + true + portable + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + portable + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs new file mode 100644 index 0000000000..614bb30271 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs @@ -0,0 +1,13 @@ +using System; + +namespace GodotTools.IdeConnection +{ + public interface ILogger + { + void LogDebug(string message); + void LogInfo(string message); + void LogWarning(string message); + void LogError(string message); + void LogError(string message, Exception e); + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs new file mode 100644 index 0000000000..f24d324ae3 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs @@ -0,0 +1,21 @@ +using System.Linq; + +namespace GodotTools.IdeConnection +{ + public struct Message + { + public string Id { get; set; } + public string[] Arguments { get; set; } + + public Message(string id, params string[] arguments) + { + Id = id; + Arguments = arguments; + } + + public override string ToString() + { + return $"(Id: '{Id}', Arguments: '{string.Join(",", Arguments)}')"; + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs new file mode 100644 index 0000000000..9e4cd6ec1a --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs @@ -0,0 +1,46 @@ +using System.Linq; +using System.Text; + +namespace GodotTools.IdeConnection +{ + public class MessageComposer + { + private readonly StringBuilder stringBuilder = new StringBuilder(); + + private static readonly char[] CharsToEscape = { '\\', '"' }; + + public void AddArgument(string argument) + { + AddArgument(argument, quoted: argument.Contains(",")); + } + + public void AddArgument(string argument, bool quoted) + { + if (stringBuilder.Length > 0) + stringBuilder.Append(','); + + if (quoted) + { + stringBuilder.Append('"'); + + foreach (char @char in argument) + { + if (CharsToEscape.Contains(@char)) + stringBuilder.Append('\\'); + stringBuilder.Append(@char); + } + + stringBuilder.Append('"'); + } + else + { + stringBuilder.Append(argument); + } + } + + public override string ToString() + { + return stringBuilder.ToString(); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs new file mode 100644 index 0000000000..ed691e481f --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace GodotTools.IdeConnection +{ + public static class MessageParser + { + public static bool TryParse(string messageLine, out Message message) + { + var arguments = new List(); + var stringBuilder = new StringBuilder(); + + bool expectingArgument = true; + + for (int i = 0; i < messageLine.Length; i++) + { + char @char = messageLine[i]; + + if (@char == ',') + { + if (expectingArgument) + arguments.Add(string.Empty); + + expectingArgument = true; + continue; + } + + bool quoted = false; + + if (messageLine[i] == '"') + { + quoted = true; + i++; + } + + while (i < messageLine.Length) + { + @char = messageLine[i]; + + if (quoted && @char == '"') + { + i++; + break; + } + + if (@char == '\\') + { + i++; + if (i < messageLine.Length) + break; + + stringBuilder.Append(messageLine[i]); + } + else if (!quoted && @char == ',') + { + break; // We don't increment the counter to allow the colon to be parsed after this + } + else + { + stringBuilder.Append(@char); + } + + i++; + } + + arguments.Add(stringBuilder.ToString()); + stringBuilder.Clear(); + + expectingArgument = false; + } + + if (arguments.Count == 0) + { + message = new Message(); + return false; + } + + message = new Message + { + Id = arguments[0], + Arguments = arguments.Skip(1).ToArray() + }; + + return true; + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..c7c00e66a2 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("GodotTools.IdeConnection")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("Godot Engine contributors")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("92600954-25F0-4291-8E11-1FEE9FC4BE20")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/modules/mono/editor/GodotTools/GodotTools.sln b/modules/mono/editor/GodotTools/GodotTools.sln index 6f7d44bec2..a3438ea5f3 100644 --- a/modules/mono/editor/GodotTools/GodotTools.sln +++ b/modules/mono/editor/GodotTools/GodotTools.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.Core", "GodotToo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.BuildLogger", "GodotTools.BuildLogger\GodotTools.BuildLogger.csproj", "{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.IdeConnection", "GodotTools.IdeConnection\GodotTools.IdeConnection.csproj", "{92600954-25F0-4291-8E11-1FEE9FC4BE20}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,5 +33,9 @@ Global {6CE9A984-37B1-4F8A-8FE9-609F05F071B3}.Debug|Any CPU.Build.0 = Debug|Any CPU {6CE9A984-37B1-4F8A-8FE9-609F05F071B3}.Release|Any CPU.ActiveCfg = Release|Any CPU {6CE9A984-37B1-4F8A-8FE9-609F05F071B3}.Release|Any CPU.Build.0 = Release|Any CPU + {92600954-25F0-4291-8E11-1FEE9FC4BE20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {92600954-25F0-4291-8E11-1FEE9FC4BE20}.Debug|Any CPU.Build.0 = Debug|Any CPU + {92600954-25F0-4291-8E11-1FEE9FC4BE20}.Release|Any CPU.ActiveCfg = Release|Any CPU + {92600954-25F0-4291-8E11-1FEE9FC4BE20}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs b/modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs new file mode 100644 index 0000000000..44813f962c --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs @@ -0,0 +1,343 @@ +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 ToolButton warningsBtn; + private ToolButton errorsBtn; + private Button viewLogBtn; + + private void _UpdateBuildTabsList() + { + buildTabsList.Clear(); + + int currentTab = buildTabs.CurrentTab; + + bool noCurrentTab = currentTab < 0 || currentTab >= buildTabs.GetTabCount(); + + for (int i = 0; i < buildTabs.GetChildCount(); i++) + { + var tab = (BuildTab) buildTabs.GetChild(i); + + if (tab == null) + continue; + + 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(i, itemTooltip); + + if (noCurrentTab || currentTab == i) + { + buildTabsList.Select(i); + _BuildTabsItemSelected(i); + } + } + } + + public BuildTab GetBuildTabFor(BuildInfo buildInfo) + { + foreach (var buildTab in new Array(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; + } + } + + var godotDefines = new[] + { + OS.GetName(), + Internal.GodotIs32Bits() ? "32" : "64" + }; + + bool buildSuccess = BuildManager.BuildProjectBlocking("Tools", godotDefines); + + if (!buildSuccess) + return; + + // Notify running game for hot-reload + Internal.ScriptEditorDebuggerReloadScripts(); + + // Hot-reload in the editor + GodotSharpEditor.Instance.GetNode("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.AddStyleboxOverride("panel", editorBaseControl.GetStylebox("DebuggerPanel", "EditorStyles")); + panelTabs.AddStyleboxOverride("tab_fg", editorBaseControl.GetStylebox("DebuggerTabFG", "EditorStyles")); + panelTabs.AddStyleboxOverride("tab_bg", editorBaseControl.GetStylebox("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.AddStyleboxOverride("panel", editorBaseControl.GetStylebox("DebuggerPanel", "EditorStyles")); + panelTabs.AddStyleboxOverride("tab_fg", editorBaseControl.GetStylebox("DebuggerTabFG", "EditorStyles")); + panelTabs.AddStyleboxOverride("tab_bg", editorBaseControl.GetStylebox("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.Connect("pressed", this, nameof(BuildProjectPressed)); + toolBarHBox.AddChild(buildProjectBtn); + + toolBarHBox.AddSpacer(begin: false); + + warningsBtn = new ToolButton + { + Text = "Warnings".TTR(), + ToggleMode = true, + Pressed = true, + Visible = false, + FocusMode = FocusModeEnum.None + }; + warningsBtn.Connect("toggled", this, nameof(_WarningsToggled)); + toolBarHBox.AddChild(warningsBtn); + + errorsBtn = new ToolButton + { + Text = "Errors".TTR(), + ToggleMode = true, + Pressed = true, + Visible = false, + FocusMode = FocusModeEnum.None + }; + errorsBtn.Connect("toggled", this, nameof(_ErrorsToggled)); + toolBarHBox.AddChild(errorsBtn); + + toolBarHBox.AddSpacer(begin: false); + + viewLogBtn = new Button + { + Text = "View log".TTR(), + FocusMode = FocusModeEnum.None, + Visible = false + }; + viewLogBtn.Connect("pressed", this, nameof(_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.Connect("item_selected", this, nameof(_BuildTabsItemSelected)); + buildTabsList.Connect("nothing_selected", this, nameof(_BuildTabsNothingSelected)); + hsc.AddChild(buildTabsList); + + buildTabs = new TabContainer + { + TabAlign = TabContainer.TabAlignEnum.Left, + SizeFlagsHorizontal = (int) SizeFlags.ExpandFill, + TabsVisible = false + }; + hsc.AddChild(buildTabs); + } + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs index d8cb9024c3..9a2b2e3a26 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs @@ -46,8 +46,8 @@ namespace GodotTools.Build { if (OS.IsWindows()) { - return (GodotSharpBuilds.BuildTool) EditorSettings.GetSetting("mono/builds/build_tool") - == GodotSharpBuilds.BuildTool.MsBuildMono; + return (BuildManager.BuildTool) EditorSettings.GetSetting("mono/builds/build_tool") + == BuildManager.BuildTool.MsBuildMono; } return false; @@ -103,16 +103,16 @@ namespace GodotTools.Build return process; } - public static int Build(MonoBuildInfo monoBuildInfo) + public static int Build(BuildInfo buildInfo) { - return Build(monoBuildInfo.Solution, monoBuildInfo.Configuration, - monoBuildInfo.LogsDirPath, monoBuildInfo.CustomProperties); + return Build(buildInfo.Solution, buildInfo.Configuration, + buildInfo.LogsDirPath, buildInfo.CustomProperties); } - public static async Task BuildAsync(MonoBuildInfo monoBuildInfo) + public static async Task BuildAsync(BuildInfo buildInfo) { - return await BuildAsync(monoBuildInfo.Solution, monoBuildInfo.Configuration, - monoBuildInfo.LogsDirPath, monoBuildInfo.CustomProperties); + return await BuildAsync(buildInfo.Solution, buildInfo.Configuration, + buildInfo.LogsDirPath, buildInfo.CustomProperties); } public static int Build(string solution, string config, string loggerOutputDir, IEnumerable customProperties = null) diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs b/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs index 926aabdf89..4c1e47ecad 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs @@ -19,13 +19,13 @@ namespace GodotTools.Build public static string FindMsBuild() { var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); - var buildTool = (GodotSharpBuilds.BuildTool) editorSettings.GetSetting("mono/builds/build_tool"); + var buildTool = (BuildManager.BuildTool) editorSettings.GetSetting("mono/builds/build_tool"); if (OS.IsWindows()) { switch (buildTool) { - case GodotSharpBuilds.BuildTool.MsBuildVs: + case BuildManager.BuildTool.MsBuildVs: { if (_msbuildToolsPath.Empty() || !File.Exists(_msbuildToolsPath)) { @@ -34,7 +34,7 @@ namespace GodotTools.Build if (_msbuildToolsPath.Empty()) { - throw new FileNotFoundException($"Cannot find executable for '{GodotSharpBuilds.PropNameMsbuildVs}'. Tried with path: {_msbuildToolsPath}"); + throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMsbuildVs}'. Tried with path: {_msbuildToolsPath}"); } } @@ -43,13 +43,13 @@ namespace GodotTools.Build return Path.Combine(_msbuildToolsPath, "MSBuild.exe"); } - case GodotSharpBuilds.BuildTool.MsBuildMono: + case BuildManager.BuildTool.MsBuildMono: { string msbuildPath = Path.Combine(Internal.MonoWindowsInstallRoot, "bin", "msbuild.bat"); if (!File.Exists(msbuildPath)) { - throw new FileNotFoundException($"Cannot find executable for '{GodotSharpBuilds.PropNameMsbuildMono}'. Tried with path: {msbuildPath}"); + throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMsbuildMono}'. Tried with path: {msbuildPath}"); } return msbuildPath; @@ -61,7 +61,7 @@ namespace GodotTools.Build if (OS.IsUnix()) { - if (buildTool == GodotSharpBuilds.BuildTool.MsBuildMono) + if (buildTool == BuildManager.BuildTool.MsBuildMono) { if (_msbuildUnixPath.Empty() || !File.Exists(_msbuildUnixPath)) { @@ -71,7 +71,7 @@ namespace GodotTools.Build if (_msbuildUnixPath.Empty()) { - throw new FileNotFoundException($"Cannot find binary for '{GodotSharpBuilds.PropNameMsbuildMono}'"); + throw new FileNotFoundException($"Cannot find binary for '{BuildManager.PropNameMsbuildMono}'"); } return _msbuildUnixPath; diff --git a/modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs b/modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs new file mode 100644 index 0000000000..70bd552f2f --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs @@ -0,0 +1,47 @@ +using System; +using Godot; +using Godot.Collections; +using GodotTools.Internals; +using Path = System.IO.Path; + +namespace GodotTools +{ + [Serializable] + public sealed class BuildInfo : Reference // TODO Remove Reference once we have proper serialization + { + public string Solution { get; } + public string Configuration { get; } + public Array CustomProperties { get; } = new Array(); // TODO Use List once we have proper serialization + + public string LogsDirPath => Path.Combine(GodotSharpDirs.BuildLogsDirs, $"{Solution.MD5Text()}_{Configuration}"); + + public override bool Equals(object obj) + { + if (obj is BuildInfo other) + return other.Solution == Solution && other.Configuration == Configuration; + + return false; + } + + public override int GetHashCode() + { + unchecked + { + int hash = 17; + hash = hash * 29 + Solution.GetHashCode(); + hash = hash * 29 + Configuration.GetHashCode(); + return hash; + } + } + + private BuildInfo() + { + } + + public BuildInfo(string solution, string configuration) + { + Solution = solution; + Configuration = configuration; + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs new file mode 100644 index 0000000000..417032da54 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using GodotTools.Build; +using GodotTools.Internals; +using GodotTools.Utils; +using static GodotTools.Internals.Globals; +using File = GodotTools.Utils.File; + +namespace GodotTools +{ + public static class BuildManager + { + private static readonly List BuildsInProgress = new List(); + + public const string PropNameMsbuildMono = "MSBuild (Mono)"; + public const string PropNameMsbuildVs = "MSBuild (VS Build Tools)"; + + public const string MsBuildIssuesFileName = "msbuild_issues.csv"; + public const string MsBuildLogFileName = "msbuild_log.txt"; + + public enum BuildTool + { + MsBuildMono, + MsBuildVs + } + + private static void RemoveOldIssuesFile(BuildInfo buildInfo) + { + var issuesFile = GetIssuesFilePath(buildInfo); + + if (!File.Exists(issuesFile)) + return; + + File.Delete(issuesFile); + } + + private static void ShowBuildErrorDialog(string message) + { + GodotSharpEditor.Instance.ShowErrorDialog(message, "Build error"); + GodotSharpEditor.Instance.BottomPanel.ShowBuildTab(); + } + + public static void RestartBuild(BuildTab buildTab) => throw new NotImplementedException(); + public static void StopBuild(BuildTab buildTab) => throw new NotImplementedException(); + + private static string GetLogFilePath(BuildInfo buildInfo) + { + return Path.Combine(buildInfo.LogsDirPath, MsBuildLogFileName); + } + + private static string GetIssuesFilePath(BuildInfo buildInfo) + { + return Path.Combine(buildInfo.LogsDirPath, MsBuildIssuesFileName); + } + + private static void PrintVerbose(string text) + { + if (Godot.OS.IsStdoutVerbose()) + Godot.GD.Print(text); + } + + public static bool Build(BuildInfo buildInfo) + { + if (BuildsInProgress.Contains(buildInfo)) + throw new InvalidOperationException("A build is already in progress"); + + BuildsInProgress.Add(buildInfo); + + try + { + BuildTab buildTab = GodotSharpEditor.Instance.BottomPanel.GetBuildTabFor(buildInfo); + buildTab.OnBuildStart(); + + // Required in order to update the build tasks list + Internal.GodotMainIteration(); + + try + { + RemoveOldIssuesFile(buildInfo); + } + catch (IOException e) + { + buildTab.OnBuildExecFailed($"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}"); + Console.Error.WriteLine(e); + } + + try + { + int exitCode = BuildSystem.Build(buildInfo); + + if (exitCode != 0) + PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}"); + + buildTab.OnBuildExit(exitCode == 0 ? BuildTab.BuildResults.Success : BuildTab.BuildResults.Error); + + return exitCode == 0; + } + catch (Exception e) + { + buildTab.OnBuildExecFailed($"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}"); + Console.Error.WriteLine(e); + return false; + } + } + finally + { + BuildsInProgress.Remove(buildInfo); + } + } + + public static async Task BuildAsync(BuildInfo buildInfo) + { + if (BuildsInProgress.Contains(buildInfo)) + throw new InvalidOperationException("A build is already in progress"); + + BuildsInProgress.Add(buildInfo); + + try + { + BuildTab buildTab = GodotSharpEditor.Instance.BottomPanel.GetBuildTabFor(buildInfo); + + try + { + RemoveOldIssuesFile(buildInfo); + } + catch (IOException e) + { + buildTab.OnBuildExecFailed($"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}"); + Console.Error.WriteLine(e); + } + + try + { + int exitCode = await BuildSystem.BuildAsync(buildInfo); + + if (exitCode != 0) + PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}"); + + buildTab.OnBuildExit(exitCode == 0 ? BuildTab.BuildResults.Success : BuildTab.BuildResults.Error); + + return exitCode == 0; + } + catch (Exception e) + { + buildTab.OnBuildExecFailed($"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}"); + Console.Error.WriteLine(e); + return false; + } + } + finally + { + BuildsInProgress.Remove(buildInfo); + } + } + + public static bool BuildProjectBlocking(string config, IEnumerable godotDefines) + { + if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) + return true; // No solution to build + + // Make sure to update the API assemblies if they happen to be missing. Just in + // case the user decided to delete them at some point after they were loaded. + Internal.UpdateApiAssembliesFromPrebuilt(); + + var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); + var buildTool = (BuildTool) editorSettings.GetSetting("mono/builds/build_tool"); + + using (var pr = new EditorProgress("mono_project_debug_build", "Building project solution...", 1)) + { + pr.Step("Building project solution", 0); + + var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, config); + + // Add Godot defines + string constants = buildTool == BuildTool.MsBuildVs ? "GodotDefineConstants=\"" : "GodotDefineConstants=\\\""; + + foreach (var godotDefine in godotDefines) + constants += $"GODOT_{godotDefine.ToUpper().Replace("-", "_").Replace(" ", "_").Replace(";", "_")};"; + + if (Internal.GodotIsRealTDouble()) + constants += "GODOT_REAL_T_IS_DOUBLE;"; + + constants += buildTool == BuildTool.MsBuildVs ? "\"" : "\\\""; + + buildInfo.CustomProperties.Add(constants); + + if (!Build(buildInfo)) + { + ShowBuildErrorDialog("Failed to build project solution"); + return false; + } + } + + return true; + } + + public static bool EditorBuildCallback() + { + if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) + return true; // 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)) + File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath); + + var currentPlayRequest = GodotSharpEditor.Instance.GodotIdeManager.GodotIdeServer.CurrentPlayRequest; + + if (currentPlayRequest != null) + { + if (currentPlayRequest.Value.HasDebugger) + { + // Set the environment variable that will tell the player to connect to the IDE debugger + // TODO: We should probably add a better way to do this + Environment.SetEnvironmentVariable("GODOT_MONO_DEBUGGER_AGENT", + "--debugger-agent=transport=dt_socket" + + $",address={currentPlayRequest.Value.DebuggerHost}:{currentPlayRequest.Value.DebuggerPort}" + + ",server=n"); + } + + return true; // Requested play from an external editor/IDE which already built the project + } + + var godotDefines = new[] + { + Godot.OS.GetName(), + Internal.GodotIs32Bits() ? "32" : "64" + }; + + return BuildProjectBlocking("Tools", godotDefines); + } + + public static void Initialize() + { + // Build tool settings + + EditorDef("mono/builds/build_tool", OS.IsWindows() ? BuildTool.MsBuildVs : BuildTool.MsBuildMono); + + var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); + + editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary + { + ["type"] = Godot.Variant.Type.Int, + ["name"] = "mono/builds/build_tool", + ["hint"] = Godot.PropertyHint.Enum, + ["hint_string"] = OS.IsWindows() ? + $"{PropNameMsbuildMono},{PropNameMsbuildVs}" : + $"{PropNameMsbuildMono}" + }); + + EditorDef("mono/builds/print_build_output", false); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/BuildTab.cs b/modules/mono/editor/GodotTools/GodotTools/BuildTab.cs new file mode 100644 index 0000000000..807a20d9a1 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/BuildTab.cs @@ -0,0 +1,267 @@ +using Godot; +using System; +using Godot.Collections; +using GodotTools.Internals; +using File = GodotTools.Utils.File; +using Path = System.IO.Path; + +namespace GodotTools +{ + public class BuildTab : VBoxContainer + { + public enum BuildResults + { + Error, + Success + } + + [Serializable] + private class BuildIssue : Reference // TODO Remove Reference once we have proper serialization + { + public bool Warning { get; set; } + public string File { get; set; } + public int Line { get; set; } + public int Column { get; set; } + public string Code { get; set; } + public string Message { get; set; } + public string ProjectFile { get; set; } + } + + private readonly Array issues = new Array(); // TODO Use List once we have proper serialization + private ItemList issuesList; + + public bool BuildExited { get; private set; } = false; + + public BuildResults? BuildResult { get; private set; } = null; + + public int ErrorCount { get; private set; } = 0; + + public int WarningCount { get; private set; } = 0; + + public bool ErrorsVisible { get; set; } = true; + public bool WarningsVisible { get; set; } = true; + + public Texture IconTexture + { + get + { + if (!BuildExited) + return GetIcon("Stop", "EditorIcons"); + + if (BuildResult == BuildResults.Error) + return GetIcon("StatusError", "EditorIcons"); + + return GetIcon("StatusSuccess", "EditorIcons"); + } + } + + public BuildInfo BuildInfo { get; private set; } + + private void _LoadIssuesFromFile(string csvFile) + { + using (var file = new Godot.File()) + { + try + { + Error openError = file.Open(csvFile, Godot.File.ModeFlags.Read); + + if (openError != Error.Ok) + return; + + while (!file.EofReached()) + { + string[] csvColumns = file.GetCsvLine(); + + if (csvColumns.Length == 1 && csvColumns[0].Empty()) + return; + + if (csvColumns.Length != 7) + { + GD.PushError($"Expected 7 columns, got {csvColumns.Length}"); + continue; + } + + var issue = new BuildIssue + { + Warning = csvColumns[0] == "warning", + File = csvColumns[1], + Line = int.Parse(csvColumns[2]), + Column = int.Parse(csvColumns[3]), + Code = csvColumns[4], + Message = csvColumns[5], + ProjectFile = csvColumns[6] + }; + + if (issue.Warning) + WarningCount += 1; + else + ErrorCount += 1; + + issues.Add(issue); + } + } + finally + { + file.Close(); // Disposing it is not enough. We need to call Close() + } + } + } + + private void _IssueActivated(int idx) + { + if (idx < 0 || idx >= issuesList.GetItemCount()) + throw new IndexOutOfRangeException("Item list index out of range"); + + // Get correct issue idx from issue list + int issueIndex = (int) issuesList.GetItemMetadata(idx); + + if (idx < 0 || idx >= issues.Count) + throw new IndexOutOfRangeException("Issue index out of range"); + + BuildIssue issue = issues[issueIndex]; + + if (issue.ProjectFile.Empty() && issue.File.Empty()) + return; + + string projectDir = issue.ProjectFile.Length > 0 ? issue.ProjectFile.GetBaseDir() : BuildInfo.Solution.GetBaseDir(); + + string file = Path.Combine(projectDir.SimplifyGodotPath(), issue.File.SimplifyGodotPath()); + + if (!File.Exists(file)) + return; + + file = ProjectSettings.LocalizePath(file); + + if (file.StartsWith("res://")) + { + var script = (Script) ResourceLoader.Load(file, typeHint: Internal.CSharpLanguageType); + + if (script != null && Internal.ScriptEditorEdit(script, issue.Line, issue.Column)) + Internal.EditorNodeShowScriptScreen(); + } + } + + public void UpdateIssuesList() + { + issuesList.Clear(); + + using (var warningIcon = GetIcon("Warning", "EditorIcons")) + using (var errorIcon = GetIcon("Error", "EditorIcons")) + { + for (int i = 0; i < issues.Count; i++) + { + BuildIssue issue = issues[i]; + + if (!(issue.Warning ? WarningsVisible : ErrorsVisible)) + continue; + + string tooltip = string.Empty; + tooltip += $"Message: {issue.Message}"; + + if (!issue.Code.Empty()) + tooltip += $"\nCode: {issue.Code}"; + + tooltip += $"\nType: {(issue.Warning ? "warning" : "error")}"; + + string text = string.Empty; + + if (!issue.File.Empty()) + { + text += $"{issue.File}({issue.Line},{issue.Column}): "; + + tooltip += $"\nFile: {issue.File}"; + tooltip += $"\nLine: {issue.Line}"; + tooltip += $"\nColumn: {issue.Column}"; + } + + if (!issue.ProjectFile.Empty()) + tooltip += $"\nProject: {issue.ProjectFile}"; + + text += issue.Message; + + int lineBreakIdx = text.IndexOf("\n", StringComparison.Ordinal); + string itemText = lineBreakIdx == -1 ? text : text.Substring(0, lineBreakIdx); + issuesList.AddItem(itemText, issue.Warning ? warningIcon : errorIcon); + + int index = issuesList.GetItemCount() - 1; + issuesList.SetItemTooltip(index, tooltip); + issuesList.SetItemMetadata(index, i); + } + } + } + + public void OnBuildStart() + { + BuildExited = false; + + issues.Clear(); + WarningCount = 0; + ErrorCount = 0; + UpdateIssuesList(); + + GodotSharpEditor.Instance.BottomPanel.RaiseBuildTab(this); + } + + public void OnBuildExit(BuildResults result) + { + BuildExited = true; + BuildResult = result; + + _LoadIssuesFromFile(Path.Combine(BuildInfo.LogsDirPath, BuildManager.MsBuildIssuesFileName)); + UpdateIssuesList(); + + GodotSharpEditor.Instance.BottomPanel.RaiseBuildTab(this); + } + + public void OnBuildExecFailed(string cause) + { + BuildExited = true; + BuildResult = BuildResults.Error; + + issuesList.Clear(); + + var issue = new BuildIssue {Message = cause, Warning = false}; + + ErrorCount += 1; + issues.Add(issue); + + UpdateIssuesList(); + + GodotSharpEditor.Instance.BottomPanel.RaiseBuildTab(this); + } + + public void RestartBuild() + { + if (!BuildExited) + throw new InvalidOperationException("Build already started"); + + BuildManager.RestartBuild(this); + } + + public void StopBuild() + { + if (!BuildExited) + throw new InvalidOperationException("Build is not in progress"); + + BuildManager.StopBuild(this); + } + + public override void _Ready() + { + base._Ready(); + + issuesList = new ItemList {SizeFlagsVertical = (int) SizeFlags.ExpandFill}; + issuesList.Connect("item_activated", this, nameof(_IssueActivated)); + AddChild(issuesList); + } + + private BuildTab() + { + } + + public BuildTab(BuildInfo buildInfo) + { + BuildInfo = buildInfo; + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/CSharpProject.cs b/modules/mono/editor/GodotTools/GodotTools/CSharpProject.cs deleted file mode 100644 index 4535ed7247..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/CSharpProject.cs +++ /dev/null @@ -1,127 +0,0 @@ -using Godot; -using System; -using Godot.Collections; -using GodotTools.Internals; -using GodotTools.ProjectEditor; -using static GodotTools.Internals.Globals; -using File = GodotTools.Utils.File; -using Directory = GodotTools.Utils.Directory; - -namespace GodotTools -{ - public static class CSharpProject - { - public static string GenerateGameProject(string dir, string name) - { - try - { - return ProjectGenerator.GenGameProject(dir, name, compileItems: new string[] { }); - } - catch (Exception e) - { - GD.PushError(e.ToString()); - return string.Empty; - } - } - - public static void AddItem(string projectPath, string itemType, string include) - { - if (!(bool) GlobalDef("mono/project/auto_update_project", true)) - return; - - ProjectUtils.AddItemToProjectChecked(projectPath, itemType, include); - } - - public static void FixApiHintPath(string projectPath) - { - try - { - ProjectUtils.FixApiHintPath(projectPath); - } - catch (Exception e) - { - GD.PushError(e.ToString()); - } - } - - private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - - private static ulong ConvertToTimestamp(this DateTime value) - { - TimeSpan elapsedTime = value - Epoch; - return (ulong) elapsedTime.TotalSeconds; - } - - public static void GenerateScriptsMetadata(string projectPath, string outputPath) - { - if (File.Exists(outputPath)) - File.Delete(outputPath); - - var oldDict = Internal.GetScriptsMetadataOrNothing(); - var newDict = new Godot.Collections.Dictionary(); - - foreach (var includeFile in ProjectUtils.GetIncludeFiles(projectPath, "Compile")) - { - string projectIncludeFile = ("res://" + includeFile).SimplifyGodotPath(); - - ulong modifiedTime = File.GetLastWriteTime(projectIncludeFile).ConvertToTimestamp(); - - if (oldDict.TryGetValue(projectIncludeFile, out var oldFileVar)) - { - var oldFileDict = (Dictionary) oldFileVar; - - if (ulong.TryParse(oldFileDict["modified_time"] as string, out ulong storedModifiedTime)) - { - if (storedModifiedTime == modifiedTime) - { - // No changes so no need to parse again - newDict[projectIncludeFile] = oldFileDict; - continue; - } - } - } - - ScriptClassParser.ParseFileOrThrow(projectIncludeFile, out var classes); - - string searchName = System.IO.Path.GetFileNameWithoutExtension(projectIncludeFile); - - var classDict = new Dictionary(); - - foreach (var classDecl in classes) - { - if (classDecl.BaseCount == 0) - continue; // Does not inherit nor implement anything, so it can't be a script class - - string classCmp = classDecl.Nested ? - classDecl.Name.Substring(classDecl.Name.LastIndexOf(".", StringComparison.Ordinal) + 1) : - classDecl.Name; - - if (classCmp != searchName) - continue; - - classDict["namespace"] = classDecl.Namespace; - classDict["class_name"] = classDecl.Name; - classDict["nested"] = classDecl.Nested; - break; - } - - if (classDict.Count == 0) - continue; // Not found - - newDict[projectIncludeFile] = new Dictionary {["modified_time"] = $"{modifiedTime}", ["class"] = classDict}; - } - - if (newDict.Count > 0) - { - string json = JSON.Print(newDict); - - string baseDir = outputPath.GetBaseDir(); - - if (!Directory.Exists(baseDir)) - Directory.CreateDirectory(baseDir); - - File.WriteAllText(outputPath, json); - } - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs b/modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs new file mode 100644 index 0000000000..c021a9051e --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs @@ -0,0 +1,127 @@ +using Godot; +using System; +using Godot.Collections; +using GodotTools.Internals; +using GodotTools.ProjectEditor; +using static GodotTools.Internals.Globals; +using File = GodotTools.Utils.File; +using Directory = GodotTools.Utils.Directory; + +namespace GodotTools +{ + public static class CsProjOperations + { + public static string GenerateGameProject(string dir, string name) + { + try + { + return ProjectGenerator.GenGameProject(dir, name, compileItems: new string[] { }); + } + catch (Exception e) + { + GD.PushError(e.ToString()); + return string.Empty; + } + } + + public static void AddItem(string projectPath, string itemType, string include) + { + if (!(bool) GlobalDef("mono/project/auto_update_project", true)) + return; + + ProjectUtils.AddItemToProjectChecked(projectPath, itemType, include); + } + + public static void FixApiHintPath(string projectPath) + { + try + { + ProjectUtils.FixApiHintPath(projectPath); + } + catch (Exception e) + { + GD.PushError(e.ToString()); + } + } + + private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + private static ulong ConvertToTimestamp(this DateTime value) + { + TimeSpan elapsedTime = value - Epoch; + return (ulong) elapsedTime.TotalSeconds; + } + + public static void GenerateScriptsMetadata(string projectPath, string outputPath) + { + if (File.Exists(outputPath)) + File.Delete(outputPath); + + var oldDict = Internal.GetScriptsMetadataOrNothing(); + var newDict = new Godot.Collections.Dictionary(); + + foreach (var includeFile in ProjectUtils.GetIncludeFiles(projectPath, "Compile")) + { + string projectIncludeFile = ("res://" + includeFile).SimplifyGodotPath(); + + ulong modifiedTime = File.GetLastWriteTime(projectIncludeFile).ConvertToTimestamp(); + + if (oldDict.TryGetValue(projectIncludeFile, out var oldFileVar)) + { + var oldFileDict = (Dictionary) oldFileVar; + + if (ulong.TryParse(oldFileDict["modified_time"] as string, out ulong storedModifiedTime)) + { + if (storedModifiedTime == modifiedTime) + { + // No changes so no need to parse again + newDict[projectIncludeFile] = oldFileDict; + continue; + } + } + } + + ScriptClassParser.ParseFileOrThrow(projectIncludeFile, out var classes); + + string searchName = System.IO.Path.GetFileNameWithoutExtension(projectIncludeFile); + + var classDict = new Dictionary(); + + foreach (var classDecl in classes) + { + if (classDecl.BaseCount == 0) + continue; // Does not inherit nor implement anything, so it can't be a script class + + string classCmp = classDecl.Nested ? + classDecl.Name.Substring(classDecl.Name.LastIndexOf(".", StringComparison.Ordinal) + 1) : + classDecl.Name; + + if (classCmp != searchName) + continue; + + classDict["namespace"] = classDecl.Namespace; + classDict["class_name"] = classDecl.Name; + classDict["nested"] = classDecl.Nested; + break; + } + + if (classDict.Count == 0) + continue; // Not found + + newDict[projectIncludeFile] = new Dictionary {["modified_time"] = $"{modifiedTime}", ["class"] = classDict}; + } + + if (newDict.Count > 0) + { + string json = JSON.Print(newDict); + + string baseDir = outputPath.GetBaseDir(); + + if (!Directory.Exists(baseDir)) + Directory.CreateDirectory(baseDir); + + File.WriteAllText(outputPath, json); + } + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/ExternalEditorId.cs b/modules/mono/editor/GodotTools/GodotTools/ExternalEditorId.cs new file mode 100644 index 0000000000..4312ca0230 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/ExternalEditorId.cs @@ -0,0 +1,11 @@ +namespace GodotTools +{ + public enum ExternalEditorId + { + None, + VisualStudio, // TODO (Windows-only) + VisualStudioForMac, // Mac-only + MonoDevelop, + VsCode + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpBuilds.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpBuilds.cs deleted file mode 100644 index de3a4d9a6e..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpBuilds.cs +++ /dev/null @@ -1,278 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using GodotTools.Build; -using GodotTools.Internals; -using GodotTools.Utils; -using static GodotTools.Internals.Globals; -using Error = Godot.Error; -using File = GodotTools.Utils.File; -using Directory = GodotTools.Utils.Directory; - -namespace GodotTools -{ - public static class GodotSharpBuilds - { - private static readonly List BuildsInProgress = new List(); - - public const string PropNameMsbuildMono = "MSBuild (Mono)"; - public const string PropNameMsbuildVs = "MSBuild (VS Build Tools)"; - - public const string MsBuildIssuesFileName = "msbuild_issues.csv"; - public const string MsBuildLogFileName = "msbuild_log.txt"; - - public enum BuildTool - { - MsBuildMono, - MsBuildVs - } - - private static void RemoveOldIssuesFile(MonoBuildInfo buildInfo) - { - var issuesFile = GetIssuesFilePath(buildInfo); - - if (!File.Exists(issuesFile)) - return; - - File.Delete(issuesFile); - } - - private static string _ApiFolderName(ApiAssemblyType apiType) - { - ulong apiHash = apiType == ApiAssemblyType.Core ? - Internal.GetCoreApiHash() : - Internal.GetEditorApiHash(); - return $"{apiHash}_{BindingsGenerator.Version}_{BindingsGenerator.CsGlueVersion}"; - } - - private static void ShowBuildErrorDialog(string message) - { - GodotSharpEditor.Instance.ShowErrorDialog(message, "Build error"); - GodotSharpEditor.Instance.MonoBottomPanel.ShowBuildTab(); - } - - public static void RestartBuild(MonoBuildTab buildTab) => throw new NotImplementedException(); - public static void StopBuild(MonoBuildTab buildTab) => throw new NotImplementedException(); - - private static string GetLogFilePath(MonoBuildInfo buildInfo) - { - return Path.Combine(buildInfo.LogsDirPath, MsBuildLogFileName); - } - - private static string GetIssuesFilePath(MonoBuildInfo buildInfo) - { - return Path.Combine(buildInfo.LogsDirPath, MsBuildIssuesFileName); - } - - private static void PrintVerbose(string text) - { - if (Godot.OS.IsStdoutVerbose()) - Godot.GD.Print(text); - } - - public static bool Build(MonoBuildInfo buildInfo) - { - if (BuildsInProgress.Contains(buildInfo)) - throw new InvalidOperationException("A build is already in progress"); - - BuildsInProgress.Add(buildInfo); - - try - { - MonoBuildTab buildTab = GodotSharpEditor.Instance.MonoBottomPanel.GetBuildTabFor(buildInfo); - buildTab.OnBuildStart(); - - // Required in order to update the build tasks list - Internal.GodotMainIteration(); - - try - { - RemoveOldIssuesFile(buildInfo); - } - catch (IOException e) - { - buildTab.OnBuildExecFailed($"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}"); - Console.Error.WriteLine(e); - } - - try - { - int exitCode = BuildSystem.Build(buildInfo); - - if (exitCode != 0) - PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}"); - - buildTab.OnBuildExit(exitCode == 0 ? MonoBuildTab.BuildResults.Success : MonoBuildTab.BuildResults.Error); - - return exitCode == 0; - } - catch (Exception e) - { - buildTab.OnBuildExecFailed($"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}"); - Console.Error.WriteLine(e); - return false; - } - } - finally - { - BuildsInProgress.Remove(buildInfo); - } - } - - public static async Task BuildAsync(MonoBuildInfo buildInfo) - { - if (BuildsInProgress.Contains(buildInfo)) - throw new InvalidOperationException("A build is already in progress"); - - BuildsInProgress.Add(buildInfo); - - try - { - MonoBuildTab buildTab = GodotSharpEditor.Instance.MonoBottomPanel.GetBuildTabFor(buildInfo); - - try - { - RemoveOldIssuesFile(buildInfo); - } - catch (IOException e) - { - buildTab.OnBuildExecFailed($"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}"); - Console.Error.WriteLine(e); - } - - try - { - int exitCode = await BuildSystem.BuildAsync(buildInfo); - - if (exitCode != 0) - PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}"); - - buildTab.OnBuildExit(exitCode == 0 ? MonoBuildTab.BuildResults.Success : MonoBuildTab.BuildResults.Error); - - return exitCode == 0; - } - catch (Exception e) - { - buildTab.OnBuildExecFailed($"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}"); - Console.Error.WriteLine(e); - return false; - } - } - finally - { - BuildsInProgress.Remove(buildInfo); - } - } - - public static bool BuildApiSolution(string apiSlnDir, string config) - { - string apiSlnFile = Path.Combine(apiSlnDir, $"{ApiAssemblyNames.SolutionName}.sln"); - - string coreApiAssemblyDir = Path.Combine(apiSlnDir, ApiAssemblyNames.Core, "bin", config); - string coreApiAssemblyFile = Path.Combine(coreApiAssemblyDir, $"{ApiAssemblyNames.Core}.dll"); - - string editorApiAssemblyDir = Path.Combine(apiSlnDir, ApiAssemblyNames.Editor, "bin", config); - string editorApiAssemblyFile = Path.Combine(editorApiAssemblyDir, $"{ApiAssemblyNames.Editor}.dll"); - - if (File.Exists(coreApiAssemblyFile) && File.Exists(editorApiAssemblyFile)) - return true; // The assemblies are in the output folder; assume the solution is already built - - var apiBuildInfo = new MonoBuildInfo(apiSlnFile, config); - - // TODO Replace this global NoWarn with '#pragma warning' directives on generated files, - // once we start to actively document manually maintained C# classes - apiBuildInfo.CustomProperties.Add("NoWarn=1591"); // Ignore missing documentation warnings - - if (Build(apiBuildInfo)) - return true; - - ShowBuildErrorDialog($"Failed to build {ApiAssemblyNames.SolutionName} solution."); - return false; - } - - public static bool BuildProjectBlocking(string config, IEnumerable godotDefines) - { - if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) - return true; // No solution to build - - // Make sure to update the API assemblies if they happen to be missing. Just in - // case the user decided to delete them at some point after they were loaded. - Internal.UpdateApiAssembliesFromPrebuilt(); - - var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); - var buildTool = (BuildTool)editorSettings.GetSetting("mono/builds/build_tool"); - - using (var pr = new EditorProgress("mono_project_debug_build", "Building project solution...", 1)) - { - pr.Step("Building project solution", 0); - - var buildInfo = new MonoBuildInfo(GodotSharpDirs.ProjectSlnPath, config); - - // Add Godot defines - string constants = buildTool == BuildTool.MsBuildVs ? "GodotDefineConstants=\"" : "GodotDefineConstants=\\\""; - - foreach (var godotDefine in godotDefines) - constants += $"GODOT_{godotDefine.ToUpper().Replace("-", "_").Replace(" ", "_").Replace(";", "_")};"; - - if (Internal.GodotIsRealTDouble()) - constants += "GODOT_REAL_T_IS_DOUBLE;"; - - constants += buildTool == BuildTool.MsBuildVs ? "\"" : "\\\""; - - buildInfo.CustomProperties.Add(constants); - - if (!Build(buildInfo)) - { - ShowBuildErrorDialog("Failed to build project solution"); - return false; - } - } - - return true; - } - - public static bool EditorBuildCallback() - { - if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) - return true; // No solution to build - - string editorScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor"); - string playerScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor_player"); - - CSharpProject.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, editorScriptsMetadataPath); - - if (File.Exists(editorScriptsMetadataPath)) - File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath); - - var godotDefines = new[] - { - Godot.OS.GetName(), - Internal.GodotIs32Bits() ? "32" : "64" - }; - - return BuildProjectBlocking("Tools", godotDefines); - } - - public static void Initialize() - { - // Build tool settings - - EditorDef("mono/builds/build_tool", OS.IsWindows() ? BuildTool.MsBuildVs : BuildTool.MsBuildMono); - - var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); - - editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary - { - ["type"] = Godot.Variant.Type.Int, - ["name"] = "mono/builds/build_tool", - ["hint"] = Godot.PropertyHint.Enum, - ["hint_string"] = OS.IsWindows() ? - $"{PropNameMsbuildMono},{PropNameMsbuildVs}" : - $"{PropNameMsbuildMono}" - }); - - EditorDef("mono/builds/print_build_output", false); - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index 9b5afb94a3..099c7fcb56 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -2,16 +2,18 @@ using Godot; using GodotTools.Utils; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; +using GodotTools.Ides; using GodotTools.Internals; using GodotTools.ProjectEditor; using static GodotTools.Internals.Globals; using File = GodotTools.Utils.File; -using Path = System.IO.Path; using OS = GodotTools.Utils.OS; namespace GodotTools { + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] public class GodotSharpEditor : EditorPlugin, ISerializationListener { private EditorSettings editorSettings; @@ -24,12 +26,11 @@ namespace GodotTools private ToolButton bottomPanelBtn; - private MonoDevelopInstance monoDevelopInstance; - private MonoDevelopInstance visualStudioForMacInstance; + public GodotIdeManager GodotIdeManager { get; private set; } private WeakRef exportPluginWeak; // TODO Use WeakReference once we have proper serialization - public MonoBottomPanel MonoBottomPanel { get; private set; } + public BottomPanel BottomPanel { get; private set; } private bool CreateProjectSolution() { @@ -44,7 +45,7 @@ namespace GodotTools if (name.Empty()) name = "UnnamedProject"; - string guid = CSharpProject.GenerateGameProject(path, name); + string guid = CsProjOperations.GenerateGameProject(path, name); if (guid.Length > 0) { @@ -133,7 +134,7 @@ namespace GodotTools return; // Failed to create solution } - Instance.MonoBottomPanel.BuildProjectPressed(); + Instance.BottomPanel.BuildProjectPressed(); } public override void _Notification(int what) @@ -153,21 +154,12 @@ namespace GodotTools } } - public enum MenuOptions + private enum MenuOptions { CreateSln, AboutCSharp, } - public enum ExternalEditor - { - None, - VisualStudio, // TODO (Windows-only) - VisualStudioForMac, // Mac-only - MonoDevelop, - VsCode - } - public void ShowErrorDialog(string message, string title = "Error") { errorDialog.WindowTitle = title; @@ -184,11 +176,30 @@ namespace GodotTools public Error OpenInExternalEditor(Script script, int line, int col) { - var editor = (ExternalEditor) editorSettings.GetSetting("mono/editor/external_editor"); + var editor = (ExternalEditorId) editorSettings.GetSetting("mono/editor/external_editor"); switch (editor) { - case ExternalEditor.VsCode: + case ExternalEditorId.None: + // Tells the caller to fallback to the global external editor settings or the built-in editor + return Error.Unavailable; + case ExternalEditorId.VisualStudio: + throw new NotSupportedException(); + case ExternalEditorId.VisualStudioForMac: + goto case ExternalEditorId.MonoDevelop; + case ExternalEditorId.MonoDevelop: + { + string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath); + + if (line >= 0) + GodotIdeManager.SendOpenFile(scriptPath, line + 1, col); + else + GodotIdeManager.SendOpenFile(scriptPath); + + break; + } + + case ExternalEditorId.VsCode: { if (_vsCodePath.Empty() || !File.Exists(_vsCodePath)) { @@ -273,47 +284,6 @@ namespace GodotTools break; } - case ExternalEditor.VisualStudioForMac: - goto case ExternalEditor.MonoDevelop; - case ExternalEditor.MonoDevelop: - { - MonoDevelopInstance GetMonoDevelopInstance(string solutionPath) - { - if (OS.IsOSX() && editor == ExternalEditor.VisualStudioForMac) - { - if (visualStudioForMacInstance == null) - visualStudioForMacInstance = new MonoDevelopInstance(solutionPath, MonoDevelopInstance.EditorId.VisualStudioForMac); - - return visualStudioForMacInstance; - } - - if (monoDevelopInstance == null) - monoDevelopInstance = new MonoDevelopInstance(solutionPath, MonoDevelopInstance.EditorId.MonoDevelop); - - return monoDevelopInstance; - } - - string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath); - - if (line >= 0) - scriptPath += $";{line + 1};{col}"; - - try - { - GetMonoDevelopInstance(GodotSharpDirs.ProjectSlnPath).Execute(scriptPath); - } - catch (FileNotFoundException) - { - string editorName = editor == ExternalEditor.VisualStudioForMac ? "Visual Studio" : "MonoDevelop"; - GD.PushError($"Cannot find code editor: {editorName}"); - return Error.FileNotFound; - } - - break; - } - - case ExternalEditor.None: - return Error.Unavailable; default: throw new ArgumentOutOfRangeException(); } @@ -323,12 +293,12 @@ namespace GodotTools public bool OverridesExternalEditor() { - return (ExternalEditor) editorSettings.GetSetting("mono/editor/external_editor") != ExternalEditor.None; + return (ExternalEditorId) editorSettings.GetSetting("mono/editor/external_editor") != ExternalEditorId.None; } public override bool Build() { - return GodotSharpBuilds.EditorBuildCallback(); + return BuildManager.EditorBuildCallback(); } public override void EnablePlugin() @@ -347,9 +317,9 @@ namespace GodotTools errorDialog = new AcceptDialog(); editorBaseControl.AddChild(errorDialog); - MonoBottomPanel = new MonoBottomPanel(); + BottomPanel = new BottomPanel(); - bottomPanelBtn = AddControlToBottomPanel(MonoBottomPanel, "Mono".TTR()); + bottomPanelBtn = AddControlToBottomPanel(BottomPanel, "Mono".TTR()); AddChild(new HotReloadAssemblyWatcher {Name = "HotReloadAssemblyWatcher"}); @@ -407,7 +377,7 @@ namespace GodotTools if (File.Exists(GodotSharpDirs.ProjectSlnPath) && File.Exists(GodotSharpDirs.ProjectCsProjPath)) { // Make sure the existing project has Api assembly references configured correctly - CSharpProject.FixApiHintPath(GodotSharpDirs.ProjectCsProjPath); + CsProjOperations.FixApiHintPath(GodotSharpDirs.ProjectCsProjPath); } else { @@ -427,25 +397,25 @@ namespace GodotTools AddControlToContainer(CustomControlContainer.Toolbar, buildButton); // External editor settings - EditorDef("mono/editor/external_editor", ExternalEditor.None); + EditorDef("mono/editor/external_editor", ExternalEditorId.None); string settingsHintStr = "Disabled"; if (OS.IsWindows()) { - settingsHintStr += $",MonoDevelop:{(int) ExternalEditor.MonoDevelop}" + - $",Visual Studio Code:{(int) ExternalEditor.VsCode}"; + settingsHintStr += $",MonoDevelop:{(int) ExternalEditorId.MonoDevelop}" + + $",Visual Studio Code:{(int) ExternalEditorId.VsCode}"; } else if (OS.IsOSX()) { - settingsHintStr += $",Visual Studio:{(int) ExternalEditor.VisualStudioForMac}" + - $",MonoDevelop:{(int) ExternalEditor.MonoDevelop}" + - $",Visual Studio Code:{(int) ExternalEditor.VsCode}"; + settingsHintStr += $",Visual Studio:{(int) ExternalEditorId.VisualStudioForMac}" + + $",MonoDevelop:{(int) ExternalEditorId.MonoDevelop}" + + $",Visual Studio Code:{(int) ExternalEditorId.VsCode}"; } else if (OS.IsUnix()) { - settingsHintStr += $",MonoDevelop:{(int) ExternalEditor.MonoDevelop}" + - $",Visual Studio Code:{(int) ExternalEditor.VsCode}"; + settingsHintStr += $",MonoDevelop:{(int) ExternalEditorId.MonoDevelop}" + + $",Visual Studio Code:{(int) ExternalEditorId.VsCode}"; } editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary @@ -461,7 +431,10 @@ namespace GodotTools AddExportPlugin(exportPlugin); exportPluginWeak = WeakRef(exportPlugin); - GodotSharpBuilds.Initialize(); + BuildManager.Initialize(); + + GodotIdeManager = new GodotIdeManager(); + AddChild(GodotIdeManager); } protected override void Dispose(bool disposing) @@ -478,6 +451,8 @@ namespace GodotTools exportPluginWeak.Dispose(); } + + GodotIdeManager?.Dispose(); } public void OnBeforeSerialize() diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpExport.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpExport.cs index b80fe1fab7..aefc51545e 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpExport.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpExport.cs @@ -65,14 +65,14 @@ namespace GodotTools string buildConfig = isDebug ? "Debug" : "Release"; string scriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, $"scripts_metadata.{(isDebug ? "debug" : "release")}"); - CSharpProject.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, scriptsMetadataPath); + CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, scriptsMetadataPath); AddFile(scriptsMetadataPath, scriptsMetadataPath); // Turn export features into defines var godotDefines = features; - if (!GodotSharpBuilds.BuildProjectBlocking(buildConfig, godotDefines)) + if (!BuildManager.BuildProjectBlocking(buildConfig, godotDefines)) { GD.PushError("Failed to build project"); return; diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj index 01e8c87d14..e2d576caef 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj +++ b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj @@ -39,26 +39,31 @@ + + + + + - + - + - - - + + + - + @@ -66,6 +71,10 @@ {6ce9a984-37b1-4f8a-8fe9-609f05f071b3} GodotTools.BuildLogger + + {92600954-25f0-4291-8e11-1fee9fc4be20} + GodotTools.IdeConnection + {A8CDAD94-C6D4-4B19-A7E7-76C53CC92984} GodotTools.ProjectEditor @@ -75,8 +84,5 @@ GodotTools.Core - - - \ No newline at end of file diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs new file mode 100644 index 0000000000..9e24138143 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs @@ -0,0 +1,166 @@ +using System; +using System.IO; +using Godot; +using GodotTools.IdeConnection; +using GodotTools.Internals; + +namespace GodotTools.Ides +{ + public class GodotIdeManager : Node, ISerializationListener + { + public GodotIdeServer GodotIdeServer { get; private set; } + + private MonoDevelop.Instance monoDevelInstance; + private MonoDevelop.Instance vsForMacInstance; + + private GodotIdeServer GetRunningServer() + { + if (GodotIdeServer != null && !GodotIdeServer.IsDisposed) + return GodotIdeServer; + StartServer(); + return GodotIdeServer; + } + + public override void _Ready() + { + StartServer(); + } + + public void OnBeforeSerialize() + { + GodotIdeServer?.Dispose(); + } + + public void OnAfterDeserialize() + { + StartServer(); + } + + private ILogger logger; + + protected ILogger Logger + { + get => logger ?? (logger = new ConsoleLogger()); + set => logger = value; + } + + private void StartServer() + { + GodotIdeServer?.Dispose(); + GodotIdeServer = new GodotIdeServer(LaunchIde, + OS.GetExecutablePath(), + ProjectSettings.GlobalizePath(GodotSharpDirs.ResMetadataDir)); + + GodotIdeServer.Logger = Logger; + + GodotIdeServer.StartServer(); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + GodotIdeServer?.Dispose(); + } + + private void LaunchIde() + { + var editor = (ExternalEditorId) GodotSharpEditor.Instance.GetEditorInterface() + .GetEditorSettings().GetSetting("mono/editor/external_editor"); + + switch (editor) + { + case ExternalEditorId.None: + case ExternalEditorId.VisualStudio: + case ExternalEditorId.VsCode: + throw new NotSupportedException(); + case ExternalEditorId.VisualStudioForMac: + goto case ExternalEditorId.MonoDevelop; + case ExternalEditorId.MonoDevelop: + { + MonoDevelop.Instance GetMonoDevelopInstance(string solutionPath) + { + if (Utils.OS.IsOSX() && editor == ExternalEditorId.VisualStudioForMac) + { + vsForMacInstance = vsForMacInstance ?? + new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.VisualStudioForMac); + return vsForMacInstance; + } + + monoDevelInstance = monoDevelInstance ?? + new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.MonoDevelop); + return monoDevelInstance; + } + + try + { + var instance = GetMonoDevelopInstance(GodotSharpDirs.ProjectSlnPath); + + if (!instance.IsRunning) + instance.Execute(); + } + catch (FileNotFoundException) + { + string editorName = editor == ExternalEditorId.VisualStudioForMac ? "Visual Studio" : "MonoDevelop"; + GD.PushError($"Cannot find code editor: {editorName}"); + } + + break; + } + + default: + throw new ArgumentOutOfRangeException(); + } + } + + private void WriteMessage(string id, params string[] arguments) + { + GetRunningServer().WriteMessage(new Message(id, arguments)); + } + + public void SendOpenFile(string file) + { + WriteMessage("OpenFile", file); + } + + public void SendOpenFile(string file, int line) + { + WriteMessage("OpenFile", file, line.ToString()); + } + + public void SendOpenFile(string file, int line, int column) + { + WriteMessage("OpenFile", file, line.ToString(), column.ToString()); + } + + private class GodotLogger : ILogger + { + public void LogDebug(string message) + { + if (OS.IsStdoutVerbose()) + Console.WriteLine(message); + } + + public void LogInfo(string message) + { + if (OS.IsStdoutVerbose()) + Console.WriteLine(message); + } + + public void LogWarning(string message) + { + GD.PushWarning(message); + } + + public void LogError(string message) + { + GD.PushError(message); + } + + public void LogError(string message, Exception e) + { + GD.PushError(message + "\n" + e); + } + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs new file mode 100644 index 0000000000..d515254e65 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using GodotTools.IdeConnection; +using GodotTools.Internals; +using GodotTools.Utils; +using File = System.IO.File; +using Thread = System.Threading.Thread; + +namespace GodotTools.Ides +{ + public class GodotIdeServer : GodotIdeBase + { + private readonly TcpListener listener; + private readonly FileStream metaFile; + private readonly Action launchIdeAction; + private readonly NotifyAwaiter clientConnectedAwaiter = new NotifyAwaiter(); + + private async Task AwaitClientConnected() + { + return await clientConnectedAwaiter.Reset(); + } + + public GodotIdeServer(Action launchIdeAction, string editorExecutablePath, string projectMetadataDir) + : base(projectMetadataDir) + { + messageHandlers = InitializeMessageHandlers(); + + this.launchIdeAction = launchIdeAction; + + // The Godot editor's file system thread can keep the file open for writing, so we are forced to allow write sharing... + const FileShare metaFileShare = FileShare.ReadWrite; + + metaFile = File.Open(MetaFilePath, FileMode.Create, FileAccess.Write, metaFileShare); + + listener = new TcpListener(new IPEndPoint(IPAddress.Loopback, port: 0)); + listener.Start(); + + int port = ((IPEndPoint) listener.Server.LocalEndPoint).Port; + using (var metaFileWriter = new StreamWriter(metaFile, Encoding.UTF8)) + { + metaFileWriter.WriteLine(port); + metaFileWriter.WriteLine(editorExecutablePath); + } + + StartServer(); + } + + public void StartServer() + { + var serverThread = new Thread(RunServerThread) {Name = "Godot Ide Connection Server"}; + serverThread.Start(); + } + + private void RunServerThread() + { + SynchronizationContext.SetSynchronizationContext(Godot.Dispatcher.SynchronizationContext); + + try + { + while (!IsDisposed) + { + TcpClient tcpClient = listener.AcceptTcpClient(); + + Logger.LogInfo("Connection open with Ide Client"); + + lock (ConnectionLock) + { + Connection = new GodotIdeConnectionServer(tcpClient, HandleMessage); + Connection.Logger = Logger; + } + + Connected += () => clientConnectedAwaiter.SetResult(true); + + Connection.Start(); + } + } + catch (Exception e) + { + if (!IsDisposed && !(e is SocketException se && se.SocketErrorCode == SocketError.Interrupted)) + throw; + } + } + + public async void WriteMessage(Message message) + { + async Task LaunchIde() + { + if (IsConnected) + return; + + launchIdeAction(); + await Task.WhenAny(Task.Delay(10000), AwaitClientConnected()); + } + + await LaunchIde(); + + if (!IsConnected) + { + Logger.LogError("Cannot write message: Godot Ide Server not connected"); + return; + } + + Connection.WriteMessage(message); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + listener?.Stop(); + + metaFile?.Dispose(); + + File.Delete(MetaFilePath); + } + } + + protected virtual bool HandleMessage(Message message) + { + if (messageHandlers.TryGetValue(message.Id, out var action)) + { + action(message.Arguments); + return true; + } + + return false; + } + + private readonly Dictionary> messageHandlers; + + private Dictionary> InitializeMessageHandlers() + { + return new Dictionary> + { + ["Play"] = args => + { + switch (args.Length) + { + case 0: + Play(); + return; + case 2: + Play(debuggerHost: args[0], debuggerPort: int.Parse(args[1])); + return; + default: + throw new ArgumentException(); + } + }, + ["ReloadScripts"] = args => ReloadScripts() + }; + } + + private void DispatchToMainThread(Action action) + { + var d = new SendOrPostCallback(state => action()); + Godot.Dispatcher.SynchronizationContext.Post(d, null); + } + + private void Play() + { + DispatchToMainThread(() => + { + CurrentPlayRequest = new PlayRequest(); + Internal.EditorRunPlay(); + CurrentPlayRequest = null; + }); + } + + private void Play(string debuggerHost, int debuggerPort) + { + DispatchToMainThread(() => + { + CurrentPlayRequest = new PlayRequest(debuggerHost, debuggerPort); + Internal.EditorRunPlay(); + CurrentPlayRequest = null; + }); + } + + private void ReloadScripts() + { + DispatchToMainThread(Internal.ScriptEditorDebugger_ReloadScripts); + } + + public PlayRequest? CurrentPlayRequest { get; private set; } + + public struct PlayRequest + { + public bool HasDebugger { get; } + public string DebuggerHost { get; } + public int DebuggerPort { get; } + + public PlayRequest(string debuggerHost, int debuggerPort) + { + HasDebugger = true; + DebuggerHost = debuggerHost; + DebuggerPort = debuggerPort; + } + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/EditorId.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/EditorId.cs new file mode 100644 index 0000000000..1dfc91d6d1 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/EditorId.cs @@ -0,0 +1,8 @@ +namespace GodotTools.Ides.MonoDevelop +{ + public enum EditorId + { + MonoDevelop = 0, + VisualStudioForMac = 1 + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs new file mode 100644 index 0000000000..1fdccf5bbd --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs @@ -0,0 +1,131 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.Diagnostics; +using GodotTools.Internals; +using GodotTools.Utils; + +namespace GodotTools.Ides.MonoDevelop +{ + public class Instance + { + private readonly string solutionFile; + private readonly EditorId editorId; + + private Process process; + + public bool IsRunning => process != null && !process.HasExited; + + public void Execute() + { + bool newWindow = process == null || process.HasExited; + + var args = new List(); + + string command; + + if (OS.IsOSX()) + { + string bundleId = BundleIds[editorId]; + + if (Internal.IsOsxAppBundleInstalled(bundleId)) + { + command = "open"; + + args.Add("-b"); + args.Add(bundleId); + + // The 'open' process must wait until the application finishes + if (newWindow) + args.Add("--wait-apps"); + + args.Add("--args"); + } + else + { + command = OS.PathWhich(ExecutableNames[editorId]); + } + } + else + { + command = OS.PathWhich(ExecutableNames[editorId]); + } + + args.Add("--ipc-tcp"); + + if (newWindow) + args.Add("\"" + Path.GetFullPath(solutionFile) + "\""); + + if (command == null) + throw new FileNotFoundException(); + + if (newWindow) + { + process = Process.Start(new ProcessStartInfo + { + FileName = command, + Arguments = string.Join(" ", args), + UseShellExecute = true + }); + } + else + { + Process.Start(new ProcessStartInfo + { + FileName = command, + Arguments = string.Join(" ", args), + UseShellExecute = true + })?.Dispose(); + } + } + + public Instance(string solutionFile, EditorId editorId) + { + if (editorId == EditorId.VisualStudioForMac && !OS.IsOSX()) + throw new InvalidOperationException($"{nameof(EditorId.VisualStudioForMac)} not supported on this platform"); + + this.solutionFile = solutionFile; + this.editorId = editorId; + } + + private static readonly IReadOnlyDictionary ExecutableNames; + private static readonly IReadOnlyDictionary BundleIds; + + static Instance() + { + if (OS.IsOSX()) + { + ExecutableNames = new Dictionary + { + // Rely on PATH + {EditorId.MonoDevelop, "monodevelop"}, + {EditorId.VisualStudioForMac, "VisualStudio"} + }; + BundleIds = new Dictionary + { + // TODO EditorId.MonoDevelop + {EditorId.VisualStudioForMac, "com.microsoft.visual-studio"} + }; + } + else if (OS.IsWindows()) + { + ExecutableNames = new Dictionary + { + // XamarinStudio is no longer a thing, and the latest version is quite old + // MonoDevelop is available from source only on Windows. The recommendation + // is to use Visual Studio instead. Since there are no official builds, we + // will rely on custom MonoDevelop builds being added to PATH. + {EditorId.MonoDevelop, "MonoDevelop.exe"} + }; + } + else if (OS.IsUnix()) + { + ExecutableNames = new Dictionary + { + // Rely on PATH + {EditorId.MonoDevelop, "monodevelop"} + }; + } + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs index 9526dd3c6f..7783576910 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs @@ -46,6 +46,12 @@ namespace GodotTools.Internals public static string MonoWindowsInstallRoot => internal_MonoWindowsInstallRoot(); + public static void EditorRunPlay() => internal_EditorRunPlay(); + + public static void EditorRunStop() => internal_EditorRunStop(); + + public static void ScriptEditorDebugger_ReloadScripts() => internal_ScriptEditorDebugger_ReloadScripts(); + // Internal Calls [MethodImpl(MethodImplOptions.InternalCall)] @@ -95,5 +101,14 @@ namespace GodotTools.Internals [MethodImpl(MethodImplOptions.InternalCall)] private static extern string internal_MonoWindowsInstallRoot(); + + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern void internal_EditorRunPlay(); + + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern void internal_EditorRunStop(); + + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern void internal_ScriptEditorDebugger_ReloadScripts(); } } diff --git a/modules/mono/editor/GodotTools/GodotTools/MonoBottomPanel.cs b/modules/mono/editor/GodotTools/GodotTools/MonoBottomPanel.cs deleted file mode 100644 index 53ff0891d5..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/MonoBottomPanel.cs +++ /dev/null @@ -1,343 +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 MonoBottomPanel : VBoxContainer - { - private EditorInterface editorInterface; - - private TabContainer panelTabs; - - private VBoxContainer panelBuildsTab; - - private ItemList buildTabsList; - private TabContainer buildTabs; - - private ToolButton warningsBtn; - private ToolButton errorsBtn; - private Button viewLogBtn; - - private void _UpdateBuildTabsList() - { - buildTabsList.Clear(); - - int currentTab = buildTabs.CurrentTab; - - bool noCurrentTab = currentTab < 0 || currentTab >= buildTabs.GetTabCount(); - - for (int i = 0; i < buildTabs.GetChildCount(); i++) - { - var tab = (MonoBuildTab) buildTabs.GetChild(i); - - if (tab == null) - continue; - - 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 == MonoBuildTab.BuildResults.Success ? "Succeeded" : "Errored"; - else - itemTooltip += "Running"; - - if (!tab.BuildExited || tab.BuildResult == MonoBuildTab.BuildResults.Error) - itemTooltip += $"\nErrors: {tab.ErrorCount}"; - - itemTooltip += $"\nWarnings: {tab.WarningCount}"; - - buildTabsList.SetItemTooltip(i, itemTooltip); - - if (noCurrentTab || currentTab == i) - { - buildTabsList.Select(i); - _BuildTabsItemSelected(i); - } - } - } - - public MonoBuildTab GetBuildTabFor(MonoBuildInfo buildInfo) - { - foreach (var buildTab in new Array(buildTabs.GetChildren())) - { - if (buildTab.BuildInfo.Equals(buildInfo)) - return buildTab; - } - - var newBuildTab = new MonoBuildTab(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 = (MonoBuildTab) 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 = (MonoBuildTab) 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"); - - CSharpProject.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; - } - } - - var godotDefines = new[] - { - OS.GetName(), - Internal.GodotIs32Bits() ? "32" : "64" - }; - - bool buildSuccess = GodotSharpBuilds.BuildProjectBlocking("Tools", godotDefines); - - if (!buildSuccess) - return; - - // Notify running game for hot-reload - Internal.ScriptEditorDebuggerReloadScripts(); - - // Hot-reload in the editor - GodotSharpEditor.Instance.GetNode("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 = (MonoBuildTab) buildTabs.GetTabControl(selectedItem); - - OS.ShellOpen(Path.Combine(buildTab.BuildInfo.LogsDirPath, GodotSharpBuilds.MsBuildLogFileName)); - } - - public override void _Notification(int what) - { - base._Notification(what); - - if (what == EditorSettings.NotificationEditorSettingsChanged) - { - var editorBaseControl = editorInterface.GetBaseControl(); - panelTabs.AddStyleboxOverride("panel", editorBaseControl.GetStylebox("DebuggerPanel", "EditorStyles")); - panelTabs.AddStyleboxOverride("tab_fg", editorBaseControl.GetStylebox("DebuggerTabFG", "EditorStyles")); - panelTabs.AddStyleboxOverride("tab_bg", editorBaseControl.GetStylebox("DebuggerTabBG", "EditorStyles")); - } - } - - public void AddBuildTab(MonoBuildTab buildTab) - { - buildTabs.AddChild(buildTab); - RaiseBuildTab(buildTab); - } - - public void RaiseBuildTab(MonoBuildTab 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.AddStyleboxOverride("panel", editorBaseControl.GetStylebox("DebuggerPanel", "EditorStyles")); - panelTabs.AddStyleboxOverride("tab_fg", editorBaseControl.GetStylebox("DebuggerTabFG", "EditorStyles")); - panelTabs.AddStyleboxOverride("tab_bg", editorBaseControl.GetStylebox("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.Connect("pressed", this, nameof(BuildProjectPressed)); - toolBarHBox.AddChild(buildProjectBtn); - - toolBarHBox.AddSpacer(begin: false); - - warningsBtn = new ToolButton - { - Text = "Warnings".TTR(), - ToggleMode = true, - Pressed = true, - Visible = false, - FocusMode = FocusModeEnum.None - }; - warningsBtn.Connect("toggled", this, nameof(_WarningsToggled)); - toolBarHBox.AddChild(warningsBtn); - - errorsBtn = new ToolButton - { - Text = "Errors".TTR(), - ToggleMode = true, - Pressed = true, - Visible = false, - FocusMode = FocusModeEnum.None - }; - errorsBtn.Connect("toggled", this, nameof(_ErrorsToggled)); - toolBarHBox.AddChild(errorsBtn); - - toolBarHBox.AddSpacer(begin: false); - - viewLogBtn = new Button - { - Text = "View log".TTR(), - FocusMode = FocusModeEnum.None, - Visible = false - }; - viewLogBtn.Connect("pressed", this, nameof(_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.Connect("item_selected", this, nameof(_BuildTabsItemSelected)); - buildTabsList.Connect("nothing_selected", this, nameof(_BuildTabsNothingSelected)); - hsc.AddChild(buildTabsList); - - buildTabs = new TabContainer - { - TabAlign = TabContainer.TabAlignEnum.Left, - SizeFlagsHorizontal = (int) SizeFlags.ExpandFill, - TabsVisible = false - }; - hsc.AddChild(buildTabs); - } - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools/MonoBuildInfo.cs b/modules/mono/editor/GodotTools/GodotTools/MonoBuildInfo.cs deleted file mode 100644 index 858e852392..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/MonoBuildInfo.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using Godot; -using Godot.Collections; -using GodotTools.Internals; -using Path = System.IO.Path; - -namespace GodotTools -{ - [Serializable] - public sealed class MonoBuildInfo : Reference // TODO Remove Reference once we have proper serialization - { - public string Solution { get; } - public string Configuration { get; } - public Array CustomProperties { get; } = new Array(); // TODO Use List once we have proper serialization - - public string LogsDirPath => Path.Combine(GodotSharpDirs.BuildLogsDirs, $"{Solution.MD5Text()}_{Configuration}"); - - public override bool Equals(object obj) - { - if (obj is MonoBuildInfo other) - return other.Solution == Solution && other.Configuration == Configuration; - - return false; - } - - public override int GetHashCode() - { - unchecked - { - int hash = 17; - hash = hash * 29 + Solution.GetHashCode(); - hash = hash * 29 + Configuration.GetHashCode(); - return hash; - } - } - - private MonoBuildInfo() - { - } - - public MonoBuildInfo(string solution, string configuration) - { - Solution = solution; - Configuration = configuration; - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools/MonoBuildTab.cs b/modules/mono/editor/GodotTools/GodotTools/MonoBuildTab.cs deleted file mode 100644 index 3a74fa2f66..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/MonoBuildTab.cs +++ /dev/null @@ -1,267 +0,0 @@ -using Godot; -using System; -using Godot.Collections; -using GodotTools.Internals; -using File = GodotTools.Utils.File; -using Path = System.IO.Path; - -namespace GodotTools -{ - public class MonoBuildTab : VBoxContainer - { - public enum BuildResults - { - Error, - Success - } - - [Serializable] - private class BuildIssue : Reference // TODO Remove Reference once we have proper serialization - { - public bool Warning { get; set; } - public string File { get; set; } - public int Line { get; set; } - public int Column { get; set; } - public string Code { get; set; } - public string Message { get; set; } - public string ProjectFile { get; set; } - } - - private readonly Array issues = new Array(); // TODO Use List once we have proper serialization - private ItemList issuesList; - - public bool BuildExited { get; private set; } = false; - - public BuildResults? BuildResult { get; private set; } = null; - - public int ErrorCount { get; private set; } = 0; - - public int WarningCount { get; private set; } = 0; - - public bool ErrorsVisible { get; set; } = true; - public bool WarningsVisible { get; set; } = true; - - public Texture IconTexture - { - get - { - if (!BuildExited) - return GetIcon("Stop", "EditorIcons"); - - if (BuildResult == BuildResults.Error) - return GetIcon("StatusError", "EditorIcons"); - - return GetIcon("StatusSuccess", "EditorIcons"); - } - } - - public MonoBuildInfo BuildInfo { get; private set; } - - private void _LoadIssuesFromFile(string csvFile) - { - using (var file = new Godot.File()) - { - try - { - Error openError = file.Open(csvFile, Godot.File.ModeFlags.Read); - - if (openError != Error.Ok) - return; - - while (!file.EofReached()) - { - string[] csvColumns = file.GetCsvLine(); - - if (csvColumns.Length == 1 && csvColumns[0].Empty()) - return; - - if (csvColumns.Length != 7) - { - GD.PushError($"Expected 7 columns, got {csvColumns.Length}"); - continue; - } - - var issue = new BuildIssue - { - Warning = csvColumns[0] == "warning", - File = csvColumns[1], - Line = int.Parse(csvColumns[2]), - Column = int.Parse(csvColumns[3]), - Code = csvColumns[4], - Message = csvColumns[5], - ProjectFile = csvColumns[6] - }; - - if (issue.Warning) - WarningCount += 1; - else - ErrorCount += 1; - - issues.Add(issue); - } - } - finally - { - file.Close(); // Disposing it is not enough. We need to call Close() - } - } - } - - private void _IssueActivated(int idx) - { - if (idx < 0 || idx >= issuesList.GetItemCount()) - throw new IndexOutOfRangeException("Item list index out of range"); - - // Get correct issue idx from issue list - int issueIndex = (int) issuesList.GetItemMetadata(idx); - - if (idx < 0 || idx >= issues.Count) - throw new IndexOutOfRangeException("Issue index out of range"); - - BuildIssue issue = issues[issueIndex]; - - if (issue.ProjectFile.Empty() && issue.File.Empty()) - return; - - string projectDir = issue.ProjectFile.Length > 0 ? issue.ProjectFile.GetBaseDir() : BuildInfo.Solution.GetBaseDir(); - - string file = Path.Combine(projectDir.SimplifyGodotPath(), issue.File.SimplifyGodotPath()); - - if (!File.Exists(file)) - return; - - file = ProjectSettings.LocalizePath(file); - - if (file.StartsWith("res://")) - { - var script = (Script) ResourceLoader.Load(file, typeHint: Internal.CSharpLanguageType); - - if (script != null && Internal.ScriptEditorEdit(script, issue.Line, issue.Column)) - Internal.EditorNodeShowScriptScreen(); - } - } - - public void UpdateIssuesList() - { - issuesList.Clear(); - - using (var warningIcon = GetIcon("Warning", "EditorIcons")) - using (var errorIcon = GetIcon("Error", "EditorIcons")) - { - for (int i = 0; i < issues.Count; i++) - { - BuildIssue issue = issues[i]; - - if (!(issue.Warning ? WarningsVisible : ErrorsVisible)) - continue; - - string tooltip = string.Empty; - tooltip += $"Message: {issue.Message}"; - - if (!issue.Code.Empty()) - tooltip += $"\nCode: {issue.Code}"; - - tooltip += $"\nType: {(issue.Warning ? "warning" : "error")}"; - - string text = string.Empty; - - if (!issue.File.Empty()) - { - text += $"{issue.File}({issue.Line},{issue.Column}): "; - - tooltip += $"\nFile: {issue.File}"; - tooltip += $"\nLine: {issue.Line}"; - tooltip += $"\nColumn: {issue.Column}"; - } - - if (!issue.ProjectFile.Empty()) - tooltip += $"\nProject: {issue.ProjectFile}"; - - text += issue.Message; - - int lineBreakIdx = text.IndexOf("\n", StringComparison.Ordinal); - string itemText = lineBreakIdx == -1 ? text : text.Substring(0, lineBreakIdx); - issuesList.AddItem(itemText, issue.Warning ? warningIcon : errorIcon); - - int index = issuesList.GetItemCount() - 1; - issuesList.SetItemTooltip(index, tooltip); - issuesList.SetItemMetadata(index, i); - } - } - } - - public void OnBuildStart() - { - BuildExited = false; - - issues.Clear(); - WarningCount = 0; - ErrorCount = 0; - UpdateIssuesList(); - - GodotSharpEditor.Instance.MonoBottomPanel.RaiseBuildTab(this); - } - - public void OnBuildExit(BuildResults result) - { - BuildExited = true; - BuildResult = result; - - _LoadIssuesFromFile(Path.Combine(BuildInfo.LogsDirPath, GodotSharpBuilds.MsBuildIssuesFileName)); - UpdateIssuesList(); - - GodotSharpEditor.Instance.MonoBottomPanel.RaiseBuildTab(this); - } - - public void OnBuildExecFailed(string cause) - { - BuildExited = true; - BuildResult = BuildResults.Error; - - issuesList.Clear(); - - var issue = new BuildIssue {Message = cause, Warning = false}; - - ErrorCount += 1; - issues.Add(issue); - - UpdateIssuesList(); - - GodotSharpEditor.Instance.MonoBottomPanel.RaiseBuildTab(this); - } - - public void RestartBuild() - { - if (!BuildExited) - throw new InvalidOperationException("Build already started"); - - GodotSharpBuilds.RestartBuild(this); - } - - public void StopBuild() - { - if (!BuildExited) - throw new InvalidOperationException("Build is not in progress"); - - GodotSharpBuilds.StopBuild(this); - } - - public override void _Ready() - { - base._Ready(); - - issuesList = new ItemList {SizeFlagsVertical = (int) SizeFlags.ExpandFill}; - issuesList.Connect("item_activated", this, nameof(_IssueActivated)); - AddChild(issuesList); - } - - private MonoBuildTab() - { - } - - public MonoBuildTab(MonoBuildInfo buildInfo) - { - BuildInfo = buildInfo; - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools/MonoDevelopInstance.cs b/modules/mono/editor/GodotTools/GodotTools/MonoDevelopInstance.cs deleted file mode 100644 index 61a0a992ce..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/MonoDevelopInstance.cs +++ /dev/null @@ -1,146 +0,0 @@ -using GodotTools.Core; -using System; -using System.IO; -using System.Collections.Generic; -using System.Diagnostics; -using GodotTools.Internals; -using GodotTools.Utils; - -namespace GodotTools -{ - public class MonoDevelopInstance - { - public enum EditorId - { - MonoDevelop = 0, - VisualStudioForMac = 1 - } - - private readonly string solutionFile; - private readonly EditorId editorId; - - private Process process; - - public void Execute(params string[] files) - { - bool newWindow = process == null || process.HasExited; - - var args = new List(); - - string command; - - if (Utils.OS.IsOSX()) - { - string bundleId = BundleIds[editorId]; - - if (Internal.IsOsxAppBundleInstalled(bundleId)) - { - command = "open"; - - args.Add("-b"); - args.Add(bundleId); - - // The 'open' process must wait until the application finishes - if (newWindow) - args.Add("--wait-apps"); - - args.Add("--args"); - } - else - { - command = OS.PathWhich(ExecutableNames[editorId]); - } - } - else - { - command = OS.PathWhich(ExecutableNames[editorId]); - } - - args.Add("--ipc-tcp"); - - if (newWindow) - args.Add("\"" + Path.GetFullPath(solutionFile) + "\""); - - foreach (var file in files) - { - int semicolonIndex = file.IndexOf(';'); - - string filePath = semicolonIndex < 0 ? file : file.Substring(0, semicolonIndex); - string cursor = semicolonIndex < 0 ? string.Empty : file.Substring(semicolonIndex); - - args.Add("\"" + Path.GetFullPath(filePath.NormalizePath()) + cursor + "\""); - } - - if (command == null) - throw new FileNotFoundException(); - - if (newWindow) - { - process = Process.Start(new ProcessStartInfo - { - FileName = command, - Arguments = string.Join(" ", args), - UseShellExecute = false - }); - } - else - { - Process.Start(new ProcessStartInfo - { - FileName = command, - Arguments = string.Join(" ", args), - UseShellExecute = false - })?.Dispose(); - } - } - - public MonoDevelopInstance(string solutionFile, EditorId editorId) - { - if (editorId == EditorId.VisualStudioForMac && !Utils.OS.IsOSX()) - throw new InvalidOperationException($"{nameof(EditorId.VisualStudioForMac)} not supported on this platform"); - - this.solutionFile = solutionFile; - this.editorId = editorId; - } - - private static readonly IReadOnlyDictionary ExecutableNames; - private static readonly IReadOnlyDictionary BundleIds; - - static MonoDevelopInstance() - { - if (Utils.OS.IsOSX()) - { - ExecutableNames = new Dictionary - { - // Rely on PATH - {EditorId.MonoDevelop, "monodevelop"}, - {EditorId.VisualStudioForMac, "VisualStudio"} - }; - BundleIds = new Dictionary - { - // TODO EditorId.MonoDevelop - {EditorId.VisualStudioForMac, "com.microsoft.visual-studio"} - }; - } - else if (Utils.OS.IsWindows()) - { - ExecutableNames = new Dictionary - { - // XamarinStudio is no longer a thing, and the latest version is quite old - // MonoDevelop is available from source only on Windows. The recommendation - // is to use Visual Studio instead. Since there are no official builds, we - // will rely on custom MonoDevelop builds being added to PATH. - {EditorId.MonoDevelop, "MonoDevelop.exe"} - }; - } - else if (Utils.OS.IsUnix()) - { - ExecutableNames = new Dictionary - { - // Rely on PATH - {EditorId.MonoDevelop, "monodevelop"} - }; - } - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/CollectionExtensions.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/CollectionExtensions.cs index 288c65de74..e3c2c822a5 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/CollectionExtensions.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/CollectionExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; namespace GodotTools.Utils { @@ -17,5 +18,12 @@ namespace GodotTools.Utils return orElse; } + + public static IEnumerable EnumerateLines(this TextReader textReader) + { + string line; + while ((line = textReader.ReadLine()) != null) + yield return line; + } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs new file mode 100644 index 0000000000..a3490fa89f --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs @@ -0,0 +1,64 @@ +using System; +using System.Runtime.CompilerServices; + +namespace GodotTools.Utils +{ + public sealed class NotifyAwaiter : INotifyCompletion + { + private Action continuation; + private Exception exception; + private T result; + + public bool IsCompleted { get; private set; } + + public T GetResult() + { + if (exception != null) + throw exception; + return result; + } + + public void OnCompleted(Action continuation) + { + if (this.continuation != null) + throw new InvalidOperationException("This awaiter has already been listened"); + this.continuation = continuation; + } + + public void SetResult(T result) + { + if (IsCompleted) + throw new InvalidOperationException("This awaiter is already completed"); + + IsCompleted = true; + this.result = result; + + continuation?.Invoke(); + } + + public void SetException(Exception exception) + { + if (IsCompleted) + throw new InvalidOperationException("This awaiter is already completed"); + + IsCompleted = true; + this.exception = exception; + + continuation?.Invoke(); + } + + public NotifyAwaiter Reset() + { + continuation = null; + exception = null; + result = default; + IsCompleted = false; + return this; + } + + public NotifyAwaiter GetAwaiter() + { + return this; + } + } +} diff --git a/modules/mono/editor/editor_internal_calls.cpp b/modules/mono/editor/editor_internal_calls.cpp index 0014aaca70..7db1090e2a 100644 --- a/modules/mono/editor/editor_internal_calls.cpp +++ b/modules/mono/editor/editor_internal_calls.cpp @@ -350,6 +350,21 @@ MonoString *godot_icall_Internal_MonoWindowsInstallRoot() { #endif } +void godot_icall_Internal_EditorRunPlay() { + EditorNode::get_singleton()->run_play(); +} + +void godot_icall_Internal_EditorRunStop() { + EditorNode::get_singleton()->run_stop(); +} + +void godot_icall_Internal_ScriptEditorDebugger_ReloadScripts() { + ScriptEditorDebugger *sed = ScriptEditor::get_singleton()->get_debugger(); + if (sed) { + sed->reload_scripts(); + } +} + MonoString *godot_icall_Utils_OS_GetPlatformName() { String os_name = OS::get_singleton()->get_name(); return GDMonoMarshal::mono_string_from_godot(os_name); @@ -414,7 +429,9 @@ void register_editor_internal_calls() { mono_add_internal_call("GodotTools.Internals.Internal::internal_ScriptEditorEdit", (void *)godot_icall_Internal_ScriptEditorEdit); mono_add_internal_call("GodotTools.Internals.Internal::internal_EditorNodeShowScriptScreen", (void *)godot_icall_Internal_EditorNodeShowScriptScreen); mono_add_internal_call("GodotTools.Internals.Internal::internal_GetScriptsMetadataOrNothing", (void *)godot_icall_Internal_GetScriptsMetadataOrNothing); - mono_add_internal_call("GodotTools.Internals.Internal::internal_MonoWindowsInstallRoot", (void *)godot_icall_Internal_MonoWindowsInstallRoot); + mono_add_internal_call("GodotTools.Internals.Internal::internal_EditorRunPlay", (void *)godot_icall_Internal_EditorRunPlay); + mono_add_internal_call("GodotTools.Internals.Internal::internal_EditorRunStop", (void *)godot_icall_Internal_EditorRunStop); + mono_add_internal_call("GodotTools.Internals.Internal::internal_ScriptEditorDebugger_ReloadScripts", (void *)godot_icall_Internal_ScriptEditorDebugger_ReloadScripts); // Globals mono_add_internal_call("GodotTools.Internals.Globals::internal_EditorScale", (void *)godot_icall_Globals_EditorScale); -- cgit v1.2.3 From c55ce204b3065d55df2717a7e0bcb0681392ac34 Mon Sep 17 00:00:00 2001 From: Ignacio Etcheverry Date: Fri, 9 Aug 2019 03:39:45 +0200 Subject: Replace 'ERR_EXPLAIN' with 'ERR_FAIL_*_MSG' in 'modules/mono' And 'CRASH_*_MSG' as well. Also make error messages puntuation and quotation more consistent. --- modules/mono/editor/bindings_generator.cpp | 123 +++++++++++++--------------- modules/mono/editor/bindings_generator.h | 2 +- modules/mono/editor/csharp_project.cpp | 10 +-- modules/mono/editor/godotsharp_export.cpp | 13 +-- modules/mono/editor/script_class_parser.cpp | 18 ++-- 5 files changed, 73 insertions(+), 93 deletions(-) (limited to 'modules/mono/editor') diff --git a/modules/mono/editor/bindings_generator.cpp b/modules/mono/editor/bindings_generator.cpp index 45037bf637..1888bb3cb9 100644 --- a/modules/mono/editor/bindings_generator.cpp +++ b/modules/mono/editor/bindings_generator.cpp @@ -279,7 +279,7 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf Vector link_target_parts = link_target.split("."); if (link_target_parts.size() <= 0 || link_target_parts.size() > 2) { - ERR_PRINTS("Invalid reference format: " + tag); + ERR_PRINTS("Invalid reference format: '" + tag + "'."); xml_output.append(""); xml_output.append(tag); @@ -375,7 +375,7 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf xml_output.append(target_enum_itype.proxy_name); // Includes nesting class if any xml_output.append("\"/>"); } else { - ERR_PRINTS("Cannot resolve enum reference in documentation: " + link_target); + ERR_PRINTS("Cannot resolve enum reference in documentation: '" + link_target + "'."); xml_output.append(""); xml_output.append(link_target); @@ -424,7 +424,7 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf xml_output.append(target_iconst->proxy_name); xml_output.append("\"/>"); } else { - ERR_PRINTS("Cannot resolve global constant reference in documentation: " + link_target); + ERR_PRINTS("Cannot resolve global constant reference in documentation: '" + link_target + "'."); xml_output.append(""); xml_output.append(link_target); @@ -464,7 +464,7 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf xml_output.append(target_iconst->proxy_name); xml_output.append("\"/>"); } else { - ERR_PRINTS("Cannot resolve constant reference in documentation: " + link_target); + ERR_PRINTS("Cannot resolve constant reference in documentation: '" + link_target + "'."); xml_output.append(""); xml_output.append(link_target); @@ -534,7 +534,7 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf xml_output.append(target_itype->proxy_name); xml_output.append("\"/>"); } else { - ERR_PRINTS("Cannot resolve type reference in documentation: " + tag); + ERR_PRINTS("Cannot resolve type reference in documentation: '" + tag + "'."); xml_output.append(""); xml_output.append(tag); @@ -812,7 +812,7 @@ void BindingsGenerator::_generate_global_constants(StringBuilder &p_output) { CRASH_COND(enum_class_name != "Variant"); // Hard-coded... - _log("Declaring global enum `%s` inside static class `%s`\n", enum_proxy_name.utf8().get_data(), enum_class_name.utf8().get_data()); + _log("Declaring global enum '%s' inside static class '%s'\n", enum_proxy_name.utf8().get_data(), enum_class_name.utf8().get_data()); p_output.append("\n" INDENT1 "public static partial class "); p_output.append(enum_class_name); @@ -1083,7 +1083,7 @@ Error BindingsGenerator::generate_cs_api(const String &p_output_dir) { proj_err = generate_cs_core_project(core_proj_dir, core_compile_items); if (proj_err != OK) { - ERR_PRINT("Generation of the Core API C# project failed"); + ERR_PRINT("Generation of the Core API C# project failed."); return proj_err; } @@ -1094,7 +1094,7 @@ Error BindingsGenerator::generate_cs_api(const String &p_output_dir) { proj_err = generate_cs_editor_project(editor_proj_dir, editor_compile_items); if (proj_err != OK) { - ERR_PRINT("Generation of the Editor API C# project failed"); + ERR_PRINT("Generation of the Editor API C# project failed."); return proj_err; } @@ -1112,7 +1112,7 @@ Error BindingsGenerator::generate_cs_api(const String &p_output_dir) { // FIXME: There are some members that hide other inherited members. // - In the case of both members being the same kind, the new one must be declared -// explicitly as `new` to avoid the warning (and we must print a message about it). +// explicitly as 'new' to avoid the warning (and we must print a message about it). // - In the case of both members being of a different kind, then the new one must // be renamed to avoid the name collision (and we must print a warning about it). // - Csc warning e.g.: @@ -1186,7 +1186,7 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str output.append(obj_types[itype.base_name].proxy_name); output.append("\n"); } else { - ERR_PRINTS("Base type '" + itype.base_name.operator String() + "' does not exist, for class " + itype.name); + ERR_PRINTS("Base type '" + itype.base_name.operator String() + "' does not exist, for class '" + itype.name + "'."); return ERR_INVALID_DATA; } } @@ -1273,11 +1273,9 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str for (const List::Element *E = itype.properties.front(); E; E = E->next()) { const PropertyInterface &iprop = E->get(); Error prop_err = _generate_cs_property(itype, iprop, output); - if (prop_err != OK) { - ERR_EXPLAIN("Failed to generate property '" + iprop.cname.operator String() + - "' for class '" + itype.name + "'"); - ERR_FAIL_V(prop_err); - } + ERR_FAIL_COND_V_MSG(prop_err != OK, prop_err, + "Failed to generate property '" + iprop.cname.operator String() + + "' for class '" + itype.name + "'."); } } @@ -1340,10 +1338,8 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str for (const List::Element *E = itype.methods.front(); E; E = E->next()) { const MethodInterface &imethod = E->get(); Error method_err = _generate_cs_method(itype, imethod, method_bind_count, output); - if (method_err != OK) { - ERR_EXPLAIN("Failed to generate method '" + imethod.name + "' for class '" + itype.name + "'"); - ERR_FAIL_V(method_err); - } + ERR_FAIL_COND_V_MSG(method_err != OK, method_err, + "Failed to generate method '" + imethod.name + "' for class '" + itype.name + "'."); } if (itype.is_singleton) { @@ -1626,7 +1622,7 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf if (p_imethod.is_deprecated) { if (p_imethod.deprecation_message.empty()) - WARN_PRINTS("An empty deprecation message is discouraged. Method: " + p_imethod.proxy_name); + WARN_PRINTS("An empty deprecation message is discouraged. Method: '" + p_imethod.proxy_name + "'."); p_output.append(MEMBER_BEGIN "[Obsolete(\""); p_output.append(p_imethod.deprecation_message); @@ -1708,8 +1704,7 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf Error BindingsGenerator::generate_glue(const String &p_output_dir) { bool dir_exists = DirAccess::exists(p_output_dir); - ERR_EXPLAIN("The output directory does not exist."); - ERR_FAIL_COND_V(!dir_exists, ERR_FILE_BAD_PATH); + ERR_FAIL_COND_V_MSG(!dir_exists, ERR_FILE_BAD_PATH, "The output directory does not exist."); StringBuilder output; @@ -1742,10 +1737,8 @@ Error BindingsGenerator::generate_glue(const String &p_output_dir) { for (const List::Element *E = itype.methods.front(); E; E = E->next()) { const MethodInterface &imethod = E->get(); Error method_err = _generate_glue_method(itype, imethod, output); - if (method_err != OK) { - ERR_EXPLAIN("Failed to generate method '" + imethod.name + "' for class '" + itype.name + "'"); - ERR_FAIL_V(method_err); - } + ERR_FAIL_COND_V_MSG(method_err != OK, method_err, + "Failed to generate method '" + imethod.name + "' for class '" + itype.name + "'."); } if (itype.is_singleton) { @@ -1879,8 +1872,7 @@ Error BindingsGenerator::_save_file(const String &p_path, const StringBuilder &p FileAccessRef file = FileAccess::open(p_path, FileAccess::WRITE); - ERR_EXPLAIN("Cannot open file: " + p_path); - ERR_FAIL_COND_V(!file, ERR_FILE_CANT_WRITE); + ERR_FAIL_COND_V_MSG(!file, ERR_FILE_CANT_WRITE, "Cannot open file: '" + p_path + "'."); file->store_string(p_content.as_string()); file->close(); @@ -2091,7 +2083,7 @@ const BindingsGenerator::TypeInterface *BindingsGenerator::_get_type_or_placehol if (found) return found; - ERR_PRINTS(String() + "Type not found. Creating placeholder: " + p_typeref.cname.operator String()); + ERR_PRINTS(String() + "Type not found. Creating placeholder: '" + p_typeref.cname.operator String() + "'."); const Map::Element *match = placeholder_types.find(p_typeref.cname); @@ -2175,13 +2167,13 @@ void BindingsGenerator::_populate_object_type_interfaces() { } if (!ClassDB::is_class_exposed(type_cname)) { - _log("Ignoring type `%s` because it's not exposed\n", String(type_cname).utf8().get_data()); + _log("Ignoring type '%s' because it's not exposed\n", String(type_cname).utf8().get_data()); class_list.pop_front(); continue; } if (!ClassDB::is_class_enabled(type_cname)) { - _log("Ignoring type `%s` because it's not enabled\n", String(type_cname).utf8().get_data()); + _log("Ignoring type '%s' because it's not enabled\n", String(type_cname).utf8().get_data()); class_list.pop_front(); continue; } @@ -2240,7 +2232,7 @@ void BindingsGenerator::_populate_object_type_interfaces() { // Prevent the property and its enclosing type from sharing the same name if (iprop.proxy_name == itype.proxy_name) { - _log("Name of property `%s` is ambiguous with the name of its enclosing class `%s`. Renaming property to `%s_`\n", + _log("Name of property '%s' is ambiguous with the name of its enclosing class '%s'. Renaming property to '%s_'\n", iprop.proxy_name.utf8().get_data(), itype.proxy_name.utf8().get_data(), iprop.proxy_name.utf8().get_data()); iprop.proxy_name += "_"; @@ -2298,28 +2290,26 @@ void BindingsGenerator::_populate_object_type_interfaces() { imethod.is_vararg = m && m->is_vararg(); if (!m && !imethod.is_virtual) { - if (virtual_method_list.find(method_info)) { - // A virtual method without the virtual flag. This is a special case. - - // There is no method bind, so let's fallback to Godot's object.Call(string, params) - imethod.requires_object_call = true; - - // The method Object.free is registered as a virtual method, but without the virtual flag. - // This is because this method is not supposed to be overridden, but called. - // We assume the return type is void. - imethod.return_type.cname = name_cache.type_void; - - // Actually, more methods like this may be added in the future, - // which could actually will return something different. - // Let's put this to notify us if that ever happens. - if (itype.cname != name_cache.type_Object || imethod.name != "free") { - ERR_PRINTS("Notification: New unexpected virtual non-overridable method found.\n" - "We only expected Object.free, but found " + - itype.name + "." + imethod.name); - } - } else { - ERR_EXPLAIN("Missing MethodBind for non-virtual method: " + itype.name + "." + imethod.name); - ERR_FAIL(); + ERR_FAIL_COND_MSG(!virtual_method_list.find(method_info), + "Missing MethodBind for non-virtual method: '" + itype.name + "." + imethod.name + "'."); + + // A virtual method without the virtual flag. This is a special case. + + // There is no method bind, so let's fallback to Godot's object.Call(string, params) + imethod.requires_object_call = true; + + // The method Object.free is registered as a virtual method, but without the virtual flag. + // This is because this method is not supposed to be overridden, but called. + // We assume the return type is void. + imethod.return_type.cname = name_cache.type_void; + + // Actually, more methods like this may be added in the future, + // which could actually will return something different. + // Let's put this to notify us if that ever happens. + if (itype.cname != name_cache.type_Object || imethod.name != "free") { + ERR_PRINTS("Notification: New unexpected virtual non-overridable method found." + " We only expected Object.free, but found '" + + itype.name + "." + imethod.name + "'."); } } else if (return_info.type == Variant::INT && return_info.usage & PROPERTY_USAGE_CLASS_IS_ENUM) { imethod.return_type.cname = return_info.class_name; @@ -2328,8 +2318,8 @@ void BindingsGenerator::_populate_object_type_interfaces() { imethod.return_type.cname = return_info.class_name; if (!imethod.is_virtual && ClassDB::is_parent_class(return_info.class_name, name_cache.type_Reference) && return_info.hint != PROPERTY_HINT_RESOURCE_TYPE) { /* clang-format off */ - ERR_PRINTS("Return type is reference but hint is not " _STR(PROPERTY_HINT_RESOURCE_TYPE) "." - " Are you returning a reference type by pointer? Method: " + itype.name + "." + imethod.name); + ERR_PRINTS("Return type is reference but hint is not '" _STR(PROPERTY_HINT_RESOURCE_TYPE) "'." + " Are you returning a reference type by pointer? Method: '" + itype.name + "." + imethod.name + "'."); /* clang-format on */ ERR_FAIL(); } @@ -2394,7 +2384,7 @@ void BindingsGenerator::_populate_object_type_interfaces() { // Prevent the method and its enclosing type from sharing the same name if (imethod.proxy_name == itype.proxy_name) { - _log("Name of method `%s` is ambiguous with the name of its enclosing class `%s`. Renaming method to `%s_`\n", + _log("Name of method '%s' is ambiguous with the name of its enclosing class '%s'. Renaming method to '%s_'\n", imethod.proxy_name.utf8().get_data(), itype.proxy_name.utf8().get_data(), imethod.proxy_name.utf8().get_data()); imethod.proxy_name += "_"; @@ -2880,8 +2870,7 @@ void BindingsGenerator::_populate_global_constants() { if (global_constants_count > 0) { Map::Element *match = EditorHelp::get_doc_data()->class_list.find("@GlobalScope"); - ERR_EXPLAIN("Could not find `@GlobalScope` in DocData"); - CRASH_COND(!match); + CRASH_COND_MSG(!match, "Could not find '@GlobalScope' in DocData."); const DocData::ClassDoc &global_scope_doc = match->value(); @@ -2935,7 +2924,7 @@ void BindingsGenerator::_populate_global_constants() { // HARDCODED: The Error enum have the prefix 'ERR_' for everything except 'OK' and 'FAILED'. if (ienum.cname == name_cache.enum_Error) { if (prefix_length > 0) { // Just in case it ever changes - ERR_PRINTS("Prefix for enum 'Error' is not empty"); + ERR_PRINTS("Prefix for enum '" _STR(Error) "' is not empty."); } prefix_length = 1; // 'ERR_' @@ -3024,7 +3013,7 @@ void BindingsGenerator::handle_cmdline_args(const List &p_cmdline_args) glue_dir_path = path_elem->get(); elem = elem->next(); } else { - ERR_PRINTS(generate_all_glue_option + ": No output directory specified (expected path to {GODOT_ROOT}/modules/mono/glue)"); + ERR_PRINTS(generate_all_glue_option + ": No output directory specified (expected path to '{GODOT_ROOT}/modules/mono/glue')."); } --options_left; @@ -3035,7 +3024,7 @@ void BindingsGenerator::handle_cmdline_args(const List &p_cmdline_args) cs_dir_path = path_elem->get(); elem = elem->next(); } else { - ERR_PRINTS(generate_cs_glue_option + ": No output directory specified"); + ERR_PRINTS(generate_cs_glue_option + ": No output directory specified."); } --options_left; @@ -3046,7 +3035,7 @@ void BindingsGenerator::handle_cmdline_args(const List &p_cmdline_args) cpp_dir_path = path_elem->get(); elem = elem->next(); } else { - ERR_PRINTS(generate_cpp_glue_option + ": No output directory specified"); + ERR_PRINTS(generate_cpp_glue_option + ": No output directory specified."); } --options_left; @@ -3061,20 +3050,20 @@ void BindingsGenerator::handle_cmdline_args(const List &p_cmdline_args) if (glue_dir_path.length()) { if (bindings_generator.generate_glue(glue_dir_path) != OK) - ERR_PRINTS(generate_all_glue_option + ": Failed to generate the C++ glue"); + ERR_PRINTS(generate_all_glue_option + ": Failed to generate the C++ glue."); if (bindings_generator.generate_cs_api(glue_dir_path.plus_file("Managed/Generated")) != OK) - ERR_PRINTS(generate_all_glue_option + ": Failed to generate the C# API"); + ERR_PRINTS(generate_all_glue_option + ": Failed to generate the C# API."); } if (cs_dir_path.length()) { if (bindings_generator.generate_cs_api(cs_dir_path) != OK) - ERR_PRINTS(generate_cs_glue_option + ": Failed to generate the C# API"); + ERR_PRINTS(generate_cs_glue_option + ": Failed to generate the C# API."); } if (cpp_dir_path.length()) { if (bindings_generator.generate_glue(cpp_dir_path) != OK) - ERR_PRINTS(generate_cpp_glue_option + ": Failed to generate the C++ glue"); + ERR_PRINTS(generate_cpp_glue_option + ": Failed to generate the C++ glue."); } // Exit once done diff --git a/modules/mono/editor/bindings_generator.h b/modules/mono/editor/bindings_generator.h index 8be51a6c55..6f0c297575 100644 --- a/modules/mono/editor/bindings_generator.h +++ b/modules/mono/editor/bindings_generator.h @@ -147,7 +147,7 @@ class BindingsGenerator { bool requires_object_call; /** - * Determines if the method visibility is `internal` (visible only to files in the same assembly). + * Determines if the method visibility is 'internal' (visible only to files in the same assembly). * Currently, we only use this for methods that are not meant to be exposed, * but are required by properties as getters or setters. * Methods that are not meant to be exposed are those that begin with underscore and are not virtual. diff --git a/modules/mono/editor/csharp_project.cpp b/modules/mono/editor/csharp_project.cpp index d88b08c646..0e6c58c9d7 100644 --- a/modules/mono/editor/csharp_project.cpp +++ b/modules/mono/editor/csharp_project.cpp @@ -81,16 +81,14 @@ bool generate_api_solution(const String &p_solution_dir, const String &p_core_pr _GDMONO_SCOPE_DOMAIN_(temp_domain); - GDMonoAssembly *tools_project_editor_assembly = NULL; + GDMonoAssembly *tools_project_editor_asm = NULL; - if (!GDMono::get_singleton()->load_assembly("GodotTools.ProjectEditor", &tools_project_editor_assembly)) { - ERR_EXPLAIN("Failed to load assembly: 'GodotTools.ProjectEditor'"); - ERR_FAIL_V(false); - } + bool assembly_loaded = GDMono::get_singleton()->load_assembly(TOOLS_PROJECT_EDITOR_ASM_NAME, &tools_project_editor_asm); + ERR_FAIL_COND_V_MSG(!assembly_loaded, false, "Failed to load assembly: '" TOOLS_PROJECT_EDITOR_ASM_NAME "'."); return generate_api_solution_impl(p_solution_dir, p_core_proj_dir, p_core_compile_items, p_editor_proj_dir, p_editor_compile_items, - tools_project_editor_assembly); + tools_project_editor_asm); } } diff --git a/modules/mono/editor/godotsharp_export.cpp b/modules/mono/editor/godotsharp_export.cpp index 020bb70a08..80a7335b1d 100644 --- a/modules/mono/editor/godotsharp_export.cpp +++ b/modules/mono/editor/godotsharp_export.cpp @@ -85,18 +85,12 @@ Error GodotSharpExport::get_assembly_dependencies(GDMonoAssembly *p_assembly, co } } - if (!ref_assembly) { - ERR_EXPLAIN("Cannot load assembly (refonly): " + ref_name); - ERR_FAIL_V(ERR_CANT_RESOLVE); - } + ERR_FAIL_COND_V_MSG(!ref_assembly, ERR_CANT_RESOLVE, "Cannot load assembly (refonly): '" + ref_name + "'."); r_dependencies[ref_name] = ref_assembly->get_path(); Error err = get_assembly_dependencies(ref_assembly, p_search_dirs, r_dependencies); - if (err != OK) { - ERR_EXPLAIN("Cannot load one of the dependencies for the assembly: " + ref_name); - ERR_FAIL_V(err); - } + ERR_FAIL_COND_V_MSG(err != OK, err, "Cannot load one of the dependencies for the assembly: '" + ref_name + "'."); } return OK; @@ -113,8 +107,7 @@ Error GodotSharpExport::get_exported_assembly_dependencies(const String &p_proje bool load_success = GDMono::get_singleton()->load_assembly_from(p_project_dll_name, p_project_dll_src_path, &scripts_assembly, /* refonly: */ true); - ERR_EXPLAIN("Cannot load assembly (refonly): " + p_project_dll_name); - ERR_FAIL_COND_V(!load_success, ERR_CANT_RESOLVE); + ERR_FAIL_COND_V_MSG(!load_success, ERR_CANT_RESOLVE, "Cannot load assembly (refonly): '" + p_project_dll_name + "'."); Vector search_dirs; GDMonoAssembly::fill_search_dirs(search_dirs, p_build_config, p_custom_lib_dir); diff --git a/modules/mono/editor/script_class_parser.cpp b/modules/mono/editor/script_class_parser.cpp index dfb652a7aa..dcb0ca5a80 100644 --- a/modules/mono/editor/script_class_parser.cpp +++ b/modules/mono/editor/script_class_parser.cpp @@ -162,8 +162,8 @@ ScriptClassParser::Token ScriptClassParser::get_token() { error = true; return TK_ERROR; } else if (code[idx] == begin_str) { - if (verbatim && code[idx + 1] == '"') { // `""` is verbatim string's `\"` - idx += 2; // skip next `"` as well + if (verbatim && code[idx + 1] == '"') { // '""' is verbatim string's '\"' + idx += 2; // skip next '"' as well continue; } @@ -590,7 +590,7 @@ Error ScriptClassParser::parse(const String &p_code) { name = String(value); } else if (tk == TK_CURLY_BRACKET_OPEN) { if (name.empty()) { - error_str = "Expected " + get_token_name(TK_IDENTIFIER) + " after keyword `struct`, found " + get_token_name(TK_CURLY_BRACKET_OPEN); + error_str = "Expected " + get_token_name(TK_IDENTIFIER) + " after keyword 'struct', found " + get_token_name(TK_CURLY_BRACKET_OPEN); error = true; return ERR_PARSE_ERROR; } @@ -657,12 +657,12 @@ Error ScriptClassParser::parse_file(const String &p_filepath) { String source; Error ferr = read_all_file_utf8(p_filepath, source); - if (ferr != OK) { - if (ferr == ERR_INVALID_DATA) { - ERR_EXPLAIN("File '" + p_filepath + "' contains invalid unicode (utf-8), so it was not loaded. Please ensure that scripts are saved in valid utf-8 unicode."); - } - ERR_FAIL_V(ferr); - } + + ERR_FAIL_COND_V_MSG(ferr != OK, ferr, + ferr == ERR_INVALID_DATA ? + "File '" + p_filepath + "' contains invalid unicode (UTF-8), so it was not loaded." + " Please ensure that scripts are saved in valid UTF-8 unicode." : + "Failed to read file: '" + p_filepath + "'."); return parse(source); } -- cgit v1.2.3 From dae2a335c57ca50dfbb27c2aca710e599dd04fd6 Mon Sep 17 00:00:00 2001 From: Ignacio Etcheverry Date: Thu, 22 Aug 2019 14:32:47 +0200 Subject: Make sure '.mono/metadata/' exists before creating file Fixes #31549 --- modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'modules/mono/editor') diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs index d515254e65..309b917c71 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using GodotTools.IdeConnection; using GodotTools.Internals; using GodotTools.Utils; +using Directory = System.IO.Directory; using File = System.IO.File; using Thread = System.Threading.Thread; @@ -33,6 +34,9 @@ namespace GodotTools.Ides this.launchIdeAction = launchIdeAction; + // Make sure the directory exists + Directory.CreateDirectory(projectMetadataDir); + // The Godot editor's file system thread can keep the file open for writing, so we are forced to allow write sharing... const FileShare metaFileShare = FileShare.ReadWrite; -- cgit v1.2.3 From 1df0e88ae8905fef7a9ce73d7e6e53dfaa7a1eee Mon Sep 17 00:00:00 2001 From: lamonte Date: Sun, 25 Aug 2019 06:48:37 -0500 Subject: default is only supported by 7.1, reverted to support 7.0 out of the box (no conflict) --- modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'modules/mono/editor') diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs index a3490fa89f..700b786752 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs @@ -51,7 +51,7 @@ namespace GodotTools.Utils { continuation = null; exception = null; - result = default; + result = default(T); IsCompleted = false; return this; } -- cgit v1.2.3 From 27f5ab2d7b6878764df51460f2c060507e7fe984 Mon Sep 17 00:00:00 2001 From: Hugo Locurcio Date: Sun, 25 Aug 2019 15:08:32 +0200 Subject: Mention Android support in the C# alpha dialog message --- modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'modules/mono/editor') diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index 099c7fcb56..7da7cff933 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -359,7 +359,7 @@ namespace GodotTools aboutLabel.Text = "C# support in Godot Engine is in late alpha stage and, while already usable, " + "it is not meant for use in production.\n\n" + - "Projects can be exported to Linux, macOS and Windows, but not yet to mobile or web platforms. " + + "Projects can be exported to Linux, macOS, Windows and Android, but not yet to iOS, HTML5 or UWP. " + "Bugs and usability issues will be addressed gradually over future releases, " + "potentially including compatibility breaking changes as new features are implemented for a better overall C# experience.\n\n" + "If you experience issues with this Mono build, please report them on Godot's issue tracker with details about your system, MSBuild version, IDE, etc.:\n\n" + -- cgit v1.2.3 From 3af8bd071eb6e99c9b3de90f5aaa939d18cda4f3 Mon Sep 17 00:00:00 2001 From: Ignacio Etcheverry Date: Sun, 25 Aug 2019 15:49:51 +0200 Subject: Set C# 7 as LangVersion for GodotTools and Godot API projects This will make it harder for someone to accidentally commit code that requires a newer version. --- .../GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj | 1 + modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj | 1 + .../GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj | 1 + .../GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj | 1 + modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj | 1 + 5 files changed, 5 insertions(+) (limited to 'modules/mono/editor') diff --git a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj index f3ac353c0f..dcfdd83831 100644 --- a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj @@ -11,6 +11,7 @@ GodotTools.BuildLogger v4.5 512 + 7 AnyCPU diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj b/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj index f36b40f87c..24c7cb1573 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj @@ -8,6 +8,7 @@ GodotTools.Core GodotTools.Core v4.5 + 7 true diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj index 84c08251ab..94e525715b 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj @@ -11,6 +11,7 @@ GodotTools.IdeConnection v4.5 512 + 7 AnyCPU diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj index 08b8ba3946..c745fe321b 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj @@ -9,6 +9,7 @@ GodotTools.ProjectEditor v4.5 obj + 7 true diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj index e2d576caef..3c57900873 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj +++ b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj @@ -10,6 +10,7 @@ v4.5 $(SolutionDir)/../../../../ Debug + 7 true -- cgit v1.2.3 From 92ead83913e1280ebb89df12e1dfb2f8737b0f3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Verschelde?= Date: Tue, 27 Aug 2019 09:48:54 +0200 Subject: Mono: Fix OSX build due to invalid function call --- modules/mono/editor/editor_internal_calls.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'modules/mono/editor') diff --git a/modules/mono/editor/editor_internal_calls.cpp b/modules/mono/editor/editor_internal_calls.cpp index 7db1090e2a..cd1ca2a2c7 100644 --- a/modules/mono/editor/editor_internal_calls.cpp +++ b/modules/mono/editor/editor_internal_calls.cpp @@ -271,7 +271,7 @@ MonoString *godot_icall_Internal_SimplifyGodotPath(MonoString *p_path) { MonoBoolean godot_icall_Internal_IsOsxAppBundleInstalled(MonoString *p_bundle_id) { #ifdef OSX_ENABLED String bundle_id = GDMonoMarshal::mono_string_to_godot(p_bundle_id); - return (MonoBoolean)osx_is_app_bundle_installed; + return (MonoBoolean)osx_is_app_bundle_installed(bundle_id); #else (void)p_bundle_id; // UNUSED return (MonoBoolean) false; -- cgit v1.2.3 From 6d2883c9bfc1264267b7abb1472b0ded30daa7e6 Mon Sep 17 00:00:00 2001 From: Ignacio Etcheverry Date: Thu, 29 Aug 2019 18:19:01 +0200 Subject: Fix GodotTools.ProjectEditor HintPaths for referenced packages --- .../GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'modules/mono/editor') diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj index c745fe321b..ab3a5d1aea 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj @@ -37,9 +37,8 @@ entire solution. $(SolutionDir) is not defined in that case, so we need to workaround that. We make SCons restore the NuGet packages in the project directory instead in this case. --> - $(SolutionDir)\packages\DotNet.Glob.2.1.1\lib\net45\DotNet.Glob.dll - $(ProjectDir)\packages\DotNet.Glob.2.1.1\lib\net45\DotNet.Glob.dll - packages\DotNet.Glob.2.1.1\lib\net45\DotNet.Glob.dll + $(SolutionDir)\packages\DotNet.Glob.2.1.1\lib\net45\DotNet.Glob.dll + $(ProjectDir)\packages\DotNet.Glob.2.1.1\lib\net45\DotNet.Glob.dll -- cgit v1.2.3 From e439581198de92e63661c4fe71108cb59cc2d999 Mon Sep 17 00:00:00 2001 From: Ignacio Etcheverry Date: Sat, 7 Sep 2019 02:19:21 +0200 Subject: Mono: Fix PCK assembly paths when exporting from Windows Assembly paths were written to PCK files with backslash as path separator and PackedData only supports forward slash. This would make exported games unable to find the assemblies. --- modules/mono/editor/GodotTools/GodotTools/GodotSharpExport.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'modules/mono/editor') diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpExport.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpExport.cs index aefc51545e..4f93ef8530 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpExport.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpExport.cs @@ -16,7 +16,7 @@ namespace GodotTools { private void AddFile(string srcPath, string dstPath, bool remap = false) { - AddFile(dstPath, File.ReadAllBytes(srcPath), remap); + AddFile(dstPath.Replace("\\", "/"), File.ReadAllBytes(srcPath), remap); } public override void _ExportFile(string path, string type, string[] features) -- cgit v1.2.3 From 93897fb8849d5b4326ef2d16797f5e0d9c835ff7 Mon Sep 17 00:00:00 2001 From: Ignacio Etcheverry Date: Sat, 7 Sep 2019 02:31:01 +0200 Subject: Fix missing method for internal_MonoWindowsInstallRoot --- modules/mono/editor/editor_internal_calls.cpp | 1 + 1 file changed, 1 insertion(+) (limited to 'modules/mono/editor') diff --git a/modules/mono/editor/editor_internal_calls.cpp b/modules/mono/editor/editor_internal_calls.cpp index cd1ca2a2c7..5a84d9e3b8 100644 --- a/modules/mono/editor/editor_internal_calls.cpp +++ b/modules/mono/editor/editor_internal_calls.cpp @@ -429,6 +429,7 @@ void register_editor_internal_calls() { mono_add_internal_call("GodotTools.Internals.Internal::internal_ScriptEditorEdit", (void *)godot_icall_Internal_ScriptEditorEdit); mono_add_internal_call("GodotTools.Internals.Internal::internal_EditorNodeShowScriptScreen", (void *)godot_icall_Internal_EditorNodeShowScriptScreen); mono_add_internal_call("GodotTools.Internals.Internal::internal_GetScriptsMetadataOrNothing", (void *)godot_icall_Internal_GetScriptsMetadataOrNothing); + mono_add_internal_call("GodotTools.Internals.Internal::internal_MonoWindowsInstallRoot", (void *)godot_icall_Internal_MonoWindowsInstallRoot); mono_add_internal_call("GodotTools.Internals.Internal::internal_EditorRunPlay", (void *)godot_icall_Internal_EditorRunPlay); mono_add_internal_call("GodotTools.Internals.Internal::internal_EditorRunStop", (void *)godot_icall_Internal_EditorRunStop); mono_add_internal_call("GodotTools.Internals.Internal::internal_ScriptEditorDebugger_ReloadScripts", (void *)godot_icall_Internal_ScriptEditorDebugger_ReloadScripts); -- cgit v1.2.3