Files
AX-Copilot-Codex/.decompiledproj/AxCopilot/Services/LspClientService.cs

540 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace AxCopilot.Services;
public class LspClientService : IDisposable
{
private Process? _process;
private StreamWriter? _writer;
private StreamReader? _reader;
private int _requestId;
private bool _initialized;
private readonly string _language;
private static readonly JsonSerializerOptions JsonOpts = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
public bool IsConnected
{
get
{
Process process = _process;
return process != null && !process.HasExited && _initialized;
}
}
public LspClientService(string language)
{
_language = language.ToLowerInvariant();
}
public async Task<bool> StartAsync(string workspacePath, CancellationToken ct = default(CancellationToken))
{
string command;
string[] args;
(command, args) = GetServerCommand();
if (command == null)
{
LogService.Warn("LSP: " + _language + "에 대한 서버를 찾을 수 없습니다.");
return false;
}
try
{
ProcessStartInfo psi = new ProcessStartInfo
{
FileName = command,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};
string[] array = args;
foreach (string arg in array)
{
psi.ArgumentList.Add(arg);
}
_process = Process.Start(psi);
if (_process == null)
{
return false;
}
_writer = _process.StandardInput;
_reader = _process.StandardOutput;
if (!(await SendRequestAsync("initialize", new
{
processId = Environment.ProcessId,
rootUri = "file:///" + workspacePath.Replace('\\', '/').TrimStart('/'),
capabilities = new
{
textDocument = new
{
definition = new
{
dynamicRegistration = false
},
references = new
{
dynamicRegistration = false
},
documentSymbol = new
{
dynamicRegistration = false
}
}
}
}, ct)).HasValue)
{
return false;
}
await SendNotificationAsync("initialized", ct);
_initialized = true;
LogService.Info($"LSP [{_language}]: 서버 시작 완료 ({command})");
return true;
}
catch (Exception ex)
{
LogService.Warn("LSP [" + _language + "] 시작 실패: " + ex.Message);
return false;
}
}
public async Task<LspLocation?> GotoDefinitionAsync(string filePath, int line, int character, CancellationToken ct = default(CancellationToken))
{
return ParseLocation(await SendRequestAsync("textDocument/definition", new
{
textDocument = new
{
uri = FileToUri(filePath)
},
position = new { line, character }
}, ct));
}
public async Task<List<LspLocation>> FindReferencesAsync(string filePath, int line, int character, CancellationToken ct = default(CancellationToken))
{
return ParseLocations(await SendRequestAsync("textDocument/references", new
{
textDocument = new
{
uri = FileToUri(filePath)
},
position = new { line, character },
context = new
{
includeDeclaration = true
}
}, ct));
}
public async Task<List<LspSymbol>> GetDocumentSymbolsAsync(string filePath, CancellationToken ct = default(CancellationToken))
{
return ParseSymbols(await SendRequestAsync("textDocument/documentSymbol", new
{
textDocument = new
{
uri = FileToUri(filePath)
}
}, ct));
}
private async Task<JsonElement?> SendRequestAsync(string method, object parameters, CancellationToken ct)
{
if (_writer == null || _reader == null)
{
return null;
}
int id = Interlocked.Increment(ref _requestId);
var request = new
{
jsonrpc = "2.0",
id = id,
method = method,
@params = parameters
};
string json = JsonSerializer.Serialize(request, JsonOpts);
byte[] content = Encoding.UTF8.GetBytes(json);
string header = $"Content-Length: {content.Length}\r\n\r\n";
await _writer.BaseStream.WriteAsync(Encoding.ASCII.GetBytes(header), ct);
await _writer.BaseStream.WriteAsync(content, ct);
await _writer.BaseStream.FlushAsync(ct);
using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(15.0));
while (!cts.Token.IsCancellationRequested)
{
string headerLine = await ReadHeaderAsync(cts.Token);
if (headerLine == null)
{
continue;
}
int bodyLength = ParseContentLength(headerLine);
if (bodyLength <= 0)
{
continue;
}
byte[] body = new byte[bodyLength];
int n;
for (int read = 0; read < bodyLength; read += n)
{
n = await _reader.BaseStream.ReadAsync(body.AsMemory(read, bodyLength - read), cts.Token);
if (n == 0)
{
break;
}
}
string responseJson = Encoding.UTF8.GetString(body);
try
{
JsonDocument doc = JsonDocument.Parse(responseJson);
JsonElement root = doc.RootElement;
if (root.TryGetProperty("id", out var _))
{
if (root.TryGetProperty("result", out var result))
{
return result;
}
if (root.TryGetProperty("error", out var error))
{
JsonElement m;
string msg = (error.TryGetProperty("message", out m) ? m.GetString() : "Unknown");
LogService.Warn("LSP RPC 오류: " + msg);
return null;
}
result = default(JsonElement);
error = default(JsonElement);
}
}
catch
{
}
}
return null;
}
private async Task SendNotificationAsync(string method, CancellationToken ct)
{
if (_writer != null)
{
var notification = new
{
jsonrpc = "2.0",
method = method,
@params = new { }
};
string json = JsonSerializer.Serialize(notification, JsonOpts);
byte[] content = Encoding.UTF8.GetBytes(json);
string header = $"Content-Length: {content.Length}\r\n\r\n";
await _writer.BaseStream.WriteAsync(Encoding.ASCII.GetBytes(header), ct);
await _writer.BaseStream.WriteAsync(content, ct);
await _writer.BaseStream.FlushAsync(ct);
}
}
private async Task<string?> ReadHeaderAsync(CancellationToken ct)
{
StringBuilder sb = new StringBuilder();
byte[] buf = new byte[1];
bool prevCr = false;
int emptyLines = 0;
while (!ct.IsCancellationRequested)
{
if (await _reader.BaseStream.ReadAsync(buf, ct) == 0)
{
return null;
}
char ch = (char)buf[0];
switch (ch)
{
case '\r':
prevCr = true;
break;
case '\n':
if (prevCr && sb.Length > 0)
{
return sb.ToString();
}
if (prevCr)
{
emptyLines++;
if (emptyLines >= 1 && sb.Length > 0)
{
return sb.ToString();
}
}
prevCr = false;
break;
default:
prevCr = false;
sb.Append(ch);
break;
}
}
return null;
}
private static int ParseContentLength(string header)
{
if (header.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase))
{
int length = "Content-Length:".Length;
if (int.TryParse(header.Substring(length, header.Length - length).Trim(), out var result))
{
return result;
}
}
return 0;
}
private static LspLocation? ParseLocation(JsonElement? result)
{
if (!result.HasValue)
{
return null;
}
try
{
JsonElement jsonElement = result.Value;
if (jsonElement.ValueKind == JsonValueKind.Array && jsonElement.GetArrayLength() > 0)
{
jsonElement = jsonElement[0];
}
if (jsonElement.TryGetProperty("uri", out var value) && jsonElement.TryGetProperty("range", out var value2))
{
JsonElement property = value2.GetProperty("start");
return new LspLocation
{
FilePath = UriToFile(value.GetString() ?? ""),
Line = property.GetProperty("line").GetInt32(),
Character = property.GetProperty("character").GetInt32()
};
}
}
catch
{
}
return null;
}
private static List<LspLocation> ParseLocations(JsonElement? result)
{
List<LspLocation> list = new List<LspLocation>();
if (!result.HasValue || result.GetValueOrDefault().ValueKind != JsonValueKind.Array)
{
return list;
}
foreach (JsonElement item in result.Value.EnumerateArray())
{
if (item.TryGetProperty("uri", out var value) && item.TryGetProperty("range", out var value2))
{
JsonElement property = value2.GetProperty("start");
list.Add(new LspLocation
{
FilePath = UriToFile(value.GetString() ?? ""),
Line = property.GetProperty("line").GetInt32(),
Character = property.GetProperty("character").GetInt32()
});
}
}
return list;
}
private static List<LspSymbol> ParseSymbols(JsonElement? result)
{
List<LspSymbol> list = new List<LspSymbol>();
if (!result.HasValue || result.GetValueOrDefault().ValueKind != JsonValueKind.Array)
{
return list;
}
foreach (JsonElement item in result.Value.EnumerateArray())
{
JsonElement value;
string name = (item.TryGetProperty("name", out value) ? (value.GetString() ?? "") : "");
JsonElement value2;
string kind = (item.TryGetProperty("kind", out value2) ? SymbolKindName(value2.GetInt32()) : "unknown");
int line = 0;
JsonElement value4;
if (item.TryGetProperty("range", out var value3))
{
line = value3.GetProperty("start").GetProperty("line").GetInt32();
}
else if (item.TryGetProperty("location", out value4))
{
line = value4.GetProperty("range").GetProperty("start").GetProperty("line")
.GetInt32();
}
list.Add(new LspSymbol
{
Name = name,
Kind = kind,
Line = line
});
if (!item.TryGetProperty("children", out var value5))
{
continue;
}
List<LspSymbol> list2 = ParseSymbols(value5);
foreach (LspSymbol item2 in list2)
{
list.Add(new LspSymbol
{
Name = " " + item2.Name,
Kind = item2.Kind,
Line = item2.Line
});
}
}
return list;
}
private static string SymbolKindName(int kind)
{
if (1 == 0)
{
}
string result = kind switch
{
1 => "file",
2 => "module",
3 => "namespace",
4 => "package",
5 => "class",
6 => "method",
7 => "property",
8 => "field",
9 => "constructor",
10 => "enum",
11 => "interface",
12 => "function",
13 => "variable",
14 => "constant",
23 => "struct",
24 => "event",
_ => "symbol",
};
if (1 == 0)
{
}
return result;
}
private static string FileToUri(string path)
{
return "file:///" + path.Replace('\\', '/').TrimStart('/');
}
private static string UriToFile(string uri)
{
if (uri.StartsWith("file:///"))
{
return uri.Substring(8, uri.Length - 8).Replace('/', '\\');
}
return uri;
}
private (string? Command, string[] Args) GetServerCommand()
{
string language = _language;
if (1 == 0)
{
}
(string, string[]) result;
switch (language)
{
case "csharp":
case "c#":
result = FindCommand("OmniSharp", new string[2] { "omnisharp", "OmniSharp.exe" }, new string[1] { "--languageserver" });
break;
case "typescript":
case "javascript":
case "ts":
case "js":
result = FindCommand("TypeScript", new string[1] { "typescript-language-server" }, new string[1] { "--stdio" });
break;
case "python":
case "py":
result = FindCommand("Python", new string[2] { "pyright-langserver", "pylsp" }, new string[1] { "--stdio" });
break;
case "cpp":
case "c++":
case "c":
result = FindCommand("C/C++", new string[1] { "clangd" }, Array.Empty<string>());
break;
case "java":
result = FindCommand("Java", new string[1] { "jdtls" }, Array.Empty<string>());
break;
default:
result = (null, Array.Empty<string>());
break;
}
if (1 == 0)
{
}
return result;
}
private static (string? Command, string[] Args) FindCommand(string label, string[] candidates, string[] defaultArgs)
{
foreach (string text in candidates)
{
try
{
using Process process = Process.Start(new ProcessStartInfo
{
FileName = ((Environment.OSVersion.Platform == PlatformID.Win32NT) ? "where" : "which"),
Arguments = text,
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
});
process?.WaitForExit(3000);
if (process != null && process.ExitCode == 0)
{
LogService.Info($"LSP [{label}]: {text} 발견");
return (Command: text, Args: defaultArgs);
}
}
catch (Exception ex)
{
LogService.Debug($"LSP [{label}]: {text} 탐색 실패 — {ex.Message}");
}
}
return (Command: null, Args: Array.Empty<string>());
}
public void Dispose()
{
try
{
_writer?.Dispose();
_reader?.Dispose();
Process process = _process;
if (process != null && !process.HasExited)
{
_process.Kill(entireProcessTree: true);
_process.Dispose();
}
}
catch
{
}
}
}