Skip to content
This repository was archived by the owner on Mar 21, 2025. It is now read-only.

Commit a86d0c4

Browse files
committed
Basic language server infrastructure.
Part of #62.
1 parent 29d101b commit a86d0c4

11 files changed

+436
-3
lines changed

src/driver/IO/TerminalExtensions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ namespace Vezel.Celerity.Driver.IO;
22

33
internal static class TerminalExtensions
44
{
5+
public static void WriteControl(
6+
this TerminalWriter writer, string sequence)
7+
{
8+
if (writer.IsInteractive)
9+
writer.Write(sequence);
10+
}
11+
512
public static ValueTask WriteControlAsync(
613
this TerminalWriter writer, string sequence, CancellationToken cancellationToken)
714
{
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using Vezel.Celerity.Driver.IO;
2+
3+
namespace Vezel.Celerity.Driver.Logging;
4+
5+
internal sealed class TerminalLanguageServiceLogger : LanguageServiceLogger
6+
{
7+
private static readonly Color _timestampColor = Color.FromArgb(127, 127, 127);
8+
9+
private static readonly Color _nameColor = Color.FromArgb(233, 233, 233);
10+
11+
private static readonly Color _eventColor = Color.FromArgb(0, 155, 155);
12+
13+
private static readonly Color _traceColor = Color.FromArgb(127, 0, 127);
14+
15+
private static readonly Color _debugColor = Color.FromArgb(0, 127, 255);
16+
17+
private static readonly Color _informationColor = Color.FromArgb(255, 255, 255);
18+
19+
private static readonly Color _warningColor = Color.FromArgb(255, 255, 0);
20+
21+
private static readonly Color _errorColor = Color.FromArgb(255, 63, 0);
22+
23+
private static readonly Color _criticalColor = Color.FromArgb(255, 0, 0);
24+
25+
private readonly TerminalLanguageServiceLoggerProvider _provider;
26+
27+
private readonly string _name;
28+
29+
public TerminalLanguageServiceLogger(TerminalLanguageServiceLoggerProvider provider, string name)
30+
{
31+
_provider = provider;
32+
_name = name;
33+
}
34+
35+
public override void Log(LogLevel logLevel, string eventName, string message, Exception? exception)
36+
{
37+
var writer = _provider.Writer;
38+
39+
writer.Write("[");
40+
writer.WriteControl(
41+
ControlSequences.SetForegroundColor(_timestampColor.R, _timestampColor.G, _timestampColor.B));
42+
writer.Write($"{DateTime.Now:HH:mm:ss.fff}");
43+
writer.WriteControl(ControlSequences.ResetAttributes());
44+
writer.Write("]");
45+
46+
var (level, color) = logLevel switch
47+
{
48+
LogLevel.Trace => ("TRC", _traceColor),
49+
LogLevel.Debug => ("DBG", _debugColor),
50+
LogLevel.Information => ("INF", _informationColor),
51+
LogLevel.Warning => ("WRN", _warningColor),
52+
LogLevel.Error => ("ERR", _errorColor),
53+
LogLevel.Critical => ("CRT", _criticalColor),
54+
_ => throw new UnreachableException(),
55+
};
56+
57+
writer.Write("[");
58+
writer.WriteControl(ControlSequences.SetForegroundColor(color.R, color.G, color.B));
59+
writer.Write(level);
60+
writer.WriteControl(ControlSequences.ResetAttributes());
61+
writer.Write("]");
62+
63+
writer.Write("[");
64+
writer.WriteControl(ControlSequences.SetForegroundColor(_nameColor.R, _nameColor.G, _nameColor.B));
65+
writer.Write(_name);
66+
writer.WriteControl(ControlSequences.ResetAttributes());
67+
writer.Write("]");
68+
69+
writer.Write("[");
70+
writer.WriteControl(ControlSequences.SetForegroundColor(_eventColor.R, _eventColor.G, _eventColor.B));
71+
writer.Write(eventName);
72+
writer.WriteControl(ControlSequences.ResetAttributes());
73+
writer.Write("] ");
74+
75+
var hasMessage = string.IsNullOrWhiteSpace(message);
76+
77+
if (hasMessage)
78+
writer.Write(message);
79+
80+
if (exception != null)
81+
{
82+
if (hasMessage)
83+
writer.WriteLine();
84+
85+
writer.Write(exception);
86+
}
87+
}
88+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace Vezel.Celerity.Driver.Logging;
2+
3+
internal sealed class TerminalLanguageServiceLoggerProvider : LanguageServiceLoggerProvider
4+
{
5+
public TerminalWriter Writer { get; }
6+
7+
public TerminalLanguageServiceLoggerProvider(TerminalWriter writer)
8+
{
9+
Writer = writer;
10+
}
11+
12+
public override TerminalLanguageServiceLogger CreateLogger(string name)
13+
{
14+
return new(this, name);
15+
}
16+
}

src/driver/Verbs/ServeVerb.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using Vezel.Celerity.Driver.Logging;
2+
13
namespace Vezel.Celerity.Driver.Verbs;
24

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

10-
protected override ValueTask<int> RunAsync(CancellationToken cancellationToken)
12+
[Option('l', "level", Default = LogLevel.Information, HelpText = "Set log level.")]
13+
public required LogLevel Level { get; init; }
14+
15+
[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))]
16+
protected override async ValueTask<int> RunAsync(CancellationToken cancellationToken)
1117
{
1218
if (Directory != null && string.IsNullOrWhiteSpace(Directory))
1319
throw new DriverException($"Invalid workspace path '{Directory}'.");
1420

15-
// TODO: Implement this.
16-
return ValueTask.FromResult(0);
21+
await Error.WriteLineAsync("Running Celerity language server on standard input/output.", cancellationToken);
22+
23+
using var service = await LanguageService.CreateAsync(
24+
new LanguageServiceConfiguration(In.Stream, Out.Stream)
25+
.WithLogLevel(Level)
26+
.WithLoggerProvider(new TerminalLanguageServiceLoggerProvider(Error))
27+
.WithProtocolLogging(protocolLogging: true),
28+
cancellationToken);
29+
30+
await service.Completion;
31+
32+
return 0;
1733
}
1834
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using Vezel.Celerity.Language.Service.Logging;
2+
3+
namespace Vezel.Celerity.Language.Service;
4+
5+
public sealed class LanguageService : IDisposable
6+
{
7+
public Task Completion { get; }
8+
9+
private readonly TaskCompletionSource _disposed = new(TaskCreationOptions.RunContinuationsAsynchronously);
10+
11+
private readonly LanguageServer _server;
12+
13+
private LanguageService(LanguageServer server)
14+
{
15+
_server = server;
16+
Completion = Task.WhenAny(server.WaitForExit, _disposed.Task);
17+
}
18+
19+
public static ValueTask<LanguageService> CreateAsync(
20+
LanguageServiceConfiguration configuration, CancellationToken cancellationToken = default)
21+
{
22+
Check.Null(configuration);
23+
24+
return CreateAsync();
25+
26+
[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))]
27+
[SuppressMessage("", "CA2000")]
28+
async ValueTask<LanguageService> CreateAsync()
29+
{
30+
T GetAttribute<T>()
31+
where T : Attribute
32+
{
33+
#pragma warning disable CS0436 // TODO: https://github.com/dotnet/Nerdbank.GitVersioning/issues/555
34+
return typeof(ThisAssembly).Assembly.GetCustomAttribute<T>()!;
35+
#pragma warning restore CS0436
36+
}
37+
38+
return new(
39+
await LanguageServer.From(
40+
new LanguageServerOptions()
41+
.WithServerInfo(new()
42+
{
43+
Name = GetAttribute<AssemblyProductAttribute>()!.Product,
44+
Version = GetAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion,
45+
})
46+
.WithInput(configuration.Input)
47+
.WithOutput(configuration.Output)
48+
.WithContentModifiedSupport(true)
49+
.WithMaximumRequestTimeout(configuration.RequestTimeout)
50+
.ConfigureLogging(builder =>
51+
{
52+
_ = builder.SetMinimumLevel(configuration.LogLevel switch
53+
{
54+
Logging.LogLevel.Trace => Microsoft.Extensions.Logging.LogLevel.Trace,
55+
Logging.LogLevel.Debug => Microsoft.Extensions.Logging.LogLevel.Debug,
56+
Logging.LogLevel.Information => Microsoft.Extensions.Logging.LogLevel.Information,
57+
Logging.LogLevel.Warning => Microsoft.Extensions.Logging.LogLevel.Warning,
58+
Logging.LogLevel.Error => Microsoft.Extensions.Logging.LogLevel.Error,
59+
Logging.LogLevel.Critical => Microsoft.Extensions.Logging.LogLevel.Critical,
60+
_ => throw new UnreachableException(),
61+
});
62+
63+
if (configuration.LoggerProvider is { } provider)
64+
_ = builder.AddProvider(new LanguageServiceLoggerProviderAdapter(provider));
65+
66+
if (configuration.ProtocolLogging)
67+
_ = builder.AddLanguageProtocolLogging();
68+
}),
69+
cancellationToken).ConfigureAwait(false));
70+
}
71+
}
72+
73+
public void Dispose()
74+
{
75+
_server.Dispose();
76+
77+
_ = _disposed.TrySetResult();
78+
}
79+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
using Vezel.Celerity.Language.Service.Logging;
2+
3+
namespace Vezel.Celerity.Language.Service;
4+
5+
public sealed class LanguageServiceConfiguration
6+
{
7+
public Stream Input { get; private set; }
8+
9+
public Stream Output { get; private set; }
10+
11+
public TimeSpan RequestTimeout { get; private set; } = Timeout.InfiniteTimeSpan;
12+
13+
public Logging.LogLevel LogLevel { get; private set; } = Logging.LogLevel.Information;
14+
15+
public LanguageServiceLoggerProvider? LoggerProvider { get; private set; }
16+
17+
public bool ProtocolLogging { get; private set; }
18+
19+
private LanguageServiceConfiguration()
20+
{
21+
Input = null!;
22+
Output = null!;
23+
}
24+
25+
public LanguageServiceConfiguration(Stream input, Stream output)
26+
{
27+
Check.Null(input);
28+
Check.Argument(input.CanRead, input);
29+
Check.Null(output);
30+
Check.Argument(output.CanWrite, output);
31+
32+
Input = input;
33+
Output = output;
34+
}
35+
36+
private LanguageServiceConfiguration Clone()
37+
{
38+
return new()
39+
{
40+
Input = Input,
41+
Output = Output,
42+
RequestTimeout = RequestTimeout,
43+
LogLevel = LogLevel,
44+
LoggerProvider = LoggerProvider,
45+
ProtocolLogging = ProtocolLogging,
46+
};
47+
}
48+
49+
public LanguageServiceConfiguration WithInput(Stream input)
50+
{
51+
Check.Null(input);
52+
Check.Argument(input.CanRead, input);
53+
54+
var cfg = Clone();
55+
56+
cfg.Input = input;
57+
58+
return cfg;
59+
}
60+
61+
public LanguageServiceConfiguration WithOutput(Stream output)
62+
{
63+
Check.Null(output);
64+
Check.Argument(output.CanWrite, output);
65+
66+
var cfg = Clone();
67+
68+
cfg.Output = output;
69+
70+
return cfg;
71+
}
72+
73+
public LanguageServiceConfiguration WithRequestTimeout(TimeSpan requestTimeout)
74+
{
75+
Check.Range((long)requestTimeout.TotalMilliseconds is >= -1 and <= int.MaxValue, requestTimeout);
76+
77+
var cfg = Clone();
78+
79+
cfg.RequestTimeout = requestTimeout;
80+
81+
return cfg;
82+
}
83+
84+
public LanguageServiceConfiguration WithLogLevel(Logging.LogLevel logLevel)
85+
{
86+
Check.Enum(logLevel);
87+
88+
var cfg = Clone();
89+
90+
cfg.LogLevel = logLevel;
91+
92+
return cfg;
93+
}
94+
95+
public LanguageServiceConfiguration WithLoggerProvider(LanguageServiceLoggerProvider loggerProvider)
96+
{
97+
Check.Null(loggerProvider);
98+
99+
var cfg = Clone();
100+
101+
cfg.LoggerProvider = loggerProvider;
102+
103+
return cfg;
104+
}
105+
106+
public LanguageServiceConfiguration WithProtocolLogging(bool protocolLogging)
107+
{
108+
var cfg = Clone();
109+
110+
cfg.ProtocolLogging = protocolLogging;
111+
112+
return cfg;
113+
}
114+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Vezel.Celerity.Language.Service.Logging;
2+
3+
public abstract class LanguageServiceLogger
4+
{
5+
public abstract void Log(LogLevel logLevel, string eventName, string message, Exception? exception);
6+
}

0 commit comments

Comments
 (0)