Skip to content

Commit

Permalink
Basic language server infrastructure.
Browse files Browse the repository at this point in the history
Part of #62.
  • Loading branch information
alexrp committed Apr 9, 2023
1 parent 29d101b commit a86d0c4
Show file tree
Hide file tree
Showing 11 changed files with 436 additions and 3 deletions.
7 changes: 7 additions & 0 deletions src/driver/IO/TerminalExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ namespace Vezel.Celerity.Driver.IO;

internal static class TerminalExtensions
{
public static void WriteControl(
this TerminalWriter writer, string sequence)
{
if (writer.IsInteractive)
writer.Write(sequence);
}

public static ValueTask WriteControlAsync(
this TerminalWriter writer, string sequence, CancellationToken cancellationToken)
{
Expand Down
88 changes: 88 additions & 0 deletions src/driver/Logging/TerminalLanguageServiceLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using Vezel.Celerity.Driver.IO;

namespace Vezel.Celerity.Driver.Logging;

internal sealed class TerminalLanguageServiceLogger : LanguageServiceLogger
{
private static readonly Color _timestampColor = Color.FromArgb(127, 127, 127);

private static readonly Color _nameColor = Color.FromArgb(233, 233, 233);

private static readonly Color _eventColor = Color.FromArgb(0, 155, 155);

private static readonly Color _traceColor = Color.FromArgb(127, 0, 127);

private static readonly Color _debugColor = Color.FromArgb(0, 127, 255);

private static readonly Color _informationColor = Color.FromArgb(255, 255, 255);

private static readonly Color _warningColor = Color.FromArgb(255, 255, 0);

private static readonly Color _errorColor = Color.FromArgb(255, 63, 0);

private static readonly Color _criticalColor = Color.FromArgb(255, 0, 0);

private readonly TerminalLanguageServiceLoggerProvider _provider;

private readonly string _name;

public TerminalLanguageServiceLogger(TerminalLanguageServiceLoggerProvider provider, string name)
{
_provider = provider;
_name = name;
}

public override void Log(LogLevel logLevel, string eventName, string message, Exception? exception)
{
var writer = _provider.Writer;

writer.Write("[");
writer.WriteControl(
ControlSequences.SetForegroundColor(_timestampColor.R, _timestampColor.G, _timestampColor.B));
writer.Write($"{DateTime.Now:HH:mm:ss.fff}");
writer.WriteControl(ControlSequences.ResetAttributes());
writer.Write("]");

var (level, color) = logLevel switch
{
LogLevel.Trace => ("TRC", _traceColor),
LogLevel.Debug => ("DBG", _debugColor),
LogLevel.Information => ("INF", _informationColor),
LogLevel.Warning => ("WRN", _warningColor),
LogLevel.Error => ("ERR", _errorColor),
LogLevel.Critical => ("CRT", _criticalColor),
_ => throw new UnreachableException(),
};

writer.Write("[");
writer.WriteControl(ControlSequences.SetForegroundColor(color.R, color.G, color.B));
writer.Write(level);
writer.WriteControl(ControlSequences.ResetAttributes());
writer.Write("]");

writer.Write("[");
writer.WriteControl(ControlSequences.SetForegroundColor(_nameColor.R, _nameColor.G, _nameColor.B));
writer.Write(_name);
writer.WriteControl(ControlSequences.ResetAttributes());
writer.Write("]");

writer.Write("[");
writer.WriteControl(ControlSequences.SetForegroundColor(_eventColor.R, _eventColor.G, _eventColor.B));
writer.Write(eventName);
writer.WriteControl(ControlSequences.ResetAttributes());
writer.Write("] ");

var hasMessage = string.IsNullOrWhiteSpace(message);

if (hasMessage)
writer.Write(message);

if (exception != null)
{
if (hasMessage)
writer.WriteLine();

writer.Write(exception);
}
}
}
16 changes: 16 additions & 0 deletions src/driver/Logging/TerminalLanguageServiceLoggerProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Vezel.Celerity.Driver.Logging;

internal sealed class TerminalLanguageServiceLoggerProvider : LanguageServiceLoggerProvider
{
public TerminalWriter Writer { get; }

public TerminalLanguageServiceLoggerProvider(TerminalWriter writer)
{
Writer = writer;
}

public override TerminalLanguageServiceLogger CreateLogger(string name)
{
return new(this, name);
}
}
22 changes: 19 additions & 3 deletions src/driver/Verbs/ServeVerb.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Vezel.Celerity.Driver.Logging;

namespace Vezel.Celerity.Driver.Verbs;

[SuppressMessage("", "CA1812")]
Expand All @@ -7,12 +9,26 @@ internal sealed class ServeVerb : Verb
[Value(0, HelpText = "Project directory.")]
public required string? Directory { get; init; }

protected override ValueTask<int> RunAsync(CancellationToken cancellationToken)
[Option('l', "level", Default = LogLevel.Information, HelpText = "Set log level.")]
public required LogLevel Level { get; init; }

[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))]
protected override async ValueTask<int> RunAsync(CancellationToken cancellationToken)
{
if (Directory != null && string.IsNullOrWhiteSpace(Directory))
throw new DriverException($"Invalid workspace path '{Directory}'.");

// TODO: Implement this.
return ValueTask.FromResult(0);
await Error.WriteLineAsync("Running Celerity language server on standard input/output.", cancellationToken);

using var service = await LanguageService.CreateAsync(
new LanguageServiceConfiguration(In.Stream, Out.Stream)
.WithLogLevel(Level)
.WithLoggerProvider(new TerminalLanguageServiceLoggerProvider(Error))
.WithProtocolLogging(protocolLogging: true),
cancellationToken);

await service.Completion;

return 0;
}
}
79 changes: 79 additions & 0 deletions src/language/service/LanguageService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using Vezel.Celerity.Language.Service.Logging;

