summaryrefslogtreecommitdiff
path: root/modules/mono/editor/GodotTools/GodotTools.IdeConnection
diff options
context:
space:
mode:
authorIgnacio Etcheverry <ignalfonsore@gmail.com>2019-07-18 04:08:24 +0200
committerIgnacio Etcheverry <ignalfonsore@gmail.com>2019-08-04 01:57:53 +0200
commit0b94203a79d3261d4cc3bbcdb3438a5a45c8c572 (patch)
treed36bb081a3bda748a55a996578efb650d78a06b2 /modules/mono/editor/GodotTools/GodotTools.IdeConnection
parent4b7b1b0d4acf8d49505a839a0aa745ce60641545 (diff)
C#: Add Ide Connection library and server for the editor
This will be used for communicating between the Godot editor and external IDEs/editors, for things like opening files, triggering hot-reload and running the game with a debugger attached.
Diffstat (limited to 'modules/mono/editor/GodotTools/GodotTools.IdeConnection')
-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/GodotIdeMetadata.cs45
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj52
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs13
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs21
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs46
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs88
-rw-r--r--modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs35
13 files changed, 901 insertions, 0 deletions
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs
new file mode 100644
index 0000000000..7a2ff2ca56
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ConsoleLogger.cs
@@ -0,0 +1,33 @@
+using System;
+
+namespace GodotTools.IdeConnection
+{
+ public class ConsoleLogger : ILogger
+ {
+ public void LogDebug(string message)
+ {
+ Console.WriteLine("DEBUG: " + message);
+ }
+
+ public void LogInfo(string message)
+ {
+ Console.WriteLine("INFO: " + message);
+ }
+
+ public void LogWarning(string message)
+ {
+ Console.WriteLine("WARN: " + message);
+ }
+
+ public void LogError(string message)
+ {
+ Console.WriteLine("ERROR: " + message);
+ }
+
+ public void LogError(string message, Exception e)
+ {
+ Console.WriteLine("EXCEPTION: " + message);
+ Console.WriteLine(e);
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs
new file mode 100644
index 0000000000..be89638241
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeBase.cs
@@ -0,0 +1,94 @@
+using System;
+using Path = System.IO.Path;
+
+namespace GodotTools.IdeConnection
+{
+ public class GodotIdeBase : IDisposable
+ {
+ private ILogger logger;
+
+ public ILogger Logger
+ {
+ get => logger ?? (logger = new ConsoleLogger());
+ set => logger = value;
+ }
+
+ private readonly string projectMetadataDir;
+
+ protected const string MetaFileName = "ide_server_meta.txt";
+ protected string MetaFilePath => Path.Combine(projectMetadataDir, MetaFileName);
+
+ private GodotIdeConnection connection;
+ protected readonly object ConnectionLock = new object();
+
+ public bool IsDisposed { get; private set; } = false;
+
+ public bool IsConnected => connection != null && !connection.IsDisposed && connection.IsConnected;
+
+ public event Action Connected
+ {
+ add
+ {
+ if (connection != null && !connection.IsDisposed)
+ connection.Connected += value;
+ }
+ remove
+ {
+ if (connection != null && !connection.IsDisposed)
+ connection.Connected -= value;
+ }
+ }
+
+ protected GodotIdeConnection Connection
+ {
+ get => connection;
+ set
+ {
+ connection?.Dispose();
+ connection = value;
+ }
+ }
+
+ protected GodotIdeBase(string projectMetadataDir)
+ {
+ this.projectMetadataDir = projectMetadataDir;
+ }
+
+ protected void DisposeConnection()
+ {
+ lock (ConnectionLock)
+ {
+ connection?.Dispose();
+ }
+ }
+
+ ~GodotIdeBase()
+ {
+ Dispose(disposing: false);
+ }
+
+ public void Dispose()
+ {
+ if (IsDisposed)
+ return;
+
+ lock (ConnectionLock)
+ {
+ if (IsDisposed) // lock may not be fair
+ return;
+ IsDisposed = true;
+ }
+
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ connection?.Dispose();
+ }
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs
new file mode 100644
index 0000000000..4f56a8d71b
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeClient.cs
@@ -0,0 +1,219 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+
+namespace GodotTools.IdeConnection
+{
+ public abstract class GodotIdeClient : GodotIdeBase
+ {
+ protected GodotIdeMetadata GodotIdeMetadata;
+
+ private readonly FileSystemWatcher fsWatcher;
+
+ protected GodotIdeClient(string projectMetadataDir) : base(projectMetadataDir)
+ {
+ messageHandlers = InitializeMessageHandlers();
+
+ // FileSystemWatcher requires an existing directory
+ if (!File.Exists(projectMetadataDir))
+ Directory.CreateDirectory(projectMetadataDir);
+
+ fsWatcher = new FileSystemWatcher(projectMetadataDir, MetaFileName);
+ }
+
+ private void OnMetaFileChanged(object sender, FileSystemEventArgs e)
+ {
+ if (IsDisposed)
+ return;
+
+ lock (ConnectionLock)
+ {
+ if (IsDisposed)
+ return;
+
+ if (!File.Exists(MetaFilePath))
+ return;
+
+ var metadata = ReadMetadataFile();
+
+ if (metadata != null && metadata != GodotIdeMetadata)
+ {
+ GodotIdeMetadata = metadata.Value;
+ ConnectToServer();
+ }
+ }
+ }
+
+ private void OnMetaFileDeleted(object sender, FileSystemEventArgs e)
+ {
+ if (IsDisposed)
+ return;
+
+ if (IsConnected)
+ DisposeConnection();
+
+ // The file may have been re-created
+
+ lock (ConnectionLock)
+ {
+ if (IsDisposed)
+ return;
+
+ if (IsConnected || !File.Exists(MetaFilePath))
+ return;
+
+ var metadata = ReadMetadataFile();
+
+ if (metadata != null)
+ {
+ GodotIdeMetadata = metadata.Value;
+ ConnectToServer();
+ }
+ }
+ }
+
+ private GodotIdeMetadata? ReadMetadataFile()
+ {
+ using (var reader = File.OpenText(MetaFilePath))
+ {
+ string portStr = reader.ReadLine();
+
+ if (portStr == null)
+ return null;
+
+ string editorExecutablePath = reader.ReadLine();
+
+ if (editorExecutablePath == null)
+ return null;
+
+ if (!int.TryParse(portStr, out int port))
+ return null;
+
+ return new GodotIdeMetadata(port, editorExecutablePath);
+ }
+ }
+
+ private void ConnectToServer()
+ {
+ var tcpClient = new TcpClient();
+
+ Connection = new GodotIdeConnectionClient(tcpClient, HandleMessage);
+ Connection.Logger = Logger;
+
+ try
+ {
+ Logger.LogInfo("Connecting to Godot Ide Server");
+
+ tcpClient.Connect(IPAddress.Loopback, GodotIdeMetadata.Port);
+
+ Logger.LogInfo("Connection open with Godot Ide Server");
+
+ var clientThread = new Thread(Connection.Start)
+ {
+ IsBackground = true,
+ Name = "Godot Ide Connection Client"
+ };
+ clientThread.Start();
+ }
+ catch (SocketException e)
+ {
+ if (e.SocketErrorCode == SocketError.ConnectionRefused)
+ Logger.LogError("The connection to the Godot Ide Server was refused");
+ else
+ throw;
+ }
+ }
+
+ public void Start()
+ {
+ Logger.LogInfo("Starting Godot Ide Client");
+
+ fsWatcher.Changed += OnMetaFileChanged;
+ fsWatcher.Deleted += OnMetaFileDeleted;
+ fsWatcher.EnableRaisingEvents = true;
+
+ lock (ConnectionLock)
+ {
+ if (IsDisposed)
+ return;
+
+ if (!File.Exists(MetaFilePath))
+ {
+ Logger.LogInfo("There is no Godot Ide Server running");
+ return;
+ }
+
+ var metadata = ReadMetadataFile();
+
+ if (metadata != null)
+ {
+ GodotIdeMetadata = metadata.Value;
+ ConnectToServer();
+ }
+ else
+ {
+ Logger.LogError("Failed to read Godot Ide metadata file");
+ }
+ }
+ }
+
+ public bool WriteMessage(Message message)
+ {
+ return Connection.WriteMessage(message);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ if (disposing)
+ {
+ fsWatcher?.Dispose();
+ }
+ }
+
+ protected virtual bool HandleMessage(Message message)
+ {
+ if (messageHandlers.TryGetValue(message.Id, out var action))
+ {
+ action(message.Arguments);
+ return true;
+ }
+
+ return false;
+ }
+
+ private readonly Dictionary<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
new file mode 100644
index 0000000000..e7e81f175e
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnection.cs
@@ -0,0 +1,207 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Net.Sockets;
+using System.Text;
+
+namespace GodotTools.IdeConnection
+{
+ public abstract class GodotIdeConnection : IDisposable
+ {
+ protected const string Version = "1.0";
+
+ protected static readonly string ClientHandshake = $"Godot Ide Client Version {Version}";
+ protected static readonly string ServerHandshake = $"Godot Ide Server Version {Version}";
+
+ private const int ClientWriteTimeout = 8000;
+ private readonly TcpClient tcpClient;
+
+ private TextReader clientReader;
+ private TextWriter clientWriter;
+
+ private readonly object writeLock = new object();
+
+ private readonly Func<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
new file mode 100644
index 0000000000..1b11a14358
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionClient.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+
+namespace GodotTools.IdeConnection
+{
+ public class GodotIdeConnectionClient : GodotIdeConnection
+ {
+ public GodotIdeConnectionClient(TcpClient tcpClient, Func<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
new file mode 100644
index 0000000000..aa98dc7ca3
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeConnectionServer.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+
+namespace GodotTools.IdeConnection
+{
+ public class GodotIdeConnectionServer : GodotIdeConnection
+ {
+ public GodotIdeConnectionServer(TcpClient tcpClient, Func<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/GodotIdeMetadata.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs
new file mode 100644
index 0000000000..d16daba0e2
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotIdeMetadata.cs
@@ -0,0 +1,45 @@
+namespace GodotTools.IdeConnection
+{
+ public struct GodotIdeMetadata
+ {
+ public int Port { get; }
+ public string EditorExecutablePath { get; }
+
+ public GodotIdeMetadata(int port, string editorExecutablePath)
+ {
+ Port = port;
+ EditorExecutablePath = editorExecutablePath;
+ }
+
+ public static bool operator ==(GodotIdeMetadata a, GodotIdeMetadata b)
+ {
+ return a.Port == b.Port && a.EditorExecutablePath == b.EditorExecutablePath;
+ }
+
+ public static bool operator !=(GodotIdeMetadata a, GodotIdeMetadata b)
+ {
+ return !(a == b);
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (obj is GodotIdeMetadata metadata)
+ return metadata == this;
+
+ return false;
+ }
+
+ public bool Equals(GodotIdeMetadata other)
+ {
+ return Port == other.Port && EditorExecutablePath == other.EditorExecutablePath;
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (Port * 397) ^ (EditorExecutablePath != null ? EditorExecutablePath.GetHashCode() : 0);
+ }
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj
new file mode 100644
index 0000000000..84c08251ab
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/GodotTools.IdeConnection.csproj
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{92600954-25F0-4291-8E11-1FEE9FC4BE20}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>GodotTools.IdeConnection</RootNamespace>
+ <AssemblyName>GodotTools.IdeConnection</AssemblyName>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <PlatformTarget>AnyCPU</PlatformTarget>
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>portable</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <PlatformTarget>AnyCPU</PlatformTarget>
+ <DebugType>portable</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="ConsoleLogger.cs" />
+ <Compile Include="GodotIdeMetadata.cs" />
+ <Compile Include="GodotIdeBase.cs" />
+ <Compile Include="GodotIdeClient.cs" />
+ <Compile Include="GodotIdeConnection.cs" />
+ <Compile Include="GodotIdeConnectionClient.cs" />
+ <Compile Include="GodotIdeConnectionServer.cs" />
+ <Compile Include="ILogger.cs" />
+ <Compile Include="Message.cs" />
+ <Compile Include="MessageComposer.cs" />
+ <Compile Include="MessageParser.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs
new file mode 100644
index 0000000000..614bb30271
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/ILogger.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace GodotTools.IdeConnection
+{
+ public interface ILogger
+ {
+ void LogDebug(string message);
+ void LogInfo(string message);
+ void LogWarning(string message);
+ void LogError(string message);
+ void LogError(string message, Exception e);
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs
new file mode 100644
index 0000000000..f24d324ae3
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Message.cs
@@ -0,0 +1,21 @@
+using System.Linq;
+
+namespace GodotTools.IdeConnection
+{
+ public struct Message
+ {
+ public string Id { get; set; }
+ public string[] Arguments { get; set; }
+
+ public Message(string id, params string[] arguments)
+ {
+ Id = id;
+ Arguments = arguments;
+ }
+
+ public override string ToString()
+ {
+ return $"(Id: '{Id}', Arguments: '{string.Join(",", Arguments)}')";
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs
new file mode 100644
index 0000000000..9e4cd6ec1a
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageComposer.cs
@@ -0,0 +1,46 @@
+using System.Linq;
+using System.Text;
+
+namespace GodotTools.IdeConnection
+{
+ public class MessageComposer
+ {
+ private readonly StringBuilder stringBuilder = new StringBuilder();
+
+ private static readonly char[] CharsToEscape = { '\\', '"' };
+
+ public void AddArgument(string argument)
+ {
+ AddArgument(argument, quoted: argument.Contains(","));
+ }
+
+ public void AddArgument(string argument, bool quoted)
+ {
+ if (stringBuilder.Length > 0)
+ stringBuilder.Append(',');
+
+ if (quoted)
+ {
+ stringBuilder.Append('"');
+
+ foreach (char @char in argument)
+ {
+ if (CharsToEscape.Contains(@char))
+ stringBuilder.Append('\\');
+ stringBuilder.Append(@char);
+ }
+
+ stringBuilder.Append('"');
+ }
+ else
+ {
+ stringBuilder.Append(argument);
+ }
+ }
+
+ public override string ToString()
+ {
+ return stringBuilder.ToString();
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs
new file mode 100644
index 0000000000..ed691e481f
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/MessageParser.cs
@@ -0,0 +1,88 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace GodotTools.IdeConnection
+{
+ public static class MessageParser
+ {
+ public static bool TryParse(string messageLine, out Message message)
+ {
+ var arguments = new List<string>();
+ var stringBuilder = new StringBuilder();
+
+ bool expectingArgument = true;
+
+ for (int i = 0; i < messageLine.Length; i++)
+ {
+ char @char = messageLine[i];
+
+ if (@char == ',')
+ {
+ if (expectingArgument)
+ arguments.Add(string.Empty);
+
+ expectingArgument = true;
+ continue;
+ }
+
+ bool quoted = false;
+
+ if (messageLine[i] == '"')
+ {
+ quoted = true;
+ i++;
+ }
+
+ while (i < messageLine.Length)
+ {
+ @char = messageLine[i];
+
+ if (quoted && @char == '"')
+ {
+ i++;
+ break;
+ }
+
+ if (@char == '\\')
+ {
+ i++;
+ if (i < messageLine.Length)
+ break;
+
+ stringBuilder.Append(messageLine[i]);
+ }
+ else if (!quoted && @char == ',')
+ {
+ break; // We don't increment the counter to allow the colon to be parsed after this
+ }
+ else
+ {
+ stringBuilder.Append(@char);
+ }
+
+ i++;
+ }
+
+ arguments.Add(stringBuilder.ToString());
+ stringBuilder.Clear();
+
+ expectingArgument = false;
+ }
+
+ if (arguments.Count == 0)
+ {
+ message = new Message();
+ return false;
+ }
+
+ message = new Message
+ {
+ Id = arguments[0],
+ Arguments = arguments.Skip(1).ToArray()
+ };
+
+ return true;
+ }
+ }
+}
diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..c7c00e66a2
--- /dev/null
+++ b/modules/mono/editor/GodotTools/GodotTools.IdeConnection/Properties/AssemblyInfo.cs
@@ -0,0 +1,35 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("GodotTools.IdeConnection")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("")]
+[assembly: AssemblyCopyright("Godot Engine contributors")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("92600954-25F0-4291-8E11-1FEE9FC4BE20")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]