Forge
markdowndeeb25a2
1# ADR 0169: Keymap contributions — плагинные схемы ввода (VS-style chords, packs)
2
3**Статус:** Proposed
4**Дата:** 2026-06-28
5
6## Резюме
7
8Схемы клавиатурного ввода (плоские жесты, **chord trees** в духе Visual Studio `Ctrl+R` → `R`, корневой **CascadeChord** из [0060](0060-keyboard-chord-stack-fms-tactical-strategic.md)) должны поставляться как **contributions**, а не разрастаться в `if (keymap == …)` внутри shell.
9
10**Канон:**
11
121. Семантика действия — **`command_id`** ([0030](0030-command-ids-hotkeys-and-ui-registry-layers.md)); keymap только маршрутизирует жест → `command_id` (+ опциональные args).
132. **Один активный input stack** в host: приоритеты, `when`-контекст, tunnel Avalonia + Monaco ([0163](0163-monaco-native-capability-bus-full-forward-migration.md)).
143. **v1 — code-first packs** в solution (`Keymap.Default`, `Keymap.VSRefactor`); **dynamic DLL** — после [0005](0005-defer-dynamic-plugins-mef.md) / [0024](0024-ide-sdk-and-stable-contracts.md), те же контракты.
154. **Modal editor personalities** (отдельные режимы редактора с собственной машиной ввода) — **вне scope** этого ADR; отдельное решение, если понадобится.
16
17---
18
19## Связанные ADR
20
21| ADR | Роль |
22|-----|------|
23| [0005](0005-defer-dynamic-plugins-mef.md) | dynamic plugin host deferred; packs сначала in-solution |
24| [0013](0013-command-surface-and-discoverability.md) | палитра, discoverability, keyboard-first |
25| [0024](0024-ide-sdk-and-stable-contracts.md) | SDK, capability registry, будущие плагины |
26| [0030](0030-command-ids-hotkeys-and-ui-registry-layers.md) | слои `command_id` / hotkeys TOML / VM bridge |
27| [0060](0060-keyboard-chord-stack-fms-tactical-strategic.md) | CascadeChord (`Ctrl+K` + melody tail); Command Melody `c:` |
28| [0109](0109-declarative-parametric-melody-catalog-toml-and-code-binders.md) | `intent-catalog.toml`, melody slugs |
29| [0161](0161-cide-spine-and-forge-vertical-feature-module.md) | vertical feature modules; аналогия с Forge contributions |
30| [0163](0163-monaco-native-capability-bus-full-forward-migration.md) | tunnel `host/shortcut`, editor focus |
31
32### Снимок реализации (на момент ADR)
33
34| Элемент | Файл / поведение |
35|---------|------------------|
36| Плоские жесты | `Hotkeys/hotkeys.toml` + user overlay → `HotkeyTomlLoader` |
37| Tunnel + matching | `MainWindowHotkeyService`, `KeyGestureChordMatching` |
38| CascadeChord | `CascadeChordIntentSession`, `cascade_chord` в TOML |
39| Melody / slash | `IntentMelody/intent-catalog.toml` |
40| Исполнение | `IdeMcp.ExecuteCommandAsync(command_id, args)` |
41
42---
43
44## Контекст
45
46Пользователи и команды привыкли к разным **схемам ввода**. Visual Studio — **двухшаговые chords** с удержанием смысла namespace (`Ctrl+R`, `R` = Rename). CascadeIDE — **CascadeChord** + **Intent Melody** ([0060](0060-keyboard-chord-stack-fms-tactical-strategic.md), [0109](0109-declarative-parametric-melody-catalog-toml-and-code-binders.md)).
47
48Сегодня жесты размазаны по:
49
50- merged `hotkeys.toml` (плоские `KeyGesture`);
51- одной машине `CascadeChordIntentSession` (один корень `cascade_chord`);
52- Monaco bridge для фокуса в редакторе.
53
54Добавление «VS refactor pack» или второго корня аккорда **в ядро** ведёт к дублированию tunnel-логики, конфликтам приоритетов и невозможности **включить схему** без пересборки IDE.
55
56Цель — **плагинная (pack) модель**: host знает контракт; схема поставляет таблицы привязок. Примеры стилей: **Visual Studio** (chord tree `Ctrl+R` → `R`), **Cascade** (melody после `Ctrl+K`), будущие **pack’и** без правок shell.
57
58<a id="adr0169-non-goals"></a>
59
60### Вне scope (явно)
61
62- **Modal editor** с отдельной машиной состояний внутри Monaco (условный «Vim-mode» и аналоги) — не keymap pack; требует editor-surface plugin и отдельного ADR.
63- Замена палитры (**Ctrl+Q**) или слэша в Intercom — [0013](0013-command-surface-and-discoverability.md), [0119](0119-chat-slash-commands-intercom-surface.md).
64- Новые `command_id` ради keymap — команды регистрируют **фичи**; pack только **привязывает** жесты.
65
66---
67
68## Решение
69
70<a id="adr0169-p1"></a>
71
72### 1. Атом действия — `command_id`
73
74Любая привязка keymap **обязана** ссылаться на существующий `command_id` ([0030](0030-command-ids-hotkeys-and-ui-registry-layers.md)) или на зарезервированный UI-only ключ с документированной семантикой (как `debug_start_or_continue`).
75
76Pack **не** исполняет бизнес-логику; host вызывает тот же путь, что палитра и MCP:
77
78`IInputDispatchHost.ExecuteCommandAsync(command_id, args?, cancellationToken)`.
79
80<a id="adr0169-p2"></a>
81
82### 2. Три класса привязок (единый реестр)
83
84| Класс | Пример | Источник сегодня |
85|-------|--------|------------------|
86| **Flat** | `Ctrl+Shift+U` → `intercom.attach_selection` | `hotkeys.toml` |
87| **Chord tree** | `Ctrl+R` → `R` → `roslyn.rename` | *новое* (VS-style) |
88| **Melody chord** | `Ctrl+K` → `rn` → `…` | CascadeChord + `intent-catalog` |
89
90**Правило:** все три класса регистрируются через **`IKeymapContribution`** в одном **input stack**, а не через независимые `KeyDown`-обработчики в VM.
91
92Melody chord **не дублируется**: реализация [0060](0060-keyboard-chord-stack-fms-tactical-strategic.md) становится **встроенным contribution** `Keymap.Cascade` (default pack).
93
94<a id="adr0169-p3"></a>
95
96### 3. Контракт contribution (SDK / `CascadeIDE.Contracts`)
97
98Минимальный v1 (имена ориентировочные):
99
100```csharp
101/// <summary>Одна поставляемая схема ввода (pack).</summary>
102public interface IKeymapContribution
103{
104 string Id { get; } // "cascade", "vs-refactor", …
105 int Priority { get; } // выше — раньше в stack
106 IReadOnlyList<IKeyBinding> Bindings { get; }
107}
108
109public interface IKeyBinding
110{
111 string When { get; } // см. §4
112 KeyBindingKind Kind { get; } // Flat | ChordTree | MelodyRoot
113 // Flat: KeyGesture
114 // ChordTree: root gesture + ordered steps (без Ctrl на шагах 2..N)
115 // MelodyRoot: root gesture → делегат в IMelodyChordEngine (CascadeChord)
116 string CommandId { get; }
117 string? ArgsJson { get; }
118}
119```
120
121**Регистрация:** code-first при старте ([0024](0024-ide-sdk-and-stable-contracts.md)); позже — manifest в DLL pack ([0161](0161-cide-spine-and-forge-vertical-feature-module.md)).
122
123**Experimental** до стабилизации tunnel + тестов; breaking changes без SemVer.
124
125<a id="adr0169-p4"></a>
126
127### 4. Контекст `when` (ограниченный v1)
128
129Выражение **строковое**, без произвольного C# в pack:
130
131| Предикат | Смысл |
132|----------|--------|
133| `always` | глобально (как сейчас window tunnel) |
134| `editor.focus` | фокус в Forward / Monaco |
135| `composer.focus` | фокус в Intercom composer |
136| `palette.open` | открыта палитра |
137| `chord.armed` | активна фаза chord-wait |
138| `editor.lang:csharp` | язык текущего файла |
139| `editor.selection` | ненулевое выделение |
140| `editor.diagnostic_at_caret` | squiggle под кареткой |
141
142Комбинации: `&&`, `!` (без скобочной адской вложенности в v1).
143
144Host предоставляет **`IInputContextSnapshot`** на каждый `KeyDown` (фокус, файл, selection, armed chord id).
145
146<a id="adr0169-p5"></a>
147
148### 5. Input dispatch stack
149
150Единая точка: **`IInputDispatchService.TryConsumeKeyDown(KeyEventArgs)`** (имя в коде может отличаться).
151
152Порядок:
153
1541. **Modal overlays** (палитра Esc, chord overlay) — как сейчас в tunnel.
1552. **Active chord session** (если armed) — шаги 2..N chord tree / melody tail.
1563. **Flat bindings** по приоритету contribution + специфичности `when`.
1574. **Chord roots** (начало нового дерева).
1585. **PassThrough** → редактор / composer.
159
160**Monaco:** при `editor.focus` host по-прежнему может получать `host/shortcut` из bridge; pack может регистрировать **mirror** flat/chord roots для паритета с tunnel ([0163](0163-monaco-native-capability-bus-full-forward-migration.md)).
161
162**Конфликты:** при равном приоритете — **более узкий `when`** побеждает; иначе — детерминированный порядок регистрации + запись в hotkey-log (как сегодня).
163
164<a id="adr0169-p6"></a>
165
166### 6. Данные pack (TOML) vs код
167
168**Фаза 1:** bindings в C# (in-solution pack).
169**Фаза 2:** опциональный файл рядом с pack:
170
171```toml
172[keymap]
173id = "vs-refactor"
174priority = 100
175
176[[chord]]
177root = "Ctrl+R"
178when = "editor.focus && editor.lang:csharp"
179
180 [[chord.step]]
181 keys = "R"
182 command_id = "roslyn.rename"
183
184 [[chord.step]]
185 keys = "M"
186 command_id = "roslyn.extract_method"
187```
188
189Пользовательский **`hotkeys.toml`** остаётся **оверлеем flat-жестов** ([0030](0030-command-ids-hotkeys-and-ui-registry-layers.md)); **не** заменяет целый pack, но может переопределить конкретный `command_id` → gesture (как сейчас).
190
191Настройка **`[input] active_keymap = "cascade"`** (или список enabled packs) — в `settings.toml` ([0028](0028-user-settings-toml-localappdata-and-secrets.md)).
192
193<a id="adr0169-p7"></a>
194
195### 7. Discoverability
196
197Pack **обязан** дублировать привязки в discoverability-слой:
198
199- подсказки в overlay chord (как CascadeChord dropdown);
200- опционально строки в `intent-catalog` / help (`/help keymap`);
201- паритет с палитрой: каждая привязанная команда уже в `IdeCommandPaletteCatalog`, если команда discoverable.
202
203Скрытые power-user жесты допустимы, но **документируются** в pack README.
204
205<a id="adr0169-phases"></a>
206
207### 8. Фазы внедрения
208
209| Фаза | Содержание | Критерий готовности |
210|------|------------|---------------------|
211| **P0** | `IInputDispatchService` + вынести `CascadeChordIntentSession` за `IMelodyChordEngine`; flat — за `IKeymapResolver` | Существующие hotkeys + Ctrl+K без регрессий; headless-тесты stack |
212| **P1** | In-solution `Keymap.Cascade` (default) + `Keymap.VSRefactor` (пример chord tree) | `Ctrl+R`,`R` вызывает stub/real `roslyn.rename` при `editor.focus` |
213| **P2** | TOML в pack + `active_keymap` в settings | Переключение схемы без пересборки |
214| **P3** | Dynamic plugin host загружает `IKeymapContribution` | Тот же контракт, что P1 ([0005](0005-defer-dynamic-plugins-mef.md)) |
215
216**Не блокирует:** attach affordances ([0128](0128-intercom-attachment-anchors-and-code-references.md) §H0) — остаются flat bindings в default pack.
217
218---
219
220## Последствия
221
222- Новый жест: добавить **`command_id`** и handler; в pack — binding; **не** писать отдельный `KeyDown` в `MainWindowViewModel`.
223- [0060](0060-keyboard-chord-stack-fms-tactical-strategic.md) **уточняется**, не отменяется: CascadeChord = default melody contribution.
224- [0030](0030-command-ids-hotkeys-and-ui-registry-layers.md): `hotkeys.toml` = user-overridable **flat** слой; chord trees — в pack TOML/C#.
225- Тесты: unit на stack + `when`; integration на tunnel + Monaco shortcut mirror.
226
227## Отклонённые альтернативы
228
229| Альтернатива | Почему нет |
230|--------------|------------|
231| Отдельный chord engine на каждый стиль (VS, Cascade, …) в VM | Дублирование tunnel, конфликты, нет переключения |
232| Только расширить `hotkeys.toml` chord-деревьями | Нет `when`, нет приоритетов, смешение с user overlay |
233| Modal editor в этом ADR | Другая поверхность и lifecycle ([§Вне scope](#adr0169-non-goals)) |
234| MEF plugin host сейчас | [0005](0005-defer-dynamic-plugins-mef.md); packs in-solution достаточно для P0–P2 |
235
236## Открытые вопросы
237
2381. **Имя настройки:** один `active_keymap` vs stack нескольких packs (default + user overlay pack).
2392. **Chord timeout:** глобальный (как 8 с у CascadeChord) vs per-tree в TOML.
2403. **Discoverability VS chords:** буквы на пунктах меню Refactor (как VS) — UI forge или только overlay.
2414. **Генерация** фрагментов `hotkeys.toml` из pack для подсказок палитры — в scope [ide-command-registry-v1](../design/ide-command-registry-v1.md) или отдельно.
242
243---
244
245## История изменений
246
247| Дата | Изменение |
248|------|-----------|
249| 2026-06-28 | Proposed: keymap contributions, контракт, фазы P0–P3, вне scope modal editor |
250
View only · write via MCP/CIDE