Raise the declared minimums in pubspec.yaml to what our deps already require: Flutter >=3.35.0 / Dart >=3.9.0 (was 3.19.0 / 3.5.0). alchemist 0.12 needs Flutter 3.32; Dart 3.9 first ships in Flutter 3.35, so 3.35 is the binding floor. Pin the exact build toolchain in .fvmrc (Flutter 3.44.1). Moving to the Dart 3.9 language level switches `dart format` to the new "tall" style and enables two new lints. This commit is the resulting mechanical churn, isolated from any behaviour change: - whole-tree `dart format` reformat (tall style) - `dart fix` for unnecessary_underscores + use_null_aware_elements No runtime behaviour change; `make test` green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
228 lines
8.7 KiB
Dart
228 lines
8.7 KiB
Dart
/// T-206: the pure Vim motion/edit engine. Drives [applyVim] over
|
|
/// `(text, caret)` and asserts the resulting text, caret, register, and
|
|
/// insert-mode request.
|
|
library;
|
|
|
|
import 'package:clide/builtin/editor/src/vim_edit_ops.dart';
|
|
import 'package:flutter/services.dart' show TextEditingValue, TextSelection;
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
TextEditingValue _tev(String text, int caret, {int? anchor}) => TextEditingValue(
|
|
text: text,
|
|
selection: TextSelection(baseOffset: anchor ?? caret, extentOffset: caret),
|
|
);
|
|
|
|
void main() {
|
|
group('motions', () {
|
|
const t = 'hello\nworld\nfoo';
|
|
|
|
test('right / left stop at line bounds', () {
|
|
expect(applyVim(VimAction.right, _tev(t, 0)).value.selection.extentOffset, 1);
|
|
expect(applyVim(VimAction.left, _tev(t, 1)).value.selection.extentOffset, 0);
|
|
expect(applyVim(VimAction.left, _tev(t, 0)).value.selection.extentOffset, 0);
|
|
});
|
|
|
|
test('down / up preserve column', () {
|
|
expect(applyVim(VimAction.down, _tev(t, 2)).value.selection.extentOffset, 8); // world, col 2
|
|
expect(applyVim(VimAction.up, _tev(t, 8)).value.selection.extentOffset, 2);
|
|
});
|
|
|
|
test('down on the last line stays put', () {
|
|
expect(applyVim(VimAction.down, _tev(t, 13)).value.selection.extentOffset, 13);
|
|
});
|
|
|
|
test('lineStart / lineEnd / firstNonBlank', () {
|
|
expect(applyVim(VimAction.lineEnd, _tev(t, 0)).value.selection.extentOffset, 5);
|
|
expect(applyVim(VimAction.lineStart, _tev(t, 8)).value.selection.extentOffset, 6);
|
|
expect(applyVim(VimAction.firstNonBlank, _tev(' ab', 3)).value.selection.extentOffset, 2);
|
|
});
|
|
|
|
test('word motions', () {
|
|
const w = 'hello world';
|
|
expect(applyVim(VimAction.wordForward, _tev(w, 0)).value.selection.extentOffset, 6);
|
|
expect(applyVim(VimAction.wordBackward, _tev(w, 8)).value.selection.extentOffset, 6);
|
|
expect(applyVim(VimAction.wordEnd, _tev(w, 0)).value.selection.extentOffset, 4);
|
|
});
|
|
|
|
test('docStart / docEnd', () {
|
|
expect(applyVim(VimAction.docStart, _tev(t, 8)).value.selection.extentOffset, 0);
|
|
expect(applyVim(VimAction.docEnd, _tev(t, 0)).value.selection.extentOffset, 12);
|
|
});
|
|
|
|
test('count repeats a motion (3·right)', () {
|
|
expect(applyVim(VimAction.right, _tev(t, 0), count: 3).value.selection.extentOffset, 3);
|
|
});
|
|
|
|
test('visual motion extends from the anchor', () {
|
|
final r = applyVim(VimAction.right, _tev(t, 0, anchor: 0), visual: true, count: 3);
|
|
expect(r.value.selection.baseOffset, 0);
|
|
expect(r.value.selection.extentOffset, 3);
|
|
});
|
|
});
|
|
|
|
group('edits', () {
|
|
test('x deletes the char under the caret', () {
|
|
final r = applyVim(VimAction.deleteChar, _tev('hello', 0));
|
|
expect(r.value.text, 'ello');
|
|
expect(r.value.selection.extentOffset, 0);
|
|
expect(r.register?.text, 'h');
|
|
});
|
|
|
|
test('x with a count', () {
|
|
expect(applyVim(VimAction.deleteChar, _tev('hello', 0), count: 2).value.text, 'llo');
|
|
});
|
|
|
|
test('dd deletes the line linewise', () {
|
|
final r = applyVim(VimAction.deleteLine, _tev('hello\nworld\nfoo', 0));
|
|
expect(r.value.text, 'world\nfoo');
|
|
expect(r.value.selection.extentOffset, 0);
|
|
expect(r.register?.text, 'hello\n');
|
|
expect(r.register?.linewise, isTrue);
|
|
});
|
|
|
|
test('dd on the last line drops the preceding newline', () {
|
|
final r = applyVim(VimAction.deleteLine, _tev('a\nb', 2));
|
|
expect(r.value.text, 'a');
|
|
expect(r.value.selection.extentOffset, 0);
|
|
});
|
|
|
|
test('2dd deletes two lines', () {
|
|
final r = applyVim(VimAction.deleteLine, _tev('a\nb\nc', 0), count: 2);
|
|
expect(r.value.text, 'c');
|
|
});
|
|
|
|
test('D deletes to end of line', () {
|
|
final r = applyVim(VimAction.deleteToEnd, _tev('hello', 2));
|
|
expect(r.value.text, 'he');
|
|
expect(r.register?.text, 'llo');
|
|
});
|
|
|
|
test('dw deletes to the next word', () {
|
|
final r = applyVim(VimAction.deleteWord, _tev('hello world', 0));
|
|
expect(r.value.text, 'world');
|
|
expect(r.value.selection.extentOffset, 0);
|
|
});
|
|
|
|
test('yy yanks linewise without changing text', () {
|
|
final r = applyVim(VimAction.yankLine, _tev('hello\nworld', 0));
|
|
expect(r.value.text, 'hello\nworld');
|
|
expect(r.register?.text, 'hello\n');
|
|
expect(r.register?.linewise, isTrue);
|
|
});
|
|
|
|
test('p pastes a linewise register below', () {
|
|
final r = applyVim(VimAction.paste, _tev('a\nb', 0), register: const VimRegister('x\n', linewise: true));
|
|
expect(r.value.text, 'a\nx\nb');
|
|
expect(r.value.selection.extentOffset, 2);
|
|
});
|
|
|
|
test('P pastes a linewise register above', () {
|
|
final r = applyVim(VimAction.pasteBefore, _tev('a\nb', 0), register: const VimRegister('x\n', linewise: true));
|
|
expect(r.value.text, 'x\na\nb');
|
|
expect(r.value.selection.extentOffset, 0);
|
|
});
|
|
|
|
test('p pastes a charwise register after the caret', () {
|
|
final r = applyVim(VimAction.paste, _tev('ac', 0), register: const VimRegister('b'));
|
|
expect(r.value.text, 'abc');
|
|
expect(r.value.selection.extentOffset, 1);
|
|
});
|
|
|
|
test('o opens a line below and enters insert', () {
|
|
final r = applyVim(VimAction.openBelow, _tev('a\nb', 0));
|
|
expect(r.value.text, 'a\n\nb');
|
|
expect(r.value.selection.extentOffset, 2);
|
|
expect(r.enterInsert, isTrue);
|
|
});
|
|
|
|
test('O opens a line above and enters insert', () {
|
|
final r = applyVim(VimAction.openAbove, _tev('a', 0));
|
|
expect(r.value.text, '\na');
|
|
expect(r.value.selection.extentOffset, 0);
|
|
expect(r.enterInsert, isTrue);
|
|
});
|
|
|
|
test('cc clears the line and enters insert', () {
|
|
final r = applyVim(VimAction.changeLine, _tev('hello\nworld', 0));
|
|
expect(r.value.text, '\nworld');
|
|
expect(r.value.selection.extentOffset, 0);
|
|
expect(r.enterInsert, isTrue);
|
|
expect(r.register?.linewise, isTrue);
|
|
});
|
|
|
|
test('cw deletes to the next word and enters insert', () {
|
|
final r = applyVim(VimAction.changeWord, _tev('hello world', 0));
|
|
expect(r.value.text, 'world');
|
|
expect(r.enterInsert, isTrue);
|
|
expect(r.register?.text, 'hello ');
|
|
});
|
|
|
|
test('x at the line end is a no-op', () {
|
|
final r = applyVim(VimAction.deleteChar, _tev('ab\ncd', 2)); // caret on the newline
|
|
expect(r.value.text, 'ab\ncd');
|
|
expect(r.register, isNull);
|
|
});
|
|
|
|
test('D at the line end is a no-op', () {
|
|
final r = applyVim(VimAction.deleteToEnd, _tev('ab', 2));
|
|
expect(r.value.text, 'ab');
|
|
});
|
|
|
|
test('p with an empty register does nothing', () {
|
|
final r = applyVim(VimAction.paste, _tev('ab', 0));
|
|
expect(r.value.text, 'ab');
|
|
});
|
|
});
|
|
|
|
group('word-motion edges', () {
|
|
test('w / b cross punctuation and underscores as their own class', () {
|
|
// foo_bar is one word (underscore is a word char); the dot is punct.
|
|
const t = 'foo_bar.baz';
|
|
expect(applyVim(VimAction.wordForward, _tev(t, 0)).value.selection.extentOffset, 7); // the '.'
|
|
expect(applyVim(VimAction.wordBackward, _tev(t, 8)).value.selection.extentOffset, 7);
|
|
});
|
|
|
|
test('e on the last word clamps to the final char', () {
|
|
final r = applyVim(VimAction.wordEnd, _tev('hi', 1));
|
|
expect(r.value.selection.extentOffset, 1);
|
|
});
|
|
});
|
|
|
|
group('insert entry', () {
|
|
test('i stays, a advances, A goes to line end, I to first non-blank', () {
|
|
expect(applyVim(VimAction.insert, _tev('ab', 0)).enterInsert, isTrue);
|
|
expect(applyVim(VimAction.insert, _tev('ab', 0)).value.selection.extentOffset, 0);
|
|
expect(applyVim(VimAction.append, _tev('ab', 0)).value.selection.extentOffset, 1);
|
|
expect(applyVim(VimAction.appendLineEnd, _tev('ab', 0)).value.selection.extentOffset, 2);
|
|
expect(applyVim(VimAction.insertLineStart, _tev(' ab', 0)).value.selection.extentOffset, 2);
|
|
});
|
|
});
|
|
|
|
group('visual range ops', () {
|
|
test('visual delete removes the inclusive range', () {
|
|
final r = applyVim(VimAction.visualDelete, _tev('hello', 2, anchor: 0), visual: true);
|
|
expect(r.value.text, 'lo');
|
|
expect(r.value.selection.extentOffset, 0);
|
|
expect(r.register?.text, 'hel');
|
|
});
|
|
|
|
test('visual yank keeps text and stores the range', () {
|
|
final r = applyVim(VimAction.visualYank, _tev('hello', 2, anchor: 0), visual: true);
|
|
expect(r.value.text, 'hello');
|
|
expect(r.register?.text, 'hel');
|
|
});
|
|
|
|
test('visual change deletes and enters insert', () {
|
|
final r = applyVim(VimAction.visualChange, _tev('hello', 2, anchor: 0), visual: true);
|
|
expect(r.value.text, 'lo');
|
|
expect(r.enterInsert, isTrue);
|
|
});
|
|
});
|
|
|
|
test('unknown action is a no-op', () {
|
|
final v = _tev('abc', 1);
|
|
final r = applyVim('editor.vim.nope', v);
|
|
expect(r.value, v);
|
|
});
|
|
}
|