feat(vim): ex command-line overlay (:w :q :wq :x :e :N, ZZ) (T-407)
Under the Vim preset, `:` opens a transient one-line ex overlay running a fixed v1 table; ZZ runs :wq directly. Completes the last built child of the T-403 cross-pane vim layer (T-405 part 2 gt/gT still open). - ExLineController + parseExCommand grammar + editor-targeted executors (lib/kernel/src/ex_line.dart); the overlay (lib/widgets/src/ex_line_overlay .dart) reuses the quick-open chrome, mounts in the root_shell Stack, and publishes the exline.open scope flag. Unknown commands flash + stay open; with no active buffer every command no-ops (2026-06-13 decision). - :q closes the active tab via editor.close on its id — the registry promotes the next buffer and the split self-collapses on the last (2026-06-12 decision); :w/:wq/:x/ZZ save (+close) the active buffer. - :e <path> seeds quick-open (new QuickOpenController.open(seed:)); :N adds the editor.goto-line IPC/CLI verb (reuses _offsetForLine). Goto needs caret sync: EditorController now handles editor.selection-changed and the editor view moves the caret on a selection-only change. - `:` and ZZ are typed intents; the editor matcher and PaneKeyNav now bubble unhandled typed intents to the app-root Actions, so they fire from any focus. vim.yaml binds `:`, ZZ (shift+z shift+z), and Esc-dismiss. Tests: parser/controller/executors, editor.goto-line daemon tests, selection-changed (controller + view), full overlay widget test. make test green; analyze + format clean. Also files T-441 (drop bold from the ticket-id card label) and T-442 (sub-agent renders as 3 cards instead of one bundle) under the T-276 UI epic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -187,6 +187,59 @@ void main() {
|
||||
expect(c.activeId, isNull); // cleared until an active-changed arrives
|
||||
});
|
||||
|
||||
test('editor.selection-changed mirrors an external caret move onto the active buffer (T-407)', () async {
|
||||
ipc.stub(
|
||||
'editor.list',
|
||||
(_) async => _ok({
|
||||
'buffers': [_buf('b_1', 'a.dart')],
|
||||
}),
|
||||
);
|
||||
ipc.stub(
|
||||
'editor.active',
|
||||
(_) async => _ok({
|
||||
'active': {'id': 'b_1'},
|
||||
}),
|
||||
);
|
||||
ipc.stub('editor.read', (_) async => _ok(_read('b_1', 'a.dart', 'line1\nline2')));
|
||||
await c.hydrate();
|
||||
expect(c.selection.start, 0);
|
||||
|
||||
// An external setSelection (ex-line :N goto) jumps the caret server-side.
|
||||
emitEditor(bus, 'editor.selection-changed', {
|
||||
'id': 'b_1',
|
||||
'selection': {'start': 6, 'end': 6},
|
||||
});
|
||||
await pumpEventQueue();
|
||||
|
||||
expect(c.selection.start, 6);
|
||||
expect(c.selection.end, 6);
|
||||
});
|
||||
|
||||
test('editor.selection-changed for a non-active buffer is ignored', () async {
|
||||
ipc.stub(
|
||||
'editor.list',
|
||||
(_) async => _ok({
|
||||
'buffers': [_buf('b_1', 'a.dart')],
|
||||
}),
|
||||
);
|
||||
ipc.stub(
|
||||
'editor.active',
|
||||
(_) async => _ok({
|
||||
'active': {'id': 'b_1'},
|
||||
}),
|
||||
);
|
||||
ipc.stub('editor.read', (_) async => _ok(_read('b_1', 'a.dart', 'hello')));
|
||||
await c.hydrate();
|
||||
|
||||
emitEditor(bus, 'editor.selection-changed', {
|
||||
'id': 'b_other',
|
||||
'selection': {'start': 3, 'end': 3},
|
||||
});
|
||||
await pumpEventQueue();
|
||||
|
||||
expect(c.selection.start, 0); // untouched
|
||||
});
|
||||
|
||||
test('editor.saved clears the dirty marker on the buffer', () async {
|
||||
ipc.stub(
|
||||
'editor.list',
|
||||
|
||||
@@ -6,6 +6,7 @@ library;
|
||||
|
||||
import 'package:clide/builtin/editor/src/editor_view.dart';
|
||||
import 'package:clide/clide.dart';
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:clide/widgets/widgets.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
@@ -76,6 +77,32 @@ void main() {
|
||||
expect(find.text('Open a file to begin editing.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('an external selection-changed moves the caret without retyping (T-407)', (tester) async {
|
||||
stubBuffers([_buf('b_1', 'lib/a.dart')], active: 'b_1');
|
||||
await tester.pumpWidget(harness(f, const EditorView()));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.widget<EditableText>(find.byType(EditableText)).controller.selection.baseOffset, 0);
|
||||
|
||||
// An ex-line `:N` goto sets the selection server-side; the buffer content
|
||||
// is unchanged, so only the caret should move (the selection-only branch).
|
||||
f.services.events.emit(
|
||||
DaemonEvent(
|
||||
subsystem: 'editor',
|
||||
kind: 'editor.selection-changed',
|
||||
data: const {
|
||||
'id': 'b_1',
|
||||
'selection': {'start': 5, 'end': 5},
|
||||
},
|
||||
ts: DateTime.now().toUtc(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final field = tester.widget<EditableText>(find.byType(EditableText));
|
||||
expect(field.controller.selection.baseOffset, 5);
|
||||
expect(field.controller.text, 'content of lib/a.dart'); // unchanged
|
||||
});
|
||||
|
||||
testWidgets('tapping an inactive tab routes to editor.activate', (tester) async {
|
||||
stubBuffers([_buf('b_1', 'lib/a.dart'), _buf('b_2', 'src/b.dart')], active: 'b_1');
|
||||
String? activated;
|
||||
|
||||
Reference in New Issue
Block a user