namespace Vezel.Celerity.Language.Service;

public sealed class LanguageService : IDisposable
{
public Task Completion { get; }

private readonly TaskCompletionSource _disposed = new(TaskCreationOptions.RunContinuationsAsynchronously);

private readonly LanguageServer _server;

private LanguageService(LanguageServer server)
{
_server = server;
Completion = Task.WhenAny(server.WaitForExit, _disposed.Task);
}

public static ValueTask<LanguageService> CreateAsync(
LanguageServiceConfiguration configuration, CancellationToken cancellationToken = default)
{
Check.Null(configuration);

return CreateAsync();

[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))]
[SuppressMessage("", "CA2000")]
async ValueTask<LanguageService> CreateAsync()
{
T GetAttribute<T>()
where T : Attribute
{
#pragma warning disable CS0436 // TODO: https://github.com/dotnet/Nerdbank.GitVersioning/issues/555
return typeof(ThisAssembly).Assembly.GetCustomAttribute<T>()!;
#pragma warning restore CS0436
}

return new(
await LanguageServer.From(
new LanguageServerOptions()
.WithServerInfo(new()
{
Name = GetAttribute<AssemblyProductAttribute>()!.Product,
Version = GetAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion,
})
.WithInput(configuration.Input)
.WithOutput(configuration.Output)
.WithContentModifiedSupport(true)
.WithMaximumRequestTimeout(configuration.RequestTimeout)
.ConfigureLogging(builder =>
{
_ = builder.SetMinimumLevel(configuration.LogLevel switch
{
Logging.LogLevel.Trace => Microsoft.Extensions.Logging.LogLevel.Trace,
Logging.LogLevel.Debug => Microsoft.Extensions.Logging.LogLevel.Debug,
Logging.LogLevel.Information => Microsoft.Extensions.Logging.LogLevel.Information,
Logging.LogLevel.Warning => Microsoft.Extensions.Logging.LogLevel.Warning,
Logging.LogLevel.Error => Microsoft.Extensions.Logging.LogLevel.Error,
Logging.LogLevel.Critical => Microsoft.Extensions.Logging.LogLevel.Critical,
_ => throw new UnreachableException(),
});

if (configuration.LoggerProvider is { } provider)
_ = builder.AddProvider(new LanguageServiceLoggerProviderAdapter(provider));

if (configuration.ProtocolLogging)
_ = builder.AddLanguageProtocolLogging();
}),
cancellationToken).ConfigureAwait(false));
}
}

public void Dispose()
{
_server.Dispose();

_ = _disposed.TrySetResult();
}
}
114 changes: 114 additions & 0 deletions src/language/service/LanguageServiceConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using Vezel.Celerity.Language.Service.Logging;

namespace Vezel.Celerity.Language.Service;

public sealed class LanguageServiceConfiguration
{
public Stream Input { get; private set; }

public Stream Output { get; private set; }

public TimeSpan RequestTimeout { get; private set; } = Timeout.InfiniteTimeSpan;

public Logging.LogLevel LogLevel { get; private set; } = Logging.LogLevel.Information;

public LanguageServiceLoggerProvider? LoggerProvider { get; private set; }

public bool ProtocolLogging { get; private set; }

private LanguageServiceConfiguration()
{
Input = null!;
Output = null!;
}

public LanguageServiceConfiguration(Stream input, Stream output)
{
Check.Null(input);
Check.Argument(input.CanRead, input);
Check.Null(output);
Check.Argument(output.CanWrite, output);

Input = input;
Output = output;
}

private LanguageServiceConfiguration Clone()
{
return new()
{
Input = Input,
Output = Output,
RequestTimeout = RequestTimeout,
LogLevel = LogLevel,
LoggerProvider = LoggerProvider,
ProtocolLogging = ProtocolLogging,
};
}

public LanguageServiceConfiguration WithInput(Stream input)
{
Check.Null(input);
Check.Argument(input.CanRead, input);

var cfg = Clone();

cfg.Input = input;

return cfg;
}

public LanguageServiceConfiguration WithOutput(Stream output)
{
Check.Null(output);
Check.Argument(output.CanWrite, output);

var cfg = Clone();

cfg.Output = output;

return cfg;
}

public LanguageServiceConfiguration WithRequestTimeout(TimeSpan requestTimeout)
{
Check.Range((long)requestTimeout.TotalMilliseconds is >= -1 and <= int.MaxValue, requestTimeout);

var cfg = Clone();

cfg.RequestTimeout = requestTimeout;

return cfg;
}

public LanguageServiceConfiguration WithLogLevel(Logging.LogLevel logLevel)
{
Check.Enum(logLevel);

var cfg = Clone();

cfg.LogLevel = logLevel;

return cfg;
}

public LanguageServiceConfiguration WithLoggerProvider(LanguageServiceLoggerProvider loggerProvider)
{
Check.Null(loggerProvider);

var cfg = Clone();

cfg.LoggerProvider = loggerProvider;

return cfg;
}

public LanguageServiceConfiguration WithProtocolLogging(bool protocolLogging)
{
var cfg = Clone();

cfg.ProtocolLogging = protocolLogging;

return cfg;
}
}
6 changes: 6 additions & 0 deletions src/language/service/Logging/LanguageServiceLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Vezel.Celerity.Language.Service.Logging;

public abstract class LanguageServiceLogger
{
public abstract void Log(LogLevel logLevel, string eventName, string message, Exception? exception);
}
Loading

0 comments on commit a86d0c4

Please sign in to comment.