Files
AX-Copilot-Codex/src/AxCopilot/Handlers/DiffHandler.cs

246 lines
8.8 KiB
C#

using System.IO;
using System.Text;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 텍스트/파일 비교 핸들러. "diff" 프리픽스로 사용합니다.
/// 클립보드 히스토리의 최근 2개 텍스트를 줄 단위로 비교하거나,
/// 파일 2개를 지정하여 비교합니다.
/// 예: diff → 클립보드 히스토리 최근 2개 비교
/// diff C:\a.txt C:\b.txt → 파일 비교
/// Enter → 비교 결과를 클립보드에 복사.
/// </summary>
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<IEnumerable<LauncherItem>> 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<IEnumerable<LauncherItem>>(
[
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<IEnumerable<LauncherItem>>(
[
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<IEnumerable<LauncherItem>>(
[
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<IEnumerable<LauncherItem>>(
[
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<IEnumerable<LauncherItem>>(
[
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<LauncherItem>
{
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<IEnumerable<LauncherItem>>(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<string>(linesA);
var setB = new HashSet<string>(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);
}