feat(settings): Keymap category — preset select (T-451)

The keybindings-ui extension contributes a Keymap SettingsCategory: a preset
select (Default / Vim / VS Code / JetBrains) reading the active preset from
kKeymapPresetSetting. Picking one routes through a new schema affordance —
SettingsField.applyCommandPrefix — running `keymap.preset.<value>`, which calls
KeymapService.setPreset to persist and reload the layered keymap live. The
prefix path keeps the engine generic: settings a subsystem applies via a
command (rather than a bare key write) declare the prefix; the scope tag and
current-value read still use the key.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 12:51:14 +02:00
co-authored by Claude Opus 4.8
parent 7aab93e776
commit 727fe8fdd4
8 changed files with 118 additions and 1 deletions
@@ -3,6 +3,7 @@
library;
import 'package:clide/builtin/keybindings_ui/keybindings_ui.dart';
import 'package:clide/extension/extension.dart';
import 'package:clide/kernel/kernel.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -46,4 +47,16 @@ void main() {
await f.services.commands.execute('keymap.preset.default');
expect(f.services.settings.get<String>(kKeymapPresetSetting), 'default');
});
test('contributes a Keymap settings category routed through the preset commands (T-451)', () {
final category = KeybindingsUiExtension().contributions
.whereType<SettingsCategoryContribution>()
.firstWhere((c) => c.id == 'keymap')
.category;
expect(category.title, 'Keymap');
final field = category.sections.expand((s) => s.fields).firstWhere((f) => f.key == kKeymapPresetSetting);
expect(field.kind, SettingsFieldKind.select);
expect(field.applyCommandPrefix, 'keymap.preset.');
expect(field.options.map((o) => o.value), containsAll(['default', 'vim', 'vscode', 'jetbrains']));
});
}
@@ -1,4 +1,5 @@
import 'package:clide/builtin/settings_ui/settings_ui.dart';
import 'package:clide/clide.dart' show IpcResponse;
import 'package:clide/extension/extension.dart';
import 'package:clide/kernel/kernel.dart';
import 'package:clide/widgets/widgets.dart';
@@ -105,6 +106,47 @@ void main() {
await tester.pump();
expect(f.services.settings.get<String>('app.demo.level'), 'debug');
});
testWidgets('a select with applyCommandPrefix runs the command, not a key write', (tester) async {
var ran = '';
f.services.commands.register(
CommandContribution(
id: 'test.apply.vim',
command: 'test.apply.vim',
run: (_) async {
ran = 'vim';
return IpcResponse.ok(id: '', data: const {});
},
),
);
const cat = SettingsCategory(
id: 'k',
title: 'K',
sections: [
SettingsSection(
label: 'P',
fields: [
SettingsField(
key: 'app.k.preset',
kind: SettingsFieldKind.select,
label: 'Preset',
defaultValue: 'default',
applyCommandPrefix: 'test.apply.',
options: [SettingsOption(value: 'default', label: 'Default'), SettingsOption(value: 'vim', label: 'Vim')],
),
],
),
],
);
await tester.pumpWidget(harness(f, _bounded(const SettingsCategoryView(category: cat))));
await tester.tap(find.bySemanticsLabel(RegExp(r'^Preset:')));
await tester.pump();
await tester.tap(find.text('Vim'));
await tester.pump();
expect(ran, 'vim');
// The key is applied by the command, not written directly by the engine.
expect(f.services.settings.get<String>('app.k.preset'), isNull);
});
});
group('scope tag (T-449)', () {