Files
clide/test/builtin/default_layout/focus_exit_vim_test.dart
T
jpmschweitzerandClaude Opus 4.8 635ae04dae fix Esc closing the editor instead of returning to Vim normal mode
panel.focusMode.exit is bound to Escape in the contributions layer, which
outranks the active preset. In Vim insert/visual mode that shadowed the
vim preset's `escape → vim.mode.normal` binding, so Esc closed the editor
instead of returning to normal mode.

Add an optional when-clause to a CommandContribution's defaultBinding
(plumbed through to KeymapService.registerCommandBinding, which already
accepts one) and guard focusMode.exit's escape with
`!vim.insert && !vim.visual` — symmetric with vim.yaml's vim.mode.normal.
Esc still exits focus / closes the editor in normal and non-Vim modes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 20:58:14 +02:00

71 lines
2.8 KiB
Dart

/// T-257: `panel.focusMode.exit` is bound to Escape in the contributions
/// layer, which outranks the active preset. Without a guard it swallowed Esc
/// in Vim insert mode and closed the editor instead of letting the vim preset's
/// `vim.mode.normal` (escape when vim.insert||vim.visual) return to normal mode.
/// The contribution now carries `bindingWhen: '!vim.insert && !vim.visual'`.
library;
import 'dart:io';
import 'package:clide/builtin/default_layout/default_layout.dart';
import 'package:clide/extension/extension.dart';
import 'package:clide/kernel/src/keymap/intents.dart';
import 'package:clide/kernel/src/keymap/key_chord.dart';
import 'package:clide/kernel/src/keymap/keymap.dart';
import 'package:clide/kernel/src/keymap/when_clause.dart';
import 'package:flutter/widgets.dart' show Intent;
import 'package:flutter_test/flutter_test.dart';
String? _cmd(Intent? i) => i is InvokeCommandIntent ? i.commandId : null;
void main() {
late CommandContribution exitCmd;
setUp(() {
exitCmd = DefaultLayoutExtension().contributions.whereType<CommandContribution>().firstWhere((c) => c.command == 'panel.focusMode.exit');
});
test('focusMode.exit escape binding is guarded against vim insert/visual', () {
expect(exitCmd.defaultBinding, 'escape');
expect(exitCmd.bindingWhen, '!vim.insert && !vim.visual');
});
group('Esc resolution with the guarded binding over the vim preset', () {
late Keymap km;
setUp(() {
// Real layering: vim.yaml preset (low) under a contributions layer
// carrying the extension's actual focusMode.exit escape binding.
final preset = KeymapLayer.fromYaml(File('assets/keymaps/vim.yaml').readAsStringSync());
final contributions = KeymapLayer(name: 'contributions', bindings: [
KeymapBinding.chord(
KeyChord.parse(exitCmd.defaultBinding!),
intent: InvokeCommandIntent(exitCmd.command),
when: WhenExpr.tryParse(exitCmd.bindingWhen),
),
]);
// Keymap flattens layers in reverse, so contributions outrank preset —
// matching KeymapService._rebuildActive's ordering.
km = Keymap([preset, contributions]);
});
Intent? esc(Map<String, bool> scope) => km.resolve(KeyChord.parse('escape'), scope);
test('insert mode → vim.mode.normal (not panel.focusMode.exit)', () {
expect(_cmd(esc(const {'vim.insert': true})), 'vim.mode.normal');
});
test('visual mode → vim.mode.normal', () {
expect(_cmd(esc(const {'vim.visual': true})), 'vim.mode.normal');
});
test('normal mode → panel.focusMode.exit (Esc still exits there)', () {
expect(_cmd(esc(const {'vim.normal': true})), 'panel.focusMode.exit');
});
test('non-vim preset (no vim scope) → panel.focusMode.exit', () {
expect(_cmd(esc(const {})), 'panel.focusMode.exit');
});
});
}