using System.IO; using System.Text; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// 텍스트/파일 비교 핸들러. "diff" 프리픽스로 사용합니다. /// 클립보드 히스토리의 최근 2개 텍스트를 줄 단위로 비교하거나, /// 파일 2개를 지정하여 비교합니다. /// 예: diff → 클립보드 히스토리 최근 2개 비교 /// diff C:\a.txt C:\b.txt → 파일 비교 /// Enter → 비교 결과를 클립보드에 복사. /// public class DiffHandler : IActionHandler { private readonly ClipboardHistoryService? _clipHistory; public DiffHandler(ClipboardHistoryService? clipHistory = null) { _clipHistory = clipHistory; } public string? Prefix => "diff"; public PluginMetadata Metadata => new( "Diff", "텍스트/파일 비교 — diff", "1.0", "AX"); public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); // 파일 비교 모드 if (!string.IsNullOrWhiteSpace(q)) { // 파일 2개 지정 var paths = q.Split(' ', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (paths.Length == 2 && File.Exists(paths[0]) && File.Exists(paths[1])) { try { var textA = File.ReadAllText(paths[0]); var textB = File.ReadAllText(paths[1]); var result = BuildDiff(textA, textB, Path.GetFileName(paths[0]), Path.GetFileName(paths[1])); return Task.FromResult>( [ new LauncherItem( $"파일 비교: {Path.GetFileName(paths[0])} ↔ {Path.GetFileName(paths[1])}", $"{result.Added}줄 추가, {result.Removed}줄 삭제, {result.Same}줄 동일 · Enter로 결과 복사", null, result.Text, Symbol: Symbols.File) ]); } catch (Exception ex) { return Task.FromResult>( [ new LauncherItem($"파일 읽기 실패: {ex.Message}", "", null, null, Symbol: Symbols.Error) ]); } } // 파일 1개만 있으면 안내 if (paths.Length >= 1 && (File.Exists(paths[0]) || Directory.Exists(Path.GetDirectoryName(paths[0]) ?? ""))) { return Task.FromResult>( [ new LauncherItem( "비교할 파일 2개의 경로를 입력하세요", "예: diff C:\\a.txt C:\\b.txt", null, null, Symbol: Symbols.Info) ]); } } // 클립보드 히스토리 비교 모드 var history = _clipHistory?.History; if (history == null || history.Count < 2) { return Task.FromResult>( [ new LauncherItem( "비교할 텍스트가 부족합니다", "클립보드에 2개 이상의 텍스트를 복사하거나, diff [파일A] [파일B]로 파일 비교", null, null, Symbol: Symbols.Info) ]); } var textEntries = history.Where(e => e.IsText).Take(2).ToList(); if (textEntries.Count < 2) { return Task.FromResult>( [ new LauncherItem("텍스트 히스토리가 2개 미만입니다", "텍스트를 2번 이상 복사하세요", null, null, Symbol: Symbols.Info) ]); } var older = textEntries[1]; // 이전 var newer = textEntries[0]; // 최근 var diff = BuildDiff(older.Text, newer.Text, $"이전 ({older.RelativeTime})", $"최근 ({newer.RelativeTime})"); var items = new List { new( $"클립보드 비교: +{diff.Added} -{diff.Removed} ={diff.Same}", $"이전 복사 ↔ 최근 복사 · Enter로 결과 복사", null, diff.Text, Symbol: Symbols.History), }; // 미리보기 (변경된 줄 최대 5개) var changedLines = diff.Text.Split('\n') .Where(l => l.StartsWith("+ ") || l.StartsWith("- ")) .Take(5); foreach (var line in changedLines) { var symbol = line.StartsWith("+ ") ? "\uE710" : "\uE711"; // + or X items.Add(new LauncherItem( line, "", null, null, Symbol: symbol)); } // 파일 선택 비교 항목 items.Add(new LauncherItem( "파일 선택하여 비교", "파일 선택 대화 상자에서 2개의 파일을 골라 비교합니다", null, "__FILE_DIALOG__", Symbol: Symbols.File)); return Task.FromResult>(items); } public async Task ExecuteAsync(LauncherItem item, CancellationToken ct) { // 파일 선택 다이얼로그 if (item.Data is string s && s == "__FILE_DIALOG__") { await Task.Delay(200, ct); // 런처 닫힘 대기 Application.Current?.Dispatcher.Invoke(() => { var dlg = new Microsoft.Win32.OpenFileDialog { Title = "비교할 첫 번째 파일 선택", Filter = "텍스트 파일|*.txt;*.cs;*.json;*.xml;*.md;*.csv;*.log|모든 파일|*.*" }; if (dlg.ShowDialog() != true) return; var fileA = dlg.FileName; dlg.Title = "비교할 두 번째 파일 선택"; if (dlg.ShowDialog() != true) return; var fileB = dlg.FileName; try { var textA = File.ReadAllText(fileA); var textB = File.ReadAllText(fileB); var result = BuildDiff(textA, textB, Path.GetFileName(fileA), Path.GetFileName(fileB)); Clipboard.SetText(result.Text); NotificationService.Notify("파일 비교 완료", $"+{result.Added} -{result.Removed} ={result.Same} · 결과 클립보드 복사됨"); } catch (Exception ex) { NotificationService.Notify("비교 실패", ex.Message); } }); return; } if (item.Data is string text && !string.IsNullOrWhiteSpace(text)) { try { Application.Current?.Dispatcher.Invoke(() => Clipboard.SetText(text)); } catch { } NotificationService.Notify("비교 결과", "클립보드에 복사되었습니다"); } } // 간단한 줄 단위 diff private static DiffResult BuildDiff(string textA, string textB, string labelA, string labelB) { var linesA = textA.Split('\n').Select(l => l.TrimEnd('\r')).ToArray(); var linesB = textB.Split('\n').Select(l => l.TrimEnd('\r')).ToArray(); var setA = new HashSet(linesA); var setB = new HashSet(linesB); var sb = new StringBuilder(); sb.AppendLine($"--- {labelA}"); sb.AppendLine($"+++ {labelB}"); sb.AppendLine(); int added = 0, removed = 0, same = 0; int maxLen = Math.Max(linesA.Length, linesB.Length); for (int i = 0; i < maxLen; i++) { var a = i < linesA.Length ? linesA[i] : null; var b = i < linesB.Length ? linesB[i] : null; if (a == b) { sb.AppendLine($" {a}"); same++; } else { if (a != null && !setB.Contains(a)) { sb.AppendLine($"- {a}"); removed++; } if (b != null && !setA.Contains(b)) { sb.AppendLine($"+ {b}"); added++; } if (a != null && setB.Contains(a) && a != b) { sb.AppendLine($" {a}"); same++; } } } return new DiffResult(sb.ToString(), added, removed, same); } private record DiffResult(string Text, int Added, int Removed, int Same); }