浏览代码

Merge pull request #38600 from neikeq/no

Switch to nuget Microsoft.Build and rewrite GodotTools messasing protocol
Ignacio Roldán Etcheverry 5 年之前
父节点
当前提交
54b20a25b9
共有 71 个文件被更改,包括 2510 次插入1645 次删除
  1. 13 1
      modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs
  2. 4 54
      modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj
  3. 0 35
      modules/mono/editor/GodotTools/GodotTools.BuildLogger/Properties/AssemblyInfo.cs
  4. 3 36
      modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj
  5. 0 26
      modules/mono/editor/GodotTools/GodotTools.Core/Properties/AssemblyInfo.cs
  6. 2 12
      modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs
  7. 0 33
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs
  8. 0 94
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs
  9. 0 219
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs
  10. 0 207
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs
  11. 0 24
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs
  12. 0 24
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs
  13. 0 53
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj
  14. 0 21
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs
  15. 0 46
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs
  16. 0 88
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs
  17. 0 35
      modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs
  18. 57 0
      modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/ForwarderMessageHandler.cs
  19. 17 0
      modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj
  20. 218 0
      modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs
  21. 332 0
      modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs
  22. 44 0
      modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientHandshake.cs
  23. 52 0
      modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientMessageHandler.cs
  24. 4 2
      modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs
  25. 24 0
      modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj
  26. 8 0
      modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IHandshake.cs
  27. 1 1
      modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ILogger.cs
  28. 9 0
      modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IMessageHandler.cs
  29. 52 0
      modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Message.cs
  30. 100 0
      modules/mono/editor/GodotTools/GodotTools.IdeMessaging/MessageDecoder.cs
  31. 302 0
      modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs
  32. 116 0
      modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Requests/Requests.cs
  33. 23 0
      modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs
  34. 2 2
      modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs
  35. 32 0
      modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/SemaphoreExtensions.cs
  36. 15 49
      modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj
  37. 4 7
      modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs
  38. 2 5
      modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs
  39. 0 27
      modules/mono/editor/GodotTools/GodotTools.ProjectEditor/Properties/AssemblyInfo.cs
  40. 0 4
      modules/mono/editor/GodotTools/GodotTools.ProjectEditor/packages.config
  41. 1 1
      modules/mono/editor/GodotTools/GodotTools.sln
  42. 2 2
      modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs
  43. 26 24
      modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs
  44. 9 10
      modules/mono/editor/GodotTools/GodotTools/BuildManager.cs
  45. 6 6
      modules/mono/editor/GodotTools/GodotTools/BuildTab.cs
  46. 1 1
      modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs
  47. 1 1
      modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs
  48. 18 14
      modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs
  49. 11 86
      modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj
  50. 113 50
      modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs
  51. 0 212
      modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs
  52. 360 0
      modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs
  53. 12 2
      modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs
  54. 6 6
      modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs
  55. 7 0
      modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs
  56. 19 0
      modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs
  57. 0 26
      modules/mono/editor/GodotTools/GodotTools/Properties/AssemblyInfo.cs
  58. 29 22
      modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs
  59. 0 5
      modules/mono/editor/GodotTools/GodotTools/packages.config
  60. 11 4
      modules/mono/editor/bindings_generator.cpp
  61. 249 0
      modules/mono/editor/code_completion.cpp
  62. 56 0
      modules/mono/editor/code_completion.h
  63. 8 0
      modules/mono/editor/editor_internal_calls.cpp
  64. 43 36
      modules/mono/editor/godotsharp_export.cpp
  65. 1 0
      modules/mono/managed_callable.cpp
  66. 21 14
      modules/mono/mono_gd/gd_mono.cpp
  67. 1 0
      modules/mono/mono_gd/gd_mono.h
  68. 57 14
      modules/mono/mono_gd/gd_mono_assembly.cpp
  69. 4 2
      modules/mono/mono_gd/gd_mono_assembly.h
  70. 1 1
      modules/mono/mono_gd/gd_mono_log.cpp
  71. 1 1
      modules/mono/utils/macros.h

+ 13 - 1
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;
+        }
+    }
 }

+ 4 - 54
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>

