| 1 | using System.Collections.Immutable; |
| 2 | using System.Text.Json; |
| 3 | using CascadeIDE.Features.UiChrome; |
| 4 | |
| 5 | namespace CascadeIDE.Services; |
| 6 | |
| 7 | /// <summary> |
| 8 | /// Единый реестр: <see cref="CommandId"/> для MCP (если есть), метаданные палитры, доступность для агента/UI, |
| 9 | /// привязки глобальных хоткеев главного окна. Жесты — только в <c>hotkeys.toml</c> (ключ = <see cref="EffectiveHotkeysTomlKey"/>). |
| 10 | /// См. <c>docs/design/ide-command-registry-v1.md</c>, <c>docs/adr/0030-command-ids-hotkeys-and-ui-registry-layers.md</c>. |
| 11 | /// Части каталога — partial-файлы <c>IdeCommandRegistry.*.cs</c> (как <c>IdeCommands.*.cs</c>). |
| 12 | /// </summary> |
| 13 | public static partial class IdeCommandRegistry |
| 14 | { |
| 15 | /// <summary>Нет отдельного <c>IdeCommands</c>-id: только UI «Начать или продолжить» (F5); агент — <c>debug_launch</c> / <c>debug_continue</c> и т.д.</summary> |
| 16 | public const string DebugStartOrContinueHotkeyId = "debug_start_or_continue"; |
| 17 | |
| 18 | public enum CommandAccessibleFrom |
| 19 | { |
| 20 | /// <summary>MCP <c>ide_execute_command</c> + палитра/хоткеи (где подключено).</summary> |
| 21 | AgentAndUI, |
| 22 | |
| 23 | /// <summary>Только UI (жесты, меню); для того же эффекта у агента — другие <c>command_id</c> (ADR 0030).</summary> |
| 24 | UIOnly, |
| 25 | } |
| 26 | |
| 27 | public enum MainWindowHotkeyVmBindingKind |
| 28 | { |
| 29 | ToggleCommandPalette, |
| 30 | CycleUiMode, |
| 31 | SetUiModeByIndex, |
| 32 | DebugStartOrContinue, |
| 33 | DebugStop, |
| 34 | DebugStepOver, |
| 35 | DebugStepInto, |
| 36 | DebugStepOut, |
| 37 | OpenGoToFilePalette, |
| 38 | FocusSolutionExplorerFilter, |
| 39 | } |
| 40 | |
| 41 | /// <summary>Привязка хоткея окна к команде VM (без строк жестов — они в TOML).</summary> |
| 42 | public readonly record struct MainWindowHotkeyVmBinding( |
| 43 | MainWindowHotkeyVmBindingKind Kind, |
| 44 | int SetUiModeIndex = 0); |
| 45 | |
| 46 | public sealed record IdeCommandRegistryEntry( |
| 47 | string PaletteId, |
| 48 | /// <summary>Id для <c>ide_execute_command</c>; <see langword="null"/> — строка не для MCP (только UI).</summary> |
| 49 | string? CommandId, |
| 50 | string Title, |
| 51 | string Category, |
| 52 | string? ArgsJson, |
| 53 | ImmutableArray<UiModeFamily>? AllowedFamilies, |
| 54 | CommandAccessibleFrom AccessibleFrom, |
| 55 | /// <summary>Показывать в палитре команд.</summary> |
| 56 | bool IncludeInPalette, |
| 57 | /// <summary>Глобальный жест на главном окне + tunnel при фокусе в редакторе.</summary> |
| 58 | MainWindowHotkeyVmBinding? WindowHotkey, |
| 59 | /// <summary>Ключ в <c>hotkeys.toml</c>, если не совпадает с <see cref="CommandId"/>.</summary> |
| 60 | string? HotkeysTomlKey = null) |
| 61 | { |
| 62 | /// <summary>Ключ для мерджа TOML и подсказок.</summary> |
| 63 | public string EffectiveHotkeysTomlKey => HotkeysTomlKey ?? CommandId ?? PaletteId; |
| 64 | } |
| 65 | |
| 66 | public static ImmutableArray<IdeCommandRegistryEntry> AllEntries { get; } = Build(); |
| 67 | |
| 68 | /// <summary>Индекс <see cref="EffectiveHotkeysTomlKey"/> → запись с <see cref="IdeCommandRegistryEntry.WindowHotkey"/>.</summary> |
| 69 | public static IReadOnlyDictionary<string, IdeCommandRegistryEntry> WindowHotkeyByTomlKey { get; } = BuildWindowHotkeyIndex(); |
| 70 | |
| 71 | /// <summary> |
| 72 | /// Детерминированный порядок tunnel (первый совпавший жест выигрывает; важно для пересечений). |
| 73 | /// Единственный источник списка ключей; множество ключей должно совпадать с множеством <see cref="WindowHotkeyByTomlKey"/> (см. тесты). |
| 74 | /// </summary> |
| 75 | public static ImmutableArray<string> TunnelShortcutKeyOrder { get; } = BuildTunnelKeyPriorityOrder(); |
| 76 | |
| 77 | private static ImmutableArray<IdeCommandRegistryEntry> Build() |
| 78 | { |
| 79 | var b = ImmutableArray.CreateBuilder<IdeCommandRegistryEntry>(); |
| 80 | RegisterFileMenu(b); |
| 81 | RegisterView(b); |
| 82 | RegisterWorkspaceSearchPalette(b); |
| 83 | RegisterBuildPalette(b); |
| 84 | RegisterDebugPalette(b); |
| 85 | RegisterGitPalette(b); |
| 86 | RegisterChatPalette(b); |
| 87 | RegisterCockpitPalette(b); |
| 88 | RegisterDocumentsPalette(b); |
| 89 | RegisterSettingsAndHelpPalette(b); |
| 90 | RegisterMfdShellPalette(b); |
| 91 | return b.ToImmutable(); |
| 92 | } |
| 93 | |
| 94 | private static void AddPalette( |
| 95 | ImmutableArray<IdeCommandRegistryEntry>.Builder b, |
| 96 | string paletteId, |
| 97 | string commandId, |
| 98 | string title, |
| 99 | string category, |
| 100 | string? args = null, |
| 101 | ImmutableArray<UiModeFamily>? allowed = null, |
| 102 | CommandAccessibleFrom access = CommandAccessibleFrom.AgentAndUI, |
| 103 | MainWindowHotkeyVmBinding? window = null, |
| 104 | string? hotkeysTomlKey = null) |
| 105 | { |
| 106 | b.Add(new IdeCommandRegistryEntry( |
| 107 | paletteId, |
| 108 | commandId, |
| 109 | title, |
| 110 | category, |
| 111 | args, |
| 112 | allowed, |
| 113 | access, |
| 114 | IncludeInPalette: true, |
| 115 | window, |
| 116 | hotkeysTomlKey)); |
| 117 | } |
| 118 | |
| 119 | /// <summary>Единственное место, где задаётся порядок tunnel (приоритет при пересечении жестов).</summary> |
| 120 | private static ImmutableArray<string> BuildTunnelKeyPriorityOrder() |
| 121 | { |
| 122 | // Только палитра: остальные команды — через Command Melody (c:) или CascadeChord (корень из hotkeys.toml). |
| 123 | return ImmutableArray.Create( |
| 124 | "workspace_go_to_file", |
| 125 | "focus_solution_explorer_filter", |
| 126 | "toggle_command_palette"); |
| 127 | } |
| 128 | |
| 129 | private static Dictionary<string, IdeCommandRegistryEntry> BuildWindowHotkeyIndex() |
| 130 | { |
| 131 | var d = new Dictionary<string, IdeCommandRegistryEntry>(StringComparer.OrdinalIgnoreCase); |
| 132 | foreach (var e in AllEntries) |
| 133 | { |
| 134 | if (e.WindowHotkey is null) |
| 135 | continue; |
| 136 | var key = e.EffectiveHotkeysTomlKey; |
| 137 | if (!d.TryAdd(key, e)) |
| 138 | throw new InvalidOperationException( |
| 139 | $"Дубликат EffectiveHotkeysTomlKey для окна: {key} (палитра/порядок: {e.PaletteId})."); |
| 140 | } |
| 141 | |
| 142 | return d; |
| 143 | } |
| 144 | |
| 145 | /// <summary>Парсит JSON args для <see cref="IdeMcpCommandExecutor.ExecuteAsync"/> (как в прежнем каталоге палитры).</summary> |
| 146 | public static IReadOnlyDictionary<string, JsonElement>? ParseArgs(string? argsJson) |
| 147 | { |
| 148 | if (string.IsNullOrWhiteSpace(argsJson)) |
| 149 | return null; |
| 150 | using var doc = JsonDocument.Parse(argsJson); |
| 151 | if (doc.RootElement.ValueKind != JsonValueKind.Object) |
| 152 | return null; |
| 153 | var result = new Dictionary<string, JsonElement>(StringComparer.Ordinal); |
| 154 | foreach (var p in doc.RootElement.EnumerateObject()) |
| 155 | result[p.Name] = p.Value.Clone(); |
| 156 | return result; |
| 157 | } |
| 158 | } |
| 159 | |