add Vim mode service, mode commands, and status indicator
T-207, first foundation piece of the Vim layer (T-65 epic). A
VimModeService (ChangeNotifier) owns the normal/insert/visual mode and
mirrors it into the keymap as mutually-exclusive vim.normal/vim.insert/
vim.visual scope flags. Those flags are the public mode interface: the
editor (T-206) will read them to decide insert-vs-command, and vim.yaml
(T-65) guards bindings with `when: vim.*`. Nothing reaches across the
builtin boundary into the service object.
The layer is gated on the active preset — the builtin.vim extension
ties VimModeService.enabled to app.keymap.preset and re-checks on every
keymap reload, so i/v/Esc never hijack input under non-Vim presets. Mode
commands (vim.mode.{normal,insert,visual}) carry no default binding for
the same reason; only vim.yaml binds keys to them. A status-bar item
shows `-- NORMAL --` etc. while enabled.
Exposes KeymapService on the extension context so the layer can publish
scope flags.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
/// T-207: the Vim mode service mirrors the active mode into the keymap as
|
||||
/// mutually-exclusive `vim.*` scope flags, and clears them when disabled.
|
||||
library;
|
||||
|
||||
import 'package:clide/builtin/vim/vim.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../helpers/kernel_fixture.dart';
|
||||
|
||||
void main() {
|
||||
late KernelFixture f;
|
||||
late VimModeService mode;
|
||||
|
||||
setUp(() async {
|
||||
f = await KernelFixture.create();
|
||||
mode = VimModeService(f.services.keymap);
|
||||
});
|
||||
tearDown(() {
|
||||
mode.dispose();
|
||||
return f.dispose();
|
||||
});
|
||||
|
||||
bool flag(String name) => f.services.keymap.scope[name] ?? false;
|
||||
|
||||
test('disabled by default and pushes no scope flags', () {
|
||||
expect(mode.enabled, isFalse);
|
||||
expect(flag('vim.normal'), isFalse);
|
||||
expect(flag('vim.insert'), isFalse);
|
||||
expect(flag('vim.visual'), isFalse);
|
||||
});
|
||||
|
||||
test('enabling resets to normal and publishes exactly one flag', () {
|
||||
mode.enabled = true;
|
||||
expect(mode.mode, VimMode.normal);
|
||||
expect(flag('vim.normal'), isTrue);
|
||||
expect(flag('vim.insert'), isFalse);
|
||||
expect(flag('vim.visual'), isFalse);
|
||||
});
|
||||
|
||||
test('mode transitions keep flags mutually exclusive', () {
|
||||
mode.enabled = true;
|
||||
|
||||
mode.enterInsert();
|
||||
expect(mode.mode, VimMode.insert);
|
||||
expect(flag('vim.insert'), isTrue);
|
||||
expect(flag('vim.normal'), isFalse);
|
||||
|
||||
mode.enterVisual();
|
||||
expect(flag('vim.visual'), isTrue);
|
||||
expect(flag('vim.insert'), isFalse);
|
||||
|
||||
mode.enterNormal();
|
||||
expect(flag('vim.normal'), isTrue);
|
||||
expect(flag('vim.visual'), isFalse);
|
||||
});
|
||||
|
||||
test('transitions are inert while disabled', () {
|
||||
mode.enterInsert();
|
||||
expect(mode.mode, VimMode.normal); // unchanged
|
||||
expect(flag('vim.insert'), isFalse);
|
||||
});
|
||||
|
||||
test('disabling clears every vim flag', () {
|
||||
mode.enabled = true;
|
||||
mode.enterInsert();
|
||||
expect(flag('vim.insert'), isTrue);
|
||||
|
||||
mode.enabled = false;
|
||||
expect(flag('vim.normal'), isFalse);
|
||||
expect(flag('vim.insert'), isFalse);
|
||||
expect(flag('vim.visual'), isFalse);
|
||||
});
|
||||
|
||||
test('notifies listeners on enable and on mode change', () {
|
||||
var n = 0;
|
||||
mode.addListener(() => n++);
|
||||
mode.enabled = true; // 1
|
||||
mode.enterInsert(); // 2
|
||||
mode.enterInsert(); // no-op, same mode
|
||||
mode.enterNormal(); // 3
|
||||
expect(n, 3);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user