+ 0 - 35
modules/mono/editor/GodotTools/GodotTools.BuildLogger/Properties/AssemblyInfo.cs

@@ -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")]

+ 3 - 36
modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj

@@ -1,40 +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="FileUtils.cs" />
-    <Compile Include="ProcessExtensions.cs" />
-    <Compile Include="Properties\AssemblyInfo.cs" />
-    <Compile Include="StringExtensions.cs" />
-  </ItemGroup>
-  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
 </Project>

+ 0 - 26
modules/mono/editor/GodotTools/GodotTools.Core/Properties/AssemblyInfo.cs

@@ -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("")]

+ 2 - 12
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)

+ 0 - 33
modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs

@@ -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);
-        }
-    }
-}

+ 0 - 94
modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs

@@ -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();
-            }
-        }
-    }
-}

+ 0 - 219
modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs

@@ -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);
-    }
-}

+ 0 - 207
modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs

@@ -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();
-        }
-    }
-}

+ 0 - 24
modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs

@@ -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;
-        }
-    }
-}

+ 0 - 24
modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs

@@ -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;
-        }
-    }
-}

+ 0 - 53
modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj

@@ -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>

+ 0 - 21
modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs

@@ -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)}')";
-        }
-    }
-}

+ 0 - 46
modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs

@@ -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();
-        }
-    }
-}

+ 0 - 88
modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs

@@ -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;
-        }
-    }
-}

+ 0 - 35
modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs

@@ -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")]

+ 57 - 0
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} =======");
+        }
+    }
+}

+ 17 - 0
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>

+ 218 - 0
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);
+                }
+            }
+        }
+    }
+}

+ 332 - 0
modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs

@@ -0,0 +1,332 @@
+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 GodotIdeMetadata godotIdeMetadata;
+        private readonly FileSystemWatcher fsWatcher;
+
+        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 (!File.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 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 metadata = ReadMetadataFile();
+
+                if (metadata != null)
+                {
+                    godotIdeMetadata = metadata.Value;
+                    _ = Task.Run(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 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.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);
+        }
+    }
+}

+ 44 - 0
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;
+        }
+    }
+}

+ 52 - 0
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);
+    }
+}

+ 4 - 2
modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs → 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;

+ 24 - 0
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.0</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>

+ 8 - 0
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);
+    }
+}

+ 1 - 1
modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs → modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ILogger.cs

@@ -1,6 +1,6 @@
 using System;
 
-namespace GodotTools.IdeConnection
+namespace GodotTools.IdeMessaging
 {
     public interface ILogger
     {

+ 9 - 0
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);
+    }
+}

+ 52 - 0
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;
+        }
+    }
+}

+ 100 - 0
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;
+        }
+    }
+}

+ 302 - 0
modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs

@@ -0,0 +1,302 @@
+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
+                    {
+                        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)
+                    {
+                        Logger.LogError($"Exception thrown from message handler. Message: {msg}", e);
+                    }
+                }
+            }
+            catch (Exception e)
+            {
+                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();
+        }
+    }
+}

+ 116 - 0
modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Requests/Requests.cs

@@ -0,0 +1,116 @@
+// 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 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
+    {
+    }
+}

+ 23 - 0
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});
+        }
+    }
+}

+ 2 - 2
modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs → 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;

+ 32 - 0
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();
+            }
+        }
+    }
+}

+ 15 - 49
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.Build.Runtime" Version="16.5.0" />
   </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>
-  </ItemGroup>
-  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+  <PropertyGroup>
+    <!--
+    The 'Microsoft.Build.Runtime' package includes an mscorlib reference assembly in contentFiles.
+    This causes our project build to fail. As a workaround, we remove {CandidateAssemblyFiles}
+    from AssemblySearchPaths as described here: https://github.com/microsoft/msbuild/issues/3486.
+    -->
+    <AssemblySearchPaths>$([System.String]::Copy('$(AssemblySearchPaths)').Replace('{CandidateAssemblyFiles}', ''))</AssemblySearchPaths>
+    <AssemblySearchPaths Condition=" '$(MSBuildRuntimeVersion)' != '' ">$(AssemblySearchPaths.Split(';'))</AssemblySearchPaths>
+  </PropertyGroup>
 </Project>

