246 lines
8.8 KiB
C#
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);
|
|
}
|