summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIgnacio Etcheverry <ignalfonsore@gmail.com>2020-05-09 20:45:43 +0200
committerIgnacio Etcheverry <ignalfonsore@gmail.com>2020-05-09 21:49:40 +0200
commit3ce09246d10a267bf00ae8bf37068ec458c69287 (patch)
treeca5fdaa109198e3a6dcd9cae55c71b9d8664e9a8
parentf3bcd5f8dd36c4ce384f00a03650f71dce5c4de6 (diff)
C#: Rewrite GodotTools messaging protocol
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs33
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs94
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs219
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs207
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs24
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs24
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj11
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs21
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs46
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs88
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/ForwarderMessageHandler.cs57
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj17
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs218
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs332
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientHandshake.cs44
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientMessageHandler.cs52
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs (renamed from modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs)6
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj24
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IHandshake.cs8
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ILogger.cs (renamed from modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs)2
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IMessageHandler.cs9
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Message.cs52
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/MessageDecoder.cs100
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs302
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Requests/Requests.cs116
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs23
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs (renamed from modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs)4
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/SemaphoreExtensions.cs32
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.sln2
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/BuildManager.cs5
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs22
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj2
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs163
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs212
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs360
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs12
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs7
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs19
-rw-r--r--modules/mono/editor/code_completion.cpp249
-rw-r--r--modules/mono/editor/code_completion.h56
-rw-r--r--modules/mono/editor/editor_internal_calls.cpp8
-rw-r--r--modules/mono/mono_gd/gd_mono.cpp4
42 files changed, 2239 insertions, 1047 deletions
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs
deleted file mode 100644
index 7a2ff2ca56..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using System;
-
-namespace GodotTools.IdeConnection
-{
- public class ConsoleLogger : ILogger
- {
- public void LogDebug(string message)
- {
- Console.WriteLine("DEBUG: " + message);
- }
-
- public void LogInfo(string message)
- {
- Console.WriteLine("INFO: " + message);
- }
-
- public void LogWarning(string message)
- {
- Console.WriteLine("WARN: " + message);
- }
-
- public void LogError(string message)
- {
- Console.WriteLine("ERROR: " + message);
- }
-
- public void LogError(string message, Exception e)
- {
- Console.WriteLine("EXCEPTION: " + message);
- Console.WriteLine(e);
- }
- }
-}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs
deleted file mode 100644
index be89638241..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs
+++ /dev/null
@@ -1,94 +0,0 @@
-using System;
-using Path = System.IO.Path;
-
-namespace GodotTools.IdeConnection
-{
- public class GodotIdeBase : IDisposable
- {
- private ILogger logger;
-
- public ILogger Logger
- {
- get => logger ?? (logger = new ConsoleLogger());
- set => logger = value;
- }
-
- private readonly string projectMetadataDir;
-
- protected const string MetaFileName = "ide_server_meta.txt";
- protected string MetaFilePath => Path.Combine(projectMetadataDir, MetaFileName);
-
- private GodotIdeConnection connection;
- protected readonly object ConnectionLock = new object();
-
- public bool IsDisposed { get; private set; } = false;
-
- public bool IsConnected => connection != null && !connection.IsDisposed && connection.IsConnected;
-
- public event Action Connected
- {
- add
- {
- if (connection != null && !connection.IsDisposed)
- connection.Connected += value;
- }
- remove
- {
- if (connection != null && !connection.IsDisposed)
- connection.Connected -= value;
- }
- }
-
- protected GodotIdeConnection Connection
- {
- get => connection;
- set
- {
- connection?.Dispose();
- connection = value;
- }
- }
-
- protected GodotIdeBase(string projectMetadataDir)
- {
- this.projectMetadataDir = projectMetadataDir;
- }
-
- protected void DisposeConnection()
- {
- lock (ConnectionLock)
- {
- connection?.Dispose();
- }
- }
-
- ~GodotIdeBase()
- {
- Dispose(disposing: false);
- }
-
- public void Dispose()
- {
- if (IsDisposed)
- return;
-
- lock (ConnectionLock)
- {
- if (IsDisposed) // lock may not be fair
- return;
- IsDisposed = true;
- }
-
- Dispose(disposing: true);
- GC.SuppressFinalize(this);
- }
-
- protected virtual void Dispose(bool disposing)
- {
- if (disposing)
- {
- connection?.Dispose();
- }
- }
- }
-}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs
deleted file mode 100644
index 2bf3b83c75..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs
+++ /dev/null
@@ -1,219 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using System.Net.Sockets;
-using System.Threading;
-
-namespace GodotTools.IdeConnection
-{
- public abstract class GodotIdeClient : GodotIdeBase
- {
- protected GodotIdeMetadata GodotIdeMetadata;
-
- private readonly FileSystemWatcher fsWatcher;
-
- protected GodotIdeClient(string projectMetadataDir) : base(projectMetadataDir)
- {
- messageHandlers = InitializeMessageHandlers();
-
- // FileSystemWatcher requires an existing directory
- if (!File.Exists(projectMetadataDir))
- Directory.CreateDirectory(projectMetadataDir);
-
- fsWatcher = new FileSystemWatcher(projectMetadataDir, MetaFileName);
- }
-
- private void OnMetaFileChanged(object sender, FileSystemEventArgs e)
- {
- if (IsDisposed)
- return;
-
- lock (ConnectionLock)
- {
- if (IsDisposed)
- return;
-
- if (!File.Exists(MetaFilePath))
- return;
-
- var metadata = ReadMetadataFile();
-
- if (metadata != null && metadata != GodotIdeMetadata)
- {
- GodotIdeMetadata = metadata.Value;
- ConnectToServer();
- }
- }
- }
-
- private void OnMetaFileDeleted(object sender, FileSystemEventArgs e)
- {
- if (IsDisposed)
- return;
-
- if (IsConnected)
- DisposeConnection();
-
- // The file may have been re-created
-
- lock (ConnectionLock)
- {
- if (IsDisposed)
- return;
-
- if (IsConnected || !File.Exists(MetaFilePath))
- return;
-
- var metadata = ReadMetadataFile();
-
- if (metadata != null)
- {
- GodotIdeMetadata = metadata.Value;
- ConnectToServer();
- }
- }
- }
-
- private GodotIdeMetadata? ReadMetadataFile()
- {
- using (var reader = File.OpenText(MetaFilePath))
- {
- string portStr = reader.ReadLine();
-
- if (portStr == null)
- return null;
-
- string editorExecutablePath = reader.ReadLine();
-
- if (editorExecutablePath == null)
- return null;
-
- if (!int.TryParse(portStr, out int port))
- return null;
-
- return new GodotIdeMetadata(port, editorExecutablePath);
- }
- }
-
- private void ConnectToServer()
- {
- var tcpClient = new TcpClient();
-
- Connection = new GodotIdeConnectionClient(tcpClient, HandleMessage);
- Connection.Logger = Logger;
-
- try
- {
- Logger.LogInfo("Connecting to Godot Ide Server");
-
- tcpClient.Connect(IPAddress.Loopback, GodotIdeMetadata.Port);
-
- Logger.LogInfo("Connection open with Godot Ide Server");
-
- var clientThread = new Thread(Connection.Start)
- {
- IsBackground = true,
- Name = "Godot Ide Connection Client"
- };
- clientThread.Start();
- }
- catch (SocketException e)
- {
- if (e.SocketErrorCode == SocketError.ConnectionRefused)
- Logger.LogError("The connection to the Godot Ide Server was refused");
- else
- throw;
- }
- }
-
- public void Start()
- {
- Logger.LogInfo("Starting Godot Ide Client");
-
- fsWatcher.Changed += OnMetaFileChanged;
- fsWatcher.Deleted += OnMetaFileDeleted;
- fsWatcher.EnableRaisingEvents = true;
-
- lock (ConnectionLock)
- {
- if (IsDisposed)
- return;
-
- if (!File.Exists(MetaFilePath))
- {
- Logger.LogInfo("There is no Godot Ide Server running");
- return;
- }
-
- var metadata = ReadMetadataFile();
-
- if (metadata != null)
- {
- GodotIdeMetadata = metadata.Value;
- ConnectToServer();
- }
- else
- {
- Logger.LogError("Failed to read Godot Ide metadata file");
- }
- }
- }
-
- public bool WriteMessage(Message message)
- {
- return Connection.WriteMessage(message);
- }
-
- protected override void Dispose(bool disposing)
- {
- base.Dispose(disposing);
-
- if (disposing)
- {
- fsWatcher?.Dispose();
- }
- }
-
- protected virtual bool HandleMessage(Message message)
- {
- if (messageHandlers.TryGetValue(message.Id, out var action))
- {
- action(message.Arguments);
- return true;
- }
-
- return false;
- }
-
- private readonly Dictionary<string, Action<string[]>> messageHandlers;
-
- private Dictionary<string, Action<string[]>> InitializeMessageHandlers()
- {
- return new Dictionary<string, Action<string[]>>
- {
- ["OpenFile"] = args =>
- {
- switch (args.Length)
- {
- case 1:
- OpenFile(file: args[0]);
- return;
- case 2:
- OpenFile(file: args[0], line: int.Parse(args[1]));
- return;
- case 3:
- OpenFile(file: args[0], line: int.Parse(args[1]), column: int.Parse(args[2]));
- return;
- default:
- throw new ArgumentException();
- }
- }
- };
- }
-
- protected abstract void OpenFile(string file);
- protected abstract void OpenFile(string file, int line);
- protected abstract void OpenFile(string file, int line, int column);
- }
-}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs
deleted file mode 100644
index 6441be8d6e..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs
+++ /dev/null
@@ -1,207 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.IO;
-using System.Net.Sockets;
-using System.Text;
-
-namespace GodotTools.IdeConnection
-{
- public abstract class GodotIdeConnection : IDisposable
- {
- protected const string Version = "1.0";
-
- protected static readonly string ClientHandshake = $"Godot Ide Client Version {Version}";
- protected static readonly string ServerHandshake = $"Godot Ide Server Version {Version}";
-
- private const int ClientWriteTimeout = 8000;
- private readonly TcpClient tcpClient;
-
- private TextReader clientReader;
- private TextWriter clientWriter;
-
- private readonly object writeLock = new object();
-
- private readonly Func<Message, bool> messageHandler;
-
- public event Action Connected;
-
- private ILogger logger;
-
- public ILogger Logger
- {
- get => logger ?? (logger = new ConsoleLogger());
- set => logger = value;
- }
-
- public bool IsDisposed { get; private set; } = false;
-
- public bool IsConnected => tcpClient.Client != null && tcpClient.Client.Connected;
-
- protected GodotIdeConnection(TcpClient tcpClient, Func<Message, bool> messageHandler)
- {
- this.tcpClient = tcpClient;
- this.messageHandler = messageHandler;
- }
-
- public void Start()
- {
- try
- {
- if (!StartConnection())
- return;
-
- string messageLine;
- while ((messageLine = ReadLine()) != null)
- {
- if (!MessageParser.TryParse(messageLine, out Message msg))
- {
- Logger.LogError($"Received message with invalid format: {messageLine}");
- continue;
- }
-
- Logger.LogDebug($"Received message: {msg}");
-
- if (msg.Id == "close")
- {
- Logger.LogInfo("Closing connection");
- return;
- }
-
- try
- {
- try
- {
- Debug.Assert(messageHandler != null);
-
- if (!messageHandler(msg))
- Logger.LogError($"Received unknown message: {msg}");
- }
- catch (Exception e)
- {
- Logger.LogError($"Message handler for '{msg}' failed with exception", e);
- }
- }
- catch (Exception e)
- {
- Logger.LogError($"Exception thrown from message handler. Message: {msg}", e);
- }
- }
- }
- catch (Exception e)
- {
- Logger.LogError($"Unhandled exception in the Godot Ide Connection thread", e);
- }
- finally
- {
- Dispose();
- }
- }
-
- private bool StartConnection()
- {
- NetworkStream clientStream = tcpClient.GetStream();
-
- clientReader = new StreamReader(clientStream, Encoding.UTF8);
-
- lock (writeLock)
- clientWriter = new StreamWriter(clientStream, Encoding.UTF8);
-
- clientStream.WriteTimeout = ClientWriteTimeout;
-
- if (!WriteHandshake())
- {
- Logger.LogError("Could not write handshake");
- return false;
- }
-
- if (!IsValidResponseHandshake(ReadLine()))
- {
- Logger.LogError("Received invalid handshake");
- return false;
- }
-
- Connected?.Invoke();
-
- Logger.LogInfo("Godot Ide connection started");
-
- return true;
- }
-
- private string ReadLine()
- {
- try
- {
- return clientReader?.ReadLine();
- }
- catch (Exception e)
- {
- if (IsDisposed)
- {
- var se = e as SocketException ?? e.InnerException as SocketException;
- if (se != null && se.SocketErrorCode == SocketError.Interrupted)
- return null;
- }
-
- throw;
- }
- }
-
- public bool WriteMessage(Message message)
- {
- Logger.LogDebug($"Sending message {message}");
-
- var messageComposer = new MessageComposer();
-
- messageComposer.AddArgument(message.Id);
- foreach (string argument in message.Arguments)
- messageComposer.AddArgument(argument);
-
- return WriteLine(messageComposer.ToString());
- }
-
- protected bool WriteLine(string text)
- {
- if (clientWriter == null || IsDisposed || !IsConnected)
- return false;
-
- lock (writeLock)
- {
- try
- {
- clientWriter.WriteLine(text);
- clientWriter.Flush();
- }
- catch (Exception e)
- {
- if (!IsDisposed)
- {
- var se = e as SocketException ?? e.InnerException as SocketException;
- if (se != null && se.SocketErrorCode == SocketError.Shutdown)
- Logger.LogInfo("Client disconnected ungracefully");
- else
- Logger.LogError("Exception thrown when trying to write to client", e);
-
- Dispose();
- }
- }
- }
-
- return true;
- }
-
- protected abstract bool WriteHandshake();
- protected abstract bool IsValidResponseHandshake(string handshakeLine);
-
- public void Dispose()
- {
- if (IsDisposed)
- return;
-
- IsDisposed = true;
-
- clientReader?.Dispose();
- clientWriter?.Dispose();
- ((IDisposable)tcpClient)?.Dispose();
- }
- }
-}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs
deleted file mode 100644
index 1b11a14358..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System;
-using System.Net.Sockets;
-using System.Threading.Tasks;
-
-namespace GodotTools.IdeConnection
-{
- public class GodotIdeConnectionClient : GodotIdeConnection
- {
- public GodotIdeConnectionClient(TcpClient tcpClient, Func<Message, bool> messageHandler)
- : base(tcpClient, messageHandler)
- {
- }
-
- protected override bool WriteHandshake()
- {
- return WriteLine(ClientHandshake);
- }
-
- protected override bool IsValidResponseHandshake(string handshakeLine)
- {
- return handshakeLine == ServerHandshake;
- }
- }
-}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs
deleted file mode 100644
index aa98dc7ca3..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System;
-using System.Net.Sockets;
-using System.Threading.Tasks;
-
-namespace GodotTools.IdeConnection
-{
- public class GodotIdeConnectionServer : GodotIdeConnection
- {
- public GodotIdeConnectionServer(TcpClient tcpClient, Func<Message, bool> messageHandler)
- : base(tcpClient, messageHandler)
- {
- }
-
- protected override bool WriteHandshake()
- {
- return WriteLine(ServerHandshake);
- }
-
- protected override bool IsValidResponseHandshake(string handshakeLine)
- {
- return handshakeLine == ClientHandshake;
- }
- }
-}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj
deleted file mode 100644
index c3ce3654ed..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj
+++ /dev/null
@@ -1,11 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <ProjectGuid>{92600954-25F0-4291-8E11-1FEE9FC4BE20}</ProjectGuid>
- <OutputType>Library</OutputType>
- <TargetFramework>net472</TargetFramework>
- <LangVersion>7.2</LangVersion>
- </PropertyGroup>
- <ItemGroup>
- <Folder Include="Properties" />
- </ItemGroup>
-</Project>
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs
deleted file mode 100644
index f24d324ae3..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System.Linq;
-
-namespace GodotTools.IdeConnection
-{
- public struct Message
- {
- public string Id { get; set; }
- public string[] Arguments { get; set; }
-
- public Message(string id, params string[] arguments)
- {
- Id = id;
- Arguments = arguments;
- }
-
- public override string ToString()
- {
- return $"(Id: '{Id}', Arguments: '{string.Join(",", Arguments)}')";
- }
- }
-}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs
deleted file mode 100644
index 30ffe7a06e..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using System.Linq;
-using System.Text;
-
-namespace GodotTools.IdeConnection
-{
- public class MessageComposer
- {
- private readonly StringBuilder stringBuilder = new StringBuilder();
-
- private static readonly char[] CharsToEscape = { '\\', '"' };
-
- public void AddArgument(string argument)
- {
- AddArgument(argument, quoted: argument.Contains(","));
- }
-
- public void AddArgument(string argument, bool quoted)
- {
- if (stringBuilder.Length > 0)
- stringBuilder.Append(',');
-
- if (quoted)
- {
- stringBuilder.Append('"');
-
- foreach (char @char in argument)
- {
- if (CharsToEscape.Contains(@char))
- stringBuilder.Append('\\');
- stringBuilder.Append(@char);
- }
-
- stringBuilder.Append('"');
- }
- else
- {
- stringBuilder.Append(argument);
- }
- }
-
- public override string ToString()
- {
- return stringBuilder.ToString();
- }
- }
-}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs
deleted file mode 100644
index 4365d69989..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
-namespace GodotTools.IdeConnection
-{
- public static class MessageParser
- {
- public static bool TryParse(string messageLine, out Message message)
- {
- var arguments = new List<string>();
- var stringBuilder = new StringBuilder();
-
- bool expectingArgument = true;
-
- for (int i = 0; i < messageLine.Length; i++)
- {
- char @char = messageLine[i];
-
- if (@char == ',')
- {
- if (expectingArgument)
- arguments.Add(string.Empty);
-
- expectingArgument = true;
- continue;
- }
-
- bool quoted = false;
-
- if (messageLine[i] == '"')
- {
- quoted = true;
- i++;
- }
-
- while (i < messageLine.Length)
- {
- @char = messageLine[i];
-
- if (quoted && @char == '"')
- {
- i++;
- break;
- }
-
- if (@char == '\\')
- {
- i++;
- if (i < messageLine.Length)
- break;
-
- stringBuilder.Append(messageLine[i]);
- }
- else if (!quoted && @char == ',')
- {
- break; // We don't increment the counter to allow the colon to be parsed after this
- }
- else
- {
- stringBuilder.Append(@char);
- }
-
- i++;
- }
-
- arguments.Add(stringBuilder.ToString());
- stringBuilder.Clear();
-
- expectingArgument = false;
- }
-
- if (arguments.Count == 0)
- {
- message = new Message();
- return false;
- }
-
- message = new Message
- {
- Id = arguments[0],
- Arguments = arguments.Skip(1).ToArray()
- };
-
- return true;
- }
- }
-}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/ForwarderMessageHandler.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/ForwarderMessageHandler.cs
new file mode 100644
index 0000000000..3cb6a6687e
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/ForwarderMessageHandler.cs
@@ -0,0 +1,57 @@
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using GodotTools.IdeMessaging.Utils;
+
+namespace GodotTools.IdeMessaging.CLI
+{
+ public class ForwarderMessageHandler : IMessageHandler
+ {
+ private readonly StreamWriter outputWriter;
+ private readonly SemaphoreSlim outputWriteSem = new SemaphoreSlim(1);
+
+ public ForwarderMessageHandler(StreamWriter outputWriter)
+ {
+ this.outputWriter = outputWriter;
+ }
+
+ public async Task<MessageContent> HandleRequest(Peer peer, string id, MessageContent content, ILogger logger)
+ {
+ await WriteRequestToOutput(id, content);
+ return new MessageContent(MessageStatus.RequestNotSupported, "null");
+ }
+
+ private async Task WriteRequestToOutput(string id, MessageContent content)
+ {
+ using (await outputWriteSem.UseAsync())
+ {
+ await outputWriter.WriteLineAsync("======= Request =======");
+ await outputWriter.WriteLineAsync(id);
+ await outputWriter.WriteLineAsync(content.Body.Count(c => c == '\n').ToString());
+ await outputWriter.WriteLineAsync(content.Body);
+ await outputWriter.WriteLineAsync("=======================");
+ await outputWriter.FlushAsync();
+ }
+ }
+
+ public async Task WriteResponseToOutput(string id, MessageContent content)
+ {
+ using (await outputWriteSem.UseAsync())
+ {
+ await outputWriter.WriteLineAsync("======= Response =======");
+ await outputWriter.WriteLineAsync(id);
+ await outputWriter.WriteLineAsync(content.Body.Count(c => c == '\n').ToString());
+ await outputWriter.WriteLineAsync(content.Body);
+ await outputWriter.WriteLineAsync("========================");
+ await outputWriter.FlushAsync();
+ }
+ }
+
+ public async Task WriteLineToOutput(string eventName)
+ {
+ using (await outputWriteSem.UseAsync())
+ await outputWriter.WriteLineAsync($"======= {eventName} =======");
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj
new file mode 100644
index 0000000000..ae78da27bc
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <ProjectGuid>{B06C2951-C8E3-4F28-80B2-717CF327EB19}</ProjectGuid>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net472</TargetFramework>
+ <LangVersion>7.2</LangVersion>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\GodotTools.IdeMessaging\GodotTools.IdeMessaging.csproj" />
+ </ItemGroup>
+ <ItemGroup>
+ <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
+ </ItemGroup>
+</Project>
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs
new file mode 100644
index 0000000000..99a55c471b
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs
@@ -0,0 +1,218 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using GodotTools.IdeMessaging.Requests;
+using Newtonsoft.Json;
+
+namespace GodotTools.IdeMessaging.CLI
+{
+ internal static class Program
+ {
+ private static readonly ILogger Logger = new CustomLogger();
+
+ public static int Main(string[] args)
+ {
+ try
+ {
+ var mainTask = StartAsync(args, Console.OpenStandardInput(), Console.OpenStandardOutput());
+ mainTask.Wait();
+ return mainTask.Result;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Unhandled exception: ", ex);
+ return 1;
+ }
+ }
+
+ private static async Task<int> StartAsync(string[] args, Stream inputStream, Stream outputStream)
+ {
+ var inputReader = new StreamReader(inputStream, Encoding.UTF8);
+ var outputWriter = new StreamWriter(outputStream, Encoding.UTF8);
+
+ try
+ {
+ if (args.Length == 0)
+ {
+ Logger.LogError("Expected at least 1 argument");
+ return 1;
+ }
+
+ string godotProjectDir = args[0];
+
+ if (!Directory.Exists(godotProjectDir))
+ {
+ Logger.LogError($"The specified Godot project directory does not exist: {godotProjectDir}");
+ return 1;
+ }
+
+ var forwarder = new ForwarderMessageHandler(outputWriter);
+
+ using (var fwdClient = new Client("VisualStudioCode", godotProjectDir, forwarder, Logger))
+ {
+ fwdClient.Start();
+
+ // ReSharper disable AccessToDisposedClosure
+ fwdClient.Connected += async () => await forwarder.WriteLineToOutput("Event=Connected");
+ fwdClient.Disconnected += async () => await forwarder.WriteLineToOutput("Event=Disconnected");
+ // ReSharper restore AccessToDisposedClosure
+
+ // TODO: Await connected with timeout
+
+ while (!fwdClient.IsDisposed)
+ {
+ string firstLine = await inputReader.ReadLineAsync();
+
+ if (firstLine == null || firstLine == "QUIT")
+ goto ExitMainLoop;
+
+ string messageId = firstLine;
+
+ string messageArgcLine = await inputReader.ReadLineAsync();
+
+ if (messageArgcLine == null)
+ {
+ Logger.LogInfo("EOF when expecting argument count");
+ goto ExitMainLoop;
+ }
+
+ if (!int.TryParse(messageArgcLine, out int messageArgc))
+ {
+ Logger.LogError("Received invalid line for argument count: " + firstLine);
+ continue;
+ }
+
+ var body = new StringBuilder();
+
+ for (int i = 0; i < messageArgc; i++)
+ {
+ string bodyLine = await inputReader.ReadLineAsync();
+
+ if (bodyLine == null)
+ {
+ Logger.LogInfo($"EOF when expecting body line #{i + 1}");
+ goto ExitMainLoop;
+ }
+
+ body.AppendLine(bodyLine);
+ }
+
+ var response = await SendRequest(fwdClient, messageId, new MessageContent(MessageStatus.Ok, body.ToString()));
+
+ if (response == null)
+ {
+ Logger.LogError($"Failed to write message to the server: {messageId}");
+ }
+ else
+ {
+ var content = new MessageContent(response.Status, JsonConvert.SerializeObject(response));
+ await forwarder.WriteResponseToOutput(messageId, content);
+ }
+ }
+
+ ExitMainLoop:
+
+ await forwarder.WriteLineToOutput("Event=Quit");
+ }
+
+ return 0;
+ }
+ catch (Exception e)
+ {
+ Logger.LogError("Unhandled exception", e);
+ return 1;
+ }
+ }
+
+ private static async Task<Response> SendRequest(Client client, string id, MessageContent content)
+ {
+ var handlers = new Dictionary<string, Func<Task<Response>>>
+ {
+ [PlayRequest.Id] = async () =>
+ {
+ var request = JsonConvert.DeserializeObject<PlayRequest>(content.Body);
+ return await client.SendRequest<PlayResponse>(request);
+ },
+ [DebugPlayRequest.Id] = async () =>
+ {
+ var request = JsonConvert.DeserializeObject<DebugPlayRequest>(content.Body);
+ return await client.SendRequest<DebugPlayResponse>(request);
+ },
+ [ReloadScriptsRequest.Id] = async () =>
+ {
+ var request = JsonConvert.DeserializeObject<ReloadScriptsRequest>(content.Body);
+ return await client.SendRequest<ReloadScriptsResponse>(request);
+ },
+ [CodeCompletionRequest.Id] = async () =>
+ {
+ var request = JsonConvert.DeserializeObject<CodeCompletionRequest>(content.Body);
+ return await client.SendRequest<CodeCompletionResponse>(request);
+ }
+ };
+
+ if (handlers.TryGetValue(id, out var handler))
+ return await handler();
+
+ Console.WriteLine("INVALID REQUEST");
+ return null;
+ }
+
+ private class CustomLogger : ILogger
+ {
+ private static string ThisAppPath => Assembly.GetExecutingAssembly().Location;
+ private static string ThisAppPathWithoutExtension => Path.ChangeExtension(ThisAppPath, null);
+
+ private static readonly string LogPath = $"{ThisAppPathWithoutExtension}.log";
+
+ private static StreamWriter NewWriter() => new StreamWriter(LogPath, append: true, encoding: Encoding.UTF8);
+
+ private static void Log(StreamWriter writer, string message)
+ {
+ writer.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}: {message}");
+ }
+
+ public void LogDebug(string message)
+ {
+ using (var writer = NewWriter())
+ {
+ Log(writer, "DEBUG: " + message);
+ }
+ }
+
+ public void LogInfo(string message)
+ {
+ using (var writer = NewWriter())
+ {
+ Log(writer, "INFO: " + message);
+ }
+ }
+
+ public void LogWarning(string message)
+ {
+ using (var writer = NewWriter())
+ {
+ Log(writer, "WARN: " + message);
+ }
+ }
+
+ public void LogError(string message)
+ {
+ using (var writer = NewWriter())
+ {
+ Log(writer, "ERROR: " + message);
+ }
+ }
+
+ public void LogError(string message, Exception e)
+ {
+ using (var writer = NewWriter())
+ {
+ Log(writer, "EXCEPTION: " + message + '\n' + e);
+ }
+ }
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs
new file mode 100644
index 0000000000..d069651dd3
--- /dev/null
+++ b/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);
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientHandshake.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientHandshake.cs
new file mode 100644
index 0000000000..43041be7be
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientHandshake.cs
@@ -0,0 +1,44 @@
+using System.Text.RegularExpressions;
+
+namespace GodotTools.IdeMessaging
+{
+ public class ClientHandshake : IHandshake
+ {
+ private static readonly string ClientHandshakeBase = $"{Peer.ClientHandshakeName},Version={Peer.ProtocolVersionMajor}.{Peer.ProtocolVersionMinor}.{Peer.ProtocolVersionRevision}";
+ private static readonly string ServerHandshakePattern = $@"{Regex.Escape(Peer.ServerHandshakeName)},Version=([0-9]+)\.([0-9]+)\.([0-9]+),([_a-zA-Z][_a-zA-Z0-9]{{0,63}})";
+
+ public string GetHandshakeLine(string identity) => $"{ClientHandshakeBase},{identity}";
+
+ public bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger)
+ {
+ identity = null;
+
+ var match = Regex.Match(handshake, ServerHandshakePattern);
+
+ if (!match.Success)
+ return false;
+
+ if (!uint.TryParse(match.Groups[1].Value, out uint serverMajor) || Peer.ProtocolVersionMajor != serverMajor)
+ {
+ logger.LogDebug("Incompatible major version: " + match.Groups[1].Value);
+ return false;
+ }
+
+ if (!uint.TryParse(match.Groups[2].Value, out uint serverMinor) || Peer.ProtocolVersionMinor < serverMinor)
+ {
+ logger.LogDebug("Incompatible minor version: " + match.Groups[2].Value);
+ return false;
+ }
+
+ if (!uint.TryParse(match.Groups[3].Value, out uint _)) // Revision
+ {
+ logger.LogDebug("Incompatible revision build: " + match.Groups[3].Value);
+ return false;
+ }
+
+ identity = match.Groups[4].Value;
+
+ return true;
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientMessageHandler.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientMessageHandler.cs
new file mode 100644
index 0000000000..64bcfd824c
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientMessageHandler.cs
@@ -0,0 +1,52 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using GodotTools.IdeMessaging.Requests;
+using Newtonsoft.Json;
+
+namespace GodotTools.IdeMessaging
+{
+ // ReSharper disable once UnusedType.Global
+ public abstract class ClientMessageHandler : IMessageHandler
+ {
+ private readonly Dictionary<string, Peer.RequestHandler> requestHandlers;
+
+ protected ClientMessageHandler()
+ {
+ requestHandlers = InitializeRequestHandlers();
+ }
+
+ public async Task<MessageContent> HandleRequest(Peer peer, string id, MessageContent content, ILogger logger)
+ {
+ if (!requestHandlers.TryGetValue(id, out var handler))
+ {
+ logger.LogError($"Received unknown request: {id}");
+ return new MessageContent(MessageStatus.RequestNotSupported, "null");
+ }
+
+ try
+ {
+ var response = await handler(peer, content);
+ return new MessageContent(response.Status, JsonConvert.SerializeObject(response));
+ }
+ catch (JsonException)
+ {
+ logger.LogError($"Received request with invalid body: {id}");
+ return new MessageContent(MessageStatus.InvalidRequestBody, "null");
+ }
+ }
+
+ private Dictionary<string, Peer.RequestHandler> InitializeRequestHandlers()
+ {
+ return new Dictionary<string, Peer.RequestHandler>
+ {
+ [OpenFileRequest.Id] = async (peer, content) =>
+ {
+ var request = JsonConvert.DeserializeObject<OpenFileRequest>(content.Body);
+ return await HandleOpenFile(request);
+ }
+ };
+ }
+
+ protected abstract Task<Response> HandleOpenFile(OpenFileRequest request);
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs
index d16daba0e2..686202e81e 100644
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs
@@ -1,10 +1,12 @@
-namespace GodotTools.IdeConnection
+namespace GodotTools.IdeMessaging
{
- public struct GodotIdeMetadata
+ public readonly struct GodotIdeMetadata
{
public int Port { get; }
public string EditorExecutablePath { get; }
+ public const string DefaultFileName = "ide_messaging_meta.txt";
+
public GodotIdeMetadata(int port, string editorExecutablePath)
{
Port = port;
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj
new file mode 100644
index 0000000000..67815959a6
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj
@@ -0,0 +1,24 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <ProjectGuid>{92600954-25F0-4291-8E11-1FEE9FC4BE20}</ProjectGuid>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <LangVersion>7.2</LangVersion>
+ <PackageId>GodotTools.IdeMessaging</PackageId>
+ <Version>1.1.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>
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IHandshake.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IHandshake.cs
new file mode 100644
index 0000000000..6387145a28
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IHandshake.cs
@@ -0,0 +1,8 @@
+namespace GodotTools.IdeMessaging
+{
+ public interface IHandshake
+ {
+ string GetHandshakeLine(string identity);
+ bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger);
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ILogger.cs
index 614bb30271..d2855f93a1 100644
--- a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ILogger.cs
@@ -1,6 +1,6 @@
using System;
-namespace GodotTools.IdeConnection
+namespace GodotTools.IdeMessaging
{
public interface ILogger
{
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IMessageHandler.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IMessageHandler.cs
new file mode 100644
index 0000000000..9622fcc96d
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IMessageHandler.cs
@@ -0,0 +1,9 @@
+using System.Threading.Tasks;
+
+namespace GodotTools.IdeMessaging
+{
+ public interface IMessageHandler
+ {
+ Task<MessageContent> HandleRequest(Peer peer, string id, MessageContent content, ILogger logger);
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Message.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Message.cs
new file mode 100644
index 0000000000..6903ec197b
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Message.cs
@@ -0,0 +1,52 @@
+namespace GodotTools.IdeMessaging
+{
+ public class Message
+ {
+ public MessageKind Kind { get; }
+ public string Id { get; }
+ public MessageContent Content { get; }
+
+ public Message(MessageKind kind, string id, MessageContent content)
+ {
+ Kind = kind;
+ Id = id;
+ Content = content;
+ }
+
+ public override string ToString()
+ {
+ return $"{Kind} | {Id}";
+ }
+ }
+
+ public enum MessageKind
+ {
+ Request,
+ Response
+ }
+
+ public enum MessageStatus
+ {
+ Ok,
+ RequestNotSupported,
+ InvalidRequestBody
+ }
+
+ public readonly struct MessageContent
+ {
+ public MessageStatus Status { get; }
+ public string Body { get; }
+
+ public MessageContent(string body)
+ {
+ Status = MessageStatus.Ok;
+ Body = body;
+ }
+
+ public MessageContent(MessageStatus status, string body)
+ {
+ Status = status;
+ Body = body;
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/MessageDecoder.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/MessageDecoder.cs
new file mode 100644
index 0000000000..a00575a2a1
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/MessageDecoder.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Text;
+
+namespace GodotTools.IdeMessaging
+{
+ public class MessageDecoder
+ {
+ private class DecodedMessage
+ {
+ public MessageKind? Kind;
+ public string Id;
+ public MessageStatus? Status;
+ public readonly StringBuilder Body = new StringBuilder();
+ public uint? PendingBodyLines;
+
+ public void Clear()
+ {
+ Kind = null;
+ Id = null;
+ Status = null;
+ Body.Clear();
+ PendingBodyLines = null;
+ }
+
+ public Message ToMessage()
+ {
+ if (!Kind.HasValue || Id == null || !Status.HasValue ||
+ !PendingBodyLines.HasValue || PendingBodyLines.Value > 0)
+ throw new InvalidOperationException();
+
+ return new Message(Kind.Value, Id, new MessageContent(Status.Value, Body.ToString()));
+ }
+ }
+
+ public enum State
+ {
+ Decoding,
+ Decoded,
+ Errored
+ }
+
+ private readonly DecodedMessage decodingMessage = new DecodedMessage();
+
+ public State Decode(string messageLine, out Message decodedMessage)
+ {
+ decodedMessage = null;
+
+ if (!decodingMessage.Kind.HasValue)
+ {
+ if (!Enum.TryParse(messageLine, ignoreCase: true, out MessageKind kind))
+ {
+ decodingMessage.Clear();
+ return State.Errored;
+ }
+
+ decodingMessage.Kind = kind;
+ }
+ else if (decodingMessage.Id == null)
+ {
+ decodingMessage.Id = messageLine;
+ }
+ else if (decodingMessage.Status == null)
+ {
+ if (!Enum.TryParse(messageLine, ignoreCase: true, out MessageStatus status))
+ {
+ decodingMessage.Clear();
+ return State.Errored;
+ }
+
+ decodingMessage.Status = status;
+ }
+ else if (decodingMessage.PendingBodyLines == null)
+ {
+ if (!uint.TryParse(messageLine, out uint pendingBodyLines))
+ {
+ decodingMessage.Clear();
+ return State.Errored;
+ }
+
+ decodingMessage.PendingBodyLines = pendingBodyLines;
+ }
+ else
+ {
+ if (decodingMessage.PendingBodyLines > 0)
+ {
+ decodingMessage.Body.AppendLine(messageLine);
+ decodingMessage.PendingBodyLines -= 1;
+ }
+ else
+ {
+ decodedMessage = decodingMessage.ToMessage();
+ decodingMessage.Clear();
+ return State.Decoded;
+ }
+ }
+
+ return State.Decoding;
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs
new file mode 100644
index 0000000000..a4e86d6177
--- /dev/null
+++ b/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();
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Requests/Requests.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Requests/Requests.cs
new file mode 100644
index 0000000000..1dd4f852e5
--- /dev/null
+++ b/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
+ {
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs
new file mode 100644
index 0000000000..548e7f06ee
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs
@@ -0,0 +1,23 @@
+using GodotTools.IdeMessaging.Requests;
+using GodotTools.IdeMessaging.Utils;
+using Newtonsoft.Json;
+
+namespace GodotTools.IdeMessaging
+{
+ public abstract class ResponseAwaiter : NotifyAwaiter<Response>
+ {
+ public abstract void SetResult(MessageContent content);
+ }
+
+ public class ResponseAwaiter<T> : ResponseAwaiter
+ where T : Response, new()
+ {
+ public override void SetResult(MessageContent content)
+ {
+ if (content.Status == MessageStatus.Ok)
+ SetResult(JsonConvert.DeserializeObject<T>(content.Body));
+ else
+ SetResult(new T {Status = content.Status});
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs
index 700b786752..d84a63c83c 100644
--- a/modules/mono/editor/GodotTools/GodotTools/Utils/NotifyAwaiter.cs
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs
@@ -1,9 +1,9 @@
using System;
using System.Runtime.CompilerServices;
-namespace GodotTools.Utils
+namespace GodotTools.IdeMessaging.Utils
{
- public sealed class NotifyAwaiter<T> : INotifyCompletion
+ public class NotifyAwaiter<T> : INotifyCompletion
{
private Action continuation;
private Exception exception;
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/SemaphoreExtensions.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/SemaphoreExtensions.cs
new file mode 100644
index 0000000000..9d593fbf8a
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/SemaphoreExtensions.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GodotTools.IdeMessaging.Utils
+{
+ public static class SemaphoreExtensions
+ {
+ public static ConfiguredTaskAwaitable<IDisposable> UseAsync(this SemaphoreSlim semaphoreSlim, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ var wrapper = new SemaphoreSlimWaitReleaseWrapper(semaphoreSlim, out Task waitAsyncTask, cancellationToken);
+ return waitAsyncTask.ContinueWith<IDisposable>(t => wrapper, cancellationToken).ConfigureAwait(false);
+ }
+
+ private struct SemaphoreSlimWaitReleaseWrapper : IDisposable
+ {
+ private readonly SemaphoreSlim semaphoreSlim;
+
+ public SemaphoreSlimWaitReleaseWrapper(SemaphoreSlim semaphoreSlim, out Task waitAsyncTask, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ this.semaphoreSlim = semaphoreSlim;
+ waitAsyncTask = this.semaphoreSlim.WaitAsync(cancellationToken);
+ }
+
+ public void Dispose()
+ {
+ semaphoreSlim.Release();
+ }
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.sln b/modules/mono/editor/GodotTools/GodotTools.sln
index a3438ea5f3..f6147eb5bb 100644
--- a/modules/mono/editor/GodotTools/GodotTools.sln
+++ b/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
diff --git a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs
index cd646e6201..2d1b873808 100644
--- a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs
@@ -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[]
diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs
index 812d575f3b..ec3cae00d5 100644
--- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs
+++ b/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,10 +251,14 @@ 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;
}
@@ -299,7 +305,7 @@ namespace GodotTools
if (line >= 0)
{
args.Add("-g");
- args.Add($"{scriptPath}:{line + 1}:{col}");
+ args.Add($"{scriptPath}:{line}:{col}");
}
else
{
diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj
index 16e3e948be..6857663a4c 100644
--- a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj
+++ b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj
@@ -29,7 +29,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GodotTools.BuildLogger\GodotTools.BuildLogger.csproj" />
- <ProjectReference Include="..\GodotTools.IdeConnection\GodotTools.IdeConnection.csproj" />
+ <ProjectReference Include="..\GodotTools.IdeMessaging\GodotTools.IdeMessaging.csproj" />
<ProjectReference Include="..\GodotTools.ProjectEditor\GodotTools.ProjectEditor.csproj" />
<ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj" />
</ItemGroup>
diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs
index 54f0ffab96..e4932ca217 100644
--- a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs
@@ -1,73 +1,104 @@
using System;
using System.IO;
+using System.Threading.Tasks;
using Godot;
-using GodotTools.IdeConnection;
+using GodotTools.IdeMessaging;
+using GodotTools.IdeMessaging.Requests;
using GodotTools.Internals;
namespace GodotTools.Ides
{
- public class GodotIdeManager : Node, ISerializationListener
+ public sealed class GodotIdeManager : Node, ISerializationListener
{
- public GodotIdeServer GodotIdeServer { get; private set; }
+ private MessagingServer MessagingServer { get; set; }
private MonoDevelop.Instance monoDevelInstance;
private MonoDevelop.Instance vsForMacInstance;
- private GodotIdeServer GetRunningServer()
+ private MessagingServer GetRunningOrNewServer()
{
- if (GodotIdeServer != null && !GodotIdeServer.IsDisposed)
- return GodotIdeServer;
- StartServer();
- return GodotIdeServer;
+ if (MessagingServer != null && !MessagingServer.IsDisposed)
+ return MessagingServer;
+
+ MessagingServer?.Dispose();
+ MessagingServer = new MessagingServer(OS.GetExecutablePath(), ProjectSettings.GlobalizePath(GodotSharpDirs.ResMetadataDir), new GodotLogger());
+
+ _ = MessagingServer.Listen();
+
+ return MessagingServer;
}
public override void _Ready()
{
- StartServer();
+ _ = GetRunningOrNewServer();
}
public void OnBeforeSerialize()
{
- GodotIdeServer?.Dispose();
}
public void OnAfterDeserialize()
{
- StartServer();
+ _ = GetRunningOrNewServer();
}
- private ILogger logger;
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
- protected ILogger Logger
+ if (disposing)
+ {
+ MessagingServer?.Dispose();
+ }
+ }
+
+ private string GetExternalEditorIdentity(ExternalEditorId editorId)
{
- get => logger ?? (logger = new GodotLogger());
+ // Manually convert to string to avoid breaking compatibility in case we rename the enum fields.
+ switch (editorId)
+ {
+ case ExternalEditorId.None:
+ return null;
+ case ExternalEditorId.VisualStudio:
+ return "VisualStudio";
+ case ExternalEditorId.VsCode:
+ return "VisualStudioCode";
+ case ExternalEditorId.Rider:
+ return "Rider";
+ case ExternalEditorId.VisualStudioForMac:
+ return "VisualStudioForMac";
+ case ExternalEditorId.MonoDevelop:
+ return "MonoDevelop";
+ default:
+ throw new NotImplementedException();
+ }
}
- private void StartServer()
+ public async Task<EditorPick?> LaunchIdeAsync(int millisecondsTimeout = 10000)
{
- GodotIdeServer?.Dispose();
- GodotIdeServer = new GodotIdeServer(LaunchIde,
- OS.GetExecutablePath(),
- ProjectSettings.GlobalizePath(GodotSharpDirs.ResMetadataDir));
+ var editorId = (ExternalEditorId)GodotSharpEditor.Instance.GetEditorInterface()
+ .GetEditorSettings().GetSetting("mono/editor/external_editor");
+ string editorIdentity = GetExternalEditorIdentity(editorId);
- GodotIdeServer.Logger = Logger;
+ var runningServer = GetRunningOrNewServer();
- GodotIdeServer.StartServer();
- }
+ if (runningServer.IsAnyConnected(editorIdentity))
+ return new EditorPick(editorIdentity);
- protected override void Dispose(bool disposing)
- {
- base.Dispose(disposing);
+ LaunchIde(editorId, editorIdentity);
+
+ var timeoutTask = Task.Delay(millisecondsTimeout);
+ var completedTask = await Task.WhenAny(timeoutTask, runningServer.AwaitClientConnected(editorIdentity));
+
+ if (completedTask != timeoutTask)
+ return new EditorPick(editorIdentity);
- GodotIdeServer?.Dispose();
+ return null;
}
- private void LaunchIde()
+ private void LaunchIde(ExternalEditorId editorId, string editorIdentity)
{
- var editor = (ExternalEditorId)GodotSharpEditor.Instance.GetEditorInterface()
- .GetEditorSettings().GetSetting("mono/editor/external_editor");
-
- switch (editor)
+ switch (editorId)
{
case ExternalEditorId.None:
case ExternalEditorId.VisualStudio:
@@ -80,14 +111,14 @@ namespace GodotTools.Ides
{
MonoDevelop.Instance GetMonoDevelopInstance(string solutionPath)
{
- if (Utils.OS.IsOSX && editor == ExternalEditorId.VisualStudioForMac)
+ if (Utils.OS.IsOSX && editorId == ExternalEditorId.VisualStudioForMac)
{
- vsForMacInstance = vsForMacInstance ??
+ vsForMacInstance = (vsForMacInstance?.IsDisposed ?? true ? null : vsForMacInstance) ??
new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.VisualStudioForMac);
return vsForMacInstance;
}
- monoDevelInstance = monoDevelInstance ??
+ monoDevelInstance = (monoDevelInstance?.IsDisposed ?? true ? null : monoDevelInstance) ??
new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.MonoDevelop);
return monoDevelInstance;
}
@@ -96,12 +127,25 @@ namespace GodotTools.Ides
{
var instance = GetMonoDevelopInstance(GodotSharpDirs.ProjectSlnPath);
- if (!instance.IsRunning)
+ if (instance.IsRunning && !GetRunningOrNewServer().IsAnyConnected(editorIdentity))
+ {
+ // After launch we wait up to 30 seconds for the IDE to connect to our messaging server.
+ var waitAfterLaunch = TimeSpan.FromSeconds(30);
+ var timeSinceLaunch = DateTime.Now - instance.LaunchTime;
+ if (timeSinceLaunch > waitAfterLaunch)
+ {
+ instance.Dispose();
+ instance.Execute();
+ }
+ }
+ else if (!instance.IsRunning)
+ {
instance.Execute();
+ }
}
catch (FileNotFoundException)
{
- string editorName = editor == ExternalEditorId.VisualStudioForMac ? "Visual Studio" : "MonoDevelop";
+ string editorName = editorId == ExternalEditorId.VisualStudioForMac ? "Visual Studio" : "MonoDevelop";
GD.PushError($"Cannot find code editor: {editorName}");
}
@@ -113,26 +157,45 @@ namespace GodotTools.Ides
}
}
- private void WriteMessage(string id, params string[] arguments)
+ public readonly struct EditorPick
{
- GetRunningServer().WriteMessage(new Message(id, arguments));
- }
+ private readonly string identity;
- public void SendOpenFile(string file)
- {
- WriteMessage("OpenFile", file);
- }
+ public EditorPick(string identity)
+ {
+ this.identity = identity;
+ }
- public void SendOpenFile(string file, int line)
- {
- WriteMessage("OpenFile", file, line.ToString());
- }
+ public bool IsAnyConnected() =>
+ GodotSharpEditor.Instance.GodotIdeManager.GetRunningOrNewServer().IsAnyConnected(identity);
- public void SendOpenFile(string file, int line, int column)
- {
- WriteMessage("OpenFile", file, line.ToString(), column.ToString());
+ private void SendRequest<TResponse>(Request request)
+ where TResponse : Response, new()
+ {
+ // Logs an error if no client is connected with the specified identity
+ GodotSharpEditor.Instance.GodotIdeManager
+ .GetRunningOrNewServer()
+ .BroadcastRequest<TResponse>(identity, request);
+ }
+
+ public void SendOpenFile(string file)
+ {
+ SendRequest<OpenFileResponse>(new OpenFileRequest {File = file});
+ }
+
+ public void SendOpenFile(string file, int line)
+ {
+ SendRequest<OpenFileResponse>(new OpenFileRequest {File = file, Line = line});
+ }
+
+ public void SendOpenFile(string file, int line, int column)
+ {
+ SendRequest<OpenFileResponse>(new OpenFileRequest {File = file, Line = line, Column = column});
+ }
}
+ public EditorPick PickEditor(ExternalEditorId editorId) => new EditorPick(GetExternalEditorIdentity(editorId));
+
private class GodotLogger : ILogger
{
public void LogDebug(string message)
diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs
deleted file mode 100644
index 72676a8b24..0000000000
--- a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeServer.cs
+++ /dev/null
@@ -1,212 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using System.Net.Sockets;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using GodotTools.IdeConnection;
-using GodotTools.Internals;
-using GodotTools.Utils;
-using Directory = System.IO.Directory;
-using File = System.IO.File;
-using Thread = System.Threading.Thread;
-
-namespace GodotTools.Ides
-{
- public class GodotIdeServer : GodotIdeBase
- {
- private readonly TcpListener listener;
- private readonly FileStream metaFile;
- private readonly Action launchIdeAction;
- private readonly NotifyAwaiter<bool> clientConnectedAwaiter = new NotifyAwaiter<bool>();
-
- private async Task<bool> AwaitClientConnected()
- {
- return await clientConnectedAwaiter.Reset();
- }
-
- public GodotIdeServer(Action launchIdeAction, string editorExecutablePath, string projectMetadataDir)
- : base(projectMetadataDir)
- {
- messageHandlers = InitializeMessageHandlers();
-
- this.launchIdeAction = launchIdeAction;
-
- // Make sure the directory exists
- Directory.CreateDirectory(projectMetadataDir);
-
- // The Godot editor's file system thread can keep the file open for writing, so we are forced to allow write sharing...
- const FileShare metaFileShare = FileShare.ReadWrite;
-
- metaFile = File.Open(MetaFilePath, FileMode.Create, FileAccess.Write, metaFileShare);
-
- listener = new TcpListener(new IPEndPoint(IPAddress.Loopback, port: 0));
- listener.Start();
-
- int port = ((IPEndPoint)listener.Server.LocalEndPoint).Port;
- using (var metaFileWriter = new StreamWriter(metaFile, Encoding.UTF8))
- {
- metaFileWriter.WriteLine(port);
- metaFileWriter.WriteLine(editorExecutablePath);
- }
-
- StartServer();
- }
-
- public void StartServer()
- {
- var serverThread = new Thread(RunServerThread) { Name = "Godot Ide Connection Server" };
- serverThread.Start();
- }
-
- private void RunServerThread()
- {
- SynchronizationContext.SetSynchronizationContext(Godot.Dispatcher.SynchronizationContext);
-
- try
- {
- while (!IsDisposed)
- {
- TcpClient tcpClient = listener.AcceptTcpClient();
-
- Logger.LogInfo("Connection open with Ide Client");
-
- lock (ConnectionLock)
- {
- Connection = new GodotIdeConnectionServer(tcpClient, HandleMessage);
- Connection.Logger = Logger;
- }
-
- Connected += () => clientConnectedAwaiter.SetResult(true);
-
- Connection.Start();
- }
- }
- catch (Exception e)
- {
- if (!IsDisposed && !(e is SocketException se && se.SocketErrorCode == SocketError.Interrupted))
- throw;
- }
- }
-
- public async void WriteMessage(Message message)
- {
- async Task LaunchIde()
- {
- if (IsConnected)
- return;
-
- launchIdeAction();
- await Task.WhenAny(Task.Delay(10000), AwaitClientConnected());
- }
-
- await LaunchIde();
-
- if (!IsConnected)
- {
- Logger.LogError("Cannot write message: Godot Ide Server not connected");
- return;
- }
-
- Connection.WriteMessage(message);
- }
-
- protected override void Dispose(bool disposing)
- {
- base.Dispose(disposing);
-
- if (disposing)
- {
- listener?.Stop();
-
- metaFile?.Dispose();
-
- File.Delete(MetaFilePath);
- }
- }
-
- protected virtual bool HandleMessage(Message message)
- {
- if (messageHandlers.TryGetValue(message.Id, out var action))
- {
- action(message.Arguments);
- return true;
- }
-
- return false;
- }
-
- private readonly Dictionary<string, Action<string[]>> messageHandlers;
-
- private Dictionary<string, Action<string[]>> InitializeMessageHandlers()
- {
- return new Dictionary<string, Action<string[]>>
- {
- ["Play"] = args =>
- {
- switch (args.Length)
- {
- case 0:
- Play();
- return;
- case 2:
- Play(debuggerHost: args[0], debuggerPort: int.Parse(args[1]));
- return;
- default:
- throw new ArgumentException();
- }
- },
- ["ReloadScripts"] = args => ReloadScripts()
- };
- }
-
- private void DispatchToMainThread(Action action)
- {
- var d = new SendOrPostCallback(state => action());
- Godot.Dispatcher.SynchronizationContext.Post(d, null);
- }
-
- private void Play()
- {
- DispatchToMainThread(() =>
- {
- CurrentPlayRequest = new PlayRequest();
- Internal.EditorRunPlay();
- CurrentPlayRequest = null;
- });
- }
-
- private void Play(string debuggerHost, int debuggerPort)
- {
- DispatchToMainThread(() =>
- {
- CurrentPlayRequest = new PlayRequest(debuggerHost, debuggerPort);
- Internal.EditorRunPlay();
- CurrentPlayRequest = null;
- });
- }
-
- private void ReloadScripts()
- {
- DispatchToMainThread(Internal.ScriptEditorDebugger_ReloadScripts);
- }
-
- public PlayRequest? CurrentPlayRequest { get; private set; }
-
- public struct PlayRequest
- {
- public bool HasDebugger { get; }
- public string DebuggerHost { get; }
- public int DebuggerPort { get; }
-
- public PlayRequest(string debuggerHost, int debuggerPort)
- {
- HasDebugger = true;
- DebuggerHost = debuggerHost;
- DebuggerPort = debuggerPort;
- }
- }
- }
-}
diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs
new file mode 100644
index 0000000000..32f264d100
--- /dev/null
+++ b/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;
+ }
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs
index 0fdc964039..d6fa2eeba7 100644
--- a/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs
@@ -7,14 +7,16 @@ using GodotTools.Utils;
namespace GodotTools.Ides.MonoDevelop
{
- public class Instance
+ public class Instance : IDisposable
{
+ public DateTime LaunchTime { get; private set; }
private readonly string solutionFile;
private readonly EditorId editorId;
private Process process;
public bool IsRunning => process != null && !process.HasExited;
+ public bool IsDisposed { get; private set; }
public void Execute()
{
@@ -59,6 +61,8 @@ namespace GodotTools.Ides.MonoDevelop
if (command == null)
throw new FileNotFoundException();
+ LaunchTime = DateTime.Now;
+
if (newWindow)
{
process = Process.Start(new ProcessStartInfo
@@ -88,6 +92,12 @@ namespace GodotTools.Ides.MonoDevelop
this.editorId = editorId;
}
+ public void Dispose()
+ {
+ IsDisposed = true;
+ process?.Dispose();
+ }
+
private static readonly IReadOnlyDictionary<EditorId, string> ExecutableNames;
private static readonly IReadOnlyDictionary<EditorId, string> BundleIds;
diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs
index 026a7db89c..7e5049e4b7 100644
--- a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs
@@ -2,6 +2,7 @@ using System;
using System.Runtime.CompilerServices;
using Godot;
using Godot.Collections;
+using GodotTools.IdeMessaging.Requests;
namespace GodotTools.Internals
{
@@ -52,6 +53,9 @@ namespace GodotTools.Internals
public static void ScriptEditorDebugger_ReloadScripts() => internal_ScriptEditorDebugger_ReloadScripts();
+ public static string[] CodeCompletionRequest(CodeCompletionRequest.CompletionKind kind, string scriptFile) =>
+ internal_CodeCompletionRequest((int)kind, scriptFile);
+
#region Internal
[MethodImpl(MethodImplOptions.InternalCall)]
@@ -111,6 +115,9 @@ namespace GodotTools.Internals
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void internal_ScriptEditorDebugger_ReloadScripts();
+ [MethodImpl(MethodImplOptions.InternalCall)]
+ private static extern string[] internal_CodeCompletionRequest(int kind, string scriptFile);
+
#endregion
}
}
diff --git a/modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs b/modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs
new file mode 100644
index 0000000000..820d0c0b83
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs
@@ -0,0 +1,19 @@
+namespace GodotTools
+{
+ public struct PlaySettings
+ {
+ public bool HasDebugger { get; }
+ public string DebuggerHost { get; }
+ public int DebuggerPort { get; }
+
+ public bool BuildBeforePlaying { get; }
+
+ public PlaySettings(string debuggerHost, int debuggerPort, bool buildBeforePlaying)
+ {
+ HasDebugger = true;
+ DebuggerHost = debuggerHost;
+ DebuggerPort = debuggerPort;
+ BuildBeforePlaying = buildBeforePlaying;
+ }
+ }
+}
diff --git a/modules/mono/editor/code_completion.cpp b/modules/mono/editor/code_completion.cpp
new file mode 100644
index 0000000000..7a5e465e7a
--- /dev/null
+++ b/modules/mono/editor/code_completion.cpp
@@ -0,0 +1,249 @@
+/*************************************************************************/
+/* code_completion.cpp */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#include "code_completion.h"
+
+#include "core/project_settings.h"
+#include "editor/editor_file_system.h"
+#include "editor/editor_settings.h"
+#include "scene/gui/control.h"
+#include "scene/main/node.h"
+
+namespace gdmono {
+
+// Almost everything here is taken from functions used by GDScript for code completion, adapted for C#.
+
+_FORCE_INLINE_ String quoted(const String &p_str) {
+ return "\"" + p_str + "\"";
+}
+
+void _add_nodes_suggestions(const Node *p_base, const Node *p_node, PackedStringArray &r_suggestions) {
+ if (p_node != p_base && !p_node->get_owner())
+ return;
+
+ String path_relative_to_orig = p_base->get_path_to(p_node);
+
+ r_suggestions.push_back(quoted(path_relative_to_orig));
+
+ for (int i = 0; i < p_node->get_child_count(); i++) {
+ _add_nodes_suggestions(p_base, p_node->get_child(i), r_suggestions);
+ }
+}
+
+Node *_find_node_for_script(Node *p_base, Node *p_current, const Ref<Script> &p_script) {
+ if (p_current->get_owner() != p_base && p_base != p_current)
+ return nullptr;
+
+ Ref<Script> c = p_current->get_script();
+
+ if (c == p_script)
+ return p_current;
+
+ for (int i = 0; i < p_current->get_child_count(); i++) {
+ Node *found = _find_node_for_script(p_base, p_current->get_child(i), p_script);
+ if (found)
+ return found;
+ }
+
+ return nullptr;
+}
+
+void _get_directory_contents(EditorFileSystemDirectory *p_dir, PackedStringArray &r_suggestions) {
+ for (int i = 0; i < p_dir->get_file_count(); i++) {
+ r_suggestions.push_back(quoted(p_dir->get_file_path(i)));
+ }
+
+ for (int i = 0; i < p_dir->get_subdir_count(); i++) {
+ _get_directory_contents(p_dir->get_subdir(i), r_suggestions);
+ }
+}
+
+Node *_try_find_owner_node_in_tree(const Ref<Script> p_script) {
+ SceneTree *tree = SceneTree::get_singleton();
+ if (!tree)
+ return nullptr;
+ Node *base = tree->get_edited_scene_root();
+ if (base) {
+ base = _find_node_for_script(base, base, p_script);
+ }
+ return base;
+}
+
+PackedStringArray get_code_completion(CompletionKind p_kind, const String &p_script_file) {
+ PackedStringArray suggestions;
+
+ switch (p_kind) {
+ case CompletionKind::INPUT_ACTIONS: {
+ List<PropertyInfo> project_props;
+ ProjectSettings::get_singleton()->get_property_list(&project_props);
+
+ for (List<PropertyInfo>::Element *E = project_props.front(); E; E = E->next()) {
+ const PropertyInfo &prop = E->get();
+
+ if (!prop.name.begins_with("input/"))
+ continue;
+
+ String name = prop.name.substr(prop.name.find("/") + 1, prop.name.length());
+ suggestions.push_back(quoted(name));
+ }
+ } break;
+ case CompletionKind::NODE_PATHS: {
+ {
+ // AutoLoads
+ List<PropertyInfo> props;
+ ProjectSettings::get_singleton()->get_property_list(&props);
+
+ for (List<PropertyInfo>::Element *E = props.front(); E; E = E->next()) {
+ String s = E->get().name;
+ if (!s.begins_with("autoload/")) {
+ continue;
+ }
+ String name = s.get_slice("/", 1);
+ suggestions.push_back(quoted("/root/" + name));
+ }
+ }
+
+ {
+ // Current edited scene tree
+ Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path());
+ Node *base = _try_find_owner_node_in_tree(script);
+ if (base) {
+ _add_nodes_suggestions(base, base, suggestions);
+ }
+ }
+ } break;
+ case CompletionKind::RESOURCE_PATHS: {
+ if (bool(EditorSettings::get_singleton()->get("text_editor/completion/complete_file_paths"))) {
+ _get_directory_contents(EditorFileSystem::get_singleton()->get_filesystem(), suggestions);
+ }
+ } break;
+ case CompletionKind::SCENE_PATHS: {
+ DirAccessRef dir_access = DirAccess::create(DirAccess::ACCESS_RESOURCES);
+ List<String> directories;
+ directories.push_back(dir_access->get_current_dir());
+
+ while (!directories.empty()) {
+ dir_access->change_dir(directories.back()->get());
+ directories.pop_back();
+
+ dir_access->list_dir_begin();
+ String filename = dir_access->get_next();
+
+ while (filename != "") {
+ if (filename == "." || filename == "..") {
+ filename = dir_access->get_next();
+ continue;
+ }
+
+ if (dir_access->dir_exists(filename)) {
+ directories.push_back(dir_access->get_current_dir().plus_file(filename));
+ } else if (filename.ends_with(".tscn") || filename.ends_with(".scn")) {
+ suggestions.push_back(quoted(dir_access->get_current_dir().plus_file(filename)));
+ }
+
+ filename = dir_access->get_next();
+ }
+ }
+ } break;
+ case CompletionKind::SHADER_PARAMS: {
+ print_verbose("Shared params completion for C# not implemented.");
+ } break;
+ case CompletionKind::SIGNALS: {
+ Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path());
+
+ List<MethodInfo> signals;
+ script->get_script_signal_list(&signals);
+
+ StringName native = script->get_instance_base_type();
+ if (native != StringName()) {
+ ClassDB::get_signal_list(native, &signals, /* p_no_inheritance: */ false);
+ }
+
+ for (List<MethodInfo>::Element *E = signals.front(); E; E = E->next()) {
+ const String &signal = E->get().name;
+ suggestions.push_back(quoted(signal));
+ }
+ } break;
+ case CompletionKind::THEME_COLORS: {
+ Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path());
+ Node *base = _try_find_owner_node_in_tree(script);
+ if (base && Object::cast_to<Control>(base)) {
+ List<StringName> sn;
+ Theme::get_default()->get_color_list(base->get_class(), &sn);
+
+ for (List<StringName>::Element *E = sn.front(); E; E = E->next()) {
+ suggestions.push_back(quoted(E->get()));
+ }
+ }
+ } break;
+ case CompletionKind::THEME_CONSTANTS: {
+ Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path());
+ Node *base = _try_find_owner_node_in_tree(script);
+ if (base && Object::cast_to<Control>(base)) {
+ List<StringName> sn;
+ Theme::get_default()->get_constant_list(base->get_class(), &sn);
+
+ for (List<StringName>::Element *E = sn.front(); E; E = E->next()) {
+ suggestions.push_back(quoted(E->get()));
+ }
+ }
+ } break;
+ case CompletionKind::THEME_FONTS: {
+ Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path());
+ Node *base = _try_find_owner_node_in_tree(script);
+ if (base && Object::cast_to<Control>(base)) {
+ List<StringName> sn;
+ Theme::get_default()->get_font_list(base->get_class(), &sn);
+
+ for (List<StringName>::Element *E = sn.front(); E; E = E->next()) {
+ suggestions.push_back(quoted(E->get()));
+ }
+ }
+ } break;
+ case CompletionKind::THEME_STYLES: {
+ Ref<Script> script = ResourceLoader::load(p_script_file.simplify_path());
+ Node *base = _try_find_owner_node_in_tree(script);
+ if (base && Object::cast_to<Control>(base)) {
+ List<StringName> sn;
+ Theme::get_default()->get_stylebox_list(base->get_class(), &sn);
+
+ for (List<StringName>::Element *E = sn.front(); E; E = E->next()) {
+ suggestions.push_back(quoted(E->get()));
+ }
+ }
+ } break;
+ default:
+ ERR_FAIL_V_MSG(suggestions, "Invalid completion kind.");
+ }
+
+ return suggestions;
+}
+
+} // namespace gdmono
diff --git a/modules/mono/editor/code_completion.h b/modules/mono/editor/code_completion.h
new file mode 100644
index 0000000000..77673b766f
--- /dev/null
+++ b/modules/mono/editor/code_completion.h
@@ -0,0 +1,56 @@
+/*************************************************************************/
+/* code_completion.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef CODE_COMPLETION_H
+#define CODE_COMPLETION_H
+
+#include "core/ustring.h"
+#include "core/variant.h"
+
+namespace gdmono {
+
+enum class CompletionKind {
+ INPUT_ACTIONS = 0,
+ NODE_PATHS,
+ RESOURCE_PATHS,
+ SCENE_PATHS,
+ SHADER_PARAMS,
+ SIGNALS,
+ THEME_COLORS,
+ THEME_CONSTANTS,
+ THEME_FONTS,
+ THEME_STYLES
+};
+
+PackedStringArray get_code_completion(CompletionKind p_kind, const String &p_script_file);
+
+} // namespace gdmono
+
+#endif // CODE_COMPLETION_H
diff --git a/modules/mono/editor/editor_internal_calls.cpp b/modules/mono/editor/editor_internal_calls.cpp
index c3e7e67ae9..c9117f1312 100644
--- a/modules/mono/editor/editor_internal_calls.cpp
+++ b/modules/mono/editor/editor_internal_calls.cpp
@@ -48,6 +48,7 @@
#include "../mono_gd/gd_mono_marshal.h"
#include "../utils/osx_utils.h"
#include "bindings_generator.h"
+#include "code_completion.h"
#include "godotsharp_export.h"
#include "script_class_parser.h"
@@ -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);
diff --git a/modules/mono/mono_gd/gd_mono.cpp b/modules/mono/mono_gd/gd_mono.cpp
index a6ed2957e6..fbaa81e83f 100644
--- a/modules/mono/mono_gd/gd_mono.cpp
+++ b/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);