diff options
Diffstat (limited to 'modules/mono/editor')
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 { |