Initial commit to new repository
This commit is contained in:
212
src/AxCopilot/Core/CommandResolver.cs
Normal file
212
src/AxCopilot/Core/CommandResolver.cs
Normal file
@@ -0,0 +1,212 @@
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 입력된 텍스트를 파싱하여 적절한 ActionHandler로 라우팅합니다.
|
||||
/// Prefix 기반 라우팅 테이블을 관리합니다.
|
||||
/// </summary>
|
||||
public class CommandResolver
|
||||
{
|
||||
private readonly FuzzyEngine _fuzzy;
|
||||
private readonly SettingsService _settings;
|
||||
private readonly Dictionary<string, IActionHandler> _handlers = new();
|
||||
/// <summary>Prefix = null 핸들러 목록 — 모든 쿼리에 병렬 실행</summary>
|
||||
private readonly List<IActionHandler> _fuzzyHandlers = new();
|
||||
|
||||
public CommandResolver(FuzzyEngine fuzzy, SettingsService settings)
|
||||
{
|
||||
_fuzzy = fuzzy;
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 핸들러를 등록합니다. 플러그인 로드 시에도 이 메서드를 호출합니다.
|
||||
/// </summary>
|
||||
public void RegisterHandler(IActionHandler handler)
|
||||
{
|
||||
// Prefix 없는 핸들러 → 모든 쿼리에 부가 결과 제공 (예: BookmarkHandler)
|
||||
if (handler.Prefix == null)
|
||||
{
|
||||
_fuzzyHandlers.Add(handler);
|
||||
LogService.Info($"FuzzyHandler 등록: name='{handler.Metadata.Name}'");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_handlers.ContainsKey(handler.Prefix))
|
||||
LogService.Warn($"Prefix '{handler.Prefix}' 중복 등록: " +
|
||||
$"'{handler.Metadata.Name}'이 기존 핸들러를 덮어씁니다.");
|
||||
|
||||
_handlers[handler.Prefix] = handler;
|
||||
LogService.Info($"Handler 등록: prefix='{handler.Prefix}', name='{handler.Metadata.Name}'");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 입력 텍스트를 분석하여 결과 목록을 반환합니다.
|
||||
/// </summary>
|
||||
public async Task<IEnumerable<SDK.LauncherItem>> ResolveAsync(string input, CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return Enumerable.Empty<SDK.LauncherItem>();
|
||||
|
||||
// 1. Prefix 기반 라우팅
|
||||
foreach (var (prefix, handler) in _handlers)
|
||||
{
|
||||
if (input.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var query = input.Length > prefix.Length
|
||||
? input[prefix.Length..].Trim()
|
||||
: "";
|
||||
|
||||
try
|
||||
{
|
||||
return await handler.GetItemsAsync(query, ct);
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Error($"Handler '{handler.Metadata.Name}' 오류: {ex.Message}");
|
||||
return [new SDK.LauncherItem($"오류: {ex.Message}", handler.Metadata.Name, null, null)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fuzzy 검색 폴백 + null-prefix 핸들러 병렬 실행
|
||||
var maxResults = _settings.Settings.Launcher.MaxResults;
|
||||
|
||||
// Path 기반 중복 제거: 같은 경로의 항목이 여러 키워드로 매칭될 때 첫 번째만 표시
|
||||
var seenPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
// SortByUsage에 lazy 시퀀스를 직접 전달 → 중간 ToList 1회 제거
|
||||
var fuzzyItems = UsageRankingService.SortByUsage(
|
||||
_fuzzy.Search(input, maxResults * 2) // 중복 제거 여유분
|
||||
.Where(r => seenPaths.Add(r.Entry.Path)) // Path가 처음 등장할 때만 통과
|
||||
.Take(maxResults)
|
||||
.Select(r => new SDK.LauncherItem(
|
||||
r.Entry.DisplayName,
|
||||
r.Entry.Type == IndexEntryType.Alias ? r.Entry.AliasType switch
|
||||
{
|
||||
"url" => "URL 단축키",
|
||||
"batch" => "명령 단축키",
|
||||
_ => r.Entry.Path
|
||||
} : r.Entry.Path + " ⇧ Shift+Enter: 폴더 열기",
|
||||
null,
|
||||
r.Entry,
|
||||
Symbol: r.Entry.Type switch
|
||||
{
|
||||
IndexEntryType.App => Symbols.App,
|
||||
IndexEntryType.Folder => Symbols.Folder,
|
||||
IndexEntryType.Alias => r.Entry.AliasType switch
|
||||
{
|
||||
"url" => Symbols.Globe,
|
||||
"batch" => Symbols.Terminal,
|
||||
_ => Symbols.Plugin
|
||||
},
|
||||
_ => Symbols.File
|
||||
}
|
||||
)),
|
||||
item => (item.Data as IndexEntry)?.Path
|
||||
).ToList(); // 단일 ToList로 List<LauncherItem> 확정
|
||||
|
||||
// null-prefix 핸들러 결과를 뒤에 추가 (최대 3개씩)
|
||||
if (_fuzzyHandlers.Count > 0)
|
||||
{
|
||||
var extraTasks = _fuzzyHandlers
|
||||
.Select(h => SafeGetItemsAsync(h, input, ct))
|
||||
.ToList();
|
||||
await Task.WhenAll(extraTasks);
|
||||
|
||||
foreach (var task in extraTasks)
|
||||
{
|
||||
if (task.IsCompletedSuccessfully)
|
||||
fuzzyItems.AddRange(task.Result.Take(3));
|
||||
}
|
||||
}
|
||||
|
||||
return fuzzyItems;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 선택된 항목을 실행합니다.
|
||||
/// </summary>
|
||||
public async Task ExecuteAsync(SDK.LauncherItem item, string lastInput, CancellationToken ct)
|
||||
{
|
||||
// Prefix 기반 실행
|
||||
foreach (var (prefix, handler) in _handlers)
|
||||
{
|
||||
if (lastInput.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// 명령어 사용 통계 기록 (prefix + 첫 단어)
|
||||
var q = lastInput.Length > prefix.Length
|
||||
? lastInput[prefix.Length..].Trim().Split(' ')[0]
|
||||
: "";
|
||||
var cmdKey = string.IsNullOrEmpty(q) ? prefix : $"{prefix}{q}";
|
||||
UsageStatisticsService.RecordCommandUsage(cmdKey);
|
||||
|
||||
await handler.ExecuteAsync(item, ct);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// null-prefix 핸들러 결과 실행 (Data가 string = URL인 경우)
|
||||
if (item.Data is string urlData && urlData.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await ExecuteNullPrefixAsync(item, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fuzzy 결과 실행 (IndexEntry 기반)
|
||||
if (item.Data is IndexEntry entry)
|
||||
{
|
||||
var expanded = Environment.ExpandEnvironmentVariables(entry.Path);
|
||||
try
|
||||
{
|
||||
// Process.Start를 먼저 실행하여 체감 속도 확보
|
||||
await Task.Run(() =>
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(expanded)
|
||||
{
|
||||
UseShellExecute = true
|
||||
}));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Error($"실행 실패: {expanded} - {ex.Message}");
|
||||
}
|
||||
|
||||
// 통계 기록은 파일 열기 이후 비동기로
|
||||
_ = Task.Run(() => UsageRankingService.RecordExecution(entry.Path));
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, IActionHandler> RegisteredHandlers => _handlers;
|
||||
|
||||
// null-prefix 핸들러 실행 (ExecuteAsync 라우팅)
|
||||
public async Task ExecuteNullPrefixAsync(SDK.LauncherItem item, CancellationToken ct)
|
||||
{
|
||||
foreach (var handler in _fuzzyHandlers)
|
||||
{
|
||||
try { await handler.ExecuteAsync(item, ct); return; }
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Error($"FuzzyHandler '{handler.Metadata.Name}' 실행 오류: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IEnumerable<SDK.LauncherItem>> SafeGetItemsAsync(
|
||||
IActionHandler handler, string query, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await handler.GetItemsAsync(query, ct);
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Error($"FuzzyHandler '{handler.Metadata.Name}' 오류: {ex.Message}");
|
||||
return Enumerable.Empty<SDK.LauncherItem>();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user