Initial commit to new repository
This commit is contained in:
202
src/AxCopilot/Services/Agent/LspTool.cs
Normal file
202
src/AxCopilot/Services/Agent/LspTool.cs
Normal file
@@ -0,0 +1,202 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
/// <summary>
|
||||
/// LSP 기반 코드 인텔리전스 도구.
|
||||
/// 정의 이동(goto_definition), 참조 검색(find_references), 심볼 목록(symbols) 3가지 액션을 제공합니다.
|
||||
/// </summary>
|
||||
public class LspTool : IAgentTool, IDisposable
|
||||
{
|
||||
public string Name => "lsp_code_intel";
|
||||
|
||||
public string Description =>
|
||||
"코드 인텔리전스 도구. 정의 이동, 참조 검색, 심볼 목록을 제공합니다.\n" +
|
||||
"- action=\"goto_definition\": 심볼의 정의 위치를 찾습니다 (파일, 라인, 컬럼)\n" +
|
||||
"- action=\"find_references\": 심볼이 사용된 모든 위치를 찾습니다\n" +
|
||||
"- action=\"symbols\": 파일 내 모든 심볼(클래스, 메서드, 필드 등)을 나열합니다\n" +
|
||||
"file_path, line, character 파라미터가 필요합니다 (line과 character는 0-based).";
|
||||
|
||||
public ToolParameterSchema Parameters => new()
|
||||
{
|
||||
Properties = new()
|
||||
{
|
||||
["action"] = new ToolProperty
|
||||
{
|
||||
Type = "string",
|
||||
Description = "수행할 작업: goto_definition | find_references | symbols",
|
||||
Enum = new() { "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() { "action", "file_path" }
|
||||
};
|
||||
|
||||
// 언어별 LSP 클라이언트 캐시
|
||||
private readonly Dictionary<string, LspClientService> _clients = new();
|
||||
|
||||
public async Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default)
|
||||
{
|
||||
// 설정 체크
|
||||
var app = System.Windows.Application.Current as App;
|
||||
if (!(app?.SettingsService?.Settings.Llm.Code.EnableLsp ?? true))
|
||||
return ToolResult.Ok("LSP 코드 인텔리전스가 비활성 상태입니다. 설정 → AX Agent → 코드에서 활성화하세요.");
|
||||
|
||||
var action = args.TryGetProperty("action", out var a) ? a.GetString() ?? "" : "";
|
||||
var filePath = args.TryGetProperty("file_path", out var f) ? f.GetString() ?? "" : "";
|
||||
var line = args.TryGetProperty("line", out var l) ? l.GetInt32() : 0;
|
||||
var character = args.TryGetProperty("character", out var 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}");
|
||||
|
||||
// 언어 감지
|
||||
var language = DetectLanguage(filePath);
|
||||
if (language == null)
|
||||
return ToolResult.Fail($"지원하지 않는 파일 형식: {Path.GetExtension(filePath)}");
|
||||
|
||||
// LSP 클라이언트 시작 (캐시)
|
||||
var client = await GetOrCreateClientAsync(language, context.WorkFolder, ct);
|
||||
if (client == null || !client.IsConnected)
|
||||
return ToolResult.Fail($"{language} 언어 서버를 시작할 수 없습니다. 해당 언어 서버가 설치되어 있는지 확인하세요.");
|
||||
|
||||
try
|
||||
{
|
||||
return 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 중 선택하세요.")
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ToolResult.Fail($"LSP 오류: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ToolResult> GotoDefinitionAsync(LspClientService client, string filePath, int line, int character, CancellationToken ct)
|
||||
{
|
||||
var loc = await client.GotoDefinitionAsync(filePath, line, character, ct);
|
||||
if (loc == null)
|
||||
return ToolResult.Ok("정의를 찾을 수 없습니다 (해당 위치에 심볼이 없거나 외부 라이브러리일 수 있습니다).");
|
||||
|
||||
// 정의 위치의 코드를 읽어서 컨텍스트 제공
|
||||
var contextCode = ReadCodeContext(loc.FilePath, loc.Line, 3);
|
||||
return ToolResult.Ok(
|
||||
$"정의 위치: {loc}\n\n```\n{contextCode}\n```",
|
||||
loc.FilePath);
|
||||
}
|
||||
|
||||
private async Task<ToolResult> FindReferencesAsync(LspClientService client, string filePath, int line, int character, CancellationToken ct)
|
||||
{
|
||||
var locations = await client.FindReferencesAsync(filePath, line, character, ct);
|
||||
if (locations.Count == 0)
|
||||
return ToolResult.Ok("참조를 찾을 수 없습니다.");
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine($"총 {locations.Count}개 참조:");
|
||||
foreach (var loc in locations.Take(30))
|
||||
sb.AppendLine($" {loc}");
|
||||
if (locations.Count > 30)
|
||||
sb.AppendLine($" ... 외 {locations.Count - 30}개");
|
||||
|
||||
return ToolResult.Ok(sb.ToString());
|
||||
}
|
||||
|
||||
private async Task<ToolResult> GetSymbolsAsync(LspClientService client, string filePath, CancellationToken ct)
|
||||
{
|
||||
var symbols = await client.GetDocumentSymbolsAsync(filePath, ct);
|
||||
if (symbols.Count == 0)
|
||||
return ToolResult.Ok("심볼을 찾을 수 없습니다.");
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine($"총 {symbols.Count}개 심볼:");
|
||||
foreach (var sym in symbols)
|
||||
sb.AppendLine($" {sym}");
|
||||
|
||||
return ToolResult.Ok(sb.ToString());
|
||||
}
|
||||
|
||||
private async Task<LspClientService?> GetOrCreateClientAsync(string language, string workFolder, CancellationToken ct)
|
||||
{
|
||||
if (_clients.TryGetValue(language, out var existing) && existing.IsConnected)
|
||||
return existing;
|
||||
|
||||
var client = new LspClientService(language);
|
||||
var started = await client.StartAsync(workFolder, ct);
|
||||
if (!started)
|
||||
{
|
||||
client.Dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
_clients[language] = client;
|
||||
return client;
|
||||
}
|
||||
|
||||
private static string? DetectLanguage(string filePath)
|
||||
{
|
||||
var ext = Path.GetExtension(filePath).ToLowerInvariant();
|
||||
return ext switch
|
||||
{
|
||||
".cs" => "csharp",
|
||||
".ts" or ".tsx" => "typescript",
|
||||
".js" or ".jsx" => "javascript",
|
||||
".py" => "python",
|
||||
".cpp" or ".cc" or ".cxx" or ".c" or ".h" or ".hpp" => "cpp",
|
||||
".java" => "java",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var client in _clients.Values)
|
||||
client.Dispose();
|
||||
_clients.Clear();
|
||||
}
|
||||
|
||||
private static string ReadCodeContext(string filePath, int targetLine, int contextLines)
|
||||
{
|
||||
try
|
||||
{
|
||||
var read = TextFileCodec.ReadAllText(filePath);
|
||||
var lines = TextFileCodec.SplitLines(read.Text);
|
||||
var start = Math.Max(0, targetLine - contextLines);
|
||||
var end = Math.Min(lines.Length - 1, targetLine + contextLines);
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
for (int i = start; i <= end; i++)
|
||||
{
|
||||
var marker = i == targetLine ? ">>>" : " ";
|
||||
sb.AppendLine($"{marker} {i + 1,4}: {lines[i].TrimEnd('\r')}");
|
||||
}
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
catch { return "(코드를 읽을 수 없습니다)"; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user