+ 4 - 7
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;

+ 2 - 5
modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs

@@ -4,8 +4,8 @@ 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
 {
@@ -133,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);
 
@@ -151,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...
 

+ 0 - 27
modules/mono/editor/GodotTools/GodotTools.ProjectEditor/Properties/AssemblyInfo.cs

@@ -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("")]
-

+ 0 - 4
modules/mono/editor/GodotTools/GodotTools.ProjectEditor/packages.config

@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<packages>
-  <package id="DotNet.Glob" version="2.1.1" targetFramework="net45" />
-</packages>

+ 1 - 1
modules/mono/editor/GodotTools/GodotTools.sln

@@ -9,7 +9,7 @@ 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
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution

+ 2 - 2
modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs

@@ -109,9 +109,9 @@ namespace GodotTools.Build
                 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.Configuration,
                 buildInfo.LogsDirPath, buildInfo.CustomProperties);
         }
 

+ 26 - 24
modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs

@@ -28,15 +28,13 @@ namespace GodotTools.Build
                 {
                     case BuildManager.BuildTool.MsBuildVs:
                     {
-                        if (_msbuildToolsPath.Empty() || !File.Exists(_msbuildToolsPath))
+                        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("\\"))
@@ -49,44 +47,48 @@ namespace GodotTools.Build
                         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;
                     }
                     case BuildManager.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;
+                    }
                     default:
                         throw new IndexOutOfRangeException("Invalid build tool in editor settings");
                 }
             }
 
-            if (OS.IsUnixLike())
+            if (OS.IsUnixLike)
             {
                 if (buildTool == BuildManager.BuildTool.MsBuildMono)
                 {
-                    if (_msbuildUnixPath.Empty() || !File.Exists(_msbuildUnixPath))
+                    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");
                     }
 
-                    if (_msbuildUnixPath.Empty())
-                    {
-                        throw new FileNotFoundException($"Cannot find binary for '{BuildManager.PropNameMsbuildMono}'");
-                    }
+                    if (string.IsNullOrEmpty(_msbuildUnixPath))
+                        throw new FileNotFoundException($"Cannot find binary for '{BuildManager.PropNameMSBuildMono}'");
 
                     return _msbuildUnixPath;
                 }
-                else
-                {
-                    throw new IndexOutOfRangeException("Invalid build tool in editor settings");
-                }
+
+                throw new IndexOutOfRangeException("Invalid build tool in editor settings");
             }
 
             throw new PlatformNotSupportedException();
@@ -114,12 +116,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 +145,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 +173,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("\\"))

+ 9 - 10
modules/mono/editor/GodotTools/GodotTools/BuildManager.cs

@@ -15,9 +15,9 @@ 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 MsBuildIssuesFileName = "msbuild_issues.csv";
         public const string MsBuildLogFileName = "msbuild_log.txt";
@@ -219,7 +219,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 +233,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[]
@@ -251,9 +252,7 @@ namespace GodotTools
             var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
             var msbuild = BuildTool.MsBuildMono;
             if (OS.IsWindows)
-                msbuild = RiderPathManager.IsExternalEditorSetToRider(editorSettings)
-                        ? BuildTool.JetBrainsMsBuild
-                        : BuildTool.MsBuildVs;
+                msbuild = RiderPathManager.IsExternalEditorSetToRider(editorSettings) ? BuildTool.JetBrainsMsBuild : BuildTool.MsBuildVs;
 
             EditorDef("mono/builds/build_tool", msbuild);
 
@@ -263,8 +262,8 @@ namespace GodotTools
                 ["name"] = "mono/builds/build_tool",
                 ["hint"] = Godot.PropertyHint.Enum,
                 ["hint_string"] = OS.IsWindows ?
-                    $"{PropNameMsbuildMono},{PropNameMsbuildVs},{PropNameMsbuildJetBrains}" :
-                    $"{PropNameMsbuildMono}"
+                    $"{PropNameMSBuildMono},{PropNameMSBuildVs},{PropNameMSBuildJetBrains}" :
+                    $"{PropNameMSBuildMono}"
             });
 
             EditorDef("mono/builds/print_build_output", false);

+ 6 - 6
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;

+ 1 - 1
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";

+ 1 - 1
modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs

@@ -414,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";

