Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Input Action and NodePath completion #102

Merged
merged 22 commits into from
Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
716324f
may the work begin
sabzugfahrt Jun 21, 2023
235ee7f
add messaging
jsbeckr Jun 23, 2023
a875265
add GodotTools.IdeMessaging
jsbeckr Jun 23, 2023
03bbe45
may the work begin
sabzugfahrt Jun 21, 2023
13d7c6c
add POC input action completion
sabzugfahrt Jun 24, 2023
f2d9a63
add IdeMessagin assemblies to build
sabzugfahrt Jun 25, 2023
aef9da7
add input action completion
sabzugfahrt Jun 25, 2023
f85bc41
add node path completion
sabzugfahrt Jun 25, 2023
e3651b3
add use ScriptFile parameter in NodePath request for correct relative…
sabzugfahrt Jun 26, 2023
89314bc
minor cleanup
van800 Jun 26, 2023
b1324d7
add check if invocation type is a Godot.Node for NodePath completion
sabzugfahrt Jun 26, 2023
0abd4b7
minor bug
van800 Jun 26, 2023
99a6aab
fix for the case of https://github.com/JetBrains/godot-support/issues/99
van800 Jun 26, 2023
240e4c5
connection indicator
van800 Jun 26, 2023
5ee2eb4
fix: handle NullPointerException
jsbeckr Jun 26, 2023
d33716b
timeout for completion items call, add ScriptFile for InputActions re…
van800 Jun 27, 2023
5fb983b
revert files in test project
sabzugfahrt Jun 27, 2023
2a663de
remove InputActionMethods, match arg type instead
van800 Jun 27, 2023
629d259
fix: Exception if it's a string literal but not an argument
sabzugfahrt Jun 27, 2023
c25ecfc
chore: whitespace/formatting
sabzugfahrt Jun 27, 2023
b0f1ade
refactor: LookupNodePaths to check for NodePath argument type
sabzugfahrt Jun 27, 2023
7ced1c1
feat: add completion for NodePath variable declaration
sabzugfahrt Jun 27, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
361 changes: 361 additions & 0 deletions resharper/GodotTools.IdeMessaging/Client.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,361 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using Newtonsoft.Json;
using System.Threading;
using System.Threading.Tasks;
using GodotTools.IdeMessaging.Requests;
using GodotTools.IdeMessaging.Utils;

