csharp-language-server-protocol
csharp-language-server-protocol copied to clipboard
Feature Request: Support handling LSP command line options for input/output
The language server protocol documents how to configure input/output via command line options.
It would be great if an extension method were available that handled command line options in this format and configured input/output accordingly.
Here's an example of what it might look like:
using OmniSharp.Extensions.LanguageServer.Server;
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO.Pipes;
using System.Net;
using System.Net.Sockets;
static class LanguageServerOptionsCommandLineExtensions
{
public static LanguageServerOptions WithCommandLineCommunicationChannel(this LanguageServerOptions options,
string[] args)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
CommandLineOptions commandLineOptions = CommandLineOptions.Parse(args);
switch (commandLineOptions.CommunicationChannel)
{
case CommunicationChannel.ConsoleInputOutput:
options.WithInput(Console.OpenStandardInput());
options.WithOutput(Console.OpenStandardOutput());
break;
case CommunicationChannel.Pipe:
NamedPipeClientStream pipe = new NamedPipeClientStream(".", commandLineOptions.PipeName,
PipeDirection.InOut, PipeOptions.Asynchronous);
// TODO: Make async? How to do that inside LanguageServer.From(Action<LanguageServerOptions>)?
//await pipe.ConnectAsync(cancellationToken);
pipe.Connect();
options.WithInput(pipe);
options.WithOutput(pipe);
break;
default:
Debug.Assert(commandLineOptions.CommunicationChannel == CommunicationChannel.Socket);
TcpClient client = new TcpClient();
options.RegisterForDisposal(client);
// TODO: Make async? How to do that inside LanguageServer.From(Action<LanguageServerOptions>)?
//await client.ConnectAsync(IPAddress.Loopback, commandLineOptions.Port, cancellationToken);
client.Connect(IPAddress.Loopback, commandLineOptions.Port);
NetworkStream stream = client.GetStream();
options.WithInput(stream);
options.WithOutput(stream);
break;
}
return options;
}
enum CommunicationChannel
{
ConsoleInputOutput,
Pipe,
Socket
}
struct CommandLineOptions
{
public CommunicationChannel CommunicationChannel { get; init; }
public string PipeName { get; init; }
public int Port { get; init; }
public static CommandLineOptions Parse(string[] args)
{
if (args == null || args.Length == 0)
{
return new CommandLineOptions { CommunicationChannel = CommunicationChannel.ConsoleInputOutput };
}
if (args.Length <= 2)
{
string firstArgument = args[0];
string secondArgument = args.Length > 1 ? args[1] : null;
if (firstArgument == "--stdio" && secondArgument == null)
{
return new CommandLineOptions { CommunicationChannel = CommunicationChannel.ConsoleInputOutput };
}
else if (firstArgument.StartsWith("--pipe"))
{
// TODO: Handle --pipe appropriately when not running on Windows.
if (firstArgument == "--pipe" && secondArgument != null && secondArgument.StartsWith(@"\\.\pipe\"))
{
return new CommandLineOptions
{
CommunicationChannel = CommunicationChannel.Pipe,
PipeName = secondArgument.Substring(@"\\.\pipe\".Length)
};
}
else if (firstArgument.StartsWith(@"--pipe=\\.\pipe\") && secondArgument == null)
{
string pipeName = firstArgument.Substring(@"--pipe=\\.\pipe\".Length);
return new CommandLineOptions
{
CommunicationChannel = CommunicationChannel.Pipe,
PipeName = pipeName
};
}
}
else if (firstArgument.StartsWith("--socket"))
{
if (firstArgument == "--socket" && secondArgument != null)
{
return new CommandLineOptions
{
CommunicationChannel = CommunicationChannel.Socket,
Port = int.Parse(secondArgument, NumberStyles.None, CultureInfo.InvariantCulture)
};
}
else if (firstArgument.StartsWith("--socket=") && secondArgument == null)
{
int port = int.Parse(firstArgument.Substring("--socket=".Length), NumberStyles.None,
CultureInfo.InvariantCulture);
return new CommandLineOptions
{
CommunicationChannel = CommunicationChannel.Socket,
Port = port
};
}
}
}
throw new ArgumentException("Invalid command line communication channel arguments.", nameof(args));
}
}
}
Thanks for the code. I have updated the pipe connection for posix platforms (linux/mac) using unix domain sockets, which is what the vs code client is using:
using OmniSharp.Extensions.LanguageServer.Server;
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO.Pipes;
using System.Net;
using System.Net.Sockets;
using Serilog;
static class LanguageServerOptionsCommandLineExtensions
{
public static LanguageServerOptions WithCommandLineCommunicationChannel(this LanguageServerOptions options,
string[] args)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
CommandLineOptions commandLineOptions = CommandLineOptions.Parse(args);
NetworkStream? stream;
switch (commandLineOptions.CommunicationChannel)
{
case CommunicationChannel.ConsoleInputOutput:
options.WithInput(Console.OpenStandardInput());
options.WithOutput(Console.OpenStandardOutput());
break;
case CommunicationChannel.Pipe:
if (OperatingSystem.IsWindows())
{
NamedPipeClientStream pipe = new NamedPipeClientStream(".", commandLineOptions.PipeName,
PipeDirection.InOut, PipeOptions.Asynchronous);
// TODO: Make async? How to do that inside LanguageServer.From(Action<LanguageServerOptions>)?
//await pipe.ConnectAsync(cancellationToken);
pipe.Connect();
options.WithInput(pipe);
options.WithOutput(pipe);
}
else
{
var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP);
var endpoint = new UnixDomainSocketEndPoint(commandLineOptions.PipeName);
socket.Connect(endpoint);
stream = new NetworkStream(socket);
options.WithInput(stream);
options.WithOutput(stream);
}
break;
default:
Debug.Assert(commandLineOptions.CommunicationChannel == CommunicationChannel.Socket);
TcpClient client = new TcpClient();
options.RegisterForDisposal(client);
// TODO: Make async? How to do that inside LanguageServer.From(Action<LanguageServerOptions>)?
//await client.ConnectAsync(IPAddress.Loopback, commandLineOptions.Port, cancellationToken);
client.Connect(IPAddress.Loopback, commandLineOptions.Port);
stream = client.GetStream();
options.WithInput(stream);
options.WithOutput(stream);
break;
}
return options;
}
enum CommunicationChannel
{
ConsoleInputOutput,
Pipe,
Socket
}
struct CommandLineOptions
{
public CommunicationChannel CommunicationChannel { get; init; }
public string PipeName { get; init; }
public int Port { get; init; }
private static string ParsePipeNameWindows(string firstArgument, string? secondArgument)
{
if (firstArgument == "--pipe" && secondArgument != null && secondArgument.StartsWith(@"\\.\pipe\"))
{
return secondArgument.Substring(@"\\.\pipe\".Length);
}
else if (firstArgument.StartsWith(@"--pipe=\\.\pipe\") && secondArgument == null)
{
return firstArgument.Substring(@"--pipe=\\.\pipe\".Length);
}
throw new Exception("Invalid pipe argument");
}
private static string ParsePipeNamePosix(string firstArgument, string? secondArgument)
{
if (firstArgument == "--pipe" && secondArgument != null)
{
return secondArgument;
}
else if (firstArgument.StartsWith(@"--pipe=") && secondArgument == null)
{
var sockPath = firstArgument.Substring(@"--pipe=".Length);
return sockPath;
}
throw new Exception("Invalid pipe argument");
}
private static string ParsePipeName(string firstArgument, string? secondArgument)
{
if (OperatingSystem.IsWindows())
{
return ParsePipeNameWindows(firstArgument, secondArgument);
}
else
{
return ParsePipeNamePosix(firstArgument, secondArgument);
}
}
public static CommandLineOptions Parse(string[] args)
{
if (args == null || args.Length == 0)
{
return new CommandLineOptions { CommunicationChannel = CommunicationChannel.ConsoleInputOutput };
}
if (args.Length <= 2)
{
string firstArgument = args[0];
string? secondArgument = args.Length > 1 ? args[1] : null;
if (firstArgument == "--stdio" && secondArgument == null)
{
return new CommandLineOptions { CommunicationChannel = CommunicationChannel.ConsoleInputOutput };
}
else if (firstArgument.StartsWith("--pipe"))
{
return new CommandLineOptions
{
CommunicationChannel = CommunicationChannel.Pipe,
PipeName = ParsePipeName(firstArgument, secondArgument)
};
}
else if (firstArgument.StartsWith("--socket"))
{
if (firstArgument == "--socket" && secondArgument != null)
{
return new CommandLineOptions
{
CommunicationChannel = CommunicationChannel.Socket,
Port = int.Parse(secondArgument, NumberStyles.None, CultureInfo.InvariantCulture)
};
}
else if (firstArgument.StartsWith("--socket=") && secondArgument == null)
{
int port = int.Parse(firstArgument.Substring("--socket=".Length), NumberStyles.None,
CultureInfo.InvariantCulture);
return new CommandLineOptions
{
CommunicationChannel = CommunicationChannel.Socket,
Port = port
};
}
}
}
throw new ArgumentException("Invalid command line communication channel arguments.", nameof(args));
}
}
}