+ 18 - 14
modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs

@@ -37,6 +37,8 @@ namespace GodotTools
 
         public BottomPanel BottomPanel { get; private set; }
 
+        public PlaySettings? CurrentPlaySettings { get; set; }
+
         public static string ProjectAssemblyName
         {
             get
@@ -228,12 +230,12 @@ 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();
@@ -249,17 +251,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);
@@ -300,7 +305,7 @@ namespace GodotTools
                     if (line >= 0)
                     {
                         args.Add("-g");
-                        args.Add($"{scriptPath}:{line + 1}:{col}");
+                        args.Add($"{scriptPath}:{line}:{col}");
                     }
                     else
                     {
@@ -311,7 +316,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;
@@ -321,7 +326,7 @@ namespace GodotTools
                     }
                     else
                     {
-                        if (_vsCodePath.Empty())
+                        if (string.IsNullOrEmpty(_vsCodePath))
                         {
                             GD.PushError("Cannot find code editor: VSCode");
                             return Error.FileNotFound;
@@ -341,7 +346,6 @@ namespace GodotTools
 
                     break;
                 }
-
                 default:
                     throw new ArgumentOutOfRangeException();
             }
@@ -505,7 +509,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}" +

+ 11 - 86
modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj

@@ -1,14 +1,8 @@
-<?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>
-    <LangVersion>7</LangVersion>
+    <TargetFramework>net472</TargetFramework>
+    <LangVersion>7.2</LangVersion>
     <GodotApiConfiguration>Debug</GodotApiConfiguration> <!-- The Godot editor uses the Debug Godot API assemblies -->
     <GodotSourceRootPath>$(SolutionDir)/../../../../</GodotSourceRootPath>
     <GodotOutputDataDir>$(GodotSourceRootPath)/bin/GodotSharp</GodotOutputDataDir>
@@ -18,32 +12,12 @@
     <!-- The project is part of the Godot source tree -->
     <!-- Use the Godot source tree output folder instead of '$(ProjectDir)/bin' -->
     <OutputPath>$(GodotOutputDataDir)/Tools</OutputPath>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
-    <DebugSymbols>true</DebugSymbols>
-    <DebugType>portable</DebugType>
-    <Optimize>false</Optimize>
-    <DefineConstants>DEBUG;</DefineConstants>
-    <ErrorReport>prompt</ErrorReport>
-    <WarningLevel>4</WarningLevel>
-    <ConsolePause>false</ConsolePause>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
-    <Optimize>true</Optimize>
-    <ErrorReport>prompt</ErrorReport>
-    <WarningLevel>4</WarningLevel>
-    <ConsolePause>false</ConsolePause>
+    <!-- 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="Newtonsoft.Json" Version="12.0.3" />
     <Reference Include="GodotSharp">
       <HintPath>$(GodotApiAssembliesDir)/GodotSharp.dll</HintPath>
       <Private>False</Private>
@@ -54,58 +28,9 @@
     </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" />
   </ItemGroup>
-  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
 </Project>

+ 113 - 50
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)

+ 0 - 212
modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs

@@ -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;
-            }
-        }
-    }
-}

+ 360 - 0
modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs

@@ -0,0 +1,360 @@
+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);
+                    },
+                    [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> 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;
+            }
+        }
+    }
+}

+ 12 - 2
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>
                 {

+ 6 - 6
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)
             {

+ 7 - 0
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
     }
 }

+ 19 - 0
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;
+        }
+    }
+}

+ 0 - 26
modules/mono/editor/GodotTools/GodotTools/Properties/AssemblyInfo.cs

@@ -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("")]

+ 29 - 22
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();

+ 0 - 5
modules/mono/editor/GodotTools/GodotTools/packages.config

@@ -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>

+ 11 - 4
modules/mono/editor/bindings_generator.cpp

@@ -1664,6 +1664,10 @@ 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("\")]");
@@ -2139,7 +2143,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);
@@ -2390,6 +2394,11 @@ bool BindingsGenerator::_populate_object_type_interfaces() {
 			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);
@@ -2402,7 +2411,7 @@ bool BindingsGenerator::_populate_object_type_interfaces() {
 
 			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));
 
