summaryrefslogtreecommitdiff
path: root/modules/mono/editor
diff options
context:
space:
mode:
Diffstat (limited to 'modules/mono/editor')
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs14
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj58
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.BuildLogger/Properties/AssemblyInfo.cs35
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.Core/FileUtils.cs27
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj38
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.Core/Properties/AssemblyInfo.cs26
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs14
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs33
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs94
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs219
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs207
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs24
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs24
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj53
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs21
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs46
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs88
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs35
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/ForwarderMessageHandler.cs57
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj17
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs218
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs351
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientHandshake.cs44
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientMessageHandler.cs52
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs (renamed from modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs)6
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj24
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IHandshake.cs8
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ILogger.cs (renamed from modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs)2
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IMessageHandler.cs9
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Message.cs52
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/MessageDecoder.cs100
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs298
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Requests/Requests.cs129
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs23
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs (renamed from modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs)4
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/SemaphoreExtensions.cs32
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj12
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs270
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.ProjectEditor/DotNetSolution.cs5
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj60
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.ProjectEditor/MSBuild.exe0
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs11
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs12
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs106
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.ProjectEditor/Properties/AssemblyInfo.cs27
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.ProjectEditor/packages.config4
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.sln8
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs52
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/Build/BuildTool.cs10
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs91
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs4
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/BuildManager.cs65
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/BuildTab.cs12
-rwxr-xr-xmodules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs2
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs56
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs91
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj130
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs163
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs212
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs371
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs14
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs12
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs7
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs19
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/Properties/AssemblyInfo.cs26
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs51
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/packages.config5
-rw-r--r--modules/mono/editor/bindings_generator.cpp545
-rw-r--r--modules/mono/editor/bindings_generator.h235
-rw-r--r--modules/mono/editor/code_completion.cpp249
-rw-r--r--modules/mono/editor/code_completion.h56
-rw-r--r--modules/mono/editor/csharp_project.cpp1
-rw-r--r--modules/mono/editor/editor_internal_calls.cpp19
-rw-r--r--modules/mono/editor/godotsharp_export.cpp93
-rw-r--r--modules/mono/editor/godotsharp_export.h4
-rw-r--r--modules/mono/editor/script_class_parser.cpp29
-rw-r--r--modules/mono/editor/script_class_parser.h1
77 files changed, 3539 insertions, 2083 deletions
diff --git a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs
index 6015cb22b6..c2549b4ad5 100644
--- a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs
+++ b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs
@@ -2,7 +2,6 @@ using System;
using System.IO;
using System.Security;
using Microsoft.Build.Framework;
-using GodotTools.Core;
namespace GodotTools.BuildLogger
{
@@ -183,4 +182,17 @@ namespace GodotTools.BuildLogger
private StreamWriter issuesStreamWriter;
private int indent;
}
+
+ internal static class StringExtensions
+ {
+ public static string CsvEscape(this string value, char delimiter = ',')
+ {
+ bool hasSpecialChar = value.IndexOfAny(new[] { '\"', '\n', '\r', delimiter }) != -1;
+
+ if (hasSpecialChar)
+ return "\"" + value.Replace("\"", "\"\"") + "\"";
+
+ return value;
+ }
+ }
}
diff --git a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj
index 8fdd485209..0afec970c6 100644
--- a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj
+++ b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj
@@ -1,60 +1,10 @@
-<?xml version="1.0" encoding="utf-8"?>
-<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
- <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
- <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}</ProjectGuid>
- <OutputType>Library</OutputType>
- <AppDesignerFolder>Properties</AppDesignerFolder>
- <RootNamespace>GodotTools.BuildLogger</RootNamespace>
- <AssemblyName>GodotTools.BuildLogger</AssemblyName>
- <TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
- <FileAlignment>512</FileAlignment>
- <LangVersion>7</LangVersion>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <LangVersion>7.2</LangVersion>
</PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
- <PlatformTarget>AnyCPU</PlatformTarget>
- <DebugSymbols>true</DebugSymbols>
- <DebugType>portable</DebugType>
- <Optimize>false</Optimize>
- <OutputPath>bin\Debug\</OutputPath>
- <DefineConstants>DEBUG;TRACE</DefineConstants>
- <ErrorReport>prompt</ErrorReport>
- <WarningLevel>4</WarningLevel>
- </PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
- <PlatformTarget>AnyCPU</PlatformTarget>
- <DebugType>portable</DebugType>
- <Optimize>true</Optimize>
- <OutputPath>bin\Release\</OutputPath>
- <DefineConstants>TRACE</DefineConstants>
- <ErrorReport>prompt</ErrorReport>
- <WarningLevel>4</WarningLevel>
- </PropertyGroup>
- <ItemGroup>
- <Reference Include="Microsoft.Build.Framework" />
- <Reference Include="System" />
- <Reference Include="System.Core" />
- <Reference Include="System.Data" />
- <Reference Include="System.Xml" />
- </ItemGroup>
- <ItemGroup>
- <Compile Include="GodotBuildLogger.cs" />
- <Compile Include="Properties\AssemblyInfo.cs" />
- </ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj">
- <Project>{639e48bd-44e5-4091-8edd-22d36dc0768d}</Project>
- <Name>GodotTools.Core</Name>
- </ProjectReference>
+ <PackageReference Include="Microsoft.Build.Framework" Version="16.5.0" />
</ItemGroup>
- <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
- <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
- Other similar extension points exist, see Microsoft.Common.targets.
- <Target Name="BeforeBuild">
- </Target>
- <Target Name="AfterBuild">
- </Target>
- -->
</Project>
diff --git a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/Properties/AssemblyInfo.cs b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/Properties/AssemblyInfo.cs
deleted file mode 100644
index 4374f21cfa..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-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.BuildLogger")]
-[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("6CE9A984-37B1-4F8A-8FE9-609F05F071B3")]
-
-// 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.Core/FileUtils.cs b/modules/mono/editor/GodotTools/GodotTools.Core/FileUtils.cs
new file mode 100644
index 0000000000..85760a3705
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.Core/FileUtils.cs
@@ -0,0 +1,27 @@
+using System.IO;
+
+namespace GodotTools.Core
+{
+ public static class FileUtils
+ {
+ public static void SaveBackupCopy(string filePath)
+ {
+ string backupPathBase = filePath + ".old";
+ string backupPath = backupPathBase;
+
+ const int maxAttempts = 5;
+ int attempt = 1;
+
+ while (File.Exists(backupPath) && attempt <= maxAttempts)
+ {
+ backupPath = backupPathBase + "." + (attempt);
+ attempt++;
+ }
+
+ if (attempt > maxAttempts + 1)
+ return;
+
+ File.Copy(filePath, backupPath, overwrite: true);
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj b/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj
index 2c35ef540a..d6d8962f90 100644
--- a/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj
+++ b/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj
@@ -1,39 +1,7 @@
-<?xml version="1.0" encoding="utf-8"?>
-<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
- <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{639E48BD-44E5-4091-8EDD-22D36DC0768D}</ProjectGuid>
- <OutputType>Library</OutputType>
- <RootNamespace>GodotTools.Core</RootNamespace>
- <AssemblyName>GodotTools.Core</AssemblyName>
- <TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
- <LangVersion>7</LangVersion>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <LangVersion>7.2</LangVersion>
</PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
- <DebugSymbols>true</DebugSymbols>
- <DebugType>full</DebugType>
- <Optimize>false</Optimize>
- <OutputPath>bin\Debug</OutputPath>
- <DefineConstants>DEBUG;</DefineConstants>
- <ErrorReport>prompt</ErrorReport>
- <WarningLevel>4</WarningLevel>
- <ConsolePause>false</ConsolePause>
- </PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
- <Optimize>true</Optimize>
- <OutputPath>bin\Release</OutputPath>
- <ErrorReport>prompt</ErrorReport>
- <WarningLevel>4</WarningLevel>
- <ConsolePause>false</ConsolePause>
- </PropertyGroup>
- <ItemGroup>
- <Reference Include="System" />
- </ItemGroup>
- <ItemGroup>
- <Compile Include="ProcessExtensions.cs" />
- <Compile Include="Properties\AssemblyInfo.cs" />
- <Compile Include="StringExtensions.cs" />
- </ItemGroup>
- <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
</Project>
diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/Properties/AssemblyInfo.cs b/modules/mono/editor/GodotTools/GodotTools.Core/Properties/AssemblyInfo.cs
deleted file mode 100644
index 699ae6e741..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.Core/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-
-// Information about this assembly is defined by the following attributes.
-// Change them to the values specific to your project.
-
-[assembly: AssemblyTitle("GodotTools.Core")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("")]
-[assembly: AssemblyCopyright("Godot Engine contributors")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
-// The form "{Major}.{Minor}.*" will automatically update the build and revision,
-// and "{Major}.{Minor}.{Build}.*" will update just the revision.
-
-[assembly: AssemblyVersion("1.0.*")]
-
-// The following attributes are used to specify the signing key for the assembly,
-// if desired. See the Mono documentation for more information about signing.
-
-//[assembly: AssemblyDelaySign(false)]
-//[assembly: AssemblyKeyFile("")]
diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs b/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs
index 326c49f096..7ab5c5fc59 100644
--- a/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs
+++ b/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs
@@ -33,23 +33,13 @@ namespace GodotTools.Core
return rooted ? Path.DirectorySeparatorChar + path : path;
}
- private static readonly string driveRoot = Path.GetPathRoot(Environment.CurrentDirectory);
+ private static readonly string DriveRoot = Path.GetPathRoot(Environment.CurrentDirectory);
public static bool IsAbsolutePath(this string path)
{
return path.StartsWith("/", StringComparison.Ordinal) ||
path.StartsWith("\\", StringComparison.Ordinal) ||
- path.StartsWith(driveRoot, StringComparison.Ordinal);
- }
-
- public static string CsvEscape(this string value, char delimiter = ',')
- {
- bool hasSpecialChar = value.IndexOfAny(new char[] { '\"', '\n', '\r', delimiter }) != -1;
-
- if (hasSpecialChar)
- return "\"" + value.Replace("\"", "\"\"") + "\"";
-
- return value;
+ path.StartsWith(DriveRoot, StringComparison.Ordinal);
}
public static string ToSafeDirName(this string dirName, bool allowDirSeparator)
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs
deleted file mode 100644
index 7a2ff2ca56..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-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
deleted file mode 100644
index be89638241..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs
+++ /dev/null
@@ -1,94 +0,0 @@
-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
deleted file mode 100644
index 2bf3b83c75..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs
+++ /dev/null
@@ -1,219 +0,0 @@
-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<string, Action<string[]>> messageHandlers;
-
- private Dictionary<string, Action<string[]>> InitializeMessageHandlers()
- {
- return new Dictionary<string, Action<string[]>>
- {
- ["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
deleted file mode 100644
index 6441be8d6e..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs
+++ /dev/null
@@ -1,207 +0,0 @@
-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<Message, bool> 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<Message, bool> 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
deleted file mode 100644
index 1b11a14358..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System;
-using System.Net.Sockets;
-using System.Threading.Tasks;
-
-namespace GodotTools.IdeConnection
-{
- public class GodotIdeConnectionClient : GodotIdeConnection
- {
- public GodotIdeConnectionClient(TcpClient tcpClient, Func<Message, bool> 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
deleted file mode 100644
index aa98dc7ca3..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System;
-using System.Net.Sockets;
-using System.Threading.Tasks;
-
-namespace GodotTools.IdeConnection
-{
- public class GodotIdeConnectionServer : GodotIdeConnection
- {
- public GodotIdeConnectionServer(TcpClient tcpClient, Func<Message, bool> 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/GodotTools.IdeConnection.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj
deleted file mode 100644
index 8454535fba..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj
+++ /dev/null
@@ -1,53 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
- <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
- <PropertyGroup>
- <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
- <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
- <ProjectGuid>{92600954-25F0-4291-8E11-1FEE9FC4BE20}</ProjectGuid>
- <OutputType>Library</OutputType>
- <AppDesignerFolder>Properties</AppDesignerFolder>
- <RootNamespace>GodotTools.IdeConnection</RootNamespace>
- <AssemblyName>GodotTools.IdeConnection</AssemblyName>
- <TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
- <FileAlignment>512</FileAlignment>
- <LangVersion>7</LangVersion>
- </PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
- <PlatformTarget>AnyCPU</PlatformTarget>
- <DebugSymbols>true</DebugSymbols>
- <DebugType>portable</DebugType>
- <Optimize>false</Optimize>
- <OutputPath>bin\Debug\</OutputPath>
- <DefineConstants>DEBUG;TRACE</DefineConstants>
- <ErrorReport>prompt</ErrorReport>
- <WarningLevel>4</WarningLevel>
- </PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
- <PlatformTarget>AnyCPU</PlatformTarget>
- <DebugType>portable</DebugType>
- <Optimize>true</Optimize>
- <OutputPath>bin\Release\</OutputPath>
- <DefineConstants>TRACE</DefineConstants>
- <ErrorReport>prompt</ErrorReport>
- <WarningLevel>4</WarningLevel>
- </PropertyGroup>
- <ItemGroup>
- <Reference Include="System" />
- </ItemGroup>
- <ItemGroup>
- <Compile Include="ConsoleLogger.cs" />
- <Compile Include="GodotIdeMetadata.cs" />
- <Compile Include="GodotIdeBase.cs" />
- <Compile Include="GodotIdeClient.cs" />
- <Compile Include="GodotIdeConnection.cs" />
- <Compile Include="GodotIdeConnectionClient.cs" />
- <Compile Include="GodotIdeConnectionServer.cs" />
- <Compile Include="ILogger.cs" />
- <Compile Include="Message.cs" />
- <Compile Include="MessageComposer.cs" />
- <Compile Include="MessageParser.cs" />
- <Compile Include="Properties\AssemblyInfo.cs" />
- </ItemGroup>
- <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
-</Project>
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs
deleted file mode 100644
index f24d324ae3..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-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
deleted file mode 100644
index 30ffe7a06e..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-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
deleted file mode 100644
index 4365d69989..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-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<string>();
- 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
deleted file mode 100644
index 0806d02ca0..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-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.IdeMessaging.CLI/ForwarderMessageHandler.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/ForwarderMessageHandler.cs
new file mode 100644
index 0000000000..3cb6a6687e
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/ForwarderMessageHandler.cs
@@ -0,0 +1,57 @@
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using GodotTools.IdeMessaging.Utils;
+
+namespace GodotTools.IdeMessaging.CLI
+{
+ public class ForwarderMessageHandler : IMessageHandler
+ {
+ private readonly StreamWriter outputWriter;
+ private readonly SemaphoreSlim outputWriteSem = new SemaphoreSlim(1);
+
+ public ForwarderMessageHandler(StreamWriter outputWriter)
+ {
+ this.outputWriter = outputWriter;
+ }
+
+ public async Task<MessageContent> HandleRequest(Peer peer, string id, MessageContent content, ILogger logger)
+ {
+ await WriteRequestToOutput(id, content);
+ return new MessageContent(MessageStatus.RequestNotSupported, "null");
+ }
+
+ private async Task WriteRequestToOutput(string id, MessageContent content)
+ {
+ using (await outputWriteSem.UseAsync())
+ {
+ await outputWriter.WriteLineAsync("======= Request =======");
+ await outputWriter.WriteLineAsync(id);
+ await outputWriter.WriteLineAsync(content.Body.Count(c => c == '\n').ToString());
+ await outputWriter.WriteLineAsync(content.Body);
+ await outputWriter.WriteLineAsync("=======================");
+ await outputWriter.FlushAsync();
+ }
+ }
+
+ public async Task WriteResponseToOutput(string id, MessageContent content)
+ {
+ using (await outputWriteSem.UseAsync())
+ {
+ await outputWriter.WriteLineAsync("======= Response =======");
+ await outputWriter.WriteLineAsync(id);
+ await outputWriter.WriteLineAsync(content.Body.Count(c => c == '\n').ToString());
+ await outputWriter.WriteLineAsync(content.Body);
+ await outputWriter.WriteLineAsync("========================");
+ await outputWriter.FlushAsync();
+ }
+ }
+
+ public async Task WriteLineToOutput(string eventName)
+ {
+ using (await outputWriteSem.UseAsync())
+ await outputWriter.WriteLineAsync($"======= {eventName} =======");
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj
new file mode 100644
index 0000000000..ae78da27bc
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <ProjectGuid>{B06C2951-C8E3-4F28-80B2-717CF327EB19}</ProjectGuid>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net472</TargetFramework>
+ <LangVersion>7.2</LangVersion>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\GodotTools.IdeMessaging\GodotTools.IdeMessaging.csproj" />
+ </ItemGroup>
+ <ItemGroup>
+ <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
+ </ItemGroup>
+</Project>
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs
new file mode 100644
index 0000000000..99a55c471b
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs
@@ -0,0 +1,218 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using GodotTools.IdeMessaging.Requests;
+using Newtonsoft.Json;
+
+namespace GodotTools.IdeMessaging.CLI
+{
+ internal static class Program
+ {
+ private static readonly ILogger Logger = new CustomLogger();
+
+ public static int Main(string[] args)
+ {
+ try
+ {
+ var mainTask = StartAsync(args, Console.OpenStandardInput(), Console.OpenStandardOutput());
+ mainTask.Wait();
+ return mainTask.Result;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Unhandled exception: ", ex);
+ return 1;
+ }
+ }
+
+ private static async Task<int> StartAsync(string[] args, Stream inputStream, Stream outputStream)
+ {
+ var inputReader = new StreamReader(inputStream, Encoding.UTF8);
+ var outputWriter = new StreamWriter(outputStream, Encoding.UTF8);
+
+ try
+ {
+ if (args.Length == 0)
+ {
+ Logger.LogError("Expected at least 1 argument");
+ return 1;
+ }
+
+ string godotProjectDir = args[0];
+
+ if (!Directory.Exists(godotProjectDir))
+ {
+ Logger.LogError($"The specified Godot project directory does not exist: {godotProjectDir}");
+ return 1;
+ }
+
+ var forwarder = new ForwarderMessageHandler(outputWriter);
+
+ using (var fwdClient = new Client("VisualStudioCode", godotProjectDir, forwarder, Logger))
+ {
+ fwdClient.Start();
+
+ // ReSharper disable AccessToDisposedClosure
+ fwdClient.Connected += async () => await forwarder.WriteLineToOutput("Event=Connected");
+ fwdClient.Disconnected += async () => await forwarder.WriteLineToOutput("Event=Disconnected");
+ // ReSharper restore AccessToDisposedClosure
+
+ // TODO: Await connected with timeout
+
+ while (!fwdClient.IsDisposed)
+ {
+ string firstLine = await inputReader.ReadLineAsync();
+
+ if (firstLine == null || firstLine == "QUIT")
+ goto ExitMainLoop;
+
+ string messageId = firstLine;
+
+ string messageArgcLine = await inputReader.ReadLineAsync();
+
+ if (messageArgcLine == null)
+ {
+ Logger.LogInfo("EOF when expecting argument count");
+ goto ExitMainLoop;
+ }
+
+ if (!int.TryParse(messageArgcLine, out int messageArgc))
+ {
+ Logger.LogError("Received invalid line for argument count: " + firstLine);
+ continue;
+ }
+
+ var body = new StringBuilder();
+
+ for (int i = 0; i < messageArgc; i++)
+ {
+ string bodyLine = await inputReader.ReadLineAsync();
+
+ if (bodyLine == null)
+ {
+ Logger.LogInfo($"EOF when expecting body line #{i + 1}");
+ goto ExitMainLoop;
+ }
+
+ body.AppendLine(bodyLine);
+ }
+
+ var response = await SendRequest(fwdClient, messageId, new MessageContent(MessageStatus.Ok, body.ToString()));
+
+ if (response == null)
+ {
+ Logger.LogError($"Failed to write message to the server: {messageId}");
+ }
+ else
+ {
+ var content = new MessageContent(response.Status, JsonConvert.SerializeObject(response));
+ await forwarder.WriteResponseToOutput(messageId, content);
+ }
+ }
+
+ ExitMainLoop:
+
+ await forwarder.WriteLineToOutput("Event=Quit");
+ }
+
+ return 0;
+ }
+ catch (Exception e)
+ {
+ Logger.LogError("Unhandled exception", e);
+ return 1;
+ }
+ }
+
+ private static async Task<Response> SendRequest(Client client, string id, MessageContent content)
+ {
+ var handlers = new Dictionary<string, Func<Task<Response>>>
+ {
+ [PlayRequest.Id] = async () =>
+ {
+ var request = JsonConvert.DeserializeObject<PlayRequest>(content.Body);
+ return await client.SendRequest<PlayResponse>(request);
+ },
+ [DebugPlayRequest.Id] = async () =>
+ {
+ var request = JsonConvert.DeserializeObject<DebugPlayRequest>(content.Body);
+ return await client.SendRequest<DebugPlayResponse>(request);
+ },
+ [ReloadScriptsRequest.Id] = async () =>
+ {
+ var request = JsonConvert.DeserializeObject<ReloadScriptsRequest>(content.Body);
+ return await client.SendRequest<ReloadScriptsResponse>(request);
+ },
+ [CodeCompletionRequest.Id] = async () =>
+ {
+ var request = JsonConvert.DeserializeObject<CodeCompletionRequest>(content.Body);
+ return await client.SendRequest<CodeCompletionResponse>(request);
+ }
+ };
+
+ if (handlers.TryGetValue(id, out var handler))
+ return await handler();
+
+ Console.WriteLine("INVALID REQUEST");
+ return null;
+ }
+
+ private class CustomLogger : ILogger
+ {
+ private static string ThisAppPath => Assembly.GetExecutingAssembly().Location;
+ private static string ThisAppPathWithoutExtension => Path.ChangeExtension(ThisAppPath, null);
+
+ private static readonly string LogPath = $"{ThisAppPathWithoutExtension}.log";
+
+ private static StreamWriter NewWriter() => new StreamWriter(LogPath, append: true, encoding: Encoding.UTF8);
+
+ private static void Log(StreamWriter writer, string message)
+ {
+ writer.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}: {message}");
+ }
+
+ public void LogDebug(string message)
+ {
+ using (var writer = NewWriter())
+ {
+ Log(writer, "DEBUG: " + message);
+ }
+ }
+
+ public void LogInfo(string message)
+ {
+ using (var writer = NewWriter())
+ {
+ Log(writer, "INFO: " + message);
+ }
+ }
+
+ public void LogWarning(string message)
+ {
+ using (var writer = NewWriter())
+ {
+ Log(writer, "WARN: " + message);
+ }
+ }
+
+ public void LogError(string message)
+ {
+ using (var writer = NewWriter())
+ {
+ Log(writer, "ERROR: " + message);
+ }
+ }
+
+ public void LogError(string message, Exception e)
+ {
+ using (var writer = NewWriter())
+ {
+ Log(writer, "EXCEPTION: " + message + '\n' + e);
+ }
+ }
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs
new file mode 100644
index 0000000000..572c541412
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs
@@ -0,0 +1,351 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using Newtonsoft.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using GodotTools.IdeMessaging.Requests;
+using GodotTools.IdeMessaging.Utils;
+
+namespace GodotTools.IdeMessaging
+{
+ // ReSharper disable once UnusedType.Global
+ public sealed class Client : IDisposable
+ {
+ private readonly ILogger logger;
+
+ private readonly string identity;
+
+ private string MetaFilePath { get; }
+ private DateTime? metaFileModifiedTime;
+ private GodotIdeMetadata godotIdeMetadata;
+ private readonly FileSystemWatcher fsWatcher;
+
+ public string GodotEditorExecutablePath => godotIdeMetadata.EditorExecutablePath;
+
+ private readonly IMessageHandler messageHandler;
+
+ private Peer peer;
+ private readonly SemaphoreSlim connectionSem = new SemaphoreSlim(1);
+
+ private readonly Queue<NotifyAwaiter<bool>> clientConnectedAwaiters = new Queue<NotifyAwaiter<bool>>();
+ private readonly Queue<NotifyAwaiter<bool>> clientDisconnectedAwaiters = new Queue<NotifyAwaiter<bool>>();
+
+ // ReSharper disable once UnusedMember.Global
+ public async Task<bool> AwaitConnected()
+ {
+ var awaiter = new NotifyAwaiter<bool>();
+ clientConnectedAwaiters.Enqueue(awaiter);
+ return await awaiter;
+ }
+
+ // ReSharper disable once UnusedMember.Global
+ public async Task<bool> AwaitDisconnected()
+ {
+ var awaiter = new NotifyAwaiter<bool>();
+ clientDisconnectedAwaiters.Enqueue(awaiter);
+ return await awaiter;
+ }
+
+ // ReSharper disable once MemberCanBePrivate.Global
+ public bool IsDisposed { get; private set; }
+
+ // ReSharper disable once MemberCanBePrivate.Global
+ public bool IsConnected => peer != null && !peer.IsDisposed && peer.IsTcpClientConnected;
+
+ // ReSharper disable once EventNeverSubscribedTo.Global
+ public event Action Connected
+ {
+ add
+ {
+ if (peer != null && !peer.IsDisposed)
+ peer.Connected += value;
+ }
+ remove
+ {
+ if (peer != null && !peer.IsDisposed)
+ peer.Connected -= value;
+ }
+ }
+
+ // ReSharper disable once EventNeverSubscribedTo.Global
+ public event Action Disconnected
+ {
+ add
+ {
+ if (peer != null && !peer.IsDisposed)
+ peer.Disconnected += value;
+ }
+ remove
+ {
+ if (peer != null && !peer.IsDisposed)
+ peer.Disconnected -= value;
+ }
+ }
+
+ ~Client()
+ {
+ Dispose(disposing: false);
+ }
+
+ public async void Dispose()
+ {
+ if (IsDisposed)
+ return;
+
+ using (await connectionSem.UseAsync())
+ {
+ if (IsDisposed) // lock may not be fair
+ return;
+ IsDisposed = true;
+ }
+
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ peer?.Dispose();
+ fsWatcher?.Dispose();
+ }
+ }
+
+ public Client(string identity, string godotProjectDir, IMessageHandler messageHandler, ILogger logger)
+ {
+ this.identity = identity;
+ this.messageHandler = messageHandler;
+ this.logger = logger;
+
+ string projectMetadataDir = Path.Combine(godotProjectDir, ".mono", "metadata");
+
+ MetaFilePath = Path.Combine(projectMetadataDir, GodotIdeMetadata.DefaultFileName);
+
+ // FileSystemWatcher requires an existing directory
+ if (!Directory.Exists(projectMetadataDir))
+ Directory.CreateDirectory(projectMetadataDir);
+
+ fsWatcher = new FileSystemWatcher(projectMetadataDir, GodotIdeMetadata.DefaultFileName);
+ }
+
+ private async void OnMetaFileChanged(object sender, FileSystemEventArgs e)
+ {
+ if (IsDisposed)
+ return;
+
+ using (await connectionSem.UseAsync())
+ {
+ if (IsDisposed)
+ return;
+
+ if (!File.Exists(MetaFilePath))
+ return;
+
+ var lastWriteTime = File.GetLastWriteTime(MetaFilePath);
+
+ if (lastWriteTime == metaFileModifiedTime)
+ return;
+
+ metaFileModifiedTime = lastWriteTime;
+
+ var metadata = ReadMetadataFile();
+
+ if (metadata != null && metadata != godotIdeMetadata)
+ {
+ godotIdeMetadata = metadata.Value;
+ _ = Task.Run(ConnectToServer);
+ }
+ }
+ }
+
+ private async void OnMetaFileDeleted(object sender, FileSystemEventArgs e)
+ {
+ if (IsDisposed)
+ return;
+
+ if (IsConnected)
+ {
+ using (await connectionSem.UseAsync())
+ peer?.Dispose();
+ }
+
+ // The file may have been re-created
+
+ using (await connectionSem.UseAsync())
+ {
+ if (IsDisposed)
+ return;
+
+ if (IsConnected || !File.Exists(MetaFilePath))
+ return;
+
+ var lastWriteTime = File.GetLastWriteTime(MetaFilePath);
+
+ if (lastWriteTime == metaFileModifiedTime)
+ return;
+
+ metaFileModifiedTime = lastWriteTime;
+
+ var metadata = ReadMetadataFile();
+
+ if (metadata != null)
+ {
+ godotIdeMetadata = metadata.Value;
+ _ = Task.Run(ConnectToServer);
+ }
+ }
+ }
+
+ private GodotIdeMetadata? ReadMetadataFile()
+ {
+ using (var fileStream = new FileStream(MetaFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+ using (var reader = new StreamReader(fileStream))
+ {
+ 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 async Task AcceptClient(TcpClient tcpClient)
+ {
+ logger.LogDebug("Accept client...");
+
+ using (peer = new Peer(tcpClient, new ClientHandshake(), messageHandler, logger))
+ {
+ // ReSharper disable AccessToDisposedClosure
+ peer.Connected += () =>
+ {
+ logger.LogInfo("Connection open with Ide Client");
+
+ while (clientConnectedAwaiters.Count > 0)
+ clientConnectedAwaiters.Dequeue().SetResult(true);
+ };
+
+ peer.Disconnected += () =>
+ {
+ while (clientDisconnectedAwaiters.Count > 0)
+ clientDisconnectedAwaiters.Dequeue().SetResult(true);
+ };
+ // ReSharper restore AccessToDisposedClosure
+
+ try
+ {
+ if (!await peer.DoHandshake(identity))
+ {
+ logger.LogError("Handshake failed");
+ return;
+ }
+ }
+ catch (Exception e)
+ {
+ logger.LogError("Handshake failed with unhandled exception: ", e);
+ return;
+ }
+
+ await peer.Process();
+
+ logger.LogInfo("Connection closed with Ide Client");
+ }
+ }
+
+ private async Task ConnectToServer()
+ {
+ var tcpClient = new TcpClient();
+
+ try
+ {
+ logger.LogInfo("Connecting to Godot Ide Server");
+
+ await tcpClient.ConnectAsync(IPAddress.Loopback, godotIdeMetadata.Port);
+
+ logger.LogInfo("Connection open with Godot Ide Server");
+
+ await AcceptClient(tcpClient);
+ }
+ catch (SocketException e)
+ {
+ if (e.SocketErrorCode == SocketError.ConnectionRefused)
+ logger.LogError("The connection to the Godot Ide Server was refused");
+ else
+ throw;
+ }
+ }
+
+ // ReSharper disable once UnusedMember.Global
+ public async void Start()
+ {
+ fsWatcher.Created += OnMetaFileChanged;
+ fsWatcher.Changed += OnMetaFileChanged;
+ fsWatcher.Deleted += OnMetaFileDeleted;
+ fsWatcher.EnableRaisingEvents = true;
+
+ using (await connectionSem.UseAsync())
+ {
+ if (IsDisposed)
+ return;
+
+ if (IsConnected)
+ return;
+
+ if (!File.Exists(MetaFilePath))
+ {
+ logger.LogInfo("There is no Godot Ide Server running");
+ return;
+ }
+
+ var metadata = ReadMetadataFile();
+
+ if (metadata != null)
+ {
+ godotIdeMetadata = metadata.Value;
+ _ = Task.Run(ConnectToServer);
+ }
+ else
+ {
+ logger.LogError("Failed to read Godot Ide metadata file");
+ }
+ }
+ }
+
+ public async Task<TResponse> SendRequest<TResponse>(Request request)
+ where TResponse : Response, new()
+ {
+ if (!IsConnected)
+ {
+ logger.LogError("Cannot write request. Not connected to the Godot Ide Server.");
+ return null;
+ }
+
+ string body = JsonConvert.SerializeObject(request);
+ return await peer.SendRequest<TResponse>(request.Id, body);
+ }
+
+ public async Task<TResponse> SendRequest<TResponse>(string id, string body)
+ where TResponse : Response, new()
+ {
+ if (!IsConnected)
+ {
+ logger.LogError("Cannot write request. Not connected to the Godot Ide Server.");
+ return null;
+ }
+
+ return await peer.SendRequest<TResponse>(id, body);
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientHandshake.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientHandshake.cs
new file mode 100644
index 0000000000..43041be7be
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientHandshake.cs
@@ -0,0 +1,44 @@
+using System.Text.RegularExpressions;
+
+namespace GodotTools.IdeMessaging
+{
+ public class ClientHandshake : IHandshake
+ {
+ private static readonly string ClientHandshakeBase = $"{Peer.ClientHandshakeName},Version={Peer.ProtocolVersionMajor}.{Peer.ProtocolVersionMinor}.{Peer.ProtocolVersionRevision}";
+ private static readonly string ServerHandshakePattern = $@"{Regex.Escape(Peer.ServerHandshakeName)},Version=([0-9]+)\.([0-9]+)\.([0-9]+),([_a-zA-Z][_a-zA-Z0-9]{{0,63}})";
+
+ public string GetHandshakeLine(string identity) => $"{ClientHandshakeBase},{identity}";
+
+ public bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger)
+ {
+ identity = null;
+
+ var match = Regex.Match(handshake, ServerHandshakePattern);
+
+ if (!match.Success)
+ return false;
+
+ if (!uint.TryParse(match.Groups[1].Value, out uint serverMajor) || Peer.ProtocolVersionMajor != serverMajor)
+ {
+ logger.LogDebug("Incompatible major version: " + match.Groups[1].Value);
+ return false;
+ }
+
+ if (!uint.TryParse(match.Groups[2].Value, out uint serverMinor) || Peer.ProtocolVersionMinor < serverMinor)
+ {
+ logger.LogDebug("Incompatible minor version: " + match.Groups[2].Value);
+ return false;
+ }
+
+ if (!uint.TryParse(match.Groups[3].Value, out uint _)) // Revision
+ {
+ logger.LogDebug("Incompatible revision build: " + match.Groups[3].Value);
+ return false;
+ }
+
+ identity = match.Groups[4].Value;
+
+ return true;
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientMessageHandler.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientMessageHandler.cs
new file mode 100644
index 0000000000..64bcfd824c
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientMessageHandler.cs
@@ -0,0 +1,52 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using GodotTools.IdeMessaging.Requests;
+using Newtonsoft.Json;
+
+namespace GodotTools.IdeMessaging
+{
+ // ReSharper disable once UnusedType.Global
+ public abstract class ClientMessageHandler : IMessageHandler
+ {
+ private readonly Dictionary<string, Peer.RequestHandler> requestHandlers;
+
+ protected ClientMessageHandler()
+ {
+ requestHandlers = InitializeRequestHandlers();
+ }
+
+ public async Task<MessageContent> HandleRequest(Peer peer, string id, MessageContent content, ILogger logger)
+ {
+ if (!requestHandlers.TryGetValue(id, out var handler))
+ {
+ logger.LogError($"Received unknown request: {id}");
+ return new MessageContent(MessageStatus.RequestNotSupported, "null");
+ }
+
+ try
+ {
+ var response = await handler(peer, content);
+ return new MessageContent(response.Status, JsonConvert.SerializeObject(response));
+ }
+ catch (JsonException)
+ {
+ logger.LogError($"Received request with invalid body: {id}");
+ return new MessageContent(MessageStatus.InvalidRequestBody, "null");
+ }
+ }
+
+ private Dictionary<string, Peer.RequestHandler> InitializeRequestHandlers()
+ {
+ return new Dictionary<string, Peer.RequestHandler>
+ {
+ [OpenFileRequest.Id] = async (peer, content) =>
+ {
+ var request = JsonConvert.DeserializeObject<OpenFileRequest>(content.Body);
+ return await HandleOpenFile(request);
+ }
+ };
+ }
+
+ protected abstract Task<Response> HandleOpenFile(OpenFileRequest request);
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs
index d16daba0e2..686202e81e 100644
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs
@@ -1,10 +1,12 @@
-namespace GodotTools.IdeConnection
+namespace GodotTools.IdeMessaging
{
- public struct GodotIdeMetadata
+ public readonly struct GodotIdeMetadata
{
public int Port { get; }
public string EditorExecutablePath { get; }
+ public const string DefaultFileName = "ide_messaging_meta.txt";
+
public GodotIdeMetadata(int port, string editorExecutablePath)
{
Port = port;
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj
new file mode 100644
index 0000000000..dad6b9ae7a
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj
@@ -0,0 +1,24 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <ProjectGuid>{92600954-25F0-4291-8E11-1FEE9FC4BE20}</ProjectGuid>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <LangVersion>7.2</LangVersion>
+ <PackageId>GodotTools.IdeMessaging</PackageId>
+ <Version>1.1.1</Version>
+ <AssemblyVersion>$(Version)</AssemblyVersion>
+ <Authors>Godot Engine contributors</Authors>
+ <Company />
+ <PackageTags>godot</PackageTags>
+ <RepositoryUrl>https://github.com/godotengine/godot/tree/master/modules/mono/editor/GodotTools/GodotTools.IdeMessaging</RepositoryUrl>
+ <PackageLicenseExpression>MIT</PackageLicenseExpression>
+ <Description>
+This library enables communication with the Godot Engine editor (the version with .NET support).
+It's intended for use in IDEs/editors plugins for a better experience working with Godot C# projects.
+
+A client using this library is only compatible with servers of the same major version and of a lower or equal minor version.
+ </Description>
+ </PropertyGroup>
+ <ItemGroup>
+ <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
+ </ItemGroup>
+</Project>
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IHandshake.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IHandshake.cs
new file mode 100644
index 0000000000..6387145a28
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IHandshake.cs
@@ -0,0 +1,8 @@
+namespace GodotTools.IdeMessaging
+{
+ public interface IHandshake
+ {
+ string GetHandshakeLine(string identity);
+ bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger);
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ILogger.cs
index 614bb30271..d2855f93a1 100644
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ILogger.cs
@@ -1,6 +1,6 @@
using System;
-namespace GodotTools.IdeConnection
+namespace GodotTools.IdeMessaging
{
public interface ILogger
{
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IMessageHandler.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IMessageHandler.cs
new file mode 100644
index 0000000000..9622fcc96d
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IMessageHandler.cs
@@ -0,0 +1,9 @@
+using System.Threading.Tasks;
+
+namespace GodotTools.IdeMessaging
+{
+ public interface IMessageHandler
+ {
+ Task<MessageContent> HandleRequest(Peer peer, string id, MessageContent content, ILogger logger);
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Message.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Message.cs
new file mode 100644
index 0000000000..6903ec197b
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Message.cs
@@ -0,0 +1,52 @@
+namespace GodotTools.IdeMessaging
+{
+ public class Message
+ {
+ public MessageKind Kind { get; }
+ public string Id { get; }
+ public MessageContent Content { get; }
+
+ public Message(MessageKind kind, string id, MessageContent content)
+ {
+ Kind = kind;
+ Id = id;
+ Content = content;
+ }
+
+ public override string ToString()
+ {
+ return $"{Kind} | {Id}";
+ }
+ }
+
+ public enum MessageKind
+ {
+ Request,
+ Response
+ }
+
+ public enum MessageStatus
+ {
+ Ok,
+ RequestNotSupported,
+ InvalidRequestBody
+ }
+
+ public readonly struct MessageContent
+ {
+ public MessageStatus Status { get; }
+ public string Body { get; }
+
+ public MessageContent(string body)
+ {
+ Status = MessageStatus.Ok;
+ Body = body;
+ }
+
+ public MessageContent(MessageStatus status, string body)
+ {
+ Status = status;
+ Body = body;
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/MessageDecoder.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/MessageDecoder.cs
new file mode 100644
index 0000000000..a00575a2a1
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/MessageDecoder.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Text;
+
+namespace GodotTools.IdeMessaging
+{
+ public class MessageDecoder
+ {
+ private class DecodedMessage
+ {
+ public MessageKind? Kind;
+ public string Id;
+ public MessageStatus? Status;
+ public readonly StringBuilder Body = new StringBuilder();
+ public uint? PendingBodyLines;
+
+ public void Clear()
+ {
+ Kind = null;
+ Id = null;
+ Status = null;
+ Body.Clear();
+ PendingBodyLines = null;
+ }
+
+ public Message ToMessage()
+ {
+ if (!Kind.HasValue || Id == null || !Status.HasValue ||
+ !PendingBodyLines.HasValue || PendingBodyLines.Value > 0)
+ throw new InvalidOperationException();
+
+ return new Message(Kind.Value, Id, new MessageContent(Status.Value, Body.ToString()));
+ }
+ }
+
+ public enum State
+ {
+ Decoding,
+ Decoded,
+ Errored
+ }
+
+ private readonly DecodedMessage decodingMessage = new DecodedMessage();
+
+ public State Decode(string messageLine, out Message decodedMessage)
+ {
+ decodedMessage = null;
+
+ if (!decodingMessage.Kind.HasValue)
+ {
+ if (!Enum.TryParse(messageLine, ignoreCase: true, out MessageKind kind))
+ {
+ decodingMessage.Clear();
+ return State.Errored;
+ }
+
+ decodingMessage.Kind = kind;
+ }
+ else if (decodingMessage.Id == null)
+ {
+ decodingMessage.Id = messageLine;
+ }
+ else if (decodingMessage.Status == null)
+ {
+ if (!Enum.TryParse(messageLine, ignoreCase: true, out MessageStatus status))
+ {
+ decodingMessage.Clear();
+ return State.Errored;
+ }
+
+ decodingMessage.Status = status;
+ }
+ else if (decodingMessage.PendingBodyLines == null)
+ {
+ if (!uint.TryParse(messageLine, out uint pendingBodyLines))
+ {
+ decodingMessage.Clear();
+ return State.Errored;
+ }
+
+ decodingMessage.PendingBodyLines = pendingBodyLines;
+ }
+ else
+ {
+ if (decodingMessage.PendingBodyLines > 0)
+ {
+ decodingMessage.Body.AppendLine(messageLine);
+ decodingMessage.PendingBodyLines -= 1;
+ }
+ else
+ {
+ decodedMessage = decodingMessage.ToMessage();
+ decodingMessage.Clear();
+ return State.Decoded;
+ }
+ }
+
+ return State.Decoding;
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs
new file mode 100644
index 0000000000..10d7e1898e
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs
@@ -0,0 +1,298 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Sockets;
+using System.Reflection;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using GodotTools.IdeMessaging.Requests;
+using GodotTools.IdeMessaging.Utils;
+
+namespace GodotTools.IdeMessaging
+{
+ public sealed class Peer : IDisposable
+ {
+ /// <summary>
+ /// Major version.
+ /// There is no forward nor backward compatibility between different major versions.
+ /// Connection is refused if client and server have different major versions.
+ /// </summary>
+ public static readonly int ProtocolVersionMajor = Assembly.GetAssembly(typeof(Peer)).GetName().Version.Major;
+
+ /// <summary>
+ /// Minor version, which clients must be backward compatible with.
+ /// Connection is refused if the client's minor version is lower than the server's.
+ /// </summary>
+ public static readonly int ProtocolVersionMinor = Assembly.GetAssembly(typeof(Peer)).GetName().Version.Minor;
+
+ /// <summary>
+ /// Revision, which doesn't affect compatibility.
+ /// </summary>
+ public static readonly int ProtocolVersionRevision = Assembly.GetAssembly(typeof(Peer)).GetName().Version.Revision;
+
+ public const string ClientHandshakeName = "GodotIdeClient";
+ public const string ServerHandshakeName = "GodotIdeServer";
+
+ private const int ClientWriteTimeout = 8000;
+
+ public delegate Task<Response> RequestHandler(Peer peer, MessageContent content);
+
+ private readonly TcpClient tcpClient;
+
+ private readonly TextReader clientReader;
+ private readonly TextWriter clientWriter;
+
+ private readonly SemaphoreSlim writeSem = new SemaphoreSlim(1);
+
+ private string remoteIdentity = string.Empty;
+ public string RemoteIdentity => remoteIdentity;
+
+ public event Action Connected;
+ public event Action Disconnected;
+
+ private ILogger Logger { get; }
+
+ public bool IsDisposed { get; private set; }
+
+ public bool IsTcpClientConnected => tcpClient.Client != null && tcpClient.Client.Connected;
+
+ private bool IsConnected { get; set; }
+
+ private readonly IHandshake handshake;
+ private readonly IMessageHandler messageHandler;
+
+ private readonly Dictionary<string, Queue<ResponseAwaiter>> requestAwaiterQueues = new Dictionary<string, Queue<ResponseAwaiter>>();
+ private readonly SemaphoreSlim requestsSem = new SemaphoreSlim(1);
+
+ public Peer(TcpClient tcpClient, IHandshake handshake, IMessageHandler messageHandler, ILogger logger)
+ {
+ this.tcpClient = tcpClient;
+ this.handshake = handshake;
+ this.messageHandler = messageHandler;
+
+ Logger = logger;
+
+ NetworkStream clientStream = tcpClient.GetStream();
+ clientStream.WriteTimeout = ClientWriteTimeout;
+
+ clientReader = new StreamReader(clientStream, Encoding.UTF8);
+ clientWriter = new StreamWriter(clientStream, Encoding.UTF8) {NewLine = "\n"};
+ }
+
+ public async Task Process()
+ {
+ try
+ {
+ var decoder = new MessageDecoder();
+
+ string messageLine;
+ while ((messageLine = await ReadLine()) != null)
+ {
+ var state = decoder.Decode(messageLine, out var msg);
+
+ if (state == MessageDecoder.State.Decoding)
+ continue; // Not finished decoding yet
+
+ if (state == MessageDecoder.State.Errored)
+ {
+ Logger.LogError($"Received message line with invalid format: {messageLine}");
+ continue;
+ }
+
+ Logger.LogDebug($"Received message: {msg}");
+
+ try
+ {
+ if (msg.Kind == MessageKind.Request)
+ {
+ var responseContent = await messageHandler.HandleRequest(this, msg.Id, msg.Content, Logger);
+ await WriteMessage(new Message(MessageKind.Response, msg.Id, responseContent));
+ }
+ else if (msg.Kind == MessageKind.Response)
+ {
+ ResponseAwaiter responseAwaiter;
+
+ using (await requestsSem.UseAsync())
+ {
+ if (!requestAwaiterQueues.TryGetValue(msg.Id, out var queue) || queue.Count <= 0)
+ {
+ Logger.LogError($"Received unexpected response: {msg.Id}");
+ return;
+ }
+
+ responseAwaiter = queue.Dequeue();
+ }
+
+ responseAwaiter.SetResult(msg.Content);
+ }
+ else
+ {
+ throw new IndexOutOfRangeException($"Invalid message kind {msg.Kind}");
+ }
+ }
+ catch (Exception e)
+ {
+ Logger.LogError($"Message handler for '{msg}' failed with exception", e);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ if (!IsDisposed || !(e is SocketException || e.InnerException is SocketException))
+ {
+ Logger.LogError("Unhandled exception in the peer loop", e);
+ }
+ }
+ }
+
+ public async Task<bool> DoHandshake(string identity)
+ {
+ if (!await WriteLine(handshake.GetHandshakeLine(identity)))
+ {
+ Logger.LogError("Could not write handshake");
+ return false;
+ }
+
+ var readHandshakeTask = ReadLine();
+
+ if (await Task.WhenAny(readHandshakeTask, Task.Delay(8000)) != readHandshakeTask)
+ {
+ Logger.LogError("Timeout waiting for the client handshake");
+ return false;
+ }
+
+ string peerHandshake = await readHandshakeTask;
+
+ if (handshake == null || !handshake.IsValidPeerHandshake(peerHandshake, out remoteIdentity, Logger))
+ {
+ Logger.LogError("Received invalid handshake: " + peerHandshake);
+ return false;
+ }
+
+ IsConnected = true;
+ Connected?.Invoke();
+
+ Logger.LogInfo("Peer connection started");
+
+ return true;
+ }
+
+ private async Task<string> ReadLine()
+ {
+ try
+ {
+ return await clientReader.ReadLineAsync();
+ }
+ catch (Exception e)
+ {
+ if (IsDisposed)
+ {
+ var se = e as SocketException ?? e.InnerException as SocketException;
+ if (se != null && se.SocketErrorCode == SocketError.Interrupted)
+ return null;
+ }
+
+ throw;
+ }
+ }
+
+ private Task<bool> WriteMessage(Message message)
+ {
+ Logger.LogDebug($"Sending message: {message}");
+ int bodyLineCount = message.Content.Body.Count(c => c == '\n');
+
+ bodyLineCount += 1; // Extra line break at the end
+
+ var builder = new StringBuilder();
+
+ builder.AppendLine(message.Kind.ToString());
+ builder.AppendLine(message.Id);
+ builder.AppendLine(message.Content.Status.ToString());
+ builder.AppendLine(bodyLineCount.ToString());
+ builder.AppendLine(message.Content.Body);
+
+ return WriteLine(builder.ToString());
+ }
+
+ public async Task<TResponse> SendRequest<TResponse>(string id, string body)
+ where TResponse : Response, new()
+ {
+ ResponseAwaiter responseAwaiter;
+
+ using (await requestsSem.UseAsync())
+ {
+ bool written = await WriteMessage(new Message(MessageKind.Request, id, new MessageContent(body)));
+
+ if (!written)
+ return null;
+
+ if (!requestAwaiterQueues.TryGetValue(id, out var queue))
+ {
+ queue = new Queue<ResponseAwaiter>();
+ requestAwaiterQueues.Add(id, queue);
+ }
+
+ responseAwaiter = new ResponseAwaiter<TResponse>();
+ queue.Enqueue(responseAwaiter);
+ }
+
+ return (TResponse)await responseAwaiter;
+ }
+
+ private async Task<bool> WriteLine(string text)
+ {
+ if (clientWriter == null || IsDisposed || !IsTcpClientConnected)
+ return false;
+
+ using (await writeSem.UseAsync())
+ {
+ try
+ {
+ await clientWriter.WriteLineAsync(text);
+ await clientWriter.FlushAsync();
+ }
+ 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;
+ }
+
+ // ReSharper disable once UnusedMember.Global
+ public void ShutdownSocketSend()
+ {
+ tcpClient.Client.Shutdown(SocketShutdown.Send);
+ }
+
+ public void Dispose()
+ {
+ if (IsDisposed)
+ return;
+
+ IsDisposed = true;
+
+ if (IsTcpClientConnected)
+ {
+ if (IsConnected)
+ Disconnected?.Invoke();
+ }
+
+ clientReader?.Dispose();
+ clientWriter?.Dispose();
+ ((IDisposable)tcpClient)?.Dispose();
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Requests/Requests.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Requests/Requests.cs
new file mode 100644
index 0000000000..e93db9377b
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Requests/Requests.cs
@@ -0,0 +1,129 @@
+// ReSharper disable ClassNeverInstantiated.Global
+// ReSharper disable UnusedMember.Global
+// ReSharper disable UnusedAutoPropertyAccessor.Global
+
+using Newtonsoft.Json;
+
+namespace GodotTools.IdeMessaging.Requests
+{
+ public abstract class Request
+ {
+ [JsonIgnore] public string Id { get; }
+
+ protected Request(string id)
+ {
+ Id = id;
+ }
+ }
+
+ public abstract class Response
+ {
+ [JsonIgnore] public MessageStatus Status { get; set; } = MessageStatus.Ok;
+ }
+
+ public sealed class CodeCompletionRequest : Request
+ {
+ public enum CompletionKind
+ {
+ InputActions = 0,
+ NodePaths,
+ ResourcePaths,
+ ScenePaths,
+ ShaderParams,
+ Signals,
+ ThemeColors,
+ ThemeConstants,
+ ThemeFonts,
+ ThemeStyles
+ }
+
+ public CompletionKind Kind { get; set; }
+ public string ScriptFile { get; set; }
+
+ public new const string Id = "CodeCompletion";
+
+ public CodeCompletionRequest() : base(Id)
+ {
+ }
+ }
+
+ public sealed class CodeCompletionResponse : Response
+ {
+ public CodeCompletionRequest.CompletionKind Kind;
+ public string ScriptFile { get; set; }
+ public string[] Suggestions { get; set; }
+ }
+
+ public sealed class PlayRequest : Request
+ {
+ public new const string Id = "Play";
+
+ public PlayRequest() : base(Id)
+ {
+ }
+ }
+
+ public sealed class PlayResponse : Response
+ {
+ }
+
+ public sealed class StopPlayRequest : Request
+ {
+ public new const string Id = "StopPlay";
+
+ public StopPlayRequest() : base(Id)
+ {
+ }
+ }
+
+ public sealed class StopPlayResponse : Response
+ {
+ }
+
+ public sealed class DebugPlayRequest : Request
+ {
+ public string DebuggerHost { get; set; }
+ public int DebuggerPort { get; set; }
+ public bool? BuildBeforePlaying { get; set; }
+
+ public new const string Id = "DebugPlay";
+
+ public DebugPlayRequest() : base(Id)
+ {
+ }
+ }
+
+ public sealed class DebugPlayResponse : Response
+ {
+ }
+
+ public sealed class OpenFileRequest : Request
+ {
+ public string File { get; set; }
+ public int? Line { get; set; }
+ public int? Column { get; set; }
+
+ public new const string Id = "OpenFile";
+
+ public OpenFileRequest() : base(Id)
+ {
+ }
+ }
+
+ public sealed class OpenFileResponse : Response
+ {
+ }
+
+ public sealed class ReloadScriptsRequest : Request
+ {
+ public new const string Id = "ReloadScripts";
+
+ public ReloadScriptsRequest() : base(Id)
+ {
+ }
+ }
+
+ public sealed class ReloadScriptsResponse : Response
+ {
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs
new file mode 100644
index 0000000000..548e7f06ee
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs
@@ -0,0 +1,23 @@
+using GodotTools.IdeMessaging.Requests;
+using GodotTools.IdeMessaging.Utils;
+using Newtonsoft.Json;
+
+namespace GodotTools.IdeMessaging
+{
+ public abstract class ResponseAwaiter : NotifyAwaiter<Response>
+ {
+ public abstract void SetResult(MessageContent content);
+ }
+
+ public class ResponseAwaiter<T> : ResponseAwaiter
+ where T : Response, new()
+ {
+ public override void SetResult(MessageContent content)
+ {
+ if (content.Status == MessageStatus.Ok)
+ SetResult(JsonConvert.DeserializeObject<T>(content.Body));
+ else
+ SetResult(new T {Status = content.Status});
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs
index 700b786752..d84a63c83c 100644
--- a/modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs
@@ -1,9 +1,9 @@
using System;
using System.Runtime.CompilerServices;
-namespace GodotTools.Utils
+namespace GodotTools.IdeMessaging.Utils
{
- public sealed class NotifyAwaiter<T> : INotifyCompletion
+ public class NotifyAwaiter<T> : INotifyCompletion
{
private Action continuation;
private Exception exception;
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/SemaphoreExtensions.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/SemaphoreExtensions.cs
new file mode 100644
index 0000000000..9d593fbf8a
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/SemaphoreExtensions.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GodotTools.IdeMessaging.Utils
+{
+ public static class SemaphoreExtensions
+ {
+ public static ConfiguredTaskAwaitable<IDisposable> UseAsync(this SemaphoreSlim semaphoreSlim, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ var wrapper = new SemaphoreSlimWaitReleaseWrapper(semaphoreSlim, out Task waitAsyncTask, cancellationToken);
+ return waitAsyncTask.ContinueWith<IDisposable>(t => wrapper, cancellationToken).ConfigureAwait(false);
+ }
+
+ private struct SemaphoreSlimWaitReleaseWrapper : IDisposable
+ {
+ private readonly SemaphoreSlim semaphoreSlim;
+
+ public SemaphoreSlimWaitReleaseWrapper(SemaphoreSlim semaphoreSlim, out Task waitAsyncTask, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ this.semaphoreSlim = semaphoreSlim;
+ waitAsyncTask = this.semaphoreSlim.WaitAsync(cancellationToken);
+ }
+
+ public void Dispose()
+ {
+ semaphoreSlim.Release();
+ }
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj
new file mode 100644
index 0000000000..5b3ed0b1b7
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <ProjectGuid>{EAFFF236-FA96-4A4D-BD23-0E51EF988277}</ProjectGuid>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net472</TargetFramework>
+ <LangVersion>7.2</LangVersion>
+ </PropertyGroup>
+ <ItemGroup>
+ <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
+ <PackageReference Include="EnvDTE" Version="8.0.2" />
+ </ItemGroup>
+</Project>
diff --git a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs
new file mode 100644
index 0000000000..affb2a47e7
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs
@@ -0,0 +1,270 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
+using System.Text.RegularExpressions;
+using EnvDTE;
+
+namespace GodotTools.OpenVisualStudio
+{
+ internal static class Program
+ {
+ [DllImport("ole32.dll")]
+ private static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable pprot);
+
+ [DllImport("ole32.dll")]
+ private static extern void CreateBindCtx(int reserved, out IBindCtx ppbc);
+
+ [DllImport("user32.dll")]
+ private static extern bool SetForegroundWindow(IntPtr hWnd);
+
+ private static void ShowHelp()
+ {
+ Console.WriteLine("Opens the file(s) in a Visual Studio instance that is editing the specified solution.");
+ Console.WriteLine("If an existing instance for the solution is not found, a new one is created.");
+ Console.WriteLine();
+ Console.WriteLine("Usage:");
+ Console.WriteLine(@" GodotTools.OpenVisualStudio.exe solution [file[;line[;col]]...]");
+ Console.WriteLine();
+ Console.WriteLine("Lines and columns begin at one. Zero or lower will result in an error.");
+ Console.WriteLine("If a line is specified but a column is not, the line is selected in the text editor.");
+ }
+
+ // STAThread needed, otherwise CoRegisterMessageFilter may return CO_E_NOT_SUPPORTED.
+ [STAThread]
+ private static int Main(string[] args)
+ {
+ if (args.Length == 0 || args[0] == "--help" || args[0] == "-h")
+ {
+ ShowHelp();
+ return 0;
+ }
+
+ string solutionFile = NormalizePath(args[0]);
+
+ var dte = FindInstanceEditingSolution(solutionFile);
+
+ if (dte == null)
+ {
+ // Open a new instance
+
+ var visualStudioDteType = Type.GetTypeFromProgID("VisualStudio.DTE.16.0", throwOnError: true);
+ dte = (DTE)Activator.CreateInstance(visualStudioDteType);
+
+ dte.UserControl = true;
+
+ try
+ {
+ dte.Solution.Open(solutionFile);
+ }
+ catch (ArgumentException)
+ {
+ Console.Error.WriteLine("Solution.Open: Invalid path or file not found");
+ return 1;
+ }
+
+ dte.MainWindow.Visible = true;
+ }
+
+ MessageFilter.Register();
+
+ try
+ {
+ // Open files
+
+ for (int i = 1; i < args.Length; i++)
+ {
+ // Both the line number and the column begin at one
+
+ string[] fileArgumentParts = args[i].Split(';');
+
+ string filePath = NormalizePath(fileArgumentParts[0]);
+
+ try
+ {
+ dte.ItemOperations.OpenFile(filePath);
+ }
+ catch (ArgumentException)
+ {
+ Console.Error.WriteLine("ItemOperations.OpenFile: Invalid path or file not found");
+ return 1;
+ }
+
+ if (fileArgumentParts.Length > 1)
+ {
+ if (int.TryParse(fileArgumentParts[1], out int line))
+ {
+ var textSelection = (TextSelection)dte.ActiveDocument.Selection;
+
+ if (fileArgumentParts.Length > 2)
+ {
+ if (int.TryParse(fileArgumentParts[2], out int column))
+ {
+ textSelection.MoveToLineAndOffset(line, column);
+ }
+ else
+ {
+ Console.Error.WriteLine("The column part of the argument must be a valid integer");
+ return 1;
+ }
+ }
+ else
+ {
+ textSelection.GotoLine(line, Select: true);
+ }
+ }
+ else
+ {
+ Console.Error.WriteLine("The line part of the argument must be a valid integer");
+ return 1;
+ }
+ }
+ }
+ }
+ finally
+ {
+ var mainWindow = dte.MainWindow;
+ mainWindow.Activate();
+ SetForegroundWindow(new IntPtr(mainWindow.HWnd));
+
+ MessageFilter.Revoke();
+ }
+
+ return 0;
+ }
+
+ private static DTE FindInstanceEditingSolution(string solutionPath)
+ {
+ if (GetRunningObjectTable(0, out IRunningObjectTable pprot) != 0)
+ return null;
+
+ try
+ {
+ pprot.EnumRunning(out IEnumMoniker ppenumMoniker);
+ ppenumMoniker.Reset();
+
+ var moniker = new IMoniker[1];
+
+ while (ppenumMoniker.Next(1, moniker, IntPtr.Zero) == 0)
+ {
+ string ppszDisplayName;
+
+ CreateBindCtx(0, out IBindCtx ppbc);
+
+ try
+ {
+ moniker[0].GetDisplayName(ppbc, null, out ppszDisplayName);
+ }
+ finally
+ {
+ Marshal.ReleaseComObject(ppbc);
+ }
+
+ if (ppszDisplayName == null)
+ continue;
+
+ // The digits after the colon are the process ID
+ if (!Regex.IsMatch(ppszDisplayName, "!VisualStudio.DTE.16.0:[0-9]"))
+ continue;
+
+ if (pprot.GetObject(moniker[0], out object ppunkObject) == 0)
+ {
+ if (ppunkObject is DTE dte && dte.Solution.FullName.Length > 0)
+ {
+ if (NormalizePath(dte.Solution.FullName) == solutionPath)
+ return dte;
+ }
+ }
+ }
+ }
+ finally
+ {
+ Marshal.ReleaseComObject(pprot);
+ }
+
+ return null;
+ }
+
+ static string NormalizePath(string path)
+ {
+ return new Uri(Path.GetFullPath(path)).LocalPath
+ .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
+ .ToUpperInvariant();
+ }
+
+ #region MessageFilter. See: http: //msdn.microsoft.com/en-us/library/ms228772.aspx
+
+ private class MessageFilter : IOleMessageFilter
+ {
+ // Class containing the IOleMessageFilter
+ // thread error-handling functions
+
+ private static IOleMessageFilter _oldFilter;
+
+ // Start the filter
+ public static void Register()
+ {
+ IOleMessageFilter newFilter = new MessageFilter();
+ int ret = CoRegisterMessageFilter(newFilter, out _oldFilter);
+ if (ret != 0)
+ Console.Error.WriteLine($"CoRegisterMessageFilter failed with error code: {ret}");
+ }
+
+ // Done with the filter, close it
+ public static void Revoke()
+ {
+ int ret = CoRegisterMessageFilter(_oldFilter, out _);
+ if (ret != 0)
+ Console.Error.WriteLine($"CoRegisterMessageFilter failed with error code: {ret}");
+ }
+
+ //
+ // IOleMessageFilter functions
+ // Handle incoming thread requests
+ int IOleMessageFilter.HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo)
+ {
+ // Return the flag SERVERCALL_ISHANDLED
+ return 0;
+ }
+
+ // Thread call was rejected, so try again.
+ int IOleMessageFilter.RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType)
+ {
+ if (dwRejectType == 2)
+ // flag = SERVERCALL_RETRYLATER
+ {
+ // Retry the thread call immediately if return >= 0 & < 100
+ return 99;
+ }
+
+ // Too busy; cancel call
+ return -1;
+ }
+
+ int IOleMessageFilter.MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType)
+ {
+ // Return the flag PENDINGMSG_WAITDEFPROCESS
+ return 2;
+ }
+
+ // Implement the IOleMessageFilter interface
+ [DllImport("ole32.dll")]
+ private static extern int CoRegisterMessageFilter(IOleMessageFilter newFilter, out IOleMessageFilter oldFilter);
+ }
+
+ [ComImport(), Guid("00000016-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ private interface IOleMessageFilter
+ {
+ [PreserveSig]
+ int HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo);
+
+ [PreserveSig]
+ int RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType);
+
+ [PreserveSig]
+ int MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType);
+ }
+
+ #endregion
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/DotNetSolution.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/DotNetSolution.cs
index 9afd9adeb1..6f318aab4a 100644
--- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/DotNetSolution.cs
+++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/DotNetSolution.cs
@@ -153,7 +153,12 @@ EndProject";
var result = regex.Replace(input,m => dict[m.Value]);
if (result != input)
+ {
+ // Save a copy of the solution before replacing it
+ FileUtils.SaveBackupCopy(slnPath);
+
File.WriteAllText(slnPath, result);
+ }
}
}
}
diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj
index b60e501beb..9cb50014b0 100644
--- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj
+++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj
@@ -1,57 +1,23 @@
-<?xml version="1.0" encoding="utf-8"?>
-<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
- <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}</ProjectGuid>
- <OutputType>Library</OutputType>
- <RootNamespace>GodotTools.ProjectEditor</RootNamespace>
- <AssemblyName>GodotTools.ProjectEditor</AssemblyName>
- <TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
- <BaseIntermediateOutputPath>obj</BaseIntermediateOutputPath>
- <LangVersion>7</LangVersion>
+ <TargetFramework>net472</TargetFramework>
+ <LangVersion>7.2</LangVersion>
</PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
- <DebugSymbols>true</DebugSymbols>
- <DebugType>portable</DebugType>
- <Optimize>false</Optimize>
- <OutputPath>bin\Debug</OutputPath>
- <DefineConstants>DEBUG;</DefineConstants>
- <ErrorReport>prompt</ErrorReport>
- <WarningLevel>4</WarningLevel>
- <ConsolePause>false</ConsolePause>
- </PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
- <Optimize>true</Optimize>
- <OutputPath>bin\Release</OutputPath>
- <ErrorReport>prompt</ErrorReport>
- <WarningLevel>4</WarningLevel>
- <ConsolePause>false</ConsolePause>
- </PropertyGroup>
- <ItemGroup>
- <Reference Include="System" />
- <Reference Include="Microsoft.Build" />
- <Reference Include="DotNet.Glob, Version=2.1.1.0, Culture=neutral, PublicKeyToken=b68cc888b4f632d1, processorArchitecture=MSIL">
- <HintPath>$(SolutionDir)\packages\DotNet.Glob.2.1.1\lib\net45\DotNet.Glob.dll</HintPath>
- </Reference>
- </ItemGroup>
<ItemGroup>
- <Compile Include="ApiAssembliesInfo.cs" />
- <Compile Include="DotNetSolution.cs" />
- <Compile Include="Properties\AssemblyInfo.cs" />
- <Compile Include="IdentifierUtils.cs" />
- <Compile Include="ProjectExtensions.cs" />
- <Compile Include="ProjectGenerator.cs" />
- <Compile Include="ProjectUtils.cs" />
+ <PackageReference Include="Microsoft.Build" Version="16.5.0" />
+ <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
- <None Include="packages.config" />
+ <ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj" />
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj">
- <Project>{639E48BD-44E5-4091-8EDD-22D36DC0768D}</Project>
- <Name>GodotTools.Core</Name>
- </ProjectReference>
+ <!--
+ The Microsoft.Build.Runtime package is too problematic so we create a MSBuild.exe stub. The workaround described
+ here doesn't work with Microsoft.NETFramework.ReferenceAssemblies: https://github.com/microsoft/msbuild/issues/3486
+ We need a MSBuild.exe file as there's an issue in Microsoft.Build where it executes platform dependent code when
+ searching for MSBuild.exe before the fallback to not using it. A stub is fine as it should never be executed.
+ -->
+ <None Include="MSBuild.exe" CopyToOutputDirectory="Always" />
</ItemGroup>
- <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
</Project>
diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/MSBuild.exe b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/MSBuild.exe
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/MSBuild.exe
diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs
index f0e0d1b33d..704f2ec194 100644
--- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs
+++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs
@@ -2,8 +2,8 @@ using GodotTools.Core;
using System;
using System.Collections.Generic;
using System.IO;
-using DotNet.Globbing;
using Microsoft.Build.Construction;
+using Microsoft.Build.Globbing;
namespace GodotTools.ProjectEditor
{
@@ -11,8 +11,6 @@ namespace GodotTools.ProjectEditor
{
public static ProjectItemElement FindItemOrNull(this ProjectRootElement root, string itemType, string include, bool noCondition = false)
{
- GlobOptions globOptions = new GlobOptions {Evaluation = {CaseInsensitive = false}};
-
string normalizedInclude = include.NormalizePath();
foreach (var itemGroup in root.ItemGroups)
@@ -25,7 +23,8 @@ namespace GodotTools.ProjectEditor
if (item.ItemType != itemType)
continue;
- var glob = Glob.Parse(item.Include.NormalizePath(), globOptions);
+ //var glob = Glob.Parse(item.Include.NormalizePath(), globOptions);
+ var glob = MSBuildGlob.Parse(item.Include.NormalizePath());
if (glob.IsMatch(normalizedInclude))
return item;
@@ -36,8 +35,6 @@ namespace GodotTools.ProjectEditor
}
public static ProjectItemElement FindItemOrNullAbs(this ProjectRootElement root, string itemType, string include, bool noCondition = false)
{
- GlobOptions globOptions = new GlobOptions {Evaluation = {CaseInsensitive = false}};
-
string normalizedInclude = Path.GetFullPath(include).NormalizePath();
foreach (var itemGroup in root.ItemGroups)
@@ -50,7 +47,7 @@ namespace GodotTools.ProjectEditor
if (item.ItemType != itemType)
continue;
- var glob = Glob.Parse(Path.GetFullPath(item.Include).NormalizePath(), globOptions);
+ var glob = MSBuildGlob.Parse(Path.GetFullPath(item.Include).NormalizePath());
if (glob.IsMatch(normalizedInclude))
return item;
diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs
index cbe3afaedd..679d5bb444 100644
--- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs
+++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs
@@ -12,6 +12,11 @@ namespace GodotTools.ProjectEditor
private const string CoreApiProjectName = "GodotSharp";
private const string EditorApiProjectName = "GodotSharpEditor";
+ public const string CSharpProjectTypeGuid = "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}";
+ public const string GodotProjectTypeGuid = "{8F3E2DF0-C35C-4265-82FC-BEA011F4A7ED}";
+
+ public static readonly string GodotDefaultProjectTypeGuids = $"{GodotProjectTypeGuid};{CSharpProjectTypeGuid}";
+
public static string GenGameProject(string dir, string name, IEnumerable<string> compileItems)
{
string path = Path.Combine(dir, name + ".csproj");
@@ -19,6 +24,7 @@ namespace GodotTools.ProjectEditor
ProjectPropertyGroupElement mainGroup;
var root = CreateLibraryProject(name, "Debug", out mainGroup);
+ mainGroup.SetProperty("ProjectTypeGuids", GodotDefaultProjectTypeGuids);
mainGroup.SetProperty("OutputPath", Path.Combine(".mono", "temp", "bin", "$(Configuration)"));
mainGroup.SetProperty("BaseIntermediateOutputPath", Path.Combine(".mono", "temp", "obj"));
mainGroup.SetProperty("IntermediateOutputPath", Path.Combine("$(BaseIntermediateOutputPath)", "$(Configuration)"));
@@ -125,6 +131,12 @@ namespace GodotTools.ProjectEditor
// References
var referenceGroup = root.AddItemGroup();
referenceGroup.AddItem("Reference", "System");
+ var frameworkRefAssembliesItem = referenceGroup.AddItem("PackageReference", "Microsoft.NETFramework.ReferenceAssemblies");
+
+ // Use metadata (child nodes) instead of attributes for the PackageReference.
+ // This is for compatibility with 3.2, where GodotTools uses an old Microsoft.Build.
+ frameworkRefAssembliesItem.AddMetadata("Version", "1.0.0");
+ frameworkRefAssembliesItem.AddMetadata("PrivateAssets", "All");
root.AddImport(Path.Combine("$(MSBuildBinPath)", "Microsoft.CSharp.targets").Replace("/", "\\"));
diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs
index 1776b46e6a..8774b4ee31 100644
--- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs
+++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs
@@ -4,13 +4,33 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
-using DotNet.Globbing;
using Microsoft.Build.Construction;
+using Microsoft.Build.Globbing;
namespace GodotTools.ProjectEditor
{
+ public sealed class MSBuildProject
+ {
+ public ProjectRootElement Root { get; }
+
+ public bool HasUnsavedChanges { get; set; }
+
+ public void Save() => Root.Save();
+
+ public MSBuildProject(ProjectRootElement root)
+ {
+ Root = root;
+ }
+ }
+
public static class ProjectUtils
{
+ public static MSBuildProject Open(string path)
+ {
+ var root = ProjectRootElement.Open(path);
+ return root != null ? new MSBuildProject(root) : null;
+ }
+
public static void AddItemToProjectChecked(string projectPath, string itemType, string include)
{
var dir = Directory.GetParent(projectPath).FullName;
@@ -43,7 +63,6 @@ namespace GodotTools.ProjectEditor
public static void RemoveItemFromProjectChecked(string projectPath, string itemType, string include)
{
- var dir = Directory.GetParent(projectPath).FullName;
var root = ProjectRootElement.Open(projectPath);
Debug.Assert(root != null);
@@ -114,9 +133,6 @@ namespace GodotTools.ProjectEditor
var result = new List<string>();
var existingFiles = GetAllFilesRecursive(Path.GetDirectoryName(projectPath), "*.cs");
- var globOptions = new GlobOptions();
- globOptions.Evaluation.CaseInsensitive = false;
-
var root = ProjectRootElement.Open(projectPath);
Debug.Assert(root != null);
@@ -132,7 +148,7 @@ namespace GodotTools.ProjectEditor
string normalizedInclude = item.Include.NormalizePath();
- var glob = Glob.Parse(normalizedInclude, globOptions);
+ var glob = MSBuildGlob.Parse(normalizedInclude);
// TODO Check somehow if path has no blob to avoid the following loop...
@@ -149,18 +165,30 @@ namespace GodotTools.ProjectEditor
return result.ToArray();
}
- /// Simple function to make sure the Api assembly references are configured correctly
- public static void FixApiHintPath(string projectPath)
+ public static void EnsureHasProjectTypeGuids(MSBuildProject project)
{
- var root = ProjectRootElement.Open(projectPath);
- Debug.Assert(root != null);
+ var root = project.Root;
- bool dirty = false;
+ bool found = root.PropertyGroups.Any(pg =>
+ string.IsNullOrEmpty(pg.Condition) && pg.Properties.Any(p => p.Name == "ProjectTypeGuids"));
+
+ if (found)
+ return;
+
+ root.AddProperty("ProjectTypeGuids", ProjectGenerator.GodotDefaultProjectTypeGuids);
+
+ project.HasUnsavedChanges = true;
+ }
+
+ /// Simple function to make sure the Api assembly references are configured correctly
+ public static void FixApiHintPath(MSBuildProject project)
+ {
+ var root = project.Root;
void AddPropertyIfNotPresent(string name, string condition, string value)
{
if (root.PropertyGroups
- .Any(g => (g.Condition == string.Empty || g.Condition.Trim() == condition) &&
+ .Any(g => (string.IsNullOrEmpty(g.Condition) || g.Condition.Trim() == condition) &&
g.Properties
.Any(p => p.Name == name &&
p.Value == value &&
@@ -170,7 +198,7 @@ namespace GodotTools.ProjectEditor
}
root.AddProperty(name, value).Condition = " " + condition + " ";
- dirty = true;
+ project.HasUnsavedChanges = true;
}
AddPropertyIfNotPresent(name: "ApiConfiguration",
@@ -212,7 +240,7 @@ namespace GodotTools.ProjectEditor
}
referenceWithHintPath.AddMetadata("HintPath", hintPath);
- dirty = true;
+ project.HasUnsavedChanges = true;
return;
}
@@ -221,14 +249,14 @@ namespace GodotTools.ProjectEditor
{
// Found a Reference item without a HintPath
referenceWithoutHintPath.AddMetadata("HintPath", hintPath);
- dirty = true;
+ project.HasUnsavedChanges = true;
return;
}
}
// Found no Reference item at all. Add it.
root.AddItem("Reference", referenceName).Condition = " " + condition + " ";
- dirty = true;
+ project.HasUnsavedChanges = true;
}
const string coreProjectName = "GodotSharp";
@@ -242,22 +270,16 @@ namespace GodotTools.ProjectEditor
SetReferenceHintPath(coreProjectName, coreCondition, coreHintPath);
SetReferenceHintPath(editorProjectName, editorCondition, editorHintPath);
-
- if (dirty)
- root.Save();
}
- public static void MigrateFromOldConfigNames(string projectPath)
+ public static void MigrateFromOldConfigNames(MSBuildProject project)
{
- var root = ProjectRootElement.Open(projectPath);
- Debug.Assert(root != null);
-
- bool dirty = false;
+ var root = project.Root;
bool hasGodotProjectGeneratorVersion = false;
bool foundOldConfiguration = false;
- foreach (var propertyGroup in root.PropertyGroups.Where(g => g.Condition == string.Empty))
+ foreach (var propertyGroup in root.PropertyGroups.Where(g => string.IsNullOrEmpty(g.Condition)))
{
if (!hasGodotProjectGeneratorVersion && propertyGroup.Properties.Any(p => p.Name == "GodotProjectGeneratorVersion"))
hasGodotProjectGeneratorVersion = true;
@@ -267,15 +289,15 @@ namespace GodotTools.ProjectEditor
{
configItem.Value = "Debug";
foundOldConfiguration = true;
- dirty = true;
+ project.HasUnsavedChanges = true;
}
}
if (!hasGodotProjectGeneratorVersion)
{
- root.PropertyGroups.First(g => g.Condition == string.Empty)?
+ root.PropertyGroups.First(g => string.IsNullOrEmpty(g.Condition))?
.AddProperty("GodotProjectGeneratorVersion", Assembly.GetExecutingAssembly().GetName().Version.ToString());
- dirty = true;
+ project.HasUnsavedChanges = true;
}
if (!foundOldConfiguration)
@@ -301,7 +323,7 @@ namespace GodotTools.ProjectEditor
foreach (var propertyGroup in root.PropertyGroups.Where(g => g.Condition.Trim() == oldCondition))
{
propertyGroup.Condition = " " + newCondition + " ";
- dirty = true;
+ project.HasUnsavedChanges = true;
}
foreach (var propertyGroup in root.PropertyGroups)
@@ -309,14 +331,14 @@ namespace GodotTools.ProjectEditor
foreach (var prop in propertyGroup.Properties.Where(p => p.Condition.Trim() == oldCondition))
{
prop.Condition = " " + newCondition + " ";
- dirty = true;
+ project.HasUnsavedChanges = true;
}
}
foreach (var itemGroup in root.ItemGroups.Where(g => g.Condition.Trim() == oldCondition))
{
itemGroup.Condition = " " + newCondition + " ";
- dirty = true;
+ project.HasUnsavedChanges = true;
}
foreach (var itemGroup in root.ItemGroups)
@@ -324,7 +346,7 @@ namespace GodotTools.ProjectEditor
foreach (var item in itemGroup.Items.Where(item => item.Condition.Trim() == oldCondition))
{
item.Condition = " " + newCondition + " ";
- dirty = true;
+ project.HasUnsavedChanges = true;
}
}
}
@@ -340,10 +362,26 @@ namespace GodotTools.ProjectEditor
MigrateConfigurationConditions("Release", "ExportRelease");
MigrateConfigurationConditions("Tools", "Debug"); // Must be last
}
+ }
+ public static void EnsureHasNugetNetFrameworkRefAssemblies(MSBuildProject project)
+ {
+ var root = project.Root;
- if (dirty)
- root.Save();
+ bool found = root.ItemGroups.Any(g => string.IsNullOrEmpty(g.Condition) && g.Items.Any(
+ item => item.ItemType == "PackageReference" && item.Include == "Microsoft.NETFramework.ReferenceAssemblies"));
+
+ if (found)
+ return;
+
+ var frameworkRefAssembliesItem = root.AddItem("PackageReference", "Microsoft.NETFramework.ReferenceAssemblies");
+
+ // Use metadata (child nodes) instead of attributes for the PackageReference.
+ // This is for compatibility with 3.2, where GodotTools uses an old Microsoft.Build.
+ frameworkRefAssembliesItem.AddMetadata("Version", "1.0.0");
+ frameworkRefAssembliesItem.AddMetadata("PrivateAssets", "All");
+
+ project.HasUnsavedChanges = true;
}
}
}
diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/Properties/AssemblyInfo.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/Properties/AssemblyInfo.cs
deleted file mode 100644
index 3a0464c9bc..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-
-// Information about this assembly is defined by the following attributes.
-// Change them to the values specific to your project.
-
-[assembly: AssemblyTitle("GodotTools.ProjectEditor")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("")]
-[assembly: AssemblyCopyright("Godot Engine contributors")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
-// The form "{Major}.{Minor}.*" will automatically update the build and revision,
-// and "{Major}.{Minor}.{Build}.*" will update just the revision.
-
-[assembly: AssemblyVersion("1.0.*")]
-
-// The following attributes are used to specify the signing key for the assembly,
-// if desired. See the Mono documentation for more information about signing.
-
-//[assembly: AssemblyDelaySign(false)]
-//[assembly: AssemblyKeyFile("")]
-
diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/packages.config b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/packages.config
deleted file mode 100644
index 2db030f9d8..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/packages.config
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<packages>
- <package id="DotNet.Glob" version="2.1.1" targetFramework="net45" />
-</packages>
diff --git a/modules/mono/editor/GodotTools/GodotTools.sln b/modules/mono/editor/GodotTools/GodotTools.sln
index a3438ea5f3..ba5379e562 100644
--- a/modules/mono/editor/GodotTools/GodotTools.sln
+++ b/modules/mono/editor/GodotTools/GodotTools.sln
@@ -9,7 +9,9 @@ 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}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.IdeMessaging", "GodotTools.IdeMessaging\GodotTools.IdeMessaging.csproj", "{92600954-25F0-4291-8E11-1FEE9FC4BE20}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.OpenVisualStudio", "GodotTools.OpenVisualStudio\GodotTools.OpenVisualStudio.csproj", "{EAFFF236-FA96-4A4D-BD23-0E51EF988277}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -37,5 +39,9 @@ Global
{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
+ {EAFFF236-FA96-4A4D-BD23-0E51EF988277}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EAFFF236-FA96-4A4D-BD23-0E51EF988277}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EAFFF236-FA96-4A4D-BD23-0E51EF988277}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EAFFF236-FA96-4A4D-BD23-0E51EF988277}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs
index 43c96d2e30..e55558c100 100644
--- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs
@@ -14,16 +14,6 @@ namespace GodotTools.Build
{
public static class BuildSystem
{
- private static string GetMsBuildPath()
- {
- string msbuildPath = MsBuildFinder.FindMsBuild();
-
- if (msbuildPath == null)
- throw new FileNotFoundException("Cannot find the MSBuild executable.");
-
- return msbuildPath;
- }
-
private static string MonoWindowsBinDir
{
get
@@ -46,8 +36,8 @@ namespace GodotTools.Build
{
if (OS.IsWindows)
{
- return (BuildManager.BuildTool)EditorSettings.GetSetting("mono/builds/build_tool")
- == BuildManager.BuildTool.MsBuildMono;
+ return (BuildTool)EditorSettings.GetSetting("mono/builds/build_tool")
+ == BuildTool.MsBuildMono;
}
return false;
@@ -57,16 +47,21 @@ namespace GodotTools.Build
private static bool PrintBuildOutput =>
(bool)EditorSettings.GetSetting("mono/builds/print_build_output");
- private static Process LaunchBuild(string solution, string config, string loggerOutputDir, IEnumerable<string> customProperties = null)
+ private static Process LaunchBuild(string solution, IEnumerable<string> targets, string config, string loggerOutputDir, IEnumerable<string> customProperties = null)
{
+ (string msbuildPath, BuildTool buildTool) = MsBuildFinder.FindMsBuild();
+
+ if (msbuildPath == null)
+ throw new FileNotFoundException("Cannot find the MSBuild executable.");
+
var customPropertiesList = new List<string>();
if (customProperties != null)
customPropertiesList.AddRange(customProperties);
- string compilerArgs = BuildArguments(solution, config, loggerOutputDir, customPropertiesList);
+ string compilerArgs = BuildArguments(buildTool, solution, targets, config, loggerOutputDir, customPropertiesList);
- var startInfo = new ProcessStartInfo(GetMsBuildPath(), compilerArgs);
+ var startInfo = new ProcessStartInfo(msbuildPath, compilerArgs);
bool redirectOutput = !IsDebugMsBuildRequested() && !PrintBuildOutput;
@@ -90,7 +85,7 @@ namespace GodotTools.Build
// Needed when running from Developer Command Prompt for VS
RemovePlatformVariable(startInfo.EnvironmentVariables);
- var process = new Process { StartInfo = startInfo };
+ var process = new Process {StartInfo = startInfo};
process.Start();
@@ -105,19 +100,19 @@ namespace GodotTools.Build
public static int Build(BuildInfo buildInfo)
{
- return Build(buildInfo.Solution, buildInfo.Configuration,
+ return Build(buildInfo.Solution, buildInfo.Targets, buildInfo.Configuration,
buildInfo.LogsDirPath, buildInfo.CustomProperties);
}
- public static async Task<int> BuildAsync(BuildInfo buildInfo)
+ public static Task<int> BuildAsync(BuildInfo buildInfo)
{
- return await BuildAsync(buildInfo.Solution, buildInfo.Configuration,
+ return BuildAsync(buildInfo.Solution, buildInfo.Targets, buildInfo.Configuration,
buildInfo.LogsDirPath, buildInfo.CustomProperties);
}
- public static int Build(string solution, string config, string loggerOutputDir, IEnumerable<string> customProperties = null)
+ public static int Build(string solution, string[] targets, string config, string loggerOutputDir, IEnumerable<string> customProperties = null)
{
- using (var process = LaunchBuild(solution, config, loggerOutputDir, customProperties))
+ using (var process = LaunchBuild(solution, targets, config, loggerOutputDir, customProperties))
{
process.WaitForExit();
@@ -125,9 +120,9 @@ namespace GodotTools.Build
}
}
- public static async Task<int> BuildAsync(string solution, string config, string loggerOutputDir, IEnumerable<string> customProperties = null)
+ public static async Task<int> BuildAsync(string solution, IEnumerable<string> targets, string config, string loggerOutputDir, IEnumerable<string> customProperties = null)
{
- using (var process = LaunchBuild(solution, config, loggerOutputDir, customProperties))
+ using (var process = LaunchBuild(solution, targets, config, loggerOutputDir, customProperties))
{
await process.WaitForExitAsync();
@@ -135,10 +130,15 @@ namespace GodotTools.Build
}
}
- private static string BuildArguments(string solution, string config, string loggerOutputDir, List<string> customProperties)
+ private static string BuildArguments(BuildTool buildTool, string solution, IEnumerable<string> targets, string config, string loggerOutputDir, IEnumerable<string> customProperties)
{
- string arguments = $@"""{solution}"" /v:normal /t:Build ""/p:{"Configuration=" + config}"" " +
- $@"""/l:{typeof(GodotBuildLogger).FullName},{GodotBuildLogger.AssemblyPath};{loggerOutputDir}""";
+ string arguments = string.Empty;
+
+ if (buildTool == BuildTool.DotnetCli)
+ arguments += "msbuild "; // `dotnet msbuild` command
+
+ arguments += $@"""{solution}"" /v:normal /t:{string.Join(",", targets)} ""/p:{"Configuration=" + config}"" " +
+ $@"""/l:{typeof(GodotBuildLogger).FullName},{GodotBuildLogger.AssemblyPath};{loggerOutputDir}""";
foreach (string customProperty in customProperties)
{
diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildTool.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildTool.cs
new file mode 100644
index 0000000000..a1a69334e3
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildTool.cs
@@ -0,0 +1,10 @@
+namespace GodotTools.Build
+{
+ public enum BuildTool
+ {
+ MsBuildMono,
+ MsBuildVs,
+ JetBrainsMsBuild,
+ DotnetCli
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs b/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs
index af8d070cbd..f36e581a5f 100644
--- a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs
@@ -17,75 +17,96 @@ namespace GodotTools.Build
private static string _msbuildToolsPath = string.Empty;
private static string _msbuildUnixPath = string.Empty;
- public static string FindMsBuild()
+ public static (string, BuildTool) FindMsBuild()
{
var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
- var buildTool = (BuildManager.BuildTool)editorSettings.GetSetting("mono/builds/build_tool");
+ var buildTool = (BuildTool)editorSettings.GetSetting("mono/builds/build_tool");
if (OS.IsWindows)
{
switch (buildTool)
{
- case BuildManager.BuildTool.MsBuildVs:
+ case BuildTool.DotnetCli:
{
- if (_msbuildToolsPath.Empty() || !File.Exists(_msbuildToolsPath))
+ string dotnetCliPath = OS.PathWhich("dotnet");
+ if (!string.IsNullOrEmpty(dotnetCliPath))
+ return (dotnetCliPath, BuildTool.DotnetCli);
+ GD.PushError("Cannot find dotnet CLI executable. Fallback to MSBuild from Visual Studio.");
+ goto case BuildTool.MsBuildVs;
+ }
+ case BuildTool.MsBuildVs:
+ {
+ if (string.IsNullOrEmpty(_msbuildToolsPath) || !File.Exists(_msbuildToolsPath))
{
// Try to search it again if it wasn't found last time or if it was removed from its location
_msbuildToolsPath = FindMsBuildToolsPathOnWindows();
- if (_msbuildToolsPath.Empty())
- {
- throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMsbuildVs}'.");
- }
+ if (string.IsNullOrEmpty(_msbuildToolsPath))
+ throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildVs}'.");
}
if (!_msbuildToolsPath.EndsWith("\\"))
_msbuildToolsPath += "\\";
- return Path.Combine(_msbuildToolsPath, "MSBuild.exe");
+ return (Path.Combine(_msbuildToolsPath, "MSBuild.exe"), BuildTool.MsBuildVs);
}
- case BuildManager.BuildTool.MsBuildMono:
+ case BuildTool.MsBuildMono:
{
string msbuildPath = Path.Combine(Internal.MonoWindowsInstallRoot, "bin", "msbuild.bat");
if (!File.Exists(msbuildPath))
- {
- throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMsbuildMono}'. Tried with path: {msbuildPath}");
- }
+ throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildMono}'. Tried with path: {msbuildPath}");
- return msbuildPath;
+ return (msbuildPath, BuildTool.MsBuildMono);
}
- case BuildManager.BuildTool.JetBrainsMsBuild:
+ case BuildTool.JetBrainsMsBuild:
+ {
var editorPath = (string)editorSettings.GetSetting(RiderPathManager.EditorPathSettingName);
+
if (!File.Exists(editorPath))
throw new FileNotFoundException($"Cannot find Rider executable. Tried with path: {editorPath}");
- var riderDir = new FileInfo(editorPath).Directory.Parent;
- return Path.Combine(riderDir.FullName, @"tools\MSBuild\Current\Bin\MSBuild.exe");
+
+ var riderDir = new FileInfo(editorPath).Directory?.Parent;
+
+ string msbuildPath = Path.Combine(riderDir.FullName, @"tools\MSBuild\Current\Bin\MSBuild.exe");
+
+ if (!File.Exists(msbuildPath))
+ throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildJetBrains}'. Tried with path: {msbuildPath}");
+
+ return (msbuildPath, BuildTool.JetBrainsMsBuild);
+ }
default:
throw new IndexOutOfRangeException("Invalid build tool in editor settings");
}
}
- if (OS.IsUnixLike())
+ if (OS.IsUnixLike)
{
- if (buildTool == BuildManager.BuildTool.MsBuildMono)
+ switch (buildTool)
{
- if (_msbuildUnixPath.Empty() || !File.Exists(_msbuildUnixPath))
+ case BuildTool.DotnetCli:
{
- // Try to search it again if it wasn't found last time or if it was removed from its location
- _msbuildUnixPath = FindBuildEngineOnUnix("msbuild");
+ string dotnetCliPath = OS.PathWhich("dotnet");
+ if (!string.IsNullOrEmpty(dotnetCliPath))
+ return (dotnetCliPath, BuildTool.DotnetCli);
+ GD.PushError("Cannot find dotnet CLI executable. Fallback to MSBuild from Mono.");
+ goto case BuildTool.MsBuildMono;
}
-
- if (_msbuildUnixPath.Empty())
+ case BuildTool.MsBuildMono:
{
- throw new FileNotFoundException($"Cannot find binary for '{BuildManager.PropNameMsbuildMono}'");
- }
+ if (string.IsNullOrEmpty(_msbuildUnixPath) || !File.Exists(_msbuildUnixPath))
+ {
+ // Try to search it again if it wasn't found last time or if it was removed from its location
+ _msbuildUnixPath = FindBuildEngineOnUnix("msbuild");
+ }
- return _msbuildUnixPath;
- }
- else
- {
- throw new IndexOutOfRangeException("Invalid build tool in editor settings");
+ if (string.IsNullOrEmpty(_msbuildUnixPath))
+ throw new FileNotFoundException($"Cannot find binary for '{BuildManager.PropNameMSBuildMono}'");
+
+ return (_msbuildUnixPath, BuildTool.MsBuildMono);
+ }
+ default:
+ throw new IndexOutOfRangeException("Invalid build tool in editor settings");
}
}
@@ -114,12 +135,12 @@ namespace GodotTools.Build
{
string ret = OS.PathWhich(name);
- if (!ret.Empty())
+ if (!string.IsNullOrEmpty(ret))
return ret;
string retFallback = OS.PathWhich($"{name}.exe");
- if (!retFallback.Empty())
+ if (!string.IsNullOrEmpty(retFallback))
return retFallback;
foreach (string hintDir in MsBuildHintDirs)
@@ -143,7 +164,7 @@ namespace GodotTools.Build
string vsWherePath = Environment.GetEnvironmentVariable(Internal.GodotIs32Bits() ? "ProgramFiles" : "ProgramFiles(x86)");
vsWherePath += "\\Microsoft Visual Studio\\Installer\\vswhere.exe";
- var vsWhereArgs = new[] { "-latest", "-products", "*", "-requires", "Microsoft.Component.MSBuild" };
+ var vsWhereArgs = new[] {"-latest", "-products", "*", "-requires", "Microsoft.Component.MSBuild"};
var outputArray = new Godot.Collections.Array<string>();
int exitCode = Godot.OS.Execute(vsWherePath, vsWhereArgs,
@@ -171,7 +192,7 @@ namespace GodotTools.Build
string value = line.Substring(sepIdx + 1).StripEdges();
- if (value.Empty())
+ if (string.IsNullOrEmpty(value))
throw new FormatException("installationPath value is empty");
if (!value.EndsWith("\\"))
diff --git a/modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs b/modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs
index 70bd552f2f..cca0983c01 100644
--- a/modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs
@@ -10,6 +10,7 @@ namespace GodotTools
public sealed class BuildInfo : Reference // TODO Remove Reference once we have proper serialization
{
public string Solution { get; }
+ public string[] Targets { get; }
public string Configuration { get; }
public Array<string> CustomProperties { get; } = new Array<string>(); // TODO Use List once we have proper serialization
@@ -38,9 +39,10 @@ namespace GodotTools
{
}
- public BuildInfo(string solution, string configuration)
+ public BuildInfo(string solution, string[] targets, string configuration)
{
Solution = solution;
+ Targets = targets;
Configuration = configuration;
}
}
diff --git a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs
index 520e665595..598787ba03 100644
--- a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs
@@ -15,20 +15,14 @@ namespace GodotTools
{
private static readonly List<BuildInfo> BuildsInProgress = new List<BuildInfo>();
- public const string PropNameMsbuildMono = "MSBuild (Mono)";
- public const string PropNameMsbuildVs = "MSBuild (VS Build Tools)";
- public const string PropNameMsbuildJetBrains = "MSBuild (JetBrains Rider)";
+ public const string PropNameMSBuildMono = "MSBuild (Mono)";
+ public const string PropNameMSBuildVs = "MSBuild (VS Build Tools)";
+ public const string PropNameMSBuildJetBrains = "MSBuild (JetBrains Rider)";
+ public const string PropNameDotnetCli = "dotnet CLI";
public const string MsBuildIssuesFileName = "msbuild_issues.csv";
public const string MsBuildLogFileName = "msbuild_log.txt";
- public enum BuildTool
- {
- MsBuildMono,
- MsBuildVs,
- JetBrainsMsBuild
- }
-
private static void RemoveOldIssuesFile(BuildInfo buildInfo)
{
var issuesFile = GetIssuesFilePath(buildInfo);
@@ -181,10 +175,12 @@ namespace GodotTools
{
pr.Step("Building project solution", 0);
- var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, config);
+ var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, targets: new[] {"Restore", "Build"}, config);
+
+ bool escapeNeedsDoubleBackslash = buildTool == BuildTool.MsBuildMono || buildTool == BuildTool.DotnetCli;
// Add Godot defines
- string constants = buildTool != BuildTool.MsBuildMono ? "GodotDefineConstants=\"" : "GodotDefineConstants=\\\"";
+ string constants = !escapeNeedsDoubleBackslash ? "GodotDefineConstants=\"" : "GodotDefineConstants=\\\"";
foreach (var godotDefine in godotDefines)
constants += $"GODOT_{godotDefine.ToUpper().Replace("-", "_").Replace(" ", "_").Replace(";", "_")};";
@@ -192,7 +188,7 @@ namespace GodotTools
if (Internal.GodotIsRealTDouble())
constants += "GODOT_REAL_T_IS_DOUBLE;";
- constants += buildTool != BuildTool.MsBuildMono ? "\"" : "\\\"";
+ constants += !escapeNeedsDoubleBackslash ? "\"" : "\\\"";
buildInfo.CustomProperties.Add(constants);
@@ -219,7 +215,7 @@ namespace GodotTools
if (File.Exists(editorScriptsMetadataPath))
File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath);
- var currentPlayRequest = GodotSharpEditor.Instance.GodotIdeManager.GodotIdeServer.CurrentPlayRequest;
+ var currentPlayRequest = GodotSharpEditor.Instance.CurrentPlaySettings;
if (currentPlayRequest != null)
{
@@ -233,7 +229,8 @@ namespace GodotTools
",server=n");
}
- return true; // Requested play from an external editor/IDE which already built the project
+ if (!currentPlayRequest.Value.BuildBeforePlaying)
+ return true; // Requested play from an external editor/IDE which already built the project
}
var godotDefines = new[]
@@ -249,22 +246,44 @@ namespace GodotTools
{
// Build tool settings
var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
- var msbuild = BuildTool.MsBuildMono;
+
+ BuildTool msbuildDefault;
+
if (OS.IsWindows)
- msbuild = RiderPathManager.IsExternalEditorSetToRider(editorSettings)
- ? BuildTool.JetBrainsMsBuild
- : BuildTool.MsBuildVs;
+ {
+ if (RiderPathManager.IsExternalEditorSetToRider(editorSettings))
+ msbuildDefault = BuildTool.JetBrainsMsBuild;
+ else
+ msbuildDefault = !string.IsNullOrEmpty(OS.PathWhich("dotnet")) ? BuildTool.DotnetCli : BuildTool.MsBuildVs;
+ }
+ else
+ {
+ msbuildDefault = !string.IsNullOrEmpty(OS.PathWhich("dotnet")) ? BuildTool.DotnetCli : BuildTool.MsBuildMono;
+ }
+
+ EditorDef("mono/builds/build_tool", msbuildDefault);
- EditorDef("mono/builds/build_tool", msbuild);
+ string hintString;
+
+ if (OS.IsWindows)
+ {
+ hintString = $"{PropNameMSBuildMono}:{(int)BuildTool.MsBuildMono}," +
+ $"{PropNameMSBuildVs}:{(int)BuildTool.MsBuildVs}," +
+ $"{PropNameMSBuildJetBrains}:{(int)BuildTool.JetBrainsMsBuild}," +
+ $"{PropNameDotnetCli}:{(int)BuildTool.DotnetCli}";
+ }
+ else
+ {
+ hintString = $"{PropNameMSBuildMono}:{(int)BuildTool.MsBuildMono}," +
+ $"{PropNameDotnetCli}:{(int)BuildTool.DotnetCli}";
+ }
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},{PropNameMsbuildJetBrains}" :
- $"{PropNameMsbuildMono}"
+ ["hint_string"] = hintString
});
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
index 938c3d8be1..0106e1f1ac 100644
--- a/modules/mono/editor/GodotTools/GodotTools/BuildTab.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/BuildTab.cs
@@ -72,7 +72,7 @@ namespace GodotTools
{
string[] csvColumns = file.GetCsvLine();
- if (csvColumns.Length == 1 && csvColumns[0].Empty())
+ if (csvColumns.Length == 1 && string.IsNullOrEmpty(csvColumns[0]))
return;
if (csvColumns.Length != 7)
@@ -115,12 +115,12 @@ namespace GodotTools
// Get correct issue idx from issue list
int issueIndex = (int)issuesList.GetItemMetadata(idx);
- if (idx < 0 || idx >= issues.Count)
+ if (issueIndex < 0 || issueIndex >= issues.Count)
throw new IndexOutOfRangeException("Issue index out of range");
BuildIssue issue = issues[issueIndex];
- if (issue.ProjectFile.Empty() && issue.File.Empty())
+ if (string.IsNullOrEmpty(issue.ProjectFile) && string.IsNullOrEmpty(issue.File))
return;
string projectDir = issue.ProjectFile.Length > 0 ? issue.ProjectFile.GetBaseDir() : BuildInfo.Solution.GetBaseDir();
@@ -158,14 +158,14 @@ namespace GodotTools
string tooltip = string.Empty;
tooltip += $"Message: {issue.Message}";
- if (!issue.Code.Empty())
+ if (!string.IsNullOrEmpty(issue.Code))
tooltip += $"\nCode: {issue.Code}";
tooltip += $"\nType: {(issue.Warning ? "warning" : "error")}";
string text = string.Empty;
- if (!issue.File.Empty())
+ if (!string.IsNullOrEmpty(issue.File))
{
text += $"{issue.File}({issue.Line},{issue.Column}): ";
@@ -174,7 +174,7 @@ namespace GodotTools
tooltip += $"\nColumn: {issue.Column}";
}
- if (!issue.ProjectFile.Empty())
+ if (!string.IsNullOrEmpty(issue.ProjectFile))
tooltip += $"\nProject: {issue.ProjectFile}";
text += issue.Message;
diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs b/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs
index f1765f7e19..f60e469503 100755
--- a/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs
@@ -587,7 +587,7 @@ MONO_AOT_MODE_LAST = 1000,
string arch = "x86_64";
return $"{platform}-{arch}";
}
- case OS.Platforms.X11:
+ case OS.Platforms.LinuxBSD:
case OS.Platforms.Server:
{
string arch = bits == "64" ? "x86_64" : "i686";
diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs
index d782d4e61b..6bfbc62f3b 100644
--- a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs
@@ -168,13 +168,13 @@ namespace GodotTools.Export
// Add dependency assemblies
- var dependencies = new Godot.Collections.Dictionary<string, string>();
+ var assemblies = new Godot.Collections.Dictionary<string, string>();
string projectDllName = GodotSharpEditor.ProjectAssemblyName;
string projectDllSrcDir = Path.Combine(GodotSharpDirs.ResTempAssembliesBaseDir, buildConfig);
string projectDllSrcPath = Path.Combine(projectDllSrcDir, $"{projectDllName}.dll");
- dependencies[projectDllName] = projectDllSrcPath;
+ assemblies[projectDllName] = projectDllSrcPath;
if (platform == OS.Platforms.Android)
{
@@ -184,15 +184,15 @@ namespace GodotTools.Export
if (!File.Exists(monoAndroidAssemblyPath))
throw new FileNotFoundException("Assembly not found: 'Mono.Android'", monoAndroidAssemblyPath);
- dependencies["Mono.Android"] = monoAndroidAssemblyPath;
+ assemblies["Mono.Android"] = monoAndroidAssemblyPath;
}
string bclDir = DeterminePlatformBclDir(platform);
- var initialDependencies = dependencies.Duplicate();
- internal_GetExportedAssemblyDependencies(initialDependencies, buildConfig, bclDir, dependencies);
+ var initialAssemblies = assemblies.Duplicate();
+ internal_GetExportedAssemblyDependencies(initialAssemblies, buildConfig, bclDir, assemblies);
- AddI18NAssemblies(dependencies, bclDir);
+ AddI18NAssemblies(assemblies, bclDir);
string outputDataDir = null;
@@ -211,20 +211,32 @@ namespace GodotTools.Export
Directory.CreateDirectory(outputDataGameAssembliesDir);
}
- foreach (var dependency in dependencies)
+ foreach (var assembly in assemblies)
{
- string dependSrcPath = dependency.Value;
-
- if (assembliesInsidePck)
- {
- string dependDstPath = Path.Combine(resAssembliesDir, dependSrcPath.GetFile());
- AddFile(dependSrcPath, dependDstPath);
- }
- else
+ void AddToAssembliesDir(string fileSrcPath)
{
- string dependDstPath = Path.Combine(outputDataDir, "Assemblies", dependSrcPath.GetFile());
- File.Copy(dependSrcPath, dependDstPath);
+ if (assembliesInsidePck)
+ {
+ string fileDstPath = Path.Combine(resAssembliesDir, fileSrcPath.GetFile());
+ AddFile(fileSrcPath, fileDstPath);
+ }
+ else
+ {
+ Debug.Assert(outputDataDir != null);
+ string fileDstPath = Path.Combine(outputDataDir, "Assemblies", fileSrcPath.GetFile());
+ File.Copy(fileSrcPath, fileDstPath);
+ }
}
+
+ string assemblySrcPath = assembly.Value;
+
+ string assemblyPathWithoutExtension = Path.ChangeExtension(assemblySrcPath, null);
+ string pdbSrcPath = assemblyPathWithoutExtension + ".pdb";
+
+ AddToAssembliesDir(assemblySrcPath);
+
+ if (File.Exists(pdbSrcPath))
+ AddToAssembliesDir(pdbSrcPath);
}
// AOT compilation
@@ -254,7 +266,7 @@ namespace GodotTools.Export
ToolchainPath = aotToolchainPath
};
- AotBuilder.CompileAssemblies(this, aotOpts, features, platform, isDebug, bclDir, outputDir, outputDataDir, dependencies);
+ AotBuilder.CompileAssemblies(this, aotOpts, features, platform, isDebug, bclDir, outputDir, outputDataDir, assemblies);
}
}
@@ -366,7 +378,7 @@ namespace GodotTools.Export
if (PlatformRequiresCustomBcl(platform))
throw new FileNotFoundException($"Missing BCL (Base Class Library) for platform: {platform}");
- platformBclDir = typeof(object).Assembly.Location; // Use the one we're running on
+ platformBclDir = typeof(object).Assembly.Location.GetBaseDir(); // Use the one we're running on
}
}
@@ -402,7 +414,7 @@ namespace GodotTools.Export
case OS.Platforms.UWP:
return "net_4_x_win";
case OS.Platforms.OSX:
- case OS.Platforms.X11:
+ case OS.Platforms.LinuxBSD:
case OS.Platforms.Server:
case OS.Platforms.Haiku:
return "net_4_x";
@@ -425,7 +437,7 @@ namespace GodotTools.Export
}
[MethodImpl(MethodImplOptions.InternalCall)]
- private static extern void internal_GetExportedAssemblyDependencies(Godot.Collections.Dictionary<string, string> initialDependencies,
- string buildConfig, string customBclDir, Godot.Collections.Dictionary<string, string> dependencies);
+ private static extern void internal_GetExportedAssemblyDependencies(Godot.Collections.Dictionary<string, string> initialAssemblies,
+ string buildConfig, string customBclDir, Godot.Collections.Dictionary<string, string> dependencyAssemblies);
}
}
diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs
index c9d7dd26f8..403e25781d 100644
--- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs
@@ -1,10 +1,12 @@
using Godot;
+using GodotTools.Core;
using GodotTools.Export;
using GodotTools.Utils;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
+using System.Linq;
using GodotTools.Ides;
using GodotTools.Ides.Rider;
using GodotTools.Internals;
@@ -36,6 +38,8 @@ namespace GodotTools
public BottomPanel BottomPanel { get; private set; }
+ public PlaySettings? CurrentPlaySettings { get; set; }
+
public static string ProjectAssemblyName
{
get
@@ -227,15 +231,39 @@ namespace GodotTools
[UsedImplicitly]
public Error OpenInExternalEditor(Script script, int line, int col)
{
- var editor = (ExternalEditorId)editorSettings.GetSetting("mono/editor/external_editor");
+ var editorId = (ExternalEditorId)editorSettings.GetSetting("mono/editor/external_editor");
- switch (editor)
+ switch (editorId)
{
case ExternalEditorId.None:
- // Tells the caller to fallback to the global external editor settings or the built-in editor
+ // Not an error. 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();
+ {
+ string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath);
+
+ var args = new List<string>
+ {
+ GodotSharpDirs.ProjectSlnPath,
+ line >= 0 ? $"{scriptPath};{line + 1};{col + 1}" : scriptPath
+ };
+
+ string command = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "GodotTools.OpenVisualStudio.exe");
+
+ try
+ {
+ if (Godot.OS.IsStdoutVerbose())
+ Console.WriteLine($"Running: \"{command}\" {string.Join(" ", args.Select(a => $"\"{a}\""))}");
+
+ OS.RunProcess(command, args);
+ }
+ catch (Exception e)
+ {
+ GD.PushError($"Error when trying to run code editor: VisualStudio. Exception message: '{e.Message}'");
+ }
+
+ break;
+ }
case ExternalEditorId.VisualStudioForMac:
goto case ExternalEditorId.MonoDevelop;
case ExternalEditorId.Rider:
@@ -248,17 +276,20 @@ namespace GodotTools
{
string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath);
- if (line >= 0)
- GodotIdeManager.SendOpenFile(scriptPath, line + 1, col);
- else
- GodotIdeManager.SendOpenFile(scriptPath);
+ GodotIdeManager.LaunchIdeAsync().ContinueWith(launchTask =>
+ {
+ var editorPick = launchTask.Result;
+ if (line >= 0)
+ editorPick?.SendOpenFile(scriptPath, line + 1, col);
+ else
+ editorPick?.SendOpenFile(scriptPath);
+ });
break;
}
-
case ExternalEditorId.VsCode:
{
- if (_vsCodePath.Empty() || !File.Exists(_vsCodePath))
+ if (string.IsNullOrEmpty(_vsCodePath) || !File.Exists(_vsCodePath))
{
// Try to search it again if it wasn't found last time or if it was removed from its location
_vsCodePath = VsCodeNames.SelectFirstNotNull(OS.PathWhich, orElse: string.Empty);
@@ -299,7 +330,7 @@ namespace GodotTools
if (line >= 0)
{
args.Add("-g");
- args.Add($"{scriptPath}:{line + 1}:{col}");
+ args.Add($"{scriptPath}:{line}:{col}");
}
else
{
@@ -310,7 +341,7 @@ namespace GodotTools
if (OS.IsOSX)
{
- if (!osxAppBundleInstalled && _vsCodePath.Empty())
+ if (!osxAppBundleInstalled && string.IsNullOrEmpty(_vsCodePath))
{
GD.PushError("Cannot find code editor: VSCode");
return Error.FileNotFound;
@@ -320,7 +351,7 @@ namespace GodotTools
}
else
{
- if (_vsCodePath.Empty())
+ if (string.IsNullOrEmpty(_vsCodePath))
{
GD.PushError("Cannot find code editor: VSCode");
return Error.FileNotFound;
@@ -340,7 +371,6 @@ namespace GodotTools
break;
}
-
default:
throw new ArgumentOutOfRangeException();
}
@@ -416,7 +446,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, Windows and Android, but not yet to iOS, HTML5 or UWP. " +
+ "Projects can be exported to Linux, macOS, Windows, Android, iOS and HTML5, but not yet to 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" +
@@ -442,13 +472,33 @@ namespace GodotTools
{
// Migrate solution from old configuration names to: Debug, ExportDebug and ExportRelease
DotNetSolution.MigrateFromOldConfigNames(GodotSharpDirs.ProjectSlnPath);
+
+ var msbuildProject = ProjectUtils.Open(GodotSharpDirs.ProjectCsProjPath)
+ ?? throw new Exception("Cannot open C# project");
+
+ // NOTE: The order in which changes are made to the project is important
+
// Migrate csproj from old configuration names to: Debug, ExportDebug and ExportRelease
- ProjectUtils.MigrateFromOldConfigNames(GodotSharpDirs.ProjectCsProjPath);
+ ProjectUtils.MigrateFromOldConfigNames(msbuildProject);
- // Apply the other fixes after configurations are migrated
+ // Apply the other fixes only after configurations have been migrated
+
+ // Make sure the existing project has the ProjectTypeGuids property (for VisualStudio)
+ ProjectUtils.EnsureHasProjectTypeGuids(msbuildProject);
// Make sure the existing project has Api assembly references configured correctly
- ProjectUtils.FixApiHintPath(GodotSharpDirs.ProjectCsProjPath);
+ ProjectUtils.FixApiHintPath(msbuildProject);
+
+ // Make sure the existing project references the Microsoft.NETFramework.ReferenceAssemblies nuget package
+ ProjectUtils.EnsureHasNugetNetFrameworkRefAssemblies(msbuildProject);
+
+ if (msbuildProject.HasUnsavedChanges)
+ {
+ // Save a copy of the project before replacing it
+ FileUtils.SaveBackupCopy(GodotSharpDirs.ProjectCsProjPath);
+
+ msbuildProject.Save();
+ }
}
catch (Exception e)
{
@@ -479,7 +529,8 @@ namespace GodotTools
if (OS.IsWindows)
{
- settingsHintStr += $",MonoDevelop:{(int)ExternalEditorId.MonoDevelop}" +
+ settingsHintStr += $",Visual Studio:{(int)ExternalEditorId.VisualStudio}" +
+ $",MonoDevelop:{(int)ExternalEditorId.MonoDevelop}" +
$",Visual Studio Code:{(int)ExternalEditorId.VsCode}" +
$",JetBrains Rider:{(int)ExternalEditorId.Rider}";
}
@@ -490,7 +541,7 @@ namespace GodotTools
$",Visual Studio Code:{(int)ExternalEditorId.VsCode}" +
$",JetBrains Rider:{(int)ExternalEditorId.Rider}";
}
- else if (OS.IsUnixLike())
+ else if (OS.IsUnixLike)
{
settingsHintStr += $",MonoDevelop:{(int)ExternalEditorId.MonoDevelop}" +
$",Visual Studio Code:{(int)ExternalEditorId.VsCode}" +
diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj
index ac9379adf8..3f14629b11 100644
--- a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj
+++ b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj
@@ -1,123 +1,39 @@
-<?xml version="1.0" encoding="utf-8"?>
-<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
- <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{27B00618-A6F2-4828-B922-05CAEB08C286}</ProjectGuid>
- <OutputType>Library</OutputType>
- <RootNamespace>GodotTools</RootNamespace>
- <AssemblyName>GodotTools</AssemblyName>
- <TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
+ <TargetFramework>net472</TargetFramework>
+ <LangVersion>7.2</LangVersion>
+ <GodotApiConfiguration>Debug</GodotApiConfiguration> <!-- The Godot editor uses the Debug Godot API assemblies -->
<GodotSourceRootPath>$(SolutionDir)/../../../../</GodotSourceRootPath>
- <DataDirToolsOutputPath>$(GodotSourceRootPath)/bin/GodotSharp/Tools</DataDirToolsOutputPath>
- <GodotApiConfiguration>Debug</GodotApiConfiguration>
- <LangVersion>7</LangVersion>
+ <GodotOutputDataDir>$(GodotSourceRootPath)/bin/GodotSharp</GodotOutputDataDir>
+ <GodotApiAssembliesDir>$(GodotOutputDataDir)/Api/$(GodotApiConfiguration)</GodotApiAssembliesDir>
</PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
- <DebugSymbols>true</DebugSymbols>
- <DebugType>portable</DebugType>
- <Optimize>false</Optimize>
- <OutputPath>bin\Debug</OutputPath>
- <DefineConstants>DEBUG;</DefineConstants>
- <ErrorReport>prompt</ErrorReport>
- <WarningLevel>4</WarningLevel>
- <ConsolePause>false</ConsolePause>
- </PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
- <Optimize>true</Optimize>
- <OutputPath>bin\Release</OutputPath>
- <ErrorReport>prompt</ErrorReport>
- <WarningLevel>4</WarningLevel>
- <ConsolePause>false</ConsolePause>
+ <PropertyGroup Condition=" Exists('$(GodotApiAssembliesDir)/GodotSharp.dll') ">
+ <!-- The project is part of the Godot source tree -->
+ <!-- Use the Godot source tree output folder instead of '$(ProjectDir)/bin' -->
+ <OutputPath>$(GodotOutputDataDir)/Tools</OutputPath>
+ <!-- Must not append '$(TargetFramework)' to the output path in this case -->
+ <AppendTargetFrameworkToOutputPath>False</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
<ItemGroup>
- <Reference Include="JetBrains.Annotations, Version=2019.1.3.0, Culture=neutral, PublicKeyToken=1010a0d8d6380325">
- <HintPath>..\packages\JetBrains.Annotations.2019.1.3\lib\net20\JetBrains.Annotations.dll</HintPath>
- <Private>True</Private>
- </Reference>
- <Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
- <HintPath>..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
- <Private>True</Private>
- </Reference>
- <Reference Include="System" />
+ <PackageReference Include="JetBrains.Annotations" Version="2019.1.3.0" ExcludeAssets="runtime" PrivateAssets="all" />
+ <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
+ <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<Reference Include="GodotSharp">
- <HintPath>$(GodotSourceRootPath)/bin/GodotSharp/Api/$(GodotApiConfiguration)/GodotSharp.dll</HintPath>
+ <HintPath>$(GodotApiAssembliesDir)/GodotSharp.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="GodotSharpEditor">
- <HintPath>$(GodotSourceRootPath)/bin/GodotSharp/Api/$(GodotApiConfiguration)/GodotSharpEditor.dll</HintPath>
+ <HintPath>$(GodotApiAssembliesDir)/GodotSharpEditor.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
- <Compile Include="Build\MsBuildFinder.cs" />
- <Compile Include="Export\AotBuilder.cs" />
- <Compile Include="Export\ExportPlugin.cs" />
- <Compile Include="Export\XcodeHelper.cs" />
- <Compile Include="ExternalEditorId.cs" />
- <Compile Include="Ides\GodotIdeManager.cs" />
- <Compile Include="Ides\GodotIdeServer.cs" />
- <Compile Include="Ides\MonoDevelop\EditorId.cs" />
- <Compile Include="Ides\MonoDevelop\Instance.cs" />
- <Compile Include="Ides\Rider\RiderPathLocator.cs" />
- <Compile Include="Ides\Rider\RiderPathManager.cs" />
- <Compile Include="Internals\EditorProgress.cs" />
- <Compile Include="Internals\GodotSharpDirs.cs" />
- <Compile Include="Internals\Internal.cs" />
- <Compile Include="Internals\ScriptClassParser.cs" />
- <Compile Include="Internals\Globals.cs" />
- <Compile Include="Properties\AssemblyInfo.cs" />
- <Compile Include="Build\BuildSystem.cs" />
- <Compile Include="Utils\Directory.cs" />
- <Compile Include="Utils\File.cs" />
- <Compile Include="Utils\NotifyAwaiter.cs" />
- <Compile Include="Utils\OS.cs" />
- <Compile Include="GodotSharpEditor.cs" />
- <Compile Include="BuildManager.cs" />
- <Compile Include="HotReloadAssemblyWatcher.cs" />
- <Compile Include="BuildInfo.cs" />
- <Compile Include="BuildTab.cs" />
- <Compile Include="BottomPanel.cs" />
- <Compile Include="CsProjOperations.cs" />
- <Compile Include="Utils\CollectionExtensions.cs" />
- <Compile Include="Utils\User32Dll.cs" />
- </ItemGroup>
- <ItemGroup>
- <ProjectReference Include="..\GodotTools.BuildLogger\GodotTools.BuildLogger.csproj">
- <Project>{6ce9a984-37b1-4f8a-8fe9-609f05f071b3}</Project>
- <Name>GodotTools.BuildLogger</Name>
- </ProjectReference>
- <ProjectReference Include="..\GodotTools.IdeConnection\GodotTools.IdeConnection.csproj">
- <Project>{92600954-25f0-4291-8e11-1fee9fc4be20}</Project>
- <Name>GodotTools.IdeConnection</Name>
- </ProjectReference>
- <ProjectReference Include="..\GodotTools.ProjectEditor\GodotTools.ProjectEditor.csproj">
- <Project>{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}</Project>
- <Name>GodotTools.ProjectEditor</Name>
- </ProjectReference>
- <ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj">
- <Project>{639E48BD-44E5-4091-8EDD-22D36DC0768D}</Project>
- <Name>GodotTools.Core</Name>
- </ProjectReference>
- </ItemGroup>
- <ItemGroup>
- <None Include="packages.config" />
+ <ProjectReference Include="..\GodotTools.BuildLogger\GodotTools.BuildLogger.csproj" />
+ <ProjectReference Include="..\GodotTools.IdeMessaging\GodotTools.IdeMessaging.csproj" />
+ <ProjectReference Include="..\GodotTools.ProjectEditor\GodotTools.ProjectEditor.csproj" />
+ <ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj" />
+ <!-- Include it if this is an SCons build targeting Windows, or if it's not an SCons build but we're on Windows -->
+ <ProjectReference Include="..\GodotTools.OpenVisualStudio\GodotTools.OpenVisualStudio.csproj" Condition=" '$(GodotPlatform)' == 'windows' Or ( '$(GodotPlatform)' == '' And '$(OS)' == 'Windows_NT' ) " />
</ItemGroup>
- <Target Name="CopyToDataDir" AfterTargets="Build">
- <ItemGroup>
- <GodotToolsCopy Include="$(OutputPath)\GodotTools*.dll" />
- <GodotToolsCopy Include="$(OutputPath)\Newtonsoft.Json.dll" />
- <GodotToolsCopy Include="$(OutputPath)\DotNet.Glob.dll" />
- </ItemGroup>
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <GodotToolsCopy Include="$(OutputPath)\GodotTools*.pdb" />
- </ItemGroup>
- <Copy SourceFiles="@(GodotToolsCopy)" DestinationFolder="$(DataDirToolsOutputPath)" ContinueOnError="false" />
- </Target>
- <Target Name="BuildAlwaysCopyToDataDir">
- <!-- Custom target run by SCons to make sure the CopyToDataDir target is always executed, without having to use DisableFastUpToDateCheck -->
- <CallTarget Targets="Build" />
- <CallTarget Targets="CopyToDataDir" />
- </Target>
- <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
</Project>
diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs
index 54f0ffab96..e4932ca217 100644
--- a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs
@@ -1,73 +1,104 @@
using System;
using System.IO;
+using System.Threading.Tasks;
using Godot;
-using GodotTools.IdeConnection;
+using GodotTools.IdeMessaging;
+using GodotTools.IdeMessaging.Requests;
using GodotTools.Internals;
namespace GodotTools.Ides
{
- public class GodotIdeManager : Node, ISerializationListener
+ public sealed class GodotIdeManager : Node, ISerializationListener
{
- public GodotIdeServer GodotIdeServer { get; private set; }
+ private MessagingServer MessagingServer { get; set; }
private MonoDevelop.Instance monoDevelInstance;
private MonoDevelop.Instance vsForMacInstance;
- private GodotIdeServer GetRunningServer()
+ private MessagingServer GetRunningOrNewServer()
{
- if (GodotIdeServer != null && !GodotIdeServer.IsDisposed)
- return GodotIdeServer;
- StartServer();
- return GodotIdeServer;
+ if (MessagingServer != null && !MessagingServer.IsDisposed)
+ return MessagingServer;
+
+ MessagingServer?.Dispose();
+ MessagingServer = new MessagingServer(OS.GetExecutablePath(), ProjectSettings.GlobalizePath(GodotSharpDirs.ResMetadataDir), new GodotLogger());
+
+ _ = MessagingServer.Listen();
+
+ return MessagingServer;
}
public override void _Ready()
{
- StartServer();
+ _ = GetRunningOrNewServer();
}
public void OnBeforeSerialize()
{
- GodotIdeServer?.Dispose();
}
public void OnAfterDeserialize()
{
- StartServer();
+ _ = GetRunningOrNewServer();
}
- private ILogger logger;
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
- protected ILogger Logger
+ if (disposing)
+ {
+ MessagingServer?.Dispose();
+ }
+ }
+
+ private string GetExternalEditorIdentity(ExternalEditorId editorId)
{
- get => logger ?? (logger = new GodotLogger());
+ // Manually convert to string to avoid breaking compatibility in case we rename the enum fields.
+ switch (editorId)
+ {
+ case ExternalEditorId.None:
+ return null;
+ case ExternalEditorId.VisualStudio:
+ return "VisualStudio";
+ case ExternalEditorId.VsCode:
+ return "VisualStudioCode";
+ case ExternalEditorId.Rider:
+ return "Rider";
+ case ExternalEditorId.VisualStudioForMac:
+ return "VisualStudioForMac";
+ case ExternalEditorId.MonoDevelop:
+ return "MonoDevelop";
+ default:
+ throw new NotImplementedException();
+ }
}
- private void StartServer()
+ public async Task<EditorPick?> LaunchIdeAsync(int millisecondsTimeout = 10000)
{
- GodotIdeServer?.Dispose();
- GodotIdeServer = new GodotIdeServer(LaunchIde,
- OS.GetExecutablePath(),
- ProjectSettings.GlobalizePath(GodotSharpDirs.ResMetadataDir));
+ var editorId = (ExternalEditorId)GodotSharpEditor.Instance.GetEditorInterface()
+ .GetEditorSettings().GetSetting("mono/editor/external_editor");
+ string editorIdentity = GetExternalEditorIdentity(editorId);
- GodotIdeServer.Logger = Logger;
+ var runningServer = GetRunningOrNewServer();
- GodotIdeServer.StartServer();
- }
+ if (runningServer.IsAnyConnected(editorIdentity))
+ return new EditorPick(editorIdentity);
- protected override void Dispose(bool disposing)
- {
- base.Dispose(disposing);
+ LaunchIde(editorId, editorIdentity);
+
+ var timeoutTask = Task.Delay(millisecondsTimeout);
+ var completedTask = await Task.WhenAny(timeoutTask, runningServer.AwaitClientConnected(editorIdentity));
+
+ if (completedTask != timeoutTask)
+ return new EditorPick(editorIdentity);
- GodotIdeServer?.Dispose();
+ return null;
}
- private void LaunchIde()
+ private void LaunchIde(ExternalEditorId editorId, string editorIdentity)
{
- var editor = (ExternalEditorId)GodotSharpEditor.Instance.GetEditorInterface()
- .GetEditorSettings().GetSetting("mono/editor/external_editor");
-
- switch (editor)
+ switch (editorId)
{
case ExternalEditorId.None:
case ExternalEditorId.VisualStudio:
@@ -80,14 +111,14 @@ namespace GodotTools.Ides
{
MonoDevelop.Instance GetMonoDevelopInstance(string solutionPath)
{
- if (Utils.OS.IsOSX && editor == ExternalEditorId.VisualStudioForMac)
+ if (Utils.OS.IsOSX && editorId == ExternalEditorId.VisualStudioForMac)
{
- vsForMacInstance = vsForMacInstance ??
+ vsForMacInstance = (vsForMacInstance?.IsDisposed ?? true ? null : vsForMacInstance) ??
new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.VisualStudioForMac);
return vsForMacInstance;
}
- monoDevelInstance = monoDevelInstance ??
+ monoDevelInstance = (monoDevelInstance?.IsDisposed ?? true ? null : monoDevelInstance) ??
new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.MonoDevelop);
return monoDevelInstance;
}
@@ -96,12 +127,25 @@ namespace GodotTools.Ides
{
var instance = GetMonoDevelopInstance(GodotSharpDirs.ProjectSlnPath);
- if (!instance.IsRunning)
+ if (instance.IsRunning && !GetRunningOrNewServer().IsAnyConnected(editorIdentity))
+ {
+ // After launch we wait up to 30 seconds for the IDE to connect to our messaging server.
+ var waitAfterLaunch = TimeSpan.FromSeconds(30);
+ var timeSinceLaunch = DateTime.Now - instance.LaunchTime;
+ if (timeSinceLaunch > waitAfterLaunch)
+ {
+ instance.Dispose();
+ instance.Execute();
+ }
+ }
+ else if (!instance.IsRunning)
+ {
instance.Execute();
+ }
}
catch (FileNotFoundException)
{
- string editorName = editor == ExternalEditorId.VisualStudioForMac ? "Visual Studio" : "MonoDevelop";
+ string editorName = editorId == ExternalEditorId.VisualStudioForMac ? "Visual Studio" : "MonoDevelop";
GD.PushError($"Cannot find code editor: {editorName}");
}
@@ -113,26 +157,45 @@ namespace GodotTools.Ides
}
}
- private void WriteMessage(string id, params string[] arguments)
+ public readonly struct EditorPick
{
- GetRunningServer().WriteMessage(new Message(id, arguments));
- }
+ private readonly string identity;
- public void SendOpenFile(string file)
- {
- WriteMessage("OpenFile", file);
- }
+ public EditorPick(string identity)
+ {
+ this.identity = identity;
+ }
- public void SendOpenFile(string file, int line)
- {
- WriteMessage("OpenFile", file, line.ToString());
- }
+ public bool IsAnyConnected() =>
+ GodotSharpEditor.Instance.GodotIdeManager.GetRunningOrNewServer().IsAnyConnected(identity);
- public void SendOpenFile(string file, int line, int column)
- {
- WriteMessage("OpenFile", file, line.ToString(), column.ToString());
+ private void SendRequest<TResponse>(Request request)
+ where TResponse : Response, new()
+ {
+ // Logs an error if no client is connected with the specified identity
+ GodotSharpEditor.Instance.GodotIdeManager
+ .GetRunningOrNewServer()
+ .BroadcastRequest<TResponse>(identity, request);
+ }
+
+ public void SendOpenFile(string file)
+ {
+ SendRequest<OpenFileResponse>(new OpenFileRequest {File = file});
+ }
+
+ public void SendOpenFile(string file, int line)
+ {
+ SendRequest<OpenFileResponse>(new OpenFileRequest {File = file, Line = line});
+ }
+
+ public void SendOpenFile(string file, int line, int column)
+ {
+ SendRequest<OpenFileResponse>(new OpenFileRequest {File = file, Line = line, Column = column});
+ }
}
+ public EditorPick PickEditor(ExternalEditorId editorId) => new EditorPick(GetExternalEditorIdentity(editorId));
+
private class GodotLogger : ILogger
{
public void LogDebug(string message)
diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs
deleted file mode 100644
index 72676a8b24..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs
+++ /dev/null
@@ -1,212 +0,0 @@
-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 Directory = System.IO.Directory;
-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<bool> clientConnectedAwaiter = new NotifyAwaiter<bool>();
-
- private async Task<bool> AwaitClientConnected()
- {
- return await clientConnectedAwaiter.Reset();
- }
-
- public GodotIdeServer(Action launchIdeAction, string editorExecutablePath, string projectMetadataDir)
- : base(projectMetadataDir)
- {
- messageHandlers = InitializeMessageHandlers();
-
- 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;
-
- 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<string, Action<string[]>> messageHandlers;
-
- private Dictionary<string, Action<string[]>> InitializeMessageHandlers()
- {
- return new Dictionary<string, Action<string[]>>
- {
- ["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/MessagingServer.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs
new file mode 100644
index 0000000000..98e8d13be0
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs
@@ -0,0 +1,371 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using GodotTools.IdeMessaging;
+using GodotTools.IdeMessaging.Requests;
+using GodotTools.IdeMessaging.Utils;
+using GodotTools.Internals;
+using Newtonsoft.Json;
+using Directory = System.IO.Directory;
+using File = System.IO.File;
+
+namespace GodotTools.Ides
+{
+ public sealed class MessagingServer : IDisposable
+ {
+ private readonly ILogger logger;
+
+ private readonly FileStream metaFile;
+ private string MetaFilePath { get; }
+
+ private readonly SemaphoreSlim peersSem = new SemaphoreSlim(1);
+
+ private readonly TcpListener listener;
+
+ private readonly Dictionary<string, Queue<NotifyAwaiter<bool>>> clientConnectedAwaiters = new Dictionary<string, Queue<NotifyAwaiter<bool>>>();
+ private readonly Dictionary<string, Queue<NotifyAwaiter<bool>>> clientDisconnectedAwaiters = new Dictionary<string, Queue<NotifyAwaiter<bool>>>();
+
+ public async Task<bool> AwaitClientConnected(string identity)
+ {
+ if (!clientConnectedAwaiters.TryGetValue(identity, out var queue))
+ {
+ queue = new Queue<NotifyAwaiter<bool>>();
+ clientConnectedAwaiters.Add(identity, queue);
+ }
+
+ var awaiter = new NotifyAwaiter<bool>();
+ queue.Enqueue(awaiter);
+ return await awaiter;
+ }
+
+ public async Task<bool> AwaitClientDisconnected(string identity)
+ {
+ if (!clientDisconnectedAwaiters.TryGetValue(identity, out var queue))
+ {
+ queue = new Queue<NotifyAwaiter<bool>>();
+ clientDisconnectedAwaiters.Add(identity, queue);
+ }
+
+ var awaiter = new NotifyAwaiter<bool>();
+ queue.Enqueue(awaiter);
+ return await awaiter;
+ }
+
+ public bool IsDisposed { get; private set; }
+
+ public bool IsAnyConnected(string identity) => string.IsNullOrEmpty(identity) ?
+ Peers.Count > 0 :
+ Peers.Any(c => c.RemoteIdentity == identity);
+
+ private List<Peer> Peers { get; } = new List<Peer>();
+
+ ~MessagingServer()
+ {
+ Dispose(disposing: false);
+ }
+
+ public async void Dispose()
+ {
+ if (IsDisposed)
+ return;
+
+ using (await peersSem.UseAsync())
+ {
+ if (IsDisposed) // lock may not be fair
+ return;
+ IsDisposed = true;
+ }
+
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ foreach (var connection in Peers)
+ connection.Dispose();
+ Peers.Clear();
+ listener?.Stop();
+
+ metaFile?.Dispose();
+
+ File.Delete(MetaFilePath);
+ }
+ }
+
+ public MessagingServer(string editorExecutablePath, string projectMetadataDir, ILogger logger)
+ {
+ this.logger = logger;
+
+ MetaFilePath = Path.Combine(projectMetadataDir, GodotIdeMetadata.DefaultFileName);
+
+ // 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;
+
+ 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);
+ }
+ }
+
+ private async Task AcceptClient(TcpClient tcpClient)
+ {
+ logger.LogDebug("Accept client...");
+
+ using (var peer = new Peer(tcpClient, new ServerHandshake(), new ServerMessageHandler(), logger))
+ {
+ // ReSharper disable AccessToDisposedClosure
+ peer.Connected += () =>
+ {
+ logger.LogInfo("Connection open with Ide Client");
+
+ if (clientConnectedAwaiters.TryGetValue(peer.RemoteIdentity, out var queue))
+ {
+ while (queue.Count > 0)
+ queue.Dequeue().SetResult(true);
+ clientConnectedAwaiters.Remove(peer.RemoteIdentity);
+ }
+ };
+
+ peer.Disconnected += () =>
+ {
+ if (clientDisconnectedAwaiters.TryGetValue(peer.RemoteIdentity, out var queue))
+ {
+ while (queue.Count > 0)
+ queue.Dequeue().SetResult(true);
+ clientDisconnectedAwaiters.Remove(peer.RemoteIdentity);
+ }
+ };
+ // ReSharper restore AccessToDisposedClosure
+
+ try
+ {
+ if (!await peer.DoHandshake("server"))
+ {
+ logger.LogError("Handshake failed");
+ return;
+ }
+ }
+ catch (Exception e)
+ {
+ logger.LogError("Handshake failed with unhandled exception: ", e);
+ return;
+ }
+
+ using (await peersSem.UseAsync())
+ Peers.Add(peer);
+
+ try
+ {
+ await peer.Process();
+ }
+ finally
+ {
+ using (await peersSem.UseAsync())
+ Peers.Remove(peer);
+ }
+ }
+ }
+
+ public async Task Listen()
+ {
+ try
+ {
+ while (!IsDisposed)
+ _ = AcceptClient(await listener.AcceptTcpClientAsync());
+ }
+ catch (Exception e)
+ {
+ if (!IsDisposed && !(e is SocketException se && se.SocketErrorCode == SocketError.Interrupted))
+ throw;
+ }
+ }
+
+ public async void BroadcastRequest<TResponse>(string identity, Request request)
+ where TResponse : Response, new()
+ {
+ using (await peersSem.UseAsync())
+ {
+ if (!IsAnyConnected(identity))
+ {
+ logger.LogError("Cannot write request. No client connected to the Godot Ide Server.");
+ return;
+ }
+
+ var selectedConnections = string.IsNullOrEmpty(identity) ?
+ Peers :
+ Peers.Where(c => c.RemoteIdentity == identity);
+
+ string body = JsonConvert.SerializeObject(request);
+
+ foreach (var connection in selectedConnections)
+ _ = connection.SendRequest<TResponse>(request.Id, body);
+ }
+ }
+
+ private class ServerHandshake : IHandshake
+ {
+ private static readonly string ServerHandshakeBase = $"{Peer.ServerHandshakeName},Version={Peer.ProtocolVersionMajor}.{Peer.ProtocolVersionMinor}.{Peer.ProtocolVersionRevision}";
+ private static readonly string ClientHandshakePattern = $@"{Regex.Escape(Peer.ClientHandshakeName)},Version=([0-9]+)\.([0-9]+)\.([0-9]+),([_a-zA-Z][_a-zA-Z0-9]{{0,63}})";
+
+ public string GetHandshakeLine(string identity) => $"{ServerHandshakeBase},{identity}";
+
+ public bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger)
+ {
+ identity = null;
+
+ var match = Regex.Match(handshake, ClientHandshakePattern);
+
+ if (!match.Success)
+ return false;
+
+ if (!uint.TryParse(match.Groups[1].Value, out uint clientMajor) || Peer.ProtocolVersionMajor != clientMajor)
+ {
+ logger.LogDebug("Incompatible major version: " + match.Groups[1].Value);
+ return false;
+ }
+
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalse
+ if (!uint.TryParse(match.Groups[2].Value, out uint clientMinor) || Peer.ProtocolVersionMinor > clientMinor)
+ {
+ logger.LogDebug("Incompatible minor version: " + match.Groups[2].Value);
+ return false;
+ }
+
+ if (!uint.TryParse(match.Groups[3].Value, out uint _)) // Revision
+ {
+ logger.LogDebug("Incompatible revision build: " + match.Groups[3].Value);
+ return false;
+ }
+
+ identity = match.Groups[4].Value;
+
+ return true;
+ }
+ }
+
+ private class ServerMessageHandler : IMessageHandler
+ {
+ private static void DispatchToMainThread(Action action)
+ {
+ var d = new SendOrPostCallback(state => action());
+ Godot.Dispatcher.SynchronizationContext.Post(d, null);
+ }
+
+ private readonly Dictionary<string, Peer.RequestHandler> requestHandlers = InitializeRequestHandlers();
+
+ public async Task<MessageContent> HandleRequest(Peer peer, string id, MessageContent content, ILogger logger)
+ {
+ if (!requestHandlers.TryGetValue(id, out var handler))
+ {
+ logger.LogError($"Received unknown request: {id}");
+ return new MessageContent(MessageStatus.RequestNotSupported, "null");
+ }
+
+ try
+ {
+ var response = await handler(peer, content);
+ return new MessageContent(response.Status, JsonConvert.SerializeObject(response));
+ }
+ catch (JsonException)
+ {
+ logger.LogError($"Received request with invalid body: {id}");
+ return new MessageContent(MessageStatus.InvalidRequestBody, "null");
+ }
+ }
+
+ private static Dictionary<string, Peer.RequestHandler> InitializeRequestHandlers()
+ {
+ return new Dictionary<string, Peer.RequestHandler>
+ {
+ [PlayRequest.Id] = async (peer, content) =>
+ {
+ _ = JsonConvert.DeserializeObject<PlayRequest>(content.Body);
+ return await HandlePlay();
+ },
+ [DebugPlayRequest.Id] = async (peer, content) =>
+ {
+ var request = JsonConvert.DeserializeObject<DebugPlayRequest>(content.Body);
+ return await HandleDebugPlay(request);
+ },
+ [StopPlayRequest.Id] = async (peer, content) =>
+ {
+ var request = JsonConvert.DeserializeObject<StopPlayRequest>(content.Body);
+ return await HandleStopPlay(request);
+ },
+ [ReloadScriptsRequest.Id] = async (peer, content) =>
+ {
+ _ = JsonConvert.DeserializeObject<ReloadScriptsRequest>(content.Body);
+ return await HandleReloadScripts();
+ },
+ [CodeCompletionRequest.Id] = async (peer, content) =>
+ {
+ var request = JsonConvert.DeserializeObject<CodeCompletionRequest>(content.Body);
+ return await HandleCodeCompletionRequest(request);
+ }
+ };
+ }
+
+ private static Task<Response> HandlePlay()
+ {
+ DispatchToMainThread(() =>
+ {
+ GodotSharpEditor.Instance.CurrentPlaySettings = new PlaySettings();
+ Internal.EditorRunPlay();
+ GodotSharpEditor.Instance.CurrentPlaySettings = null;
+ });
+ return Task.FromResult<Response>(new PlayResponse());
+ }
+
+ private static Task<Response> HandleDebugPlay(DebugPlayRequest request)
+ {
+ DispatchToMainThread(() =>
+ {
+ GodotSharpEditor.Instance.CurrentPlaySettings =
+ new PlaySettings(request.DebuggerHost, request.DebuggerPort, request.BuildBeforePlaying ?? true);
+ Internal.EditorRunPlay();
+ GodotSharpEditor.Instance.CurrentPlaySettings = null;
+ });
+ return Task.FromResult<Response>(new DebugPlayResponse());
+ }
+
+ private static Task<Response> HandleStopPlay(StopPlayRequest request)
+ {
+ DispatchToMainThread(Internal.EditorRunStop);
+ return Task.FromResult<Response>(new StopPlayResponse());
+ }
+
+ private static Task<Response> HandleReloadScripts()
+ {
+ DispatchToMainThread(Internal.ScriptEditorDebugger_ReloadScripts);
+ return Task.FromResult<Response>(new ReloadScriptsResponse());
+ }
+
+ private static async Task<Response> HandleCodeCompletionRequest(CodeCompletionRequest request)
+ {
+ var response = new CodeCompletionResponse {Kind = request.Kind, ScriptFile = request.ScriptFile};
+ response.Suggestions = await Task.Run(() => Internal.CodeCompletionRequest(response.Kind, response.ScriptFile));
+ return response;
+ }
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs
index 6026c109ad..d6fa2eeba7 100644
--- a/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs
@@ -7,14 +7,16 @@ using GodotTools.Utils;
namespace GodotTools.Ides.MonoDevelop
{
- public class Instance
+ public class Instance : IDisposable
{
+ public DateTime LaunchTime { get; private set; }
private readonly string solutionFile;
private readonly EditorId editorId;
private Process process;
public bool IsRunning => process != null && !process.HasExited;
+ public bool IsDisposed { get; private set; }
public void Execute()
{
@@ -59,6 +61,8 @@ namespace GodotTools.Ides.MonoDevelop
if (command == null)
throw new FileNotFoundException();
+ LaunchTime = DateTime.Now;
+
if (newWindow)
{
process = Process.Start(new ProcessStartInfo
@@ -88,6 +92,12 @@ namespace GodotTools.Ides.MonoDevelop
this.editorId = editorId;
}
+ public void Dispose()
+ {
+ IsDisposed = true;
+ process?.Dispose();
+ }
+
private static readonly IReadOnlyDictionary<EditorId, string> ExecutableNames;
private static readonly IReadOnlyDictionary<EditorId, string> BundleIds;
@@ -118,7 +128,7 @@ namespace GodotTools.Ides.MonoDevelop
{EditorId.MonoDevelop, "MonoDevelop.exe"}
};
}
- else if (OS.IsUnixLike())
+ else if (OS.IsUnixLike)
{
ExecutableNames = new Dictionary<EditorId, string>
{
diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs
index e3a4fa7b45..e22e9af919 100644
--- a/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs
@@ -36,7 +36,7 @@ namespace GodotTools.Ides.Rider
{
return CollectRiderInfosMac();
}
- if (OS.IsUnixLike())
+ if (OS.IsUnixLike)
{
return CollectAllRiderPathsLinux();
}
@@ -141,16 +141,16 @@ namespace GodotTools.Ides.Rider
if (OS.IsOSX)
{
var home = Environment.GetEnvironmentVariable("HOME");
- if (string.IsNullOrEmpty(home))
+ if (string.IsNullOrEmpty(home))
return string.Empty;
var localAppData = Path.Combine(home, @"Library/Application Support");
return GetToolboxRiderRootPath(localAppData);
}
- if (OS.IsUnixLike())
+ if (OS.IsUnixLike)
{
var home = Environment.GetEnvironmentVariable("HOME");
- if (string.IsNullOrEmpty(home))
+ if (string.IsNullOrEmpty(home))
return string.Empty;
var localAppData = Path.Combine(home, @".local/share");
return GetToolboxRiderRootPath(localAppData);
@@ -209,7 +209,7 @@ namespace GodotTools.Ides.Rider
private static string GetRelativePathToBuildTxt()
{
- if (OS.IsWindows || OS.IsUnixLike())
+ if (OS.IsWindows || OS.IsUnixLike)
return "../../build.txt";
if (OS.IsOSX)
return "Contents/Resources/build.txt";
@@ -322,7 +322,7 @@ namespace GodotTools.Ides.Rider
class SettingsJson
{
public string install_location;
-
+
[CanBeNull]
public static string GetInstallLocationFromJson(string json)
{
diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs
index 026a7db89c..7e5049e4b7 100644
--- a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs
@@ -2,6 +2,7 @@ using System;
using System.Runtime.CompilerServices;
using Godot;
using Godot.Collections;
+using GodotTools.IdeMessaging.Requests;
namespace GodotTools.Internals
{
@@ -52,6 +53,9 @@ namespace GodotTools.Internals
public static void ScriptEditorDebugger_ReloadScripts() => internal_ScriptEditorDebugger_ReloadScripts();
+ public static string[] CodeCompletionRequest(CodeCompletionRequest.CompletionKind kind, string scriptFile) =>
+ internal_CodeCompletionRequest((int)kind, scriptFile);
+
#region Internal
[MethodImpl(MethodImplOptions.InternalCall)]
@@ -111,6 +115,9 @@ namespace GodotTools.Internals
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void internal_ScriptEditorDebugger_ReloadScripts();
+ [MethodImpl(MethodImplOptions.InternalCall)]
+ private static extern string[] internal_CodeCompletionRequest(int kind, string scriptFile);
+
#endregion
}
}
diff --git a/modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs b/modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs
new file mode 100644
index 0000000000..820d0c0b83
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs
@@ -0,0 +1,19 @@
+namespace GodotTools
+{
+ public struct PlaySettings
+ {
+ public bool HasDebugger { get; }
+ public string DebuggerHost { get; }
+ public int DebuggerPort { get; }
+
+ public bool BuildBeforePlaying { get; }
+
+ public PlaySettings(string debuggerHost, int debuggerPort, bool buildBeforePlaying)
+ {
+ HasDebugger = true;
+ DebuggerHost = debuggerHost;
+ DebuggerPort = debuggerPort;
+ BuildBeforePlaying = buildBeforePlaying;
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools/Properties/AssemblyInfo.cs b/modules/mono/editor/GodotTools/GodotTools/Properties/AssemblyInfo.cs
deleted file mode 100644
index f5fe85c722..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-
-// Information about this assembly is defined by the following attributes.
-// Change them to the values specific to your project.
-
-[assembly: AssemblyTitle("GodotTools")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("")]
-[assembly: AssemblyCopyright("Godot Engine contributors")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
-// The form "{Major}.{Minor}.*" will automatically update the build and revision,
-// and "{Major}.{Minor}.{Build}.*" will update just the revision.
-
-[assembly: AssemblyVersion("1.0.*")]
-
-// The following attributes are used to specify the signing key for the assembly,
-// if desired. See the Mono documentation for more information about signing.
-
-//[assembly: AssemblyDelaySign(false)]
-//[assembly: AssemblyKeyFile("")]
diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs
index b057ac12c6..6c05891f2c 100644
--- a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs
@@ -22,7 +22,10 @@ namespace GodotTools.Utils
{
public const string Windows = "Windows";
public const string OSX = "OSX";
- public const string X11 = "X11";
+ public const string Linux = "Linux";
+ public const string FreeBSD = "FreeBSD";
+ public const string NetBSD = "NetBSD";
+ public const string BSD = "BSD";
public const string Server = "Server";
public const string UWP = "UWP";
public const string Haiku = "Haiku";
@@ -35,7 +38,7 @@ namespace GodotTools.Utils
{
public const string Windows = "windows";
public const string OSX = "osx";
- public const string X11 = "linuxbsd";
+ public const string LinuxBSD = "linuxbsd";
public const string Server = "server";
public const string UWP = "uwp";
public const string Haiku = "haiku";
@@ -48,7 +51,10 @@ namespace GodotTools.Utils
{
[Names.Windows] = Platforms.Windows,
[Names.OSX] = Platforms.OSX,
- [Names.X11] = Platforms.X11,
+ [Names.Linux] = Platforms.LinuxBSD,
+ [Names.FreeBSD] = Platforms.LinuxBSD,
+ [Names.NetBSD] = Platforms.LinuxBSD,
+ [Names.BSD] = Platforms.LinuxBSD,
[Names.Server] = Platforms.Server,
[Names.UWP] = Platforms.UWP,
[Names.Haiku] = Platforms.Haiku,
@@ -62,38 +68,39 @@ namespace GodotTools.Utils
return name.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase);
}
+ private static bool IsAnyOS(IEnumerable<string> names)
+ {
+ return names.Any(p => p.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase));
+ }
+
+ private static readonly IEnumerable<string> LinuxBSDPlatforms =
+ new[] {Names.Linux, Names.FreeBSD, Names.NetBSD, Names.BSD};
+
+ private static readonly IEnumerable<string> UnixLikePlatforms =
+ new[] {Names.OSX, Names.Server, Names.Haiku, Names.Android, Names.iOS}
+ .Concat(LinuxBSDPlatforms).ToArray();
+
private static readonly Lazy<bool> _isWindows = new Lazy<bool>(() => IsOS(Names.Windows));
private static readonly Lazy<bool> _isOSX = new Lazy<bool>(() => IsOS(Names.OSX));
- private static readonly Lazy<bool> _isX11 = new Lazy<bool>(() => IsOS(Names.X11));
+ private static readonly Lazy<bool> _isLinuxBSD = new Lazy<bool>(() => IsAnyOS(LinuxBSDPlatforms));
private static readonly Lazy<bool> _isServer = new Lazy<bool>(() => IsOS(Names.Server));
private static readonly Lazy<bool> _isUWP = new Lazy<bool>(() => IsOS(Names.UWP));
private static readonly Lazy<bool> _isHaiku = new Lazy<bool>(() => IsOS(Names.Haiku));
private static readonly Lazy<bool> _isAndroid = new Lazy<bool>(() => IsOS(Names.Android));
private static readonly Lazy<bool> _isiOS = new Lazy<bool>(() => IsOS(Names.iOS));
private static readonly Lazy<bool> _isHTML5 = new Lazy<bool>(() => IsOS(Names.HTML5));
+ private static readonly Lazy<bool> _isUnixLike = new Lazy<bool>(() => IsAnyOS(UnixLikePlatforms));
public static bool IsWindows => _isWindows.Value || IsUWP;
public static bool IsOSX => _isOSX.Value;
- public static bool IsX11 => _isX11.Value;
+ public static bool IsLinuxBSD => _isLinuxBSD.Value;
public static bool IsServer => _isServer.Value;
public static bool IsUWP => _isUWP.Value;
public static bool IsHaiku => _isHaiku.Value;
public static bool IsAndroid => _isAndroid.Value;
public static bool IsiOS => _isiOS.Value;
public static bool IsHTML5 => _isHTML5.Value;
-
- private static bool? _isUnixCache;
- private static readonly string[] UnixLikePlatforms = { Names.OSX, Names.X11, Names.Server, Names.Haiku, Names.Android, Names.iOS };
-
- public static bool IsUnixLike()
- {
- if (_isUnixCache.HasValue)
- return _isUnixCache.Value;
-
- string osName = GetPlatformName();
- _isUnixCache = UnixLikePlatforms.Any(p => p.Equals(osName, StringComparison.OrdinalIgnoreCase));
- return _isUnixCache.Value;
- }
+ public static bool IsUnixLike => _isUnixLike.Value;
public static char PathSep => IsWindows ? ';' : ':';
@@ -121,10 +128,10 @@ namespace GodotTools.Utils
return searchDirs.Select(dir => Path.Combine(dir, name)).FirstOrDefault(File.Exists);
return (from dir in searchDirs
- select Path.Combine(dir, name)
+ select Path.Combine(dir, name)
into path
- from ext in windowsExts
- select path + ext).FirstOrDefault(File.Exists);
+ from ext in windowsExts
+ select path + ext).FirstOrDefault(File.Exists);
}
private static string PathWhichUnix([NotNull] string name)
@@ -189,7 +196,7 @@ namespace GodotTools.Utils
startInfo.UseShellExecute = false;
- using (var process = new Process { StartInfo = startInfo })
+ using (var process = new Process {StartInfo = startInfo})
{
process.Start();
process.WaitForExit();
diff --git a/modules/mono/editor/GodotTools/GodotTools/packages.config b/modules/mono/editor/GodotTools/GodotTools/packages.config
deleted file mode 100644
index dd3de2865a..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools/packages.config
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<packages>
- <package id="JetBrains.Annotations" version="2019.1.3" targetFramework="net45" />
- <package id="Newtonsoft.Json" version="12.0.3" targetFramework="net45" />
-</packages>
diff --git a/modules/mono/editor/bindings_generator.cpp b/modules/mono/editor/bindings_generator.cpp
index 71bb8ff851..730ffcb945 100644
--- a/modules/mono/editor/bindings_generator.cpp
+++ b/modules/mono/editor/bindings_generator.cpp
@@ -105,7 +105,6 @@
const char *BindingsGenerator::TypeInterface::DEFAULT_VARARG_C_IN("\t%0 %1_in = %1;\n");
static String fix_doc_description(const String &p_bbcode) {
-
// This seems to be the correct way to do this. It's the same EditorHelp does.
return p_bbcode.dedent()
@@ -115,7 +114,6 @@ static String fix_doc_description(const String &p_bbcode) {
}
static String snake_to_pascal_case(const String &p_identifier, bool p_input_is_upper = false) {
-
String ret;
Vector<String> parts = p_identifier.split("_", true);
@@ -125,8 +123,9 @@ static String snake_to_pascal_case(const String &p_identifier, bool p_input_is_u
if (part.length()) {
part[0] = _find_upper(part[0]);
if (p_input_is_upper) {
- for (int j = 1; j < part.length(); j++)
+ for (int j = 1; j < part.length(); j++) {
part[j] = _find_lower(part[j]);
+ }
}
ret += part;
} else {
@@ -148,7 +147,6 @@ static String snake_to_pascal_case(const String &p_identifier, bool p_input_is_u
}
static String snake_to_camel_case(const String &p_identifier, bool p_input_is_upper = false) {
-
String ret;
Vector<String> parts = p_identifier.split("_", true);
@@ -160,8 +158,9 @@ static String snake_to_camel_case(const String &p_identifier, bool p_input_is_up
part[0] = _find_upper(part[0]);
}
if (p_input_is_upper) {
- for (int j = i != 0 ? 1 : 0; j < part.length(); j++)
+ for (int j = i != 0 ? 1 : 0; j < part.length(); j++) {
part[j] = _find_lower(part[j]);
+ }
}
ret += part;
} else {
@@ -183,11 +182,11 @@ static String snake_to_camel_case(const String &p_identifier, bool p_input_is_up
}
String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterface *p_itype) {
-
// Based on the version in EditorHelp
- if (p_bbcode.empty())
+ if (p_bbcode.empty()) {
return String();
+ }
DocData *doc = EditorHelp::get_doc_data();
@@ -204,8 +203,9 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf
while (pos < bbcode.length()) {
int brk_pos = bbcode.find("[", pos);
- if (brk_pos < 0)
+ if (brk_pos < 0) {
brk_pos = bbcode.length();
+ }
if (brk_pos > pos) {
String text = bbcode.substr(pos, brk_pos - pos);
@@ -214,19 +214,22 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf
} else {
Vector<String> lines = text.split("\n");
for (int i = 0; i < lines.size(); i++) {
- if (i != 0)
+ if (i != 0) {
xml_output.append("<para>");
+ }
xml_output.append(lines[i].xml_escape());
- if (i != lines.size() - 1)
+ if (i != lines.size() - 1) {
xml_output.append("</para>\n");
+ }
}
}
}
- if (brk_pos == bbcode.length())
+ if (brk_pos == bbcode.length()) {
break; // nothing else to add
+ }
int brk_end = bbcode.find("]", brk_pos + 1);
@@ -237,13 +240,15 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf
} else {
Vector<String> lines = text.split("\n");
for (int i = 0; i < lines.size(); i++) {
- if (i != 0)
+ if (i != 0) {
xml_output.append("<para>");
+ }
xml_output.append(lines[i].xml_escape());
- if (i != lines.size() - 1)
+ if (i != lines.size() - 1) {
xml_output.append("</para>\n");
+ }
}
}
@@ -416,8 +421,9 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf
for (const List<EnumInterface>::Element *E = global_enums.front(); E; E = E->next()) {
target_ienum = &E->get();
target_iconst = find_constant_by_name(target_name, target_ienum->constants);
- if (target_iconst)
+ if (target_iconst) {
break;
+ }
}
if (target_iconst) {
@@ -454,8 +460,9 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf
for (const List<EnumInterface>::Element *E = target_itype->enums.front(); E; E = E->next()) {
target_ienum = &E->get();
target_iconst = find_constant_by_name(target_name, target_ienum->constants);
- if (target_iconst)
+ if (target_iconst) {
break;
+ }
}
if (target_iconst) {
@@ -566,8 +573,12 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf
code_tag = true;
pos = brk_end + 1;
tag_stack.push_front(tag);
+ } else if (tag == "kbd") {
+ // keyboard combinations are not supported in xml comments
+ pos = brk_end + 1;
+ tag_stack.push_front(tag);
} else if (tag == "center") {
- // center is alignment not supported in xml comments
+ // center alignment is not supported in xml comments
pos = brk_end + 1;
tag_stack.push_front(tag);
} else if (tag == "br") {
@@ -583,8 +594,9 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf
tag_stack.push_front(tag);
} else if (tag == "url") {
int end = bbcode.find("[", brk_end);
- if (end == -1)
+ if (end == -1) {
end = bbcode.length();
+ }
String url = bbcode.substr(brk_end + 1, end - brk_end - 1);
xml_output.append("<a href=\"");
xml_output.append(url);
@@ -603,8 +615,9 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf
tag_stack.push_front("url");
} else if (tag == "img") {
int end = bbcode.find("[", brk_end);
- if (end == -1)
+ if (end == -1) {
end = bbcode.length();
+ }
String image = bbcode.substr(brk_end + 1, end - brk_end - 1);
// Not supported. Just append the bbcode.
@@ -634,15 +647,15 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf
}
int BindingsGenerator::_determine_enum_prefix(const EnumInterface &p_ienum) {
-
CRASH_COND(p_ienum.constants.empty());
const ConstantInterface &front_iconstant = p_ienum.constants.front()->get();
Vector<String> front_parts = front_iconstant.name.split("_", /* p_allow_empty: */ true);
int candidate_len = front_parts.size() - 1;
- if (candidate_len == 0)
+ if (candidate_len == 0) {
return 0;
+ }
for (const List<ConstantInterface>::Element *E = p_ienum.constants.front()->next(); E; E = E->next()) {
const ConstantInterface &iconstant = E->get();
@@ -654,21 +667,22 @@ int BindingsGenerator::_determine_enum_prefix(const EnumInterface &p_ienum) {
if (front_parts[i] != parts[i]) {
// HARDCODED: Some Flag enums have the prefix 'FLAG_' for everything except 'FLAGS_DEFAULT' (same for 'METHOD_FLAG_' and'METHOD_FLAGS_DEFAULT').
bool hardcoded_exc = (i == candidate_len - 1 && ((front_parts[i] == "FLAGS" && parts[i] == "FLAG") || (front_parts[i] == "FLAG" && parts[i] == "FLAGS")));
- if (!hardcoded_exc)
+ if (!hardcoded_exc) {
break;
+ }
}
}
candidate_len = i;
- if (candidate_len == 0)
+ if (candidate_len == 0) {
return 0;
+ }
}
return candidate_len;
}
void BindingsGenerator::_apply_prefix_to_enum_constants(BindingsGenerator::EnumInterface &p_ienum, int p_prefix_length) {
-
if (p_prefix_length > 0) {
for (List<ConstantInterface>::Element *E = p_ienum.constants.front(); E; E = E->next()) {
int curr_prefix_length = p_prefix_length;
@@ -679,22 +693,25 @@ void BindingsGenerator::_apply_prefix_to_enum_constants(BindingsGenerator::EnumI
Vector<String> parts = constant_name.split("_", /* p_allow_empty: */ true);
- if (parts.size() <= curr_prefix_length)
+ if (parts.size() <= curr_prefix_length) {
continue;
+ }
if (parts[curr_prefix_length][0] >= '0' && parts[curr_prefix_length][0] <= '9') {
// The name of enum constants may begin with a numeric digit when strip from the enum prefix,
// so we make the prefix for this constant one word shorter in those cases.
for (curr_prefix_length = curr_prefix_length - 1; curr_prefix_length > 0; curr_prefix_length--) {
- if (parts[curr_prefix_length][0] < '0' || parts[curr_prefix_length][0] > '9')
+ if (parts[curr_prefix_length][0] < '0' || parts[curr_prefix_length][0] > '9') {
break;
+ }
}
}
constant_name = "";
for (int i = curr_prefix_length; i < parts.size(); i++) {
- if (i > curr_prefix_length)
+ if (i > curr_prefix_length) {
constant_name += "_";
+ }
constant_name += parts[i];
}
@@ -704,12 +721,12 @@ void BindingsGenerator::_apply_prefix_to_enum_constants(BindingsGenerator::EnumI
}
void BindingsGenerator::_generate_method_icalls(const TypeInterface &p_itype) {
-
for (const List<MethodInterface>::Element *E = p_itype.methods.front(); E; E = E->next()) {
const MethodInterface &imethod = E->get();
- if (imethod.is_virtual)
+ if (imethod.is_virtual) {
continue;
+ }
const TypeInterface *return_type = _get_type_or_placeholder(imethod.return_type);
@@ -758,8 +775,9 @@ void BindingsGenerator::_generate_method_icalls(const TypeInterface &p_itype) {
List<InternalCall>::Element *match = method_icalls.find(im_icall);
if (match) {
- if (p_itype.api_type != ClassDB::API_EDITOR)
+ if (p_itype.api_type != ClassDB::API_EDITOR) {
match->get().editor_only = false;
+ }
method_icalls_map.insert(&E->get(), &match->get());
} else {
List<InternalCall>::Element *added = method_icalls.push_back(im_icall);
@@ -769,7 +787,6 @@ void BindingsGenerator::_generate_method_icalls(const TypeInterface &p_itype) {
}
void BindingsGenerator::_generate_global_constants(StringBuilder &p_output) {
-
// Constants (in partial GD class)
p_output.append("\n#pragma warning disable CS1591 // Disable warning: "
@@ -805,8 +822,9 @@ void BindingsGenerator::_generate_global_constants(StringBuilder &p_output) {
p_output.append(";");
}
- if (!global_constants.empty())
+ if (!global_constants.empty()) {
p_output.append("\n");
+ }
p_output.append(INDENT1 CLOSE_BLOCK); // end of GD class
@@ -868,8 +886,9 @@ void BindingsGenerator::_generate_global_constants(StringBuilder &p_output) {
p_output.append(INDENT1 CLOSE_BLOCK);
- if (enum_in_static_class)
+ if (enum_in_static_class) {
p_output.append(INDENT1 CLOSE_BLOCK);
+ }
}
p_output.append(CLOSE_BLOCK); // end of namespace
@@ -878,7 +897,6 @@ void BindingsGenerator::_generate_global_constants(StringBuilder &p_output) {
}
Error BindingsGenerator::generate_cs_core_project(const String &p_proj_dir) {
-
ERR_FAIL_COND_V(!initialized, ERR_UNCONFIGURED);
DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
@@ -904,8 +922,9 @@ Error BindingsGenerator::generate_cs_core_project(const String &p_proj_dir) {
_generate_global_constants(constants_source);
String output_file = path::join(base_gen_dir, BINDINGS_GLOBAL_SCOPE_CLASS "_constants.cs");
Error save_err = _save_file(output_file, constants_source);
- if (save_err != OK)
+ if (save_err != OK) {
return save_err;
+ }
compile_items.push_back(output_file);
}
@@ -913,17 +932,20 @@ Error BindingsGenerator::generate_cs_core_project(const String &p_proj_dir) {
for (OrderedHashMap<StringName, TypeInterface>::Element E = obj_types.front(); E; E = E.next()) {
const TypeInterface &itype = E.get();
- if (itype.api_type == ClassDB::API_EDITOR)
+ if (itype.api_type == ClassDB::API_EDITOR) {
continue;
+ }
String output_file = path::join(godot_objects_gen_dir, itype.proxy_name + ".cs");
Error err = _generate_cs_type(itype, output_file);
- if (err == ERR_SKIP)
+ if (err == ERR_SKIP) {
continue;
+ }
- if (err != OK)
+ if (err != OK) {
return err;
+ }
compile_items.push_back(output_file);
}
@@ -954,10 +976,12 @@ Error BindingsGenerator::generate_cs_core_project(const String &p_proj_dir) {
cs_icalls_content.append(m_icall.im_sig + ");\n"); \
}
- for (const List<InternalCall>::Element *E = core_custom_icalls.front(); E; E = E->next())
+ for (const List<InternalCall>::Element *E = core_custom_icalls.front(); E; E = E->next()) {
ADD_INTERNAL_CALL(E->get());
- for (const List<InternalCall>::Element *E = method_icalls.front(); E; E = E->next())
+ }
+ for (const List<InternalCall>::Element *E = method_icalls.front(); E; E = E->next()) {
ADD_INTERNAL_CALL(E->get());
+ }
#undef ADD_INTERNAL_CALL
@@ -966,8 +990,9 @@ Error BindingsGenerator::generate_cs_core_project(const String &p_proj_dir) {
String internal_methods_file = path::join(base_gen_dir, BINDINGS_CLASS_NATIVECALLS ".cs");
Error err = _save_file(internal_methods_file, cs_icalls_content);
- if (err != OK)
+ if (err != OK) {
return err;
+ }
compile_items.push_back(internal_methods_file);
@@ -986,14 +1011,14 @@ Error BindingsGenerator::generate_cs_core_project(const String &p_proj_dir) {
String includes_props_file = path::join(base_gen_dir, "GeneratedIncludes.props");
err = _save_file(includes_props_file, includes_props_content);
- if (err != OK)
+ if (err != OK) {
return err;
+ }
return OK;
}
Error BindingsGenerator::generate_cs_editor_project(const String &p_proj_dir) {
-
ERR_FAIL_COND_V(!initialized, ERR_UNCONFIGURED);
DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
@@ -1016,17 +1041,20 @@ Error BindingsGenerator::generate_cs_editor_project(const String &p_proj_dir) {
for (OrderedHashMap<StringName, TypeInterface>::Element E = obj_types.front(); E; E = E.next()) {
const TypeInterface &itype = E.get();
- if (itype.api_type != ClassDB::API_EDITOR)
+ if (itype.api_type != ClassDB::API_EDITOR) {
continue;
+ }
String output_file = path::join(godot_objects_gen_dir, itype.proxy_name + ".cs");
Error err = _generate_cs_type(itype, output_file);
- if (err == ERR_SKIP)
+ if (err == ERR_SKIP) {
continue;
+ }
- if (err != OK)
+ if (err != OK) {
return err;
+ }
compile_items.push_back(output_file);
}
@@ -1056,10 +1084,12 @@ Error BindingsGenerator::generate_cs_editor_project(const String &p_proj_dir) {
cs_icalls_content.append(m_icall.im_sig + ");\n"); \
}
- for (const List<InternalCall>::Element *E = editor_custom_icalls.front(); E; E = E->next())
+ for (const List<InternalCall>::Element *E = editor_custom_icalls.front(); E; E = E->next()) {
ADD_INTERNAL_CALL(E->get());
- for (const List<InternalCall>::Element *E = method_icalls.front(); E; E = E->next())
+ }
+ for (const List<InternalCall>::Element *E = method_icalls.front(); E; E = E->next()) {
ADD_INTERNAL_CALL(E->get());
+ }
#undef ADD_INTERNAL_CALL
@@ -1068,8 +1098,9 @@ Error BindingsGenerator::generate_cs_editor_project(const String &p_proj_dir) {
String internal_methods_file = path::join(base_gen_dir, BINDINGS_CLASS_NATIVECALLS_EDITOR ".cs");
Error err = _save_file(internal_methods_file, cs_icalls_content);
- if (err != OK)
+ if (err != OK) {
return err;
+ }
compile_items.push_back(internal_methods_file);
@@ -1088,14 +1119,14 @@ Error BindingsGenerator::generate_cs_editor_project(const String &p_proj_dir) {
String includes_props_file = path::join(base_gen_dir, "GeneratedIncludes.props");
err = _save_file(includes_props_file, includes_props_content);
- if (err != OK)
+ if (err != OK) {
return err;
+ }
return OK;
}
Error BindingsGenerator::generate_cs_api(const String &p_output_dir) {
-
ERR_FAIL_COND_V(!initialized, ERR_UNCONFIGURED);
String output_dir = path::abspath(path::realpath(p_output_dir));
@@ -1143,7 +1174,6 @@ Error BindingsGenerator::generate_cs_api(const String &p_output_dir) {
// - Csc warning e.g.:
// ObjectType/LineEdit.cs(140,38): warning CS0108: 'LineEdit.FocusMode' hides inherited member 'Control.FocusMode'. Use the new keyword if hiding was intended.
Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const String &p_output_file) {
-
CRASH_COND(!itype.is_object_type);
bool is_derived_type = itype.base_name != StringName();
@@ -1218,93 +1248,89 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str
output.append(INDENT1 "{");
- if (class_doc) {
+ // Add constants
- // Add constants
-
- for (const List<ConstantInterface>::Element *E = itype.constants.front(); E; E = E->next()) {
- const ConstantInterface &iconstant = E->get();
-
- if (iconstant.const_doc && iconstant.const_doc->description.size()) {
- String xml_summary = bbcode_to_xml(fix_doc_description(iconstant.const_doc->description), &itype);
- Vector<String> summary_lines = xml_summary.length() ? xml_summary.split("\n") : Vector<String>();
+ for (const List<ConstantInterface>::Element *E = itype.constants.front(); E; E = E->next()) {
+ const ConstantInterface &iconstant = E->get();
- if (summary_lines.size()) {
- output.append(MEMBER_BEGIN "/// <summary>\n");
+ if (iconstant.const_doc && iconstant.const_doc->description.size()) {
+ String xml_summary = bbcode_to_xml(fix_doc_description(iconstant.const_doc->description), &itype);
+ Vector<String> summary_lines = xml_summary.length() ? xml_summary.split("\n") : Vector<String>();
- for (int i = 0; i < summary_lines.size(); i++) {
- output.append(INDENT2 "/// ");
- output.append(summary_lines[i]);
- output.append("\n");
- }
+ if (summary_lines.size()) {
+ output.append(MEMBER_BEGIN "/// <summary>\n");
- output.append(INDENT2 "/// </summary>");
+ for (int i = 0; i < summary_lines.size(); i++) {
+ output.append(INDENT2 "/// ");
+ output.append(summary_lines[i]);
+ output.append("\n");
}
- }
- output.append(MEMBER_BEGIN "public const int ");
- output.append(iconstant.proxy_name);
- output.append(" = ");
- output.append(itos(iconstant.value));
- output.append(";");
+ output.append(INDENT2 "/// </summary>");
+ }
}
- if (itype.constants.size())
- output.append("\n");
+ output.append(MEMBER_BEGIN "public const int ");
+ output.append(iconstant.proxy_name);
+ output.append(" = ");
+ output.append(itos(iconstant.value));
+ output.append(";");
+ }
- // Add enums
+ if (itype.constants.size()) {
+ output.append("\n");
+ }
- for (const List<EnumInterface>::Element *E = itype.enums.front(); E; E = E->next()) {
- const EnumInterface &ienum = E->get();
+ // Add enums
- ERR_FAIL_COND_V(ienum.constants.empty(), ERR_BUG);
+ for (const List<EnumInterface>::Element *E = itype.enums.front(); E; E = E->next()) {
+ const EnumInterface &ienum = E->get();
- output.append(MEMBER_BEGIN "public enum ");
- output.append(ienum.cname.operator String());
- output.append(MEMBER_BEGIN OPEN_BLOCK);
+ ERR_FAIL_COND_V(ienum.constants.empty(), ERR_BUG);
- for (const List<ConstantInterface>::Element *F = ienum.constants.front(); F; F = F->next()) {
- const ConstantInterface &iconstant = F->get();
+ output.append(MEMBER_BEGIN "public enum ");
+ output.append(ienum.cname.operator String());
+ output.append(MEMBER_BEGIN OPEN_BLOCK);
- if (iconstant.const_doc && iconstant.const_doc->description.size()) {
- String xml_summary = bbcode_to_xml(fix_doc_description(iconstant.const_doc->description), &itype);
- Vector<String> summary_lines = xml_summary.length() ? xml_summary.split("\n") : Vector<String>();
+ for (const List<ConstantInterface>::Element *F = ienum.constants.front(); F; F = F->next()) {
+ const ConstantInterface &iconstant = F->get();
- if (summary_lines.size()) {
- output.append(INDENT3 "/// <summary>\n");
+ if (iconstant.const_doc && iconstant.const_doc->description.size()) {
+ String xml_summary = bbcode_to_xml(fix_doc_description(iconstant.const_doc->description), &itype);
+ Vector<String> summary_lines = xml_summary.length() ? xml_summary.split("\n") : Vector<String>();
- for (int i = 0; i < summary_lines.size(); i++) {
- output.append(INDENT3 "/// ");
- output.append(summary_lines[i]);
- output.append("\n");
- }
+ if (summary_lines.size()) {
+ output.append(INDENT3 "/// <summary>\n");
- output.append(INDENT3 "/// </summary>\n");
+ for (int i = 0; i < summary_lines.size(); i++) {
+ output.append(INDENT3 "/// ");
+ output.append(summary_lines[i]);
+ output.append("\n");
}
- }
- output.append(INDENT3);
- output.append(iconstant.proxy_name);
- output.append(" = ");
- output.append(itos(iconstant.value));
- output.append(F != ienum.constants.back() ? ",\n" : "\n");
+ output.append(INDENT3 "/// </summary>\n");
+ }
}
- output.append(INDENT2 CLOSE_BLOCK);
+ output.append(INDENT3);
+ output.append(iconstant.proxy_name);
+ output.append(" = ");
+ output.append(itos(iconstant.value));
+ output.append(F != ienum.constants.back() ? ",\n" : "\n");
}
- // Add properties
-
- for (const List<PropertyInterface>::Element *E = itype.properties.front(); E; E = E->next()) {
- const PropertyInterface &iprop = E->get();
- Error prop_err = _generate_cs_property(itype, iprop, output);
- ERR_FAIL_COND_V_MSG(prop_err != OK, prop_err,
- "Failed to generate property '" + iprop.cname.operator String() +
- "' for class '" + itype.name + "'.");
- }
+ output.append(INDENT2 CLOSE_BLOCK);
}
- // TODO: BINDINGS_NATIVE_NAME_FIELD should be StringName, once we support it in C#
+ // Add properties
+
+ for (const List<PropertyInterface>::Element *E = itype.properties.front(); E; E = E->next()) {
+ const PropertyInterface &iprop = E->get();
+ Error prop_err = _generate_cs_property(itype, iprop, output);
+ ERR_FAIL_COND_V_MSG(prop_err != OK, prop_err,
+ "Failed to generate property '" + iprop.cname.operator String() +
+ "' for class '" + itype.name + "'.");
+ }
if (itype.is_singleton) {
// Add the type name and the singleton pointer as static fields
@@ -1377,15 +1403,17 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str
if (itype.is_singleton) {
InternalCall singleton_icall = InternalCall(itype.api_type, ICALL_PREFIX + itype.name + SINGLETON_ICALL_SUFFIX, "IntPtr");
- if (!find_icall_by_name(singleton_icall.name, custom_icalls))
+ if (!find_icall_by_name(singleton_icall.name, custom_icalls)) {
custom_icalls.push_back(singleton_icall);
+ }
}
if (is_derived_type && itype.is_instantiable) {
InternalCall ctor_icall = InternalCall(itype.api_type, ctor_method, "IntPtr", itype.proxy_name + " obj");
- if (!find_icall_by_name(ctor_icall.name, custom_icalls))
+ if (!find_icall_by_name(ctor_icall.name, custom_icalls)) {
custom_icalls.push_back(ctor_icall);
+ }
}
output.append(INDENT1 CLOSE_BLOCK /* class */
@@ -1399,7 +1427,6 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str
}
Error BindingsGenerator::_generate_cs_property(const BindingsGenerator::TypeInterface &p_itype, const PropertyInterface &p_iprop, StringBuilder &p_output) {
-
const MethodInterface *setter = p_itype.find_method_by_name(p_iprop.setter);
// Search it in base types too
@@ -1452,6 +1479,9 @@ Error BindingsGenerator::_generate_cs_property(const BindingsGenerator::TypeInte
const TypeInterface *prop_itype = _get_type_or_null(proptype_name);
ERR_FAIL_NULL_V(prop_itype, ERR_BUG); // Property type not found
+ ERR_FAIL_COND_V_MSG(prop_itype->is_singleton, ERR_BUG,
+ "Property type is a singleton: '" + p_itype.name + "." + String(p_iprop.cname) + "'.");
+
if (p_iprop.prop_doc && p_iprop.prop_doc->description.size()) {
String xml_summary = bbcode_to_xml(fix_doc_description(p_iprop.prop_doc->description), &p_itype);
Vector<String> summary_lines = xml_summary.length() ? xml_summary.split("\n") : Vector<String>();
@@ -1471,8 +1501,9 @@ Error BindingsGenerator::_generate_cs_property(const BindingsGenerator::TypeInte
p_output.append(MEMBER_BEGIN "public ");
- if (p_itype.is_singleton)
+ if (p_itype.is_singleton) {
p_output.append("static ");
+ }
p_output.append(prop_itype->cs_type);
p_output.append(" ");
@@ -1542,9 +1573,11 @@ Error BindingsGenerator::_generate_cs_property(const BindingsGenerator::TypeInte
}
Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterface &p_itype, const BindingsGenerator::MethodInterface &p_imethod, int &p_method_bind_count, StringBuilder &p_output) {
-
const TypeInterface *return_type = _get_type_or_placeholder(p_imethod.return_type);
+ ERR_FAIL_COND_V_MSG(return_type->is_singleton, ERR_BUG,
+ "Method return type is a singleton: '" + p_itype.name + "." + p_imethod.name + "'.");
+
String method_bind_field = "__method_bind_" + itos(p_method_bind_count);
String arguments_sig;
@@ -1560,29 +1593,41 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf
const ArgumentInterface &iarg = F->get();
const TypeInterface *arg_type = _get_type_or_placeholder(iarg.type);
+ ERR_FAIL_COND_V_MSG(arg_type->is_singleton, ERR_BUG,
+ "Argument type is a singleton: '" + iarg.name + "' of method '" + p_itype.name + "." + p_imethod.name + "'.");
+
+ if (iarg.default_argument.size()) {
+ CRASH_COND_MSG(!_arg_default_value_is_assignable_to_type(iarg.def_param_value, *arg_type),
+ "Invalid default value for parameter '" + iarg.name + "' of method '" + p_itype.name + "." + p_imethod.name + "'.");
+ }
+
// Add the current arguments to the signature
// If the argument has a default value which is not a constant, we will make it Nullable
{
- if (F != p_imethod.arguments.front())
+ if (F != p_imethod.arguments.front()) {
arguments_sig += ", ";
+ }
- if (iarg.def_param_mode == ArgumentInterface::NULLABLE_VAL)
+ if (iarg.def_param_mode == ArgumentInterface::NULLABLE_VAL) {
arguments_sig += "Nullable<";
+ }
arguments_sig += arg_type->cs_type;
- if (iarg.def_param_mode == ArgumentInterface::NULLABLE_VAL)
+ if (iarg.def_param_mode == ArgumentInterface::NULLABLE_VAL) {
arguments_sig += "> ";
- else
+ } else {
arguments_sig += " ";
+ }
arguments_sig += iarg.name;
if (iarg.default_argument.size()) {
- if (iarg.def_param_mode != ArgumentInterface::CONSTANT)
+ if (iarg.def_param_mode != ArgumentInterface::CONSTANT) {
arguments_sig += " = null";
- else
+ } else {
arguments_sig += " = " + sformat(iarg.default_argument, arg_type->cs_type);
+ }
}
}
@@ -1600,17 +1645,19 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf
cs_in_statements += " = ";
cs_in_statements += iarg.name;
- if (iarg.def_param_mode == ArgumentInterface::NULLABLE_VAL)
+ if (iarg.def_param_mode == ArgumentInterface::NULLABLE_VAL) {
cs_in_statements += ".HasValue ? ";
- else
+ } else {
cs_in_statements += " != null ? ";
+ }
cs_in_statements += iarg.name;
- if (iarg.def_param_mode == ArgumentInterface::NULLABLE_VAL)
+ if (iarg.def_param_mode == ArgumentInterface::NULLABLE_VAL) {
cs_in_statements += ".Value : ";
- else
+ } else {
cs_in_statements += " : ";
+ }
String def_arg = sformat(iarg.default_argument, arg_type->cs_type);
@@ -1660,14 +1707,19 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf
}
if (!p_imethod.is_internal) {
+ // TODO: This alone adds ~0.2 MB of bloat to the core API assembly. It would be
+ // better to generate a table in the C++ glue instead. That way the strings wouldn't
+ // add that much extra bloat as they're already used in engine code. Also, it would
+ // probably be much faster than looking up the attributes when fetching methods.
p_output.append(MEMBER_BEGIN "[GodotMethod(\"");
p_output.append(p_imethod.name);
p_output.append("\")]");
}
if (p_imethod.is_deprecated) {
- if (p_imethod.deprecation_message.empty())
+ if (p_imethod.deprecation_message.empty()) {
WARN_PRINT("An empty deprecation message is discouraged. Method: '" + p_imethod.proxy_name + "'.");
+ }
p_output.append(MEMBER_BEGIN "[Obsolete(\"");
p_output.append(p_imethod.deprecation_message);
@@ -1727,8 +1779,9 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf
im_call += ".";
im_call += im_icall->name;
- if (p_imethod.arguments.size())
+ if (p_imethod.arguments.size()) {
p_output.append(cs_in_statements);
+ }
if (return_type->cname == name_cache.type_void) {
p_output.append(im_call + "(" + icall_params + ");\n");
@@ -1755,10 +1808,14 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf
const ArgumentInterface &iarg = F->get();
const TypeInterface *arg_type = _get_type_or_placeholder(iarg.type);
+ ERR_FAIL_COND_V_MSG(arg_type->is_singleton, ERR_BUG,
+ "Argument type is a singleton: '" + iarg.name + "' of signal" + p_itype.name + "." + p_isignal.name + "'.");
+
// Add the current arguments to the signature
- if (F != p_isignal.arguments.front())
+ if (F != p_isignal.arguments.front()) {
arguments_sig += ", ";
+ }
arguments_sig += arg_type->cs_type;
arguments_sig += " ";
@@ -1785,8 +1842,9 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf
}
if (p_isignal.is_deprecated) {
- if (p_isignal.deprecation_message.empty())
+ if (p_isignal.deprecation_message.empty()) {
WARN_PRINT("An empty deprecation message is discouraged. Signal: '" + p_isignal.proxy_name + "'.");
+ }
p_output.append(MEMBER_BEGIN "[Obsolete(\"");
p_output.append(p_isignal.deprecation_message);
@@ -1817,8 +1875,9 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf
// Generate event
p_output.append(MEMBER_BEGIN "[Signal]" MEMBER_BEGIN "public ");
- if (p_itype.is_singleton)
+ if (p_itype.is_singleton) {
p_output.append("static ");
+ }
p_output.append("event ");
p_output.append(delegate_name);
@@ -1826,18 +1885,20 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf
p_output.append(p_isignal.proxy_name);
p_output.append("\n" OPEN_BLOCK_L2);
- if (p_itype.is_singleton)
+ if (p_itype.is_singleton) {
p_output.append("add => Singleton.Connect(__signal_name_");
- else
+ } else {
p_output.append("add => Connect(__signal_name_");
+ }
p_output.append(p_isignal.name);
p_output.append(", new Callable(value));\n");
- if (p_itype.is_singleton)
+ if (p_itype.is_singleton) {
p_output.append(INDENT3 "remove => Singleton.Disconnect(__signal_name_");
- else
+ } else {
p_output.append(INDENT3 "remove => Disconnect(__signal_name_");
+ }
p_output.append(p_isignal.name);
p_output.append(", new Callable(value));\n");
@@ -1848,7 +1909,6 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf
}
Error BindingsGenerator::generate_glue(const String &p_output_dir) {
-
ERR_FAIL_COND_V(!initialized, ERR_UNCONFIGURED);
bool dir_exists = DirAccess::exists(p_output_dir);
@@ -1872,7 +1932,6 @@ Error BindingsGenerator::generate_glue(const String &p_output_dir) {
CRASH_COND(itype.cname != name_cache.type_Object);
CRASH_COND(!itype.is_instantiable);
CRASH_COND(itype.api_type != ClassDB::API_CORE);
- CRASH_COND(itype.is_reference);
CRASH_COND(itype.is_singleton);
}
@@ -1893,8 +1952,9 @@ Error BindingsGenerator::generate_glue(const String &p_output_dir) {
String singleton_icall_name = ICALL_PREFIX + itype.name + SINGLETON_ICALL_SUFFIX;
InternalCall singleton_icall = InternalCall(itype.api_type, singleton_icall_name, "IntPtr");
- if (!find_icall_by_name(singleton_icall.name, custom_icalls))
+ if (!find_icall_by_name(singleton_icall.name, custom_icalls)) {
custom_icalls.push_back(singleton_icall);
+ }
output.append("Object* ");
output.append(singleton_icall_name);
@@ -1906,8 +1966,9 @@ Error BindingsGenerator::generate_glue(const String &p_output_dir) {
if (is_derived_type && itype.is_instantiable) {
InternalCall ctor_icall = InternalCall(itype.api_type, ctor_method, "IntPtr", itype.proxy_name + " obj");
- if (!find_icall_by_name(ctor_icall.name, custom_icalls))
+ if (!find_icall_by_name(ctor_icall.name, custom_icalls)) {
custom_icalls.push_back(ctor_icall);
+ }
output.append("Object* ");
output.append(ctor_method);
@@ -1953,7 +2014,6 @@ Error BindingsGenerator::generate_glue(const String &p_output_dir) {
bool tools_sequence = false;
for (const List<InternalCall>::Element *E = core_custom_icalls.front(); E; E = E->next()) {
-
if (tools_sequence) {
if (!E->get().editor_only) {
tools_sequence = false;
@@ -2007,8 +2067,9 @@ Error BindingsGenerator::generate_glue(const String &p_output_dir) {
output.append("\n#endif // MONO_GLUE_ENABLED\n");
Error save_err = _save_file(path::join(p_output_dir, "mono_glue.gen.cpp"), output);
- if (save_err != OK)
+ if (save_err != OK) {
return save_err;
+ }
OS::get_singleton()->print("Mono glue generated successfully\n");
@@ -2020,7 +2081,6 @@ uint32_t BindingsGenerator::get_version() {
}
Error BindingsGenerator::_save_file(const String &p_path, const StringBuilder &p_content) {
-
FileAccessRef file = FileAccess::open(p_path, FileAccess::WRITE);
ERR_FAIL_COND_V_MSG(!file, ERR_FILE_CANT_WRITE, "Cannot open file: '" + p_path + "'.");
@@ -2032,9 +2092,9 @@ Error BindingsGenerator::_save_file(const String &p_path, const StringBuilder &p
}
Error BindingsGenerator::_generate_glue_method(const BindingsGenerator::TypeInterface &p_itype, const BindingsGenerator::MethodInterface &p_imethod, StringBuilder &p_output) {
-
- if (p_imethod.is_virtual)
+ if (p_imethod.is_virtual) {
return OK; // Ignore
+ }
bool ret_void = p_imethod.return_type.cname == name_cache.type_void;
@@ -2062,10 +2122,12 @@ Error BindingsGenerator::_generate_glue_method(const BindingsGenerator::TypeInte
c_in_statements += sformat(", &%s_in);\n", c_param_name);
}
} else {
- if (i > 0)
+ if (i > 0) {
c_args_var_content += ", ";
- if (arg_type->c_in.size())
+ }
+ if (arg_type->c_in.size()) {
c_in_statements += sformat(arg_type->c_in, arg_type->c_type, c_param_name);
+ }
c_args_var_content += sformat(arg_type->c_arg_in, c_param_name);
}
@@ -2095,8 +2157,9 @@ Error BindingsGenerator::_generate_glue_method(const BindingsGenerator::TypeInte
if (!generated_icall_funcs.find(im_icall)) {
generated_icall_funcs.push_back(im_icall);
- if (im_icall->editor_only)
+ if (im_icall->editor_only) {
p_output.append("#ifdef TOOLS_ENABLED\n");
+ }
// Generate icall function
@@ -2135,7 +2198,7 @@ Error BindingsGenerator::_generate_glue_method(const BindingsGenerator::TypeInte
if (return_type->ret_as_byref_arg) {
p_output.append("\tif (" CS_PARAM_INSTANCE " == nullptr) { *arg_ret = ");
p_output.append(fail_ret);
- p_output.append("; ERR_FAIL_MSG(\"Parameter ' arg_ret ' is null.\"); }\n");
+ p_output.append("; ERR_FAIL_MSG(\"Parameter ' " CS_PARAM_INSTANCE " ' is null.\"); }\n");
} else {
p_output.append("\tERR_FAIL_NULL_V(" CS_PARAM_INSTANCE ", ");
p_output.append(fail_ret);
@@ -2214,30 +2277,33 @@ Error BindingsGenerator::_generate_glue_method(const BindingsGenerator::TypeInte
p_output.append(CLOSE_BLOCK "\n");
- if (im_icall->editor_only)
+ if (im_icall->editor_only) {
p_output.append("#endif // TOOLS_ENABLED\n");
+ }
}
return OK;
}
const BindingsGenerator::TypeInterface *BindingsGenerator::_get_type_or_null(const TypeReference &p_typeref) {
-
const Map<StringName, TypeInterface>::Element *builtin_type_match = builtin_types.find(p_typeref.cname);
- if (builtin_type_match)
+ if (builtin_type_match) {
return &builtin_type_match->get();
+ }
const OrderedHashMap<StringName, TypeInterface>::Element obj_type_match = obj_types.find(p_typeref.cname);
- if (obj_type_match)
+ if (obj_type_match) {
return &obj_type_match.get();
+ }
if (p_typeref.is_enum) {
const Map<StringName, TypeInterface>::Element *enum_match = enum_types.find(p_typeref.cname);
- if (enum_match)
+ if (enum_match) {
return &enum_match->get();
+ }
// Enum not found. Most likely because none of its constants were bound, so it's empty. That's fine. Use int instead.
const Map<StringName, TypeInterface>::Element *int_match = builtin_types.find(name_cache.type_int);
@@ -2249,18 +2315,19 @@ const BindingsGenerator::TypeInterface *BindingsGenerator::_get_type_or_null(con
}
const BindingsGenerator::TypeInterface *BindingsGenerator::_get_type_or_placeholder(const TypeReference &p_typeref) {
-
const TypeInterface *found = _get_type_or_null(p_typeref);
- if (found)
+ if (found) {
return found;
+ }
ERR_PRINT(String() + "Type not found. Creating placeholder: '" + p_typeref.cname.operator String() + "'.");
const Map<StringName, TypeInterface>::Element *match = placeholder_types.find(p_typeref.cname);
- if (match)
+ if (match) {
return &match->get();
+ }
TypeInterface placeholder;
TypeInterface::create_placeholder_type(placeholder, p_typeref.cname);
@@ -2269,7 +2336,6 @@ const BindingsGenerator::TypeInterface *BindingsGenerator::_get_type_or_placehol
}
StringName BindingsGenerator::_get_int_type_name_from_meta(GodotTypeInfo::Metadata p_meta) {
-
switch (p_meta) {
case GodotTypeInfo::METADATA_INT_IS_INT8:
return "sbyte";
@@ -2302,7 +2368,6 @@ StringName BindingsGenerator::_get_int_type_name_from_meta(GodotTypeInfo::Metada
}
StringName BindingsGenerator::_get_float_type_name_from_meta(GodotTypeInfo::Metadata p_meta) {
-
switch (p_meta) {
case GodotTypeInfo::METADATA_REAL_IS_FLOAT:
return "float";
@@ -2320,8 +2385,85 @@ StringName BindingsGenerator::_get_float_type_name_from_meta(GodotTypeInfo::Meta
}
}
-bool BindingsGenerator::_populate_object_type_interfaces() {
+bool BindingsGenerator::_arg_default_value_is_assignable_to_type(const Variant &p_val, const TypeInterface &p_arg_type) {
+ if (p_arg_type.name == name_cache.type_Variant) {
+ // Variant can take anything
+ return true;
+ }
+
+ switch (p_val.get_type()) {
+ case Variant::NIL:
+ return p_arg_type.is_object_type ||
+ name_cache.is_nullable_type(p_arg_type.name);
+ case Variant::BOOL:
+ return p_arg_type.name == name_cache.type_bool;
+ case Variant::INT:
+ return p_arg_type.name == name_cache.type_sbyte ||
+ p_arg_type.name == name_cache.type_short ||
+ p_arg_type.name == name_cache.type_int ||
+ p_arg_type.name == name_cache.type_byte ||
+ p_arg_type.name == name_cache.type_ushort ||
+ p_arg_type.name == name_cache.type_uint ||
+ p_arg_type.name == name_cache.type_long ||
+ p_arg_type.name == name_cache.type_ulong ||
+ p_arg_type.name == name_cache.type_float ||
+ p_arg_type.name == name_cache.type_double ||
+ p_arg_type.is_enum;
+ case Variant::FLOAT:
+ return p_arg_type.name == name_cache.type_float ||
+ p_arg_type.name == name_cache.type_double;
+ case Variant::STRING:
+ case Variant::STRING_NAME:
+ return p_arg_type.name == name_cache.type_String ||
+ p_arg_type.name == name_cache.type_StringName ||
+ p_arg_type.name == name_cache.type_NodePath;
+ case Variant::NODE_PATH:
+ return p_arg_type.name == name_cache.type_NodePath;
+ case Variant::TRANSFORM:
+ case Variant::TRANSFORM2D:
+ case Variant::BASIS:
+ case Variant::QUAT:
+ case Variant::PLANE:
+ case Variant::AABB:
+ case Variant::COLOR:
+ case Variant::VECTOR2:
+ case Variant::RECT2:
+ case Variant::VECTOR3:
+ case Variant::_RID:
+ case Variant::ARRAY:
+ case Variant::DICTIONARY:
+ case Variant::PACKED_BYTE_ARRAY:
+ case Variant::PACKED_INT32_ARRAY:
+ case Variant::PACKED_INT64_ARRAY:
+ case Variant::PACKED_FLOAT32_ARRAY:
+ case Variant::PACKED_FLOAT64_ARRAY:
+ case Variant::PACKED_STRING_ARRAY:
+ case Variant::PACKED_VECTOR2_ARRAY:
+ case Variant::PACKED_VECTOR3_ARRAY:
+ case Variant::PACKED_COLOR_ARRAY:
+ case Variant::CALLABLE:
+ case Variant::SIGNAL:
+ return p_arg_type.name == Variant::get_type_name(p_val.get_type());
+ case Variant::OBJECT:
+ return p_arg_type.is_object_type;
+ case Variant::VECTOR2I:
+ return p_arg_type.name == name_cache.type_Vector2 ||
+ p_arg_type.name == Variant::get_type_name(p_val.get_type());
+ case Variant::RECT2I:
+ return p_arg_type.name == name_cache.type_Rect2 ||
+ p_arg_type.name == Variant::get_type_name(p_val.get_type());
+ case Variant::VECTOR3I:
+ return p_arg_type.name == name_cache.type_Vector3 ||
+ p_arg_type.name == Variant::get_type_name(p_val.get_type());
+ default:
+ CRASH_NOW_MSG("Unexpected Variant type: " + itos(p_val.get_type()));
+ break;
+ }
+ return false;
+}
+
+bool BindingsGenerator::_populate_object_type_interfaces() {
obj_types.clear();
List<StringName> class_list;
@@ -2383,22 +2525,30 @@ bool BindingsGenerator::_populate_object_type_interfaces() {
for (const List<PropertyInfo>::Element *E = property_list.front(); E; E = E->next()) {
const PropertyInfo &property = E->get();
- if (property.usage & PROPERTY_USAGE_GROUP || property.usage & PROPERTY_USAGE_CATEGORY)
+ if (property.usage & PROPERTY_USAGE_GROUP || property.usage & PROPERTY_USAGE_SUBGROUP || property.usage & PROPERTY_USAGE_CATEGORY) {
continue;
+ }
+
+ if (property.name.find("/") >= 0) {
+ // Ignore properties with '/' (slash) in the name. These are only meant for use in the inspector.
+ continue;
+ }
PropertyInterface iprop;
iprop.cname = property.name;
iprop.setter = ClassDB::get_property_setter(type_cname, iprop.cname);
iprop.getter = ClassDB::get_property_getter(type_cname, iprop.cname);
- if (iprop.setter != StringName())
+ if (iprop.setter != StringName()) {
accessor_methods[iprop.setter] = iprop.cname;
- if (iprop.getter != StringName())
+ }
+ if (iprop.getter != StringName()) {
accessor_methods[iprop.getter] = iprop.cname;
+ }
bool valid = false;
iprop.index = ClassDB::get_property_index(type_cname, iprop.cname, &valid);
- ERR_FAIL_COND_V(!valid, false);
+ ERR_FAIL_COND_V_MSG(!valid, false, "Invalid property: '" + itype.name + "." + String(iprop.cname) + "'.");
iprop.proxy_name = escape_csharp_keyword(snake_to_pascal_case(iprop.cname));
@@ -2410,8 +2560,6 @@ bool BindingsGenerator::_populate_object_type_interfaces() {
iprop.proxy_name += "_";
}
- iprop.proxy_name = iprop.proxy_name.replace("/", "__"); // Some members have a slash...
-
iprop.prop_doc = nullptr;
for (int i = 0; i < itype.class_doc->properties.size(); i++) {
@@ -2440,20 +2588,23 @@ bool BindingsGenerator::_populate_object_type_interfaces() {
int argc = method_info.arguments.size();
- if (method_info.name.empty())
+ if (method_info.name.empty()) {
continue;
+ }
String cname = method_info.name;
- if (blacklisted_methods.find(itype.cname) && blacklisted_methods[itype.cname].find(cname))
+ if (blacklisted_methods.find(itype.cname) && blacklisted_methods[itype.cname].find(cname)) {
continue;
+ }
MethodInterface imethod;
imethod.name = method_info.name;
imethod.cname = cname;
- if (method_info.flags & METHOD_FLAG_VIRTUAL)
+ if (method_info.flags & METHOD_FLAG_VIRTUAL) {
imethod.is_virtual = true;
+ }
PropertyInfo return_info = method_info.return_val;
@@ -2475,9 +2626,8 @@ bool BindingsGenerator::_populate_object_type_interfaces() {
// 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.
+ // Actually, more methods like this may be added in the future, which could return
+ // something different. Let's put this check to notify us if that ever happens.
if (itype.cname != name_cache.type_Object || imethod.name != "free") {
WARN_PRINT("Notification: New unexpected virtual non-overridable method found."
" We only expected Object.free, but found '" +
@@ -2488,13 +2638,12 @@ bool BindingsGenerator::_populate_object_type_interfaces() {
imethod.return_type.is_enum = true;
} else if (return_info.class_name != StringName()) {
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_PRINT("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_V(false);
- }
+
+ bool bad_reference_hint = !imethod.is_virtual && return_info.hint != PROPERTY_HINT_RESOURCE_TYPE &&
+ ClassDB::is_parent_class(return_info.class_name, name_cache.type_Reference);
+ ERR_FAIL_COND_V_MSG(bad_reference_hint, false,
+ String() + "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 + "'.");
} else if (return_info.hint == PROPERTY_HINT_RESOURCE_TYPE) {
imethod.return_type.cname = return_info.hint_string;
} else if (return_info.type == Variant::NIL && return_info.usage & PROPERTY_USAGE_NIL_IS_VARIANT) {
@@ -2585,6 +2734,10 @@ bool BindingsGenerator::_populate_object_type_interfaces() {
}
}
+ ERR_FAIL_COND_V_MSG(itype.find_property_by_name(imethod.cname), false,
+ "Method name conflicts with property: '" + itype.name + "." + imethod.name + "'.");
+
+ // Classes starting with an underscore are ignored unless they're used as a property setter or getter
if (!imethod.is_virtual && imethod.name[0] == '_') {
for (const List<PropertyInterface>::Element *F = itype.properties.front(); F; F = F->next()) {
const PropertyInterface &iprop = F->get();
@@ -2764,8 +2917,8 @@ bool BindingsGenerator::_populate_object_type_interfaces() {
}
bool BindingsGenerator::_arg_default_value_from_variant(const Variant &p_val, ArgumentInterface &r_iarg) {
-
- r_iarg.default_argument = p_val;
+ r_iarg.def_param_value = p_val;
+ r_iarg.default_argument = p_val.operator String();
switch (p_val.get_type()) {
case Variant::NIL:
@@ -2798,8 +2951,9 @@ bool BindingsGenerator::_arg_default_value_from_variant(const Variant &p_val, Ar
}
break;
case Variant::TRANSFORM:
- if (p_val.operator Transform() == Transform())
+ if (p_val.operator Transform() == Transform()) {
r_iarg.default_argument.clear();
+ }
r_iarg.default_argument = "new %s(" + r_iarg.default_argument + ")";
r_iarg.def_param_mode = ArgumentInterface::NULLABLE_VAL;
break;
@@ -2865,14 +3019,14 @@ bool BindingsGenerator::_arg_default_value_from_variant(const Variant &p_val, Ar
break;
}
- if (r_iarg.def_param_mode == ArgumentInterface::CONSTANT && r_iarg.type.cname == name_cache.type_Variant && r_iarg.default_argument != "null")
+ if (r_iarg.def_param_mode == ArgumentInterface::CONSTANT && r_iarg.type.cname == name_cache.type_Variant && r_iarg.default_argument != "null") {
r_iarg.def_param_mode = ArgumentInterface::NULLABLE_REF;
+ }
return true;
}
void BindingsGenerator::_populate_builtin_type_interfaces() {
-
builtin_types.clear();
TypeInterface itype;
@@ -3248,7 +3402,6 @@ void BindingsGenerator::_populate_builtin_type_interfaces() {
}
void BindingsGenerator::_populate_global_constants() {
-
int global_constants_count = GlobalConstants::get_global_constant_count();
if (global_constants_count > 0) {
@@ -3259,7 +3412,6 @@ void BindingsGenerator::_populate_global_constants() {
const DocData::ClassDoc &global_scope_doc = match->value();
for (int i = 0; i < global_constants_count; i++) {
-
String constant_name = GlobalConstants::get_global_constant_name(i);
const DocData::ConstantDoc *const_doc = nullptr;
@@ -3338,14 +3490,12 @@ void BindingsGenerator::_populate_global_constants() {
}
void BindingsGenerator::_initialize_blacklisted_methods() {
-
blacklisted_methods["Object"].push_back("to_string"); // there is already ToString
blacklisted_methods["Object"].push_back("_to_string"); // override ToString instead
blacklisted_methods["Object"].push_back("_init"); // never called in C# (TODO: implement it)
}
void BindingsGenerator::_log(const char *p_format, ...) {
-
if (log_print_enabled) {
va_list list;
@@ -3356,7 +3506,6 @@ void BindingsGenerator::_log(const char *p_format, ...) {
}
void BindingsGenerator::_initialize() {
-
initialized = false;
EditorHelp::generate_doc();
@@ -3377,14 +3526,14 @@ void BindingsGenerator::_initialize() {
core_custom_icalls.clear();
editor_custom_icalls.clear();
- for (OrderedHashMap<StringName, TypeInterface>::Element E = obj_types.front(); E; E = E.next())
+ for (OrderedHashMap<StringName, TypeInterface>::Element E = obj_types.front(); E; E = E.next()) {
_generate_method_icalls(E.get());
+ }
initialized = true;
}
void BindingsGenerator::handle_cmdline_args(const List<String> &p_cmdline_args) {
-
const int NUM_OPTIONS = 2;
String generate_all_glue_option = "--generate-mono-glue";
String generate_cs_glue_option = "--generate-mono-cs-glue";
@@ -3447,21 +3596,25 @@ void BindingsGenerator::handle_cmdline_args(const List<String> &p_cmdline_args)
}
if (glue_dir_path.length()) {
- if (bindings_generator.generate_glue(glue_dir_path) != OK)
+ if (bindings_generator.generate_glue(glue_dir_path) != OK) {
ERR_PRINT(generate_all_glue_option + ": Failed to generate the C++ glue.");
+ }
- if (bindings_generator.generate_cs_api(glue_dir_path.plus_file(API_SOLUTION_NAME)) != OK)
+ if (bindings_generator.generate_cs_api(glue_dir_path.plus_file(API_SOLUTION_NAME)) != OK) {
ERR_PRINT(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)
+ if (bindings_generator.generate_cs_api(cs_dir_path) != OK) {
ERR_PRINT(generate_cs_glue_option + ": Failed to generate the C# API.");
+ }
}
if (cpp_dir_path.length()) {
- if (bindings_generator.generate_glue(cpp_dir_path) != OK)
+ if (bindings_generator.generate_glue(cpp_dir_path) != OK) {
ERR_PRINT(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 7c87c688db..90c1c9f3ee 100644
--- a/modules/mono/editor/bindings_generator.h
+++ b/modules/mono/editor/bindings_generator.h
@@ -41,7 +41,6 @@
#include "core/ustring.h"
class BindingsGenerator {
-
struct ConstantInterface {
String name;
String proxy_name;
@@ -85,16 +84,12 @@ class BindingsGenerator {
struct TypeReference {
StringName cname;
- bool is_enum;
+ bool is_enum = false;
- TypeReference() :
- is_enum(false) {
- }
+ TypeReference() {}
TypeReference(const StringName &p_cname) :
- cname(p_cname),
- is_enum(false) {
- }
+ cname(p_cname) {}
};
struct ArgumentInterface {
@@ -107,7 +102,9 @@ class BindingsGenerator {
TypeReference type;
String name;
- DefaultParamMode def_param_mode;
+
+ Variant def_param_value;
+ DefaultParamMode def_param_mode = CONSTANT;
/**
* Determines the expression for the parameter default value.
@@ -116,9 +113,7 @@ class BindingsGenerator {
*/
String default_argument;
- ArgumentInterface() {
- def_param_mode = CONSTANT;
- }
+ ArgumentInterface() {}
};
struct MethodInterface {
@@ -138,19 +133,19 @@ class BindingsGenerator {
/**
* Determines if the method has a variable number of arguments (VarArg)
*/
- bool is_vararg;
+ bool is_vararg = false;
/**
* Virtual methods ("virtual" as defined by the Godot API) are methods that by default do nothing,
* but can be overridden by the user to add custom functionality.
* e.g.: _ready, _process, etc.
*/
- bool is_virtual;
+ bool is_virtual = false;
/**
* Determines if the call should fallback to Godot's object.Call(string, params) in C#.
*/
- bool requires_object_call;
+ bool requires_object_call = false;
/**
* Determines if the method visibility is 'internal' (visible only to files in the same assembly).
@@ -158,27 +153,20 @@ class BindingsGenerator {
* 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.
*/
- bool is_internal;
+ bool is_internal = false;
List<ArgumentInterface> arguments;
- const DocData::MethodDoc *method_doc;
+ const DocData::MethodDoc *method_doc = nullptr;
- bool is_deprecated;
+ bool is_deprecated = false;
String deprecation_message;
void add_argument(const ArgumentInterface &argument) {
arguments.push_back(argument);
}
- MethodInterface() {
- is_vararg = false;
- is_virtual = false;
- requires_object_call = false;
- is_internal = false;
- method_doc = nullptr;
- is_deprecated = false;
- }
+ MethodInterface() {}
};
struct SignalInterface {
@@ -192,19 +180,16 @@ class BindingsGenerator {
List<ArgumentInterface> arguments;
- const DocData::MethodDoc *method_doc;
+ const DocData::MethodDoc *method_doc = nullptr;
- bool is_deprecated;
+ bool is_deprecated = false;
String deprecation_message;
void add_argument(const ArgumentInterface &argument) {
arguments.push_back(argument);
}
- SignalInterface() {
- method_doc = nullptr;
- is_deprecated = false;
- }
+ SignalInterface() {}
};
struct TypeInterface {
@@ -225,26 +210,26 @@ class BindingsGenerator {
*/
String proxy_name;
- ClassDB::APIType api_type;
+ ClassDB::APIType api_type = ClassDB::API_NONE;
- bool is_enum;
- bool is_object_type;
- bool is_singleton;
- bool is_reference;
+ bool is_enum = false;
+ bool is_object_type = false;
+ bool is_singleton = false;
+ bool is_reference = false;
/**
* Used only by Object-derived types.
* Determines if this type is not abstract (incomplete).
* e.g.: CanvasItem cannot be instantiated.
*/
- bool is_instantiable;
+ bool is_instantiable = false;
/**
* Used only by Object-derived types.
* Determines if the C# class owns the native handle and must free it somehow when disposed.
* e.g.: Reference types must notify when the C# instance is disposed, for proper refcounting.
*/
- bool memory_own;
+ bool memory_own = false;
/**
* This must be set to true for any struct bigger than 32-bits. Those cannot be passed/returned by value
@@ -252,7 +237,7 @@ class BindingsGenerator {
* In this case, [c_out] and [cs_out] must have a different format, explained below.
* The Mono IL interpreter icall trampolines don't support passing structs bigger than 32-bits by value (at least not on WASM).
*/
- bool ret_as_byref_arg;
+ bool ret_as_byref_arg = false;
// !! The comments of the following fields make reference to other fields via square brackets, e.g.: [field_name]
// !! When renaming those fields, make sure to rename their references in the comments
@@ -279,7 +264,7 @@ class BindingsGenerator {
* Formatting elements:
* %0 or %s: name of the parameter
*/
- String c_arg_in;
+ String c_arg_in = "%s";
/**
* One or more statements that determine how a variable of this type is returned from a function.
@@ -362,7 +347,7 @@ class BindingsGenerator {
*/
String im_type_out;
- const DocData::ClassDoc *class_doc;
+ const DocData::ClassDoc *class_doc = nullptr;
List<ConstantInterface> constants;
List<EnumInterface> enums;
@@ -372,8 +357,9 @@ class BindingsGenerator {
const MethodInterface *find_method_by_name(const StringName &p_cname) const {
for (const List<MethodInterface>::Element *E = methods.front(); E; E = E->next()) {
- if (E->get().cname == p_cname)
+ if (E->get().cname == p_cname) {
return &E->get();
+ }
}
return nullptr;
@@ -381,8 +367,9 @@ class BindingsGenerator {
const PropertyInterface *find_property_by_name(const StringName &p_cname) const {
for (const List<PropertyInterface>::Element *E = properties.front(); E; E = E->next()) {
- if (E->get().cname == p_cname)
+ if (E->get().cname == p_cname) {
return &E->get();
+ }
}
return nullptr;
@@ -390,8 +377,9 @@ class BindingsGenerator {
const PropertyInterface *find_property_by_proxy_name(const String &p_proxy_name) const {
for (const List<PropertyInterface>::Element *E = properties.front(); E; E = E->next()) {
- if (E->get().proxy_name == p_proxy_name)
+ if (E->get().proxy_name == p_proxy_name) {
return &E->get();
+ }
}
return nullptr;
@@ -399,8 +387,9 @@ class BindingsGenerator {
const MethodInterface *find_method_by_proxy_name(const String &p_proxy_name) const {
for (const List<MethodInterface>::Element *E = methods.front(); E; E = E->next()) {
- if (E->get().proxy_name == p_proxy_name)
+ if (E->get().proxy_name == p_proxy_name) {
return &E->get();
+ }
}
return nullptr;
@@ -482,24 +471,7 @@ class BindingsGenerator {
r_enum_itype.class_doc = &EditorHelp::get_doc_data()->class_list[r_enum_itype.proxy_name];
}
- TypeInterface() {
-
- api_type = ClassDB::API_NONE;
-
- is_enum = false;
- is_object_type = false;
- is_singleton = false;
- is_reference = false;
- is_instantiable = false;
-
- memory_own = false;
-
- ret_as_byref_arg = false;
-
- c_arg_in = "%s";
-
- class_doc = nullptr;
- }
+ TypeInterface() {}
};
struct InternalCall {
@@ -532,8 +504,8 @@ class BindingsGenerator {
}
};
- bool log_print_enabled;
- bool initialized;
+ bool log_print_enabled = true;
+ bool initialized = false;
OrderedHashMap<StringName, TypeInterface> obj_types;
@@ -557,58 +529,70 @@ class BindingsGenerator {
void _initialize_blacklisted_methods();
struct NameCache {
- StringName type_void;
- StringName type_Array;
- StringName type_Dictionary;
- StringName type_Variant;
- StringName type_VarArg;
- StringName type_Object;
- StringName type_Reference;
- StringName type_RID;
- StringName type_String;
- StringName type_StringName;
- StringName type_NodePath;
- StringName type_at_GlobalScope;
- StringName enum_Error;
-
- StringName type_sbyte;
- StringName type_short;
- StringName type_int;
- StringName type_long;
- StringName type_byte;
- StringName type_ushort;
- StringName type_uint;
- StringName type_ulong;
- StringName type_float;
- StringName type_double;
-
- NameCache() {
- type_void = StaticCString::create("void");
- type_Array = StaticCString::create("Array");
- type_Dictionary = StaticCString::create("Dictionary");
- type_Variant = StaticCString::create("Variant");
- type_VarArg = StaticCString::create("VarArg");
- type_Object = StaticCString::create("Object");
- type_Reference = StaticCString::create("Reference");
- type_RID = StaticCString::create("RID");
- type_String = StaticCString::create("String");
- type_StringName = StaticCString::create("StringName");
- type_NodePath = StaticCString::create("NodePath");
- type_at_GlobalScope = StaticCString::create("@GlobalScope");
- enum_Error = StaticCString::create("Error");
-
- type_sbyte = StaticCString::create("sbyte");
- type_short = StaticCString::create("short");
- type_int = StaticCString::create("int");
- type_long = StaticCString::create("long");
- type_byte = StaticCString::create("byte");
- type_ushort = StaticCString::create("ushort");
- type_uint = StaticCString::create("uint");
- type_ulong = StaticCString::create("ulong");
- type_float = StaticCString::create("float");
- type_double = StaticCString::create("double");
+ StringName type_void = StaticCString::create("void");
+ StringName type_Variant = StaticCString::create("Variant");
+ StringName type_VarArg = StaticCString::create("VarArg");
+ StringName type_Object = StaticCString::create("Object");
+ StringName type_Reference = StaticCString::create("Reference");
+ StringName type_RID = StaticCString::create("RID");
+ StringName type_String = StaticCString::create("String");
+ StringName type_StringName = StaticCString::create("StringName");
+ StringName type_NodePath = StaticCString::create("NodePath");
+ StringName type_at_GlobalScope = StaticCString::create("@GlobalScope");
+ StringName enum_Error = StaticCString::create("Error");
+
+ StringName type_sbyte = StaticCString::create("sbyte");
+ StringName type_short = StaticCString::create("short");
+ StringName type_int = StaticCString::create("int");
+ StringName type_byte = StaticCString::create("byte");
+ StringName type_ushort = StaticCString::create("ushort");
+ StringName type_uint = StaticCString::create("uint");
+ StringName type_long = StaticCString::create("long");
+ StringName type_ulong = StaticCString::create("ulong");
+
+ StringName type_bool = StaticCString::create("bool");
+ StringName type_float = StaticCString::create("float");
+ StringName type_double = StaticCString::create("double");
+
+ StringName type_Vector2 = StaticCString::create("Vector2");
+ StringName type_Rect2 = StaticCString::create("Rect2");
+ StringName type_Vector3 = StaticCString::create("Vector3");
+
+ // Object not included as it must be checked for all derived classes
+ static constexpr int nullable_types_count = 17;
+ StringName nullable_types[nullable_types_count] = {
+ type_String,
+ type_StringName,
+ type_NodePath,
+
+ StaticCString::create(_STR(Array)),
+ StaticCString::create(_STR(Dictionary)),
+ StaticCString::create(_STR(Callable)),
+ StaticCString::create(_STR(Signal)),
+
+ StaticCString::create(_STR(PackedByteArray)),
+ StaticCString::create(_STR(PackedInt32Array)),
+ StaticCString::create(_STR(PackedInt64rray)),
+ StaticCString::create(_STR(PackedFloat32Array)),
+ StaticCString::create(_STR(PackedFloat64Array)),
+ StaticCString::create(_STR(PackedStringArray)),
+ StaticCString::create(_STR(PackedVector2Array)),
+ StaticCString::create(_STR(PackedVector3Array)),
+ StaticCString::create(_STR(PackedColorArray)),
+ };
+
+ bool is_nullable_type(const StringName &p_type) const {
+ for (int i = 0; i < nullable_types_count; i++) {
+ if (p_type == nullable_types[i]) {
+ return true;
+ }
+ }
+
+ return false;
}
+ NameCache() {}
+
private:
NameCache(const NameCache &);
NameCache &operator=(const NameCache &);
@@ -619,7 +603,9 @@ class BindingsGenerator {
const List<InternalCall>::Element *find_icall_by_name(const String &p_name, const List<InternalCall> &p_list) {
const List<InternalCall>::Element *it = p_list.front();
while (it) {
- if (it->get().name == p_name) return it;
+ if (it->get().name == p_name) {
+ return it;
+ }
it = it->next();
}
return nullptr;
@@ -627,20 +613,22 @@ class BindingsGenerator {
const ConstantInterface *find_constant_by_name(const String &p_name, const List<ConstantInterface> &p_constants) const {
for (const List<ConstantInterface>::Element *E = p_constants.front(); E; E = E->next()) {
- if (E->get().name == p_name)
+ if (E->get().name == p_name) {
return &E->get();
+ }
}
return nullptr;
}
inline String get_unique_sig(const TypeInterface &p_type) {
- if (p_type.is_reference)
+ if (p_type.is_reference) {
return "Ref";
- else if (p_type.is_object_type)
+ } else if (p_type.is_object_type) {
return "Obj";
- else if (p_type.is_enum)
+ } else if (p_type.is_enum) {
return "int";
+ }
return p_type.name;
}
@@ -659,6 +647,7 @@ class BindingsGenerator {
StringName _get_float_type_name_from_meta(GodotTypeInfo::Metadata p_meta);
bool _arg_default_value_from_variant(const Variant &p_val, ArgumentInterface &r_iarg);
+ bool _arg_default_value_is_assignable_to_type(const Variant &p_val, const TypeInterface &p_arg_type);
bool _populate_object_type_interfaces();
void _populate_builtin_type_interfaces();
@@ -696,9 +685,7 @@ public:
static void handle_cmdline_args(const List<String> &p_cmdline_args);
- BindingsGenerator() :
- log_print_enabled(true),
- initialized(false) {
+ BindingsGenerator() {
_initialize();
}
};
diff --git a/modules/mono/editor/code_completion.cpp b/modules/mono/editor/code_completion.cpp
new file mode 100644
index 0000000000..7a5e465e7a
--- /dev/null
+++ b/modules/mono/editor/code_completion.cpp
@@ -0,0 +1,249 @@
+/*************************************************************************/
+/* code_completion.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "code_completion.h"
+
+#include "core/project_settings.h"
+#include "editor/editor_file_system.h"
+#include "editor/editor_settings.h"
+#include "scene/gui/control.h"
+#include "scene/main/node.h"
+
+namespace gdmono {
+
+// Almost everything here is taken from functions used by GDScript for code completion, adapted for C#.
+
+_FORCE_INLINE_ String quoted(const String &p_str) {
+ return "\"" + p_str + "\"";
+}
+
+void _add_nodes_suggestions(const Node *p_base, const Node *p_node, PackedStringArray &r_suggestions) {
+ if (p_node != p_base && !p_node->get_owner())
+ return;
+
+ String path_relative_to_orig = p_base->get_path_to(p_node);
+
+ r_suggestions.push_back(quoted(path_relative_to_orig));
+
+ for (int i = 0; i < p_node->get_child_count(); i++) {
+ _add_nodes_suggestions(p_base, p_node->get_child(i), r_suggestions);
+ }
+}
+
+Node *_find_node_for_script(Node *p_base, Node *p_current, const Ref<Script> &p_script) {
+ if (p_current->get_owner() != p_base && p_base != p_current)
+ return nullptr;
+
+ Ref<Script> c = p_current->get_script();
+
+ if (c == p_script)
+ return p_current;
+
+ for (int i = 0; i < p_current->get_child_count(); i++) {
+ Node *found = _find_node_for_script(p_base, p_current->get_child(i), p_script);
+ if (found)
+ return found;
+ }
+
+ return nullptr;
+}
+
+void _get_directory_contents(EditorFileSystemDirectory *p_dir, PackedStringArray &r_suggestions) {
+ for (int i = 0; i < p_dir->get_file_count(); i++) {
+ r_suggestions.push_back(quoted(p_dir->get_file_path(i)));
+ }
+
+ for (int i = 0; i < p_dir->get_subdir_count(); i++) {
+ _get_directory_contents(p_dir->get_subdir(i), r_suggestions);
+ }
+}
+
+Node *_try_find_owner_node_in_tree(const Ref<Script> p_script) {
+ SceneTree *tree = SceneTree::get_singleton();
+ if (!tree)
+ return nullptr;
+ Node *base = tree->get_edited_scene_root();
+ if (base) {
+ base = _find_node_for_script(base, base, p_script);
+ }
+ return base;
+}
+
+PackedStringArray get_code_completion(CompletionKind p_kind, const String &p_script_file) {
+ PackedStringArray suggestions;
+
+ switch (p_kind) {
+ case CompletionKind::INPUT_ACTIONS: {
+ List<PropertyInfo> project_props;
+ ProjectSettings::get_singleton()->get_property_list(&project_props);
+
+ for (List<PropertyInfo>::Element *E = project_props.front(); E; E = E->next()) {
+ const PropertyInfo &prop = E->get();
+
+ if (!prop.name.begins_with("input/"))
+ continue;
+
+ String name = prop.name.substr(prop.name.find("/") + 1, prop.name.length());
+ suggestions.push_back(quoted(name));
+ }
+ } break;
+ case CompletionKind::NODE_PATHS: {
+ {
+ // AutoLoads
+ List<PropertyInfo> props;
+ ProjectSettings::get_singleton()->get_property_list(&props);
+
+ for (List<PropertyInfo>::Element *E = props.front(); E; E = E->next()) {
+ String s = E->get().name;
+ if (!s.begins_with("autoload/")) {
+ continue;
+ }
+ String name = s.get_slice("/", 1);
+ suggestions.push_back(quoted("/root/" + name));
+ }
+ }
+
+ {
+ // Current edited scene tree
+ Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path());
+ Node *base = _try_find_owner_node_in_tree(script);
+ if (base) {
+ _add_nodes_suggestions(base, base, suggestions);
+ }
+ }
+ } break;
+ case CompletionKind::RESOURCE_PATHS: {
+ if (bool(EditorSettings::get_singleton()->get("text_editor/completion/complete_file_paths"))) {
+ _get_directory_contents(EditorFileSystem::get_singleton()->get_filesystem(), suggestions);
+ }
+ } break;
+ case CompletionKind::SCENE_PATHS: {
+ DirAccessRef dir_access = DirAccess::create(DirAccess::ACCESS_RESOURCES);
+ List<String> directories;
+ directories.push_back(dir_access->get_current_dir());
+
+ while (!directories.empty()) {
+ dir_access->change_dir(directories.back()->get());
+ directories.pop_back();
+
+ dir_access->list_dir_begin();
+ String filename = dir_access->get_next();
+
+ while (filename != "") {
+ if (filename == "." || filename == "..") {
+ filename = dir_access->get_next();
+ continue;
+ }
+
+ if (dir_access->dir_exists(filename)) {
+ directories.push_back(dir_access->get_current_dir().plus_file(filename));
+ } else if (filename.ends_with(".tscn") || filename.ends_with(".scn")) {
+ suggestions.push_back(quoted(dir_access->get_current_dir().plus_file(filename)));
+ }
+
+ filename = dir_access->get_next();
+ }
+ }
+ } break;
+ case CompletionKind::SHADER_PARAMS: {
+ print_verbose("Shared params completion for C# not implemented.");
+ } break;
+ case CompletionKind::SIGNALS: {
+ Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path());
+
+ List<MethodInfo> signals;
+ script->get_script_signal_list(&signals);
+
+ StringName native = script->get_instance_base_type();
+ if (native != StringName()) {
+ ClassDB::get_signal_list(native, &signals, /* p_no_inheritance: */ false);
+ }
+
+ for (List<MethodInfo>::Element *E = signals.front(); E; E = E->next()) {
+ const String &signal = E->get().name;
+ suggestions.push_back(quoted(signal));
+ }
+ } break;
+ case CompletionKind::THEME_COLORS: {
+ Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path());
+ Node *base = _try_find_owner_node_in_tree(script);
+ if (base && Object::cast_to<Control>(base)) {
+ List<StringName> sn;
+ Theme::get_default()->get_color_list(base->get_class(), &sn);
+
+ for (List<StringName>::Element *E = sn.front(); E; E = E->next()) {
+ suggestions.push_back(quoted(E->get()));
+ }
+ }
+ } break;
+ case CompletionKind::THEME_CONSTANTS: {
+ Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path());
+ Node *base = _try_find_owner_node_in_tree(script);
+ if (base && Object::cast_to<Control>(base)) {
+ List<StringName> sn;
+ Theme::get_default()->get_constant_list(base->get_class(), &sn);
+
+ for (List<StringName>::Element *E = sn.front(); E; E = E->next()) {
+ suggestions.push_back(quoted(E->get()));
+ }
+ }
+ } break;
+ case CompletionKind::THEME_FONTS: {
+ Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path());
+ Node *base = _try_find_owner_node_in_tree(script);
+ if (base && Object::cast_to<Control>(base)) {
+ List<StringName> sn;
+ Theme::get_default()->get_font_list(base->get_class(), &sn);
+
+ for (List<StringName>::Element *E = sn.front(); E; E = E->next()) {
+ suggestions.push_back(quoted(E->get()));
+ }
+ }
+ } break;
+ case CompletionKind::THEME_STYLES: {
+ Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path());
+ Node *base = _try_find_owner_node_in_tree(script);
+ if (base && Object::cast_to<Control>(base)) {
+ List<StringName> sn;
+ Theme::get_default()->get_stylebox_list(base->get_class(), &sn);
+
+ for (List<StringName>::Element *E = sn.front(); E; E = E->next()) {
+ suggestions.push_back(quoted(E->get()));
+ }
+ }
+ } break;
+ default:
+ ERR_FAIL_V_MSG(suggestions, "Invalid completion kind.");
+ }
+
+ return suggestions;
+}
+
+} // namespace gdmono
diff --git a/modules/mono/editor/code_completion.h b/modules/mono/editor/code_completion.h
new file mode 100644
index 0000000000..77673b766f
--- /dev/null
+++ b/modules/mono/editor/code_completion.h
@@ -0,0 +1,56 @@
+/*************************************************************************/
+/* code_completion.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef CODE_COMPLETION_H
+#define CODE_COMPLETION_H
+
+#include "core/ustring.h"
+#include "core/variant.h"
+
+namespace gdmono {
+
+enum class CompletionKind {
+ INPUT_ACTIONS = 0,
+ NODE_PATHS,
+ RESOURCE_PATHS,
+ SCENE_PATHS,
+ SHADER_PARAMS,
+ SIGNALS,
+ THEME_COLORS,
+ THEME_CONSTANTS,
+ THEME_FONTS,
+ THEME_STYLES
+};
+
+PackedStringArray get_code_completion(CompletionKind p_kind, const String &p_script_file);
+
+} // namespace gdmono
+
+#endif // CODE_COMPLETION_H
diff --git a/modules/mono/editor/csharp_project.cpp b/modules/mono/editor/csharp_project.cpp
index e5c2d023d3..3a30f3106c 100644
--- a/modules/mono/editor/csharp_project.cpp
+++ b/modules/mono/editor/csharp_project.cpp
@@ -45,7 +45,6 @@
namespace CSharpProject {
void add_item(const String &p_project_path, const String &p_item_type, const String &p_include) {
-
if (!GLOBAL_DEF("mono/project/auto_update_project", true))
return;
diff --git a/modules/mono/editor/editor_internal_calls.cpp b/modules/mono/editor/editor_internal_calls.cpp
index 283d4beb8e..b183787618 100644
--- a/modules/mono/editor/editor_internal_calls.cpp
+++ b/modules/mono/editor/editor_internal_calls.cpp
@@ -48,6 +48,7 @@
#include "../mono_gd/gd_mono_marshal.h"
#include "../utils/osx_utils.h"
#include "bindings_generator.h"
+#include "code_completion.h"
#include "godotsharp_export.h"
#include "script_class_parser.h"
@@ -231,14 +232,14 @@ int32_t godot_icall_ScriptClassParser_ParseFile(MonoString *p_filepath, MonoObje
return err;
}
-uint32_t godot_icall_ExportPlugin_GetExportedAssemblyDependencies(MonoObject *p_initial_dependencies,
- MonoString *p_build_config, MonoString *p_custom_bcl_dir, MonoObject *r_dependencies) {
- Dictionary initial_dependencies = GDMonoMarshal::mono_object_to_variant(p_initial_dependencies);
+uint32_t godot_icall_ExportPlugin_GetExportedAssemblyDependencies(MonoObject *p_initial_assemblies,
+ MonoString *p_build_config, MonoString *p_custom_bcl_dir, MonoObject *r_assembly_dependencies) {
+ Dictionary initial_dependencies = GDMonoMarshal::mono_object_to_variant(p_initial_assemblies);
String build_config = GDMonoMarshal::mono_string_to_godot(p_build_config);
String custom_bcl_dir = GDMonoMarshal::mono_string_to_godot(p_custom_bcl_dir);
- Dictionary dependencies = GDMonoMarshal::mono_object_to_variant(r_dependencies);
+ Dictionary assembly_dependencies = GDMonoMarshal::mono_object_to_variant(r_assembly_dependencies);
- return GodotSharpExport::get_exported_assembly_dependencies(initial_dependencies, build_config, custom_bcl_dir, dependencies);
+ return GodotSharpExport::get_exported_assembly_dependencies(initial_dependencies, build_config, custom_bcl_dir, assembly_dependencies);
}
MonoString *godot_icall_Internal_UpdateApiAssembliesFromPrebuilt(MonoString *p_config) {
@@ -354,6 +355,12 @@ void godot_icall_Internal_ScriptEditorDebugger_ReloadScripts() {
}
}
+MonoArray *godot_icall_Internal_CodeCompletionRequest(int32_t p_kind, MonoString *p_script_file) {
+ String script_file = GDMonoMarshal::mono_string_to_godot(p_script_file);
+ PackedStringArray suggestions = gdmono::get_code_completion((gdmono::CompletionKind)p_kind, script_file);
+ return GDMonoMarshal::PackedStringArray_to_mono_array(suggestions);
+}
+
float godot_icall_Globals_EditorScale() {
return EDSCALE;
}
@@ -392,7 +399,6 @@ MonoBoolean godot_icall_Utils_OS_UnixFileHasExecutableAccess(MonoString *p_file_
}
void register_editor_internal_calls() {
-
// GodotSharpDirs
mono_add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_ResDataDir", (void *)godot_icall_GodotSharpDirs_ResDataDir);
mono_add_internal_call("GodotTools.Internals.GodotSharpDirs::internal_ResMetadataDir", (void *)godot_icall_GodotSharpDirs_ResMetadataDir);
@@ -454,6 +460,7 @@ void register_editor_internal_calls() {
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);
+ mono_add_internal_call("GodotTools.Internals.Internal::internal_CodeCompletionRequest", (void *)godot_icall_Internal_CodeCompletionRequest);
// Globals
mono_add_internal_call("GodotTools.Internals.Globals::internal_EditorScale", (void *)godot_icall_Globals_EditorScale);
diff --git a/modules/mono/editor/godotsharp_export.cpp b/modules/mono/editor/godotsharp_export.cpp
index 324013e5e2..1cdb08d50e 100644
--- a/modules/mono/editor/godotsharp_export.cpp
+++ b/modules/mono/editor/godotsharp_export.cpp
@@ -32,78 +32,80 @@
#include <mono/metadata/image.h>
+#include "core/io/file_access_pack.h"
#include "core/os/os.h"
+#include "core/project_settings.h"
#include "../mono_gd/gd_mono.h"
#include "../mono_gd/gd_mono_assembly.h"
#include "../mono_gd/gd_mono_cache.h"
+#include "../utils/macros.h"
namespace GodotSharpExport {
-String get_assemblyref_name(MonoImage *p_image, int index) {
+struct AssemblyRefInfo {
+ String name;
+ uint16_t major;
+ uint16_t minor;
+ uint16_t build;
+ uint16_t revision;
+};
+
+AssemblyRefInfo get_assemblyref_name(MonoImage *p_image, int index) {
const MonoTableInfo *table_info = mono_image_get_table_info(p_image, MONO_TABLE_ASSEMBLYREF);
uint32_t cols[MONO_ASSEMBLYREF_SIZE];
mono_metadata_decode_row(table_info, index, cols, MONO_ASSEMBLYREF_SIZE);
- return String::utf8(mono_metadata_string_heap(p_image, cols[MONO_ASSEMBLYREF_NAME]));
+ return {
+ String::utf8(mono_metadata_string_heap(p_image, cols[MONO_ASSEMBLYREF_NAME])),
+ (uint16_t)cols[MONO_ASSEMBLYREF_MAJOR_VERSION],
+ (uint16_t)cols[MONO_ASSEMBLYREF_MINOR_VERSION],
+ (uint16_t)cols[MONO_ASSEMBLYREF_BUILD_NUMBER],
+ (uint16_t)cols[MONO_ASSEMBLYREF_REV_NUMBER]
+ };
}
-Error get_assembly_dependencies(GDMonoAssembly *p_assembly, const Vector<String> &p_search_dirs, Dictionary &r_dependencies) {
+Error get_assembly_dependencies(GDMonoAssembly *p_assembly, const Vector<String> &p_search_dirs, Dictionary &r_assembly_dependencies) {
MonoImage *image = p_assembly->get_image();
for (int i = 0; i < mono_image_get_table_rows(image, MONO_TABLE_ASSEMBLYREF); i++) {
- String ref_name = get_assemblyref_name(image, i);
+ AssemblyRefInfo ref_info = get_assemblyref_name(image, i);
+
+ const String &ref_name = ref_info.name;
- if (r_dependencies.has(ref_name))
+ if (r_assembly_dependencies.has(ref_name))
continue;
GDMonoAssembly *ref_assembly = nullptr;
- String path;
- bool has_extension = ref_name.ends_with(".dll") || ref_name.ends_with(".exe");
-
- for (int j = 0; j < p_search_dirs.size(); j++) {
- const String &search_dir = p_search_dirs[j];
-
- if (has_extension) {
- path = search_dir.plus_file(ref_name);
- if (FileAccess::exists(path)) {
- GDMono::get_singleton()->load_assembly_from(ref_name.get_basename(), path, &ref_assembly, true);
- if (ref_assembly != nullptr)
- break;
- }
- } else {
- path = search_dir.plus_file(ref_name + ".dll");
- if (FileAccess::exists(path)) {
- GDMono::get_singleton()->load_assembly_from(ref_name, path, &ref_assembly, true);
- if (ref_assembly != nullptr)
- break;
- }
-
- path = search_dir.plus_file(ref_name + ".exe");
- if (FileAccess::exists(path)) {
- GDMono::get_singleton()->load_assembly_from(ref_name, path, &ref_assembly, true);
- if (ref_assembly != nullptr)
- break;
- }
- }
- }
- ERR_FAIL_COND_V_MSG(!ref_assembly, ERR_CANT_RESOLVE, "Cannot load assembly (refonly): '" + ref_name + "'.");
+ {
+ MonoAssemblyName *ref_aname = mono_assembly_name_new("A"); // We can't allocate an empty MonoAssemblyName, hence "A"
+ CRASH_COND(ref_aname == nullptr);
+ SCOPE_EXIT {
+ mono_assembly_name_free(ref_aname);
+ mono_free(ref_aname);
+ };
+
+ mono_assembly_get_assemblyref(image, i, ref_aname);
- // Use the path we got from the search. Don't try to get the path from the loaded assembly as we can't trust it will be from the selected BCL dir.
- r_dependencies[ref_name] = path;
+ if (!GDMono::get_singleton()->load_assembly(ref_name, ref_aname, &ref_assembly, /* refonly: */ true, p_search_dirs)) {
+ ERR_FAIL_V_MSG(ERR_CANT_RESOLVE, "Cannot load assembly (refonly): '" + ref_name + "'.");
+ }
+
+ r_assembly_dependencies[ref_name] = ref_assembly->get_path();
+ }
- Error err = get_assembly_dependencies(ref_assembly, p_search_dirs, r_dependencies);
+ Error err = get_assembly_dependencies(ref_assembly, p_search_dirs, r_assembly_dependencies);
ERR_FAIL_COND_V_MSG(err != OK, err, "Cannot load one of the dependencies for the assembly: '" + ref_name + "'.");
}
return OK;
}
-Error get_exported_assembly_dependencies(const Dictionary &p_initial_dependencies,
- const String &p_build_config, const String &p_custom_bcl_dir, Dictionary &r_dependencies) {
+Error get_exported_assembly_dependencies(const Dictionary &p_initial_assemblies,
+ const String &p_build_config, const String &p_custom_bcl_dir, Dictionary &r_assembly_dependencies) {
MonoDomain *export_domain = GDMonoUtils::create_domain("GodotEngine.Domain.ProjectExport");
ERR_FAIL_NULL_V(export_domain, FAILED);
_GDMONO_SCOPE_EXIT_DOMAIN_UNLOAD_(export_domain);
@@ -113,16 +115,21 @@ Error get_exported_assembly_dependencies(const Dictionary &p_initial_dependencie
Vector<String> search_dirs;
GDMonoAssembly::fill_search_dirs(search_dirs, p_build_config, p_custom_bcl_dir);
- for (const Variant *key = p_initial_dependencies.next(); key; key = p_initial_dependencies.next(key)) {
+ if (p_custom_bcl_dir.length()) {
+ // Only one mscorlib can be loaded. We need this workaround to make sure we get it from the right BCL directory.
+ r_assembly_dependencies["mscorlib"] = p_custom_bcl_dir.plus_file("mscorlib.dll").simplify_path();
+ }
+
+ for (const Variant *key = p_initial_assemblies.next(); key; key = p_initial_assemblies.next(key)) {
String assembly_name = *key;
- String assembly_path = p_initial_dependencies[*key];
+ String assembly_path = p_initial_assemblies[*key];
GDMonoAssembly *assembly = nullptr;
bool load_success = GDMono::get_singleton()->load_assembly_from(assembly_name, assembly_path, &assembly, /* refonly: */ true);
ERR_FAIL_COND_V_MSG(!load_success, ERR_CANT_RESOLVE, "Cannot load assembly (refonly): '" + assembly_name + "'.");
- Error err = get_assembly_dependencies(assembly, search_dirs, r_dependencies);
+ Error err = get_assembly_dependencies(assembly, search_dirs, r_assembly_dependencies);
if (err != OK)
return err;
}
diff --git a/modules/mono/editor/godotsharp_export.h b/modules/mono/editor/godotsharp_export.h
index 36138f81b7..9ab57755de 100644
--- a/modules/mono/editor/godotsharp_export.h
+++ b/modules/mono/editor/godotsharp_export.h
@@ -41,8 +41,8 @@ namespace GodotSharpExport {
Error get_assembly_dependencies(GDMonoAssembly *p_assembly, const Vector<String> &p_search_dirs, Dictionary &r_dependencies);
-Error get_exported_assembly_dependencies(const Dictionary &p_initial_dependencies,
- const String &p_build_config, const String &p_custom_lib_dir, Dictionary &r_dependencies);
+Error get_exported_assembly_dependencies(const Dictionary &p_initial_assemblies,
+ const String &p_build_config, const String &p_custom_lib_dir, Dictionary &r_assembly_dependencies);
} // namespace GodotSharpExport
diff --git a/modules/mono/editor/script_class_parser.cpp b/modules/mono/editor/script_class_parser.cpp
index bece23c9a6..7276612230 100644
--- a/modules/mono/editor/script_class_parser.cpp
+++ b/modules/mono/editor/script_class_parser.cpp
@@ -54,13 +54,11 @@ const char *ScriptClassParser::token_names[ScriptClassParser::TK_MAX] = {
};
String ScriptClassParser::get_token_name(ScriptClassParser::Token p_token) {
-
ERR_FAIL_INDEX_V(p_token, TK_MAX, "<error>");
return token_names[p_token];
}
ScriptClassParser::Token ScriptClassParser::get_token() {
-
while (true) {
switch (code[idx]) {
case '\n': {
@@ -181,14 +179,24 @@ ScriptClassParser::Token ScriptClassParser::get_token() {
CharType res = 0;
switch (next) {
- case 'b': res = 8; break;
- case 't': res = 9; break;
- case 'n': res = 10; break;
- case 'f': res = 12; break;
+ case 'b':
+ res = 8;
+ break;
+ case 't':
+ res = 9;
+ break;
+ case 'n':
+ res = 10;
+ break;
+ case 'f':
+ res = 12;
+ break;
case 'r':
res = 13;
break;
- case '\"': res = '\"'; break;
+ case '\"':
+ res = '\"';
+ break;
case '\\':
res = '\\';
break;
@@ -258,7 +266,6 @@ ScriptClassParser::Token ScriptClassParser::get_token() {
}
Error ScriptClassParser::_skip_generic_type_params() {
-
Token tk;
while (true) {
@@ -327,7 +334,6 @@ Error ScriptClassParser::_skip_generic_type_params() {
}
Error ScriptClassParser::_parse_type_full_name(String &r_full_name) {
-
Token tk = get_token();
if (tk != TK_IDENTIFIER) {
@@ -360,7 +366,6 @@ Error ScriptClassParser::_parse_type_full_name(String &r_full_name) {
}
Error ScriptClassParser::_parse_class_base(Vector<String> &r_base) {
-
String name;
Error err = _parse_type_full_name(name);
@@ -460,7 +465,6 @@ Error ScriptClassParser::_parse_type_constraints() {
}
Error ScriptClassParser::_parse_namespace_name(String &r_name, int &r_curly_stack) {
-
Token tk = get_token();
if (tk == TK_IDENTIFIER) {
@@ -487,7 +491,6 @@ Error ScriptClassParser::_parse_namespace_name(String &r_name, int &r_curly_stac
}
Error ScriptClassParser::parse(const String &p_code) {
-
code = p_code;
idx = 0;
line = 0;
@@ -643,7 +646,6 @@ static String get_preprocessor_directive(const String &p_line, int p_from) {
}
static void run_dummy_preprocessor(String &r_source, const String &p_filepath) {
-
Vector<String> lines = r_source.split("\n", /* p_allow_empty: */ true);
bool *include_lines = memnew_arr(bool, lines.size());
@@ -710,7 +712,6 @@ static void run_dummy_preprocessor(String &r_source, const String &p_filepath) {
}
Error ScriptClassParser::parse_file(const String &p_filepath) {
-
String source;
Error ferr = read_all_file_utf8(p_filepath, source);
diff --git a/modules/mono/editor/script_class_parser.h b/modules/mono/editor/script_class_parser.h
index a76a3a50a9..c194ed1422 100644
--- a/modules/mono/editor/script_class_parser.h
+++ b/modules/mono/editor/script_class_parser.h
@@ -36,7 +36,6 @@
#include "core/vector.h"
class ScriptClassParser {
-
public:
struct NameDecl {
enum Type {