Files
clide/test/kernel/src/keymap/sequence_matcher_test.dart
T
jpmschweitzerandClaude Opus 4.8 adcc79c9a5 cover Vim edit-ops and extension edge branches
Lift line coverage back over the 95% floor after the Flutter 3.44.1
merge nudged it to 94.99%. Adds tests for the changeWord/no-op-edit/
empty-paste/word-motion-edge branches in vim_edit_ops, the lone-key
flush path in SequenceMatcher, and the keybindings-ui / vim extension
identity + deactivate paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:48:27 +02:00

184 lines
5.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// T-205 / D-82: multi-chord sequences, the stateless [Keymap.match]
/// query, and the stateful [SequenceMatcher] (prefix buffering, repeat
/// counts, the d-vs-dd timeout case, and broken-sequence recovery).
library;
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';
const _yaml = '''
name: t
bindings:
- intent: command:del.pending
keys: d
- intent: command:del.line
keys: 'd d'
- intent: command:del.word
keys: 'd w'
- intent: command:goto.top
keys: 'g g'
- intent: command:down
keys: j
- intent: command:line.start
keys: '0'
- intent: command:del.char
keys: x
- intent: command:open
keys: [ctrl+p, meta+p]
''';
String? _cmd(Intent? i) => i is InvokeCommandIntent ? i.commandId : null;
void main() {
group('KeyChord.parseSequence', () {
test('single chord → one element', () {
expect(KeyChord.parseSequence('escape'), hasLength(1));
});
test('space separates a sequence', () {
final seq = KeyChord.parseSequence('d d');
expect(seq, hasLength(2));
expect(seq[0], KeyChord.parse('d'));
expect(seq[1], KeyChord.parse('d'));
});
test('chorded sequence (ctrl+k ctrl+s)', () {
final seq = KeyChord.parseSequence('ctrl+k ctrl+s');
expect(seq, hasLength(2));
expect(seq[1], KeyChord.parse('ctrl+s'));
});
test('empty throws', () {
expect(() => KeyChord.parseSequence(' '), throwsFormatException);
});
});
group('KeyChord.digit', () {
test('bare digit', () => expect(KeyChord.parse('5').digit, 5));
test('letter is not a digit', () => expect(KeyChord.parse('j').digit, isNull));
test('modified digit is not a count digit', () => expect(KeyChord.parse('ctrl+5').digit, isNull));
});
group('Keymap.match', () {
late Keymap km;
setUp(() => km = Keymap([KeymapLayer.fromYaml(_yaml)]));
test('lone d is both an exact (del.pending) and a prefix (dd/dw)', () {
final m = km.match([KeyChord.parse('d')], const {});
expect(_cmd(m.exact), 'del.pending');
expect(m.isPrefix, isTrue);
});
test('d d is an exact, not a prefix', () {
final m = km.match(KeyChord.parseSequence('d d'), const {});
expect(_cmd(m.exact), 'del.line');
expect(m.isPrefix, isFalse);
});
test('g is a pure prefix (no single-g binding)', () {
final m = km.match([KeyChord.parse('g')], const {});
expect(m.exact, isNull);
expect(m.isPrefix, isTrue);
});
test('unbound key matches nothing', () {
final m = km.match([KeyChord.parse('z')], const {});
expect(m.none, isTrue);
});
});
group('SequenceMatcher', () {
late SequenceMatcher matcher;
setUp(() {
final km = Keymap([KeymapLayer.fromYaml(_yaml)]);
matcher = SequenceMatcher(keymap: () => km, context: () => const {});
});
SeqResult feed(String spec) => matcher.feed(KeyChord.parse(spec));
test('completes d d → del.line', () {
expect(feed('d').outcome, SeqOutcome.pending);
final r = feed('d');
expect(r.outcome, SeqOutcome.fired);
expect(_cmd(r.intent), 'del.line');
expect(r.count, 1);
});
test('completes d w → del.word', () {
feed('d');
expect(_cmd(feed('w').intent), 'del.word');
});
test('g g → goto.top', () {
expect(feed('g').outcome, SeqOutcome.pending);
expect(_cmd(feed('g').intent), 'goto.top');
});
test('single-chord binding fires immediately', () {
final r = feed('j');
expect(r.outcome, SeqOutcome.fired);
expect(_cmd(r.intent), 'down');
});
test('repeat count: 5 j → down ×5', () {
expect(feed('5').outcome, SeqOutcome.pending);
final r = feed('j');
expect(r.outcome, SeqOutcome.fired);
expect(_cmd(r.intent), 'down');
expect(r.count, 5);
});
test('multi-digit count accumulates', () {
feed('1');
feed('2');
expect(feed('j').count, 12);
});
test('leading 0 is the line-start motion, not a count', () {
final r = feed('0');
expect(r.outcome, SeqOutcome.fired);
expect(_cmd(r.intent), 'line.start');
expect(r.count, 1);
});
test('unbound key passes through for insertion', () {
final r = feed('z');
expect(r.outcome, SeqOutcome.unmatched);
expect(r.passKey, KeyChord.parse('z'));
});
test('d-then-flush fires the pending single-d binding', () {
expect(feed('d').outcome, SeqOutcome.pending);
final r = matcher.flush();
expect(r.outcome, SeqOutcome.fired);
expect(_cmd(r.intent), 'del.pending');
});
test('broken sequence restarts on the last chord', () {
feed('d'); // pending (prefix of dd/dw)
final r = feed('x'); // d x matches nothing → discard, retry x
expect(r.outcome, SeqOutcome.fired);
expect(_cmd(r.intent), 'del.char');
});
test('reset clears pending and count', () {
feed('5');
feed('d');
expect(matcher.hasPending, isTrue);
matcher.reset();
expect(matcher.hasPending, isFalse);
expect(matcher.pendingLength, 0);
});
test('flush with nothing buffered is a no-op', () {
expect(matcher.flush().outcome, SeqOutcome.pending);
});
test('flush of a lone prefix key (no exact) passes it through', () {
// `g` is a pure prefix of `g g` with no single-`g` binding.
expect(feed('g').outcome, SeqOutcome.pending);
final r = matcher.flush();
expect(r.outcome, SeqOutcome.unmatched);
expect(r.passKey, KeyChord.parse('g'));
});
});
}