@@ -2414,8 +2423,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++) {

+ 249 - 0
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

+ 56 - 0
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

+ 8 - 0
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"
 
@@ -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;
 }
@@ -454,6 +461,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);

+ 43 - 36
modules/mono/editor/godotsharp_export.cpp

@@ -32,68 +32,70 @@
 
 #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_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_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;
-				}
-			}
-		}
+		GDMonoAssembly *ref_assembly = NULL;
+
+		{
+			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);
+			};
 
-		ERR_FAIL_COND_V_MSG(!ref_assembly, ERR_CANT_RESOLVE, "Cannot load assembly (refonly): '" + ref_name + "'.");
+			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_assembly_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_assembly_dependencies);
 		ERR_FAIL_COND_V_MSG(err != OK, err, "Cannot load one of the dependencies for the assembly: '" + ref_name + "'.");
@@ -113,6 +115,11 @@ Error get_exported_assembly_dependencies(const Dictionary &p_initial_assemblies,
 	Vector<String> search_dirs;
 	GDMonoAssembly::fill_search_dirs(search_dirs, p_build_config, p_custom_bcl_dir);
 
+	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_assemblies[*key];

+ 1 - 0
modules/mono/managed_callable.cpp

@@ -82,6 +82,7 @@ CallableCustom::CompareLessFunc ManagedCallable::get_compare_less_func() const {
 }
 
 ObjectID ManagedCallable::get_object() const {
+	// TODO: If the delegate target extends Godot.Object, use that instead!
 	return CSharpLanguage::get_singleton()->get_managed_callable_middleman()->get_instance_id();
 }
 

+ 21 - 14
modules/mono/mono_gd/gd_mono.cpp

@@ -133,6 +133,10 @@ void gd_mono_debug_init() {
 
 	CharString da_args = OS::get_singleton()->get_environment("GODOT_MONO_DEBUGGER_AGENT").utf8();
 
+	if (da_args.length()) {
+		OS::get_singleton()->set_environment("GODOT_MONO_DEBUGGER_AGENT", String());
+	}
+
 #ifdef TOOLS_ENABLED
 	int da_port = GLOBAL_DEF("mono/debugger_agent/port", 23685);
 	bool da_suspend = GLOBAL_DEF("mono/debugger_agent/wait_for_debugger", false);
@@ -515,8 +519,8 @@ void GDMono::add_assembly(uint32_t p_domain_id, GDMonoAssembly *p_assembly) {
 
 GDMonoAssembly *GDMono::get_loaded_assembly(const String &p_name) {
 
-	if (p_name == "mscorlib")
-		return get_corlib_assembly();
+	if (p_name == "mscorlib" && corlib_assembly)
+		return corlib_assembly;
 
 	MonoDomain *domain = mono_domain_get();
 	uint32_t domain_id = domain ? mono_domain_get_id(domain) : 0;
@@ -526,7 +530,9 @@ GDMonoAssembly *GDMono::get_loaded_assembly(const String &p_name) {
 
 bool GDMono::load_assembly(const String &p_name, GDMonoAssembly **r_assembly, bool p_refonly) {
 
+#ifdef DEBUG_ENABLED
 	CRASH_COND(!r_assembly);
+#endif
 
 	MonoAssemblyName *aname = mono_assembly_name_new(p_name.utf8());
 	bool result = load_assembly(p_name, aname, r_assembly, p_refonly);
@@ -538,26 +544,27 @@ bool GDMono::load_assembly(const String &p_name, GDMonoAssembly **r_assembly, bo
 
 bool GDMono::load_assembly(const String &p_name, MonoAssemblyName *p_aname, GDMonoAssembly **r_assembly, bool p_refonly) {
 
+#ifdef DEBUG_ENABLED
 	CRASH_COND(!r_assembly);
+#endif
 
-	print_verbose("Mono: Loading assembly " + p_name + (p_refonly ? " (refonly)" : "") + "...");
+	return load_assembly(p_name, p_aname, r_assembly, p_refonly, GDMonoAssembly::get_default_search_dirs());
+}
 
-	MonoImageOpenStatus status = MONO_IMAGE_OK;
-	MonoAssembly *assembly = mono_assembly_load_full(p_aname, nullptr, &status, p_refonly);
+bool GDMono::load_assembly(const String &p_name, MonoAssemblyName *p_aname, GDMonoAssembly **r_assembly, bool p_refonly, const Vector<String> &p_search_dirs) {
 
-	if (!assembly)
-		return false;
-
-	ERR_FAIL_COND_V(status != MONO_IMAGE_OK, false);
+#ifdef DEBUG_ENABLED
+	CRASH_COND(!r_assembly);
+#endif
 
-	uint32_t domain_id = mono_domain_get_id(mono_domain_get());
+	print_verbose("Mono: Loading assembly " + p_name + (p_refonly ? " (refonly)" : "") + "...");
 
-	GDMonoAssembly **stored_assembly = assemblies[domain_id].getptr(p_name);
+	GDMonoAssembly *assembly = GDMonoAssembly::load(p_name, p_aname, p_refonly, p_search_dirs);
 
-	ERR_FAIL_COND_V(stored_assembly == nullptr, false);
-	ERR_FAIL_COND_V((*stored_assembly)->get_assembly() != assembly, false);
+	if (!assembly)
+		return false;
 
-	*r_assembly = *stored_assembly;
+	*r_assembly = assembly;
 
 	print_verbose("Mono: Assembly " + p_name + (p_refonly ? " (refonly)" : "") + " loaded from path: " + (*r_assembly)->get_path());
 

+ 1 - 0
modules/mono/mono_gd/gd_mono.h

@@ -241,6 +241,7 @@ public:
 
 	bool load_assembly(const String &p_name, GDMonoAssembly **r_assembly, bool p_refonly = false);
 	bool load_assembly(const String &p_name, MonoAssemblyName *p_aname, GDMonoAssembly **r_assembly, bool p_refonly = false);
+	bool load_assembly(const String &p_name, MonoAssemblyName *p_aname, GDMonoAssembly **r_assembly, bool p_refonly, const Vector<String> &p_search_dirs);
 	bool load_assembly_from(const String &p_name, const String &p_path, GDMonoAssembly **r_assembly, bool p_refonly = false);
 
 	Error finalize_and_unload_domain(MonoDomain *p_domain);

+ 57 - 14
modules/mono/mono_gd/gd_mono_assembly.cpp

@@ -33,6 +33,7 @@
 #include <mono/metadata/mono-debug.h>
 #include <mono/metadata/tokentype.h>
 
+#include "core/io/file_access_pack.h"
 #include "core/list.h"
 #include "core/os/file_access.h"
 #include "core/os/os.h"
@@ -99,7 +100,7 @@ void GDMonoAssembly::fill_search_dirs(Vector<String> &r_search_dirs, const Strin
 // - The 'load' hook is called after the assembly has been loaded. Its job is to add the
 //   assembly to the list of loaded assemblies so that the 'search' hook can look it up.
 
-void GDMonoAssembly::assembly_load_hook(MonoAssembly *assembly, void *user_data) {
+void GDMonoAssembly::assembly_load_hook(MonoAssembly *assembly, [[maybe_unused]] void *user_data) {
 
 	String name = String::utf8(mono_assembly_name_get_name(mono_assembly_get_name(assembly)));
 
@@ -133,9 +134,7 @@ MonoAssembly *GDMonoAssembly::assembly_refonly_preload_hook(MonoAssemblyName *an
 	return GDMonoAssembly::_preload_hook(aname, assemblies_path, user_data, true);
 }
 
-MonoAssembly *GDMonoAssembly::_search_hook(MonoAssemblyName *aname, void *user_data, bool refonly) {
-
-	(void)user_data; // UNUSED
+MonoAssembly *GDMonoAssembly::_search_hook(MonoAssemblyName *aname, [[maybe_unused]] void *user_data, bool refonly) {
 
 	String name = String::utf8(mono_assembly_name_get_name(aname));
 	bool has_extension = name.ends_with(".dll") || name.ends_with(".exe");
@@ -147,15 +146,13 @@ MonoAssembly *GDMonoAssembly::_search_hook(MonoAssemblyName *aname, void *user_d
 	return nullptr;
 }
 
-MonoAssembly *GDMonoAssembly::_preload_hook(MonoAssemblyName *aname, char **, void *user_data, bool refonly) {
-
-	(void)user_data; // UNUSED
+MonoAssembly *GDMonoAssembly::_preload_hook(MonoAssemblyName *aname, char **, [[maybe_unused]] void *user_data, bool refonly) {
 
 	String name = String::utf8(mono_assembly_name_get_name(aname));
-	return _load_assembly_search(name, search_dirs, refonly);
+	return _load_assembly_search(name, aname, refonly, search_dirs);
 }
 
-MonoAssembly *GDMonoAssembly::_load_assembly_search(const String &p_name, const Vector<String> &p_search_dirs, bool p_refonly) {
+MonoAssembly *GDMonoAssembly::_load_assembly_search(const String &p_name, MonoAssemblyName *p_aname, bool p_refonly, const Vector<String> &p_search_dirs) {
 
 	MonoAssembly *res = nullptr;
 	String path;
@@ -168,21 +165,21 @@ MonoAssembly *GDMonoAssembly::_load_assembly_search(const String &p_name, const
 		if (has_extension) {
 			path = search_dir.plus_file(p_name);
 			if (FileAccess::exists(path)) {
-				res = _real_load_assembly_from(path, p_refonly);
+				res = _real_load_assembly_from(path, p_refonly, p_aname);
 				if (res != nullptr)
 					return res;
 			}
 		} else {
 			path = search_dir.plus_file(p_name + ".dll");
 			if (FileAccess::exists(path)) {
-				res = _real_load_assembly_from(path, p_refonly);
+				res = _real_load_assembly_from(path, p_refonly, p_aname);
 				if (res != nullptr)
 					return res;
 			}
 
 			path = search_dir.plus_file(p_name + ".exe");
 			if (FileAccess::exists(path)) {
-				res = _real_load_assembly_from(path, p_refonly);
+				res = _real_load_assembly_from(path, p_refonly, p_aname);
 				if (res != nullptr)
 					return res;
 			}
@@ -230,7 +227,7 @@ void GDMonoAssembly::initialize() {
 	mono_install_assembly_load_hook(&assembly_load_hook, nullptr);
 }
 
-MonoAssembly *GDMonoAssembly::_real_load_assembly_from(const String &p_path, bool p_refonly) {
+MonoAssembly *GDMonoAssembly::_real_load_assembly_from(const String &p_path, bool p_refonly, MonoAssemblyName *p_aname) {
 
 	Vector<uint8_t> data = FileAccess::get_file_as_array(p_path);
 	ERR_FAIL_COND_V_MSG(data.empty(), nullptr, "Could read the assembly in the specified location");
@@ -255,7 +252,33 @@ MonoAssembly *GDMonoAssembly::_real_load_assembly_from(const String &p_path, boo
 			true, &status, p_refonly,
 			image_filename.utf8());
 
-	ERR_FAIL_COND_V_MSG(status != MONO_IMAGE_OK || !image, nullptr, "Failed to open assembly image from the loaded data");
+	ERR_FAIL_COND_V_MSG(status != MONO_IMAGE_OK || !image, nullptr, "Failed to open assembly image from memory: '" + p_path + "'.");
+
+	if (p_aname != nullptr) {
+		// Check assembly version
+		const MonoTableInfo *table = mono_image_get_table_info(image, MONO_TABLE_ASSEMBLY);
+
+		ERR_FAIL_NULL_V(table, nullptr);
+
+		if (mono_table_info_get_rows(table)) {
+			uint32_t cols[MONO_ASSEMBLY_SIZE];
+			mono_metadata_decode_row(table, 0, cols, MONO_ASSEMBLY_SIZE);
+
+			// Not sure about .NET's policy. We will only ensure major and minor are equal, and ignore build and revision.
+			uint16_t major = cols[MONO_ASSEMBLY_MAJOR_VERSION];
+			uint16_t minor = cols[MONO_ASSEMBLY_MINOR_VERSION];
+
+			uint16_t required_minor;
+			uint16_t required_major = mono_assembly_name_get_version(p_aname, &required_minor, nullptr, nullptr);
+
+			if (required_major != 0) {
+				if (major != required_major && minor != required_minor) {
+					mono_image_close(image);
+					return nullptr;
+				}
+			}
+		}
+	}
 
 #ifdef DEBUG_ENABLED
 	Vector<uint8_t> pdb_data;
@@ -425,6 +448,26 @@ GDMonoClass *GDMonoAssembly::get_object_derived_class(const StringName &p_class)
 	return match;
 }
 
+GDMonoAssembly *GDMonoAssembly::load(const String &p_name, MonoAssemblyName *p_aname, bool p_refonly, const Vector<String> &p_search_dirs) {
+
+	if (GDMono::get_singleton()->get_corlib_assembly() && (p_name == "mscorlib" || p_name == "mscorlib.dll"))
+		return GDMono::get_singleton()->get_corlib_assembly();
+
+	// We need to manually call the search hook in this case, as it won't be called in the next step
+	MonoAssembly *assembly = mono_assembly_invoke_search_hook(p_aname);
+
+	if (!assembly) {
+		assembly = _load_assembly_search(p_name, p_aname, p_refonly, p_search_dirs);
+		ERR_FAIL_NULL_V(assembly, nullptr);
+	}
+
+	GDMonoAssembly *loaded_asm = GDMono::get_singleton()->get_loaded_assembly(p_name);
+	ERR_FAIL_NULL_V_MSG(loaded_asm, nullptr, "Loaded assembly missing from table. Did we not receive the load hook?");
+	ERR_FAIL_COND_V(loaded_asm->get_assembly() != assembly, nullptr);
+
+	return loaded_asm;
+}
+
 GDMonoAssembly *GDMonoAssembly::load_from(const String &p_name, const String &p_path, bool p_refonly) {
 
 	if (p_name == "mscorlib" || p_name == "mscorlib.dll")

+ 4 - 2
modules/mono/mono_gd/gd_mono_assembly.h

@@ -93,8 +93,8 @@ class GDMonoAssembly {
 	static MonoAssembly *_search_hook(MonoAssemblyName *aname, void *user_data, bool refonly);
 	static MonoAssembly *_preload_hook(MonoAssemblyName *aname, char **assemblies_path, void *user_data, bool refonly);
 
-	static MonoAssembly *_real_load_assembly_from(const String &p_path, bool p_refonly);
-	static MonoAssembly *_load_assembly_search(const String &p_name, const Vector<String> &p_search_dirs, bool p_refonly);
+	static MonoAssembly *_real_load_assembly_from(const String &p_path, bool p_refonly, MonoAssemblyName *p_aname = nullptr);
+	static MonoAssembly *_load_assembly_search(const String &p_name, MonoAssemblyName *p_aname, bool p_refonly, const Vector<String> &p_search_dirs);
 
 	friend class GDMono;
 	static void initialize();
@@ -120,7 +120,9 @@ public:
 	static String find_assembly(const String &p_name);
 
 	static void fill_search_dirs(Vector<String> &r_search_dirs, const String &p_custom_config = String(), const String &p_custom_bcl_dir = String());
+	static const Vector<String> &get_default_search_dirs() { return search_dirs; }
 
+	static GDMonoAssembly *load(const String &p_name, MonoAssemblyName *p_aname, bool p_refonly, const Vector<String> &p_search_dirs);
 	static GDMonoAssembly *load_from(const String &p_name, const String &p_path, bool p_refonly);
 
 	GDMonoAssembly(const String &p_name, MonoImage *p_image, MonoAssembly *p_assembly);

+ 1 - 1
modules/mono/mono_gd/gd_mono_log.cpp

@@ -175,7 +175,7 @@ void GDMonoLog::initialize() {
 	log_level_id = get_log_level_id(log_level.get_data());
 
 	if (log_file) {
-		OS::get_singleton()->print("Mono: Logfile is: %s\n", log_file_path.utf8().get_data());
+		OS::get_singleton()->print("Mono: Log file is: '%s'\n", log_file_path.utf8().get_data());
 		mono_trace_set_log_handler(mono_log_callback, this);
 	} else {
 		OS::get_singleton()->printerr("Mono: No log file, using default log handler\n");

+ 1 - 1
modules/mono/utils/macros.h

@@ -68,6 +68,6 @@ public:
 } // namespace gdmono
 
 #define SCOPE_EXIT \
-	auto GD_UNIQUE_NAME(gd_scope_exit) = gdmono::ScopeExitAux() + [=]()
+	auto GD_UNIQUE_NAME(gd_scope_exit) = gdmono::ScopeExitAux() + [=]() -> void
 
 #endif // UTIL_MACROS_H