Files
clide/test/builtin/editor/vim_preset_test.dart
T
jpmschweitzerandClaude Opus 4.8 bcb57bc75f ship the Vim keymap preset
T-65 — the capstone of the Vim epic. assets/keymaps/vim.yaml binds the
muscle-memory set guarded on vim.normal/insert/visual: hjkl/w/b/e/0/$/^/
gg/G motions, x/dd/dw/D/yy/p/P/cc/cw edits, i/a/I/A/o/O insert entries,
v + d/y/c in visual, Esc back to normal, and counts via the matcher. App
shortcuts (palette, find, zoom) are carried in the preset so they survive
under Vim.

The editor now feeds Shift chords to the matcher (Vim's capitals: G, D,
A, P, $) while still bubbling Ctrl/Alt/Meta to the global handler. The
keybindings-ui stub gains palette commands (`Keymap: Vim` / `Keymap:
Default`) to switch presets — the user-facing way to turn Vim on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 21:32:40 +02:00

87 lines
3.3 KiB
Dart

/// T-65: the shipped Vim preset. Loads the real assets/keymaps/vim.yaml
/// and asserts representative bindings resolve in the right mode — motions,
/// the i→insert / Esc→normal / v→visual mode changes, a dd sequence, a
/// shifted capital (G), and that app shortcuts (palette) survive.
library;
import 'dart:io';
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/sequence_matcher.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 Keymap km;
setUp(() {
final src = File('assets/keymaps/vim.yaml').readAsStringSync();
km = Keymap([KeymapLayer.fromYaml(src)]);
});
const normal = {'vim.normal': true};
const insert = {'vim.insert': true};
const visual = {'vim.visual': true};
Intent? resolve(String chord, Map<String, bool> scope) => km.resolve(KeyChord.parse(chord), scope);
test('motions resolve in normal mode', () {
expect(_cmd(resolve('j', normal)), 'editor.vim.down');
expect(_cmd(resolve('h', normal)), 'editor.vim.left');
expect(_cmd(resolve('w', normal)), 'editor.vim.wordForward');
expect(_cmd(resolve('0', normal)), 'editor.vim.lineStart');
});
test('capitals (shifted) resolve', () {
expect(_cmd(resolve('shift+g', normal)), 'editor.vim.docEnd');
expect(_cmd(resolve('shift+d', normal)), 'editor.vim.deleteToEnd');
expect(_cmd(resolve('shift+a', normal)), 'editor.vim.appendLineEnd');
});
test('motions also resolve in visual mode', () {
expect(_cmd(resolve('l', visual)), 'editor.vim.right');
expect(_cmd(resolve('w', visual)), 'editor.vim.wordForward');
});
test('mode changes', () {
expect(_cmd(resolve('i', normal)), 'editor.vim.insert');
expect(_cmd(resolve('v', normal)), 'vim.mode.visual');
expect(_cmd(resolve('escape', insert)), 'vim.mode.normal');
expect(_cmd(resolve('escape', visual)), 'vim.mode.normal');
});
test('normal-mode i does not fire in visual mode', () {
expect(resolve('i', visual), isNull);
});
test('visual operators resolve only in visual mode', () {
expect(_cmd(resolve('y', visual)), 'editor.vim.visualYank');
expect(_cmd(resolve('d', visual)), 'editor.vim.visualDelete'); // single d in visual
});
test('app shortcuts survive under the vim preset', () {
expect(resolve('ctrl+shift+p', normal), isA<PaletteOpenIntent>());
expect(resolve('ctrl+shift+f', insert), isA<FindInFilesIntent>());
});
test('dd / dw sequences resolve through the matcher in normal mode', () {
final m = SequenceMatcher(keymap: () => km, context: () => normal);
expect(m.feed(KeyChord.parse('d')).outcome, SeqOutcome.pending);
final r = m.feed(KeyChord.parse('d'));
expect(_cmd(r.intent), 'editor.vim.deleteLine');
m.reset();
m.feed(KeyChord.parse('d'));
expect(_cmd(m.feed(KeyChord.parse('w')).intent), 'editor.vim.deleteWord');
});
test('gg sequence resolves to docStart', () {
final m = SequenceMatcher(keymap: () => km, context: () => normal);
expect(m.feed(KeyChord.parse('g')).outcome, SeqOutcome.pending);
expect(_cmd(m.feed(KeyChord.parse('g')).intent), 'editor.vim.docStart');
});
}