Initial commit to new repository
This commit is contained in:
245
src/AxCopilot/Handlers/DiffHandler.cs
Normal file
245
src/AxCopilot/Handlers/DiffHandler.cs
Normal file
@@ -0,0 +1,245 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user