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 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 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> 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> GetDocumentSymbolsAsync(string filePath, CancellationToken ct = default(CancellationToken)) { return ParseSymbols(await SendRequestAsync("textDocument/documentSymbol", new { textDocument = new { uri = FileToUri(filePath) } }, ct)); } private async Task 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 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 ParseLocations(JsonElement? result) { List list = new List(); 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 ParseSymbols(JsonElement? result) { List list = new List(); 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 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()); break; case "java": result = FindCommand("Java", new string[1] { "jdtls" }, Array.Empty()); break; default: result = (null, Array.Empty()); 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()); } public void Dispose() { try { _writer?.Dispose(); _reader?.Dispose(); Process process = _process; if (process != null && !process.HasExited) { _process.Kill(entireProcessTree: true); _process.Dispose(); } } catch { } } }