using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using System.Windows; namespace AxCopilot.Services.Agent; public class LspTool : IAgentTool, IDisposable { private readonly Dictionary _clients = new Dictionary(); public string Name => "lsp_code_intel"; public string Description => "코드 인텔리전스 도구. 정의 이동, 참조 검색, 심볼 목록을 제공합니다.\n- action=\"goto_definition\": 심볼의 정의 위치를 찾습니다 (파일, 라인, 컬럼)\n- action=\"find_references\": 심볼이 사용된 모든 위치를 찾습니다\n- action=\"symbols\": 파일 내 모든 심볼(클래스, 메서드, 필드 등)을 나열합니다\nfile_path, line, character 파라미터가 필요합니다 (line과 character는 0-based)."; public ToolParameterSchema Parameters => new ToolParameterSchema { Properties = new Dictionary { ["action"] = new ToolProperty { Type = "string", Description = "수행할 작업: goto_definition | find_references | symbols", Enum = new List { "goto_definition", "find_references", "symbols" } }, ["file_path"] = new ToolProperty { Type = "string", Description = "대상 파일 경로 (절대 또는 작업 폴더 기준 상대 경로)" }, ["line"] = new ToolProperty { Type = "integer", Description = "대상 라인 번호 (0-based). symbols 액션에서는 불필요." }, ["character"] = new ToolProperty { Type = "integer", Description = "라인 내 문자 위치 (0-based). symbols 액션에서는 불필요." } }, Required = new List { "action", "file_path" } }; public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default(CancellationToken)) { if (!((Application.Current as App)?.SettingsService?.Settings.Llm.Code.EnableLsp ?? true)) { return ToolResult.Ok("LSP 코드 인텔리전스가 비활성 상태입니다. 설정 → AX Agent → 코드에서 활성화하세요."); } JsonElement a; string action = (args.TryGetProperty("action", out a) ? (a.GetString() ?? "") : ""); JsonElement f; string filePath = (args.TryGetProperty("file_path", out f) ? (f.GetString() ?? "") : ""); JsonElement l; int line = (args.TryGetProperty("line", out l) ? l.GetInt32() : 0); JsonElement ch; int character = (args.TryGetProperty("character", out ch) ? ch.GetInt32() : 0); if (string.IsNullOrEmpty(filePath)) { return ToolResult.Fail("file_path가 필요합니다."); } if (!Path.IsPathRooted(filePath) && !string.IsNullOrEmpty(context.WorkFolder)) { filePath = Path.Combine(context.WorkFolder, filePath); } if (!File.Exists(filePath)) { return ToolResult.Fail("파일을 찾을 수 없습니다: " + filePath); } string language = DetectLanguage(filePath); if (language == null) { return ToolResult.Fail("지원하지 않는 파일 형식: " + Path.GetExtension(filePath)); } LspClientService client = await GetOrCreateClientAsync(language, context.WorkFolder, ct); if (client == null || !client.IsConnected) { return ToolResult.Fail(language + " 언어 서버를 시작할 수 없습니다. 해당 언어 서버가 설치되어 있는지 확인하세요."); } try { if (1 == 0) { } ToolResult result = action switch { "goto_definition" => await GotoDefinitionAsync(client, filePath, line, character, ct), "find_references" => await FindReferencesAsync(client, filePath, line, character, ct), "symbols" => await GetSymbolsAsync(client, filePath, ct), _ => ToolResult.Fail("알 수 없는 액션: " + action + ". goto_definition | find_references | symbols 중 선택하세요."), }; if (1 == 0) { } return result; } catch (Exception ex) { return ToolResult.Fail("LSP 오류: " + ex.Message); } } private async Task GotoDefinitionAsync(LspClientService client, string filePath, int line, int character, CancellationToken ct) { LspLocation loc = await client.GotoDefinitionAsync(filePath, line, character, ct); if (loc == null) { return ToolResult.Ok("정의를 찾을 수 없습니다 (해당 위치에 심볼이 없거나 외부 라이브러리일 수 있습니다)."); } string contextCode = ReadCodeContext(loc.FilePath, loc.Line, 3); return ToolResult.Ok($"정의 위치: {loc}\n\n```\n{contextCode}\n```", loc.FilePath); } private async Task FindReferencesAsync(LspClientService client, string filePath, int line, int character, CancellationToken ct) { List locations = await client.FindReferencesAsync(filePath, line, character, ct); if (locations.Count == 0) { return ToolResult.Ok("참조를 찾을 수 없습니다."); } StringBuilder sb = new StringBuilder(); StringBuilder stringBuilder = sb; StringBuilder stringBuilder2 = stringBuilder; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(7, 1, stringBuilder); handler.AppendLiteral("총 "); handler.AppendFormatted(locations.Count); handler.AppendLiteral("개 참조:"); stringBuilder2.AppendLine(ref handler); foreach (LspLocation loc in locations.Take(30)) { stringBuilder = sb; StringBuilder stringBuilder3 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(2, 1, stringBuilder); handler.AppendLiteral(" "); handler.AppendFormatted(loc); stringBuilder3.AppendLine(ref handler); } if (locations.Count > 30) { stringBuilder = sb; StringBuilder stringBuilder4 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder); handler.AppendLiteral(" ... 외 "); handler.AppendFormatted(locations.Count - 30); handler.AppendLiteral("개"); stringBuilder4.AppendLine(ref handler); } return ToolResult.Ok(sb.ToString()); } private async Task GetSymbolsAsync(LspClientService client, string filePath, CancellationToken ct) { List symbols = await client.GetDocumentSymbolsAsync(filePath, ct); if (symbols.Count == 0) { return ToolResult.Ok("심볼을 찾을 수 없습니다."); } StringBuilder sb = new StringBuilder(); StringBuilder stringBuilder = sb; StringBuilder stringBuilder2 = stringBuilder; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(7, 1, stringBuilder); handler.AppendLiteral("총 "); handler.AppendFormatted(symbols.Count); handler.AppendLiteral("개 심볼:"); stringBuilder2.AppendLine(ref handler); foreach (LspSymbol sym in symbols) { stringBuilder = sb; StringBuilder stringBuilder3 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(2, 1, stringBuilder); handler.AppendLiteral(" "); handler.AppendFormatted(sym); stringBuilder3.AppendLine(ref handler); } return ToolResult.Ok(sb.ToString()); } private async Task GetOrCreateClientAsync(string language, string workFolder, CancellationToken ct) { if (_clients.TryGetValue(language, out LspClientService existing) && existing.IsConnected) { return existing; } LspClientService client = new LspClientService(language); if (!(await client.StartAsync(workFolder, ct))) { client.Dispose(); return null; } _clients[language] = client; return client; } private static string? DetectLanguage(string filePath) { string text = Path.GetExtension(filePath).ToLowerInvariant(); if (1 == 0) { } string result; switch (text) { case ".cs": result = "csharp"; break; case ".ts": case ".tsx": result = "typescript"; break; case ".js": case ".jsx": result = "javascript"; break; case ".py": result = "python"; break; case ".cpp": case ".cc": case ".cxx": case ".c": case ".h": case ".hpp": result = "cpp"; break; case ".java": result = "java"; break; default: result = null; break; } if (1 == 0) { } return result; } public void Dispose() { foreach (LspClientService value in _clients.Values) { value.Dispose(); } _clients.Clear(); } private static string ReadCodeContext(string filePath, int targetLine, int contextLines) { try { string[] array = File.ReadAllLines(filePath); int num = Math.Max(0, targetLine - contextLines); int num2 = Math.Min(array.Length - 1, targetLine + contextLines); StringBuilder stringBuilder = new StringBuilder(); for (int i = num; i <= num2; i++) { string value = ((i == targetLine) ? ">>>" : " "); StringBuilder stringBuilder2 = stringBuilder; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(3, 3, stringBuilder2); handler.AppendFormatted(value); handler.AppendLiteral(" "); handler.AppendFormatted(i + 1, 4); handler.AppendLiteral(": "); handler.AppendFormatted(array[i]); stringBuilder2.AppendLine(ref handler); } return stringBuilder.ToString().TrimEnd(); } catch { return "(코드를 읽을 수 없습니다)"; } } }