Forge
csharpdeeb25a2
1using System.Collections.Immutable;
2using System.Text.Json;
3using CascadeIDE.Features.UiChrome;
4
5namespace 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>
13public 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
View only · write via MCP/CIDE