| 1 | using System.Buffers; |
| 2 | using System.Text; |
| 3 | using System.Text.Json; |
| 4 | |
| 5 | namespace CascadeIDE.Services.Lsp; |
| 6 | |
| 7 | /// <summary>Чтение/запись LSP по stdio (заголовок Content-Length + тело JSON UTF-8).</summary> |
| 8 | internal static class LspStdioFraming |
| 9 | { |
| 10 | public static async Task<JsonDocument?> ReadMessageAsync(Stream input, CancellationToken ct) |
| 11 | { |
| 12 | int? length = null; |
| 13 | while (true) |
| 14 | { |
| 15 | var line = await ReadHeaderLineAsync(input, ct).ConfigureAwait(false); |
| 16 | if (line is null) |
| 17 | return null; |
| 18 | if (line.Length == 0) |
| 19 | break; |
| 20 | const string prefix = "Content-Length:"; |
| 21 | if (line.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) |
| 22 | { |
| 23 | var n = line.AsSpan(prefix.Length).Trim(); |
| 24 | if (int.TryParse(n, out var len) && len >= 0) |
| 25 | length = len; |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | if (length is null or <= 0) |
| 30 | return null; |
| 31 | |
| 32 | var rent = ArrayPool<byte>.Shared.Rent(length.Value); |
| 33 | try |
| 34 | { |
| 35 | var mem = rent.AsMemory(0, length.Value); |
| 36 | var read = 0; |
| 37 | while (read < length.Value) |
| 38 | { |
| 39 | var n = await input.ReadAsync(mem[read..], ct).ConfigureAwait(false); |
| 40 | if (n == 0) |
| 41 | return null; |
| 42 | read += n; |
| 43 | } |
| 44 | |
| 45 | var copy = new byte[length.Value]; |
| 46 | rent.AsSpan(0, length.Value).CopyTo(copy); |
| 47 | return JsonDocument.Parse(copy); |
| 48 | } |
| 49 | finally |
| 50 | { |
| 51 | ArrayPool<byte>.Shared.Return(rent); |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | private static async Task<string?> ReadHeaderLineAsync(Stream input, CancellationToken ct) |
| 56 | { |
| 57 | var buf = new List<byte>(64); |
| 58 | while (true) |
| 59 | { |
| 60 | var b = new byte[1]; |
| 61 | var n = await input.ReadAsync(b, ct).ConfigureAwait(false); |
| 62 | if (n == 0) |
| 63 | return buf.Count == 0 ? null : Encoding.ASCII.GetString(buf.ToArray()); |
| 64 | if (b[0] == (byte)'\n') |
| 65 | { |
| 66 | var s = Encoding.ASCII.GetString(buf.ToArray()); |
| 67 | if (s.Length > 0 && s[^1] == '\r') |
| 68 | s = s[..^1]; |
| 69 | return s; |
| 70 | } |
| 71 | |
| 72 | buf.Add(b[0]); |
| 73 | if (buf.Count > 65536) |
| 74 | return null; |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | public static async Task WriteMessageAsync(Stream output, ReadOnlyMemory<byte> utf8Body, CancellationToken ct) |
| 79 | { |
| 80 | var header = Encoding.ASCII.GetBytes($"Content-Length: {utf8Body.Length}\r\n\r\n"); |
| 81 | await output.WriteAsync(header, ct).ConfigureAwait(false); |
| 82 | await output.WriteAsync(utf8Body, ct).ConfigureAwait(false); |
| 83 | await output.FlushAsync(ct).ConfigureAwait(false); |
| 84 | } |
| 85 | |
| 86 | public static Task WriteJsonAsync(Stream output, JsonElement payload, CancellationToken ct) |
| 87 | { |
| 88 | var bytes = JsonSerializer.SerializeToUtf8Bytes(payload); |
| 89 | return WriteMessageAsync(output, bytes, ct); |
| 90 | } |
| 91 | } |
| 92 | |