namespace GodotTools.IdeMessaging
{
// ReSharper disable once UnusedType.Global
public sealed class Client : IDisposable
{
private readonly ILogger logger;

private readonly string identity;

private string MetaFilePath { get; }
private DateTime? metaFileModifiedTime;
private GodotIdeMetadata godotIdeMetadata;
private readonly FileSystemWatcher fsWatcher;

public string GodotEditorExecutablePath => godotIdeMetadata.EditorExecutablePath;

private readonly IMessageHandler messageHandler;

private Peer peer;
private readonly SemaphoreSlim connectionSem = new SemaphoreSlim(1);

private readonly Queue<NotifyAwaiter<bool>> clientConnectedAwaiters = new Queue<NotifyAwaiter<bool>>();
private readonly Queue<NotifyAwaiter<bool>> clientDisconnectedAwaiters = new Queue<NotifyAwaiter<bool>>();

// ReSharper disable once UnusedMember.Global
public async Task<bool> AwaitConnected()
{
var awaiter = new NotifyAwaiter<bool>();
clientConnectedAwaiters.Enqueue(awaiter);
return await awaiter;
}

// ReSharper disable once UnusedMember.Global
public async Task<bool> AwaitDisconnected()
{
var awaiter = new NotifyAwaiter<bool>();
clientDisconnectedAwaiters.Enqueue(awaiter);
return await awaiter;
}

// ReSharper disable once MemberCanBePrivate.Global
public bool IsDisposed { get; private set; }

// ReSharper disable once MemberCanBePrivate.Global
public bool IsConnected => peer != null && !peer.IsDisposed && peer.IsTcpClientConnected;

// ReSharper disable once EventNeverSubscribedTo.Global
public event Action Connected
{
add
{
if (peer != null && !peer.IsDisposed)
peer.Connected += value;
}
remove
{
if (peer != null && !peer.IsDisposed)
peer.Connected -= value;
}
}

// ReSharper disable once EventNeverSubscribedTo.Global
public event Action Disconnected
{
add
{
if (peer != null && !peer.IsDisposed)
peer.Disconnected += value;
}
remove
{
if (peer != null && !peer.IsDisposed)
peer.Disconnected -= value;
}
}

~Client()
{
Dispose(disposing: false);
}

public async void Dispose()
{
if (IsDisposed)
return;

using (await connectionSem.UseAsync())
{
if (IsDisposed) // lock may not be fair
return;
IsDisposed = true;
}

Dispose(disposing: true);
GC.SuppressFinalize(this);
}

private void Dispose(bool disposing)
{
if (disposing)
{
peer?.Dispose();
fsWatcher?.Dispose();
}
}

public Client(string identity, string godotProjectDir, IMessageHandler messageHandler, ILogger logger)
{
this.identity = identity;
this.messageHandler = messageHandler;
this.logger = logger;

string projectMetadataDir = Path.Combine(godotProjectDir, ".godot", "mono", "metadata");
// FileSystemWatcher requires an existing directory
if (!Directory.Exists(projectMetadataDir))
{
// Check if the non hidden version exists
string nonHiddenProjectMetadataDir = Path.Combine(godotProjectDir, "godot", "mono", "metadata");
if (Directory.Exists(nonHiddenProjectMetadataDir))
{
projectMetadataDir = nonHiddenProjectMetadataDir;
}
else
{
Directory.CreateDirectory(projectMetadataDir);
}
}

MetaFilePath = Path.Combine(projectMetadataDir, GodotIdeMetadata.DefaultFileName);

fsWatcher = new FileSystemWatcher(projectMetadataDir, GodotIdeMetadata.DefaultFileName);
}

private async void OnMetaFileChanged(object sender, FileSystemEventArgs e)
{
if (IsDisposed)
return;

using (await connectionSem.UseAsync())
{
if (IsDisposed)
return;

if (!File.Exists(MetaFilePath))
return;

var lastWriteTime = File.GetLastWriteTime(MetaFilePath);

if (lastWriteTime == metaFileModifiedTime)
return;

metaFileModifiedTime = lastWriteTime;

var metadata = ReadMetadataFile();

if (metadata != null && metadata != godotIdeMetadata)
{
godotIdeMetadata = metadata.Value;
_ = Task.Run(ConnectToServer);
}
}
}

private async void OnMetaFileDeleted(object sender, FileSystemEventArgs e)
{
if (IsDisposed)
return;

if (IsConnected)
{
using (await connectionSem.UseAsync())
peer?.Dispose();
}

// The file may have been re-created

using (await connectionSem.UseAsync())
{
if (IsDisposed)
return;

if (IsConnected || !File.Exists(MetaFilePath))
return;

var lastWriteTime = File.GetLastWriteTime(MetaFilePath);

if (lastWriteTime == metaFileModifiedTime)
return;

metaFileModifiedTime = lastWriteTime;

var metadata = ReadMetadataFile();

if (metadata != null)
{
godotIdeMetadata = metadata.Value;
_ = Task.Run(ConnectToServer);
}
}
}

private GodotIdeMetadata? ReadMetadataFile()
{
using (var fileStream = new FileStream(MetaFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (var reader = new StreamReader(fileStream))
{
string portStr = reader.ReadLine();

if (portStr == null)
return null;

string editorExecutablePath = reader.ReadLine();

if (editorExecutablePath == null)
return null;

if (!int.TryParse(portStr, out int port))
return null;

return new GodotIdeMetadata(port, editorExecutablePath);
}
}

private async Task AcceptClient(TcpClient tcpClient)
{
logger.LogDebug("Accept client...");

using (peer = new Peer(tcpClient, new ClientHandshake(), messageHandler, logger))
{
// ReSharper disable AccessToDisposedClosure
peer.Connected += () =>
{
logger.LogInfo("Connection open with Ide Client");

while (clientConnectedAwaiters.Count > 0)
clientConnectedAwaiters.Dequeue().SetResult(true);
};

peer.Disconnected += () =>
{
while (clientDisconnectedAwaiters.Count > 0)
clientDisconnectedAwaiters.Dequeue().SetResult(true);
};
// ReSharper restore AccessToDisposedClosure

try
{
if (!await peer.DoHandshake(identity))
{
logger.LogError("Handshake failed");
return;
}
}
catch (Exception e)
{
logger.LogError("Handshake failed with unhandled exception: ", e);
return;
}

await peer.Process();

logger.LogInfo("Connection closed with Ide Client");
}
}

private async Task ConnectToServer()
{
var tcpClient = new TcpClient();

try
{
logger.LogInfo("Connecting to Godot Ide Server");

await tcpClient.ConnectAsync(IPAddress.Loopback, godotIdeMetadata.Port);

logger.LogInfo("Connection open with Godot Ide Server");

await AcceptClient(tcpClient);
}
catch (SocketException e)
{
if (e.SocketErrorCode == SocketError.ConnectionRefused)
logger.LogError("The connection to the Godot Ide Server was refused");
else
throw;
}
}

// ReSharper disable once UnusedMember.Global
public async void Start()
{
fsWatcher.Created += OnMetaFileChanged;
fsWatcher.Changed += OnMetaFileChanged;
fsWatcher.Deleted += OnMetaFileDeleted;
fsWatcher.EnableRaisingEvents = true;

using (await connectionSem.UseAsync())
{
if (IsDisposed)
return;

if (IsConnected)
return;

if (!File.Exists(MetaFilePath))
{
logger.LogInfo("There is no Godot Ide Server running");
return;
}

var metadata = ReadMetadataFile();

if (metadata != null)
{
godotIdeMetadata = metadata.Value;
_ = Task.Run(ConnectToServer);
}
else
{
logger.LogError("Failed to read Godot Ide metadata file");
}
}
}

public async Task<TResponse> SendRequest<TResponse>(Request request)
where TResponse : Response, new()
{
if (!IsConnected)
{
logger.LogError("Cannot write request. Not connected to the Godot Ide Server.");
return null;
}

string body = JsonConvert.SerializeObject(request);
return await peer.SendRequest<TResponse>(request.Id, body);
}

public async Task<TResponse> SendRequest<TResponse>(string id, string body)
where TResponse : Response, new()
{
if (!IsConnected)
{
logger.LogError("Cannot write request. Not connected to the Godot Ide Server.");
return null;
}

return await peer.SendRequest<TResponse>(id, body);
}
}
}
Loading
Loading