feat(vim): gt / gT cycle workspace tabs (T-405 part 2)

Closes out the T-403 cross-pane vim layer. gt/gT bind to the existing
workspace.tab.next/previous commands (also on ctrl+pagedown/up for every
preset), resolved by the focused editor's matcher or a pane's PaneKeyNav —
bare-g sequences stay editor/pane-local (T-404's global matcher only engages
on modified-chord prefixes), so no global-matcher surgery.

- vim.yaml: g t -> command:workspace.tab.next, g shift+t -> .previous
  (vim.normal); shares the `g` prefix with `g g` (docStart / nav.top),
  distinguished by the final chord.
- PaneKeyNav now EXECUTES non-editor.vim.* command intents (e.g.
  workspace.tab.*) instead of swallowing all command intents, so gt/gT work
  from a focused pane; editor.vim.* buffer edits stay blocked in panes.

Tests: vim-preset resolution (gt/gT bind, gg intact), PaneKeyNav executes
the command from a pane, and editor.vim.* is never run from a pane. make
test green; analyze + format clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 12:08:07 +02:00
co-authored by Claude Opus 4.8
parent 3a59d5be8a
commit 2e4f87455a
7 changed files with 151 additions and 6 deletions
+13 -6
View File
@@ -21,6 +21,8 @@
/// intent here and to the editor motion in the editor — see vim.yaml.
library;
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
@@ -101,15 +103,20 @@ class _PaneKeyNavState extends State<PaneKeyNav> {
final r = _matcher!.feed(chord);
switch (r.outcome) {
case SeqOutcome.fired:
// The vim preset also binds these keys to editor.vim.* motions; in a
// pane only nav.* applies, and editor.vim.* (an InvokeCommandIntent) is
// swallowed — never run buffer edits from a pane. A typed *app* intent
// (e.g. the ex-line `:` / ZZ) bubbles to the app-root Actions for its
// global handler (T-407).
// The vim preset binds these keys to several intent kinds. In a pane:
// - nav.* drives the pane (onNav);
// - editor.vim.* buffer edits are swallowed — never edit from a pane;
// - other commands (workspace.tab.* gt/gT, panel.*) execute (T-405);
// - a typed app intent (ex-line `:` / ZZ) bubbles to the app-root
// Actions for its global handler (T-407).
final fired = r.intent;
if (fired is NavIntent) {
widget.onNav(fired, r.count);
} else if (fired != null && fired is! InvokeCommandIntent) {
} else if (fired is InvokeCommandIntent) {
if (!fired.commandId.startsWith('editor.vim.')) {
unawaited(kernel.commands.execute(fired.commandId));
}
} else if (fired != null) {
Actions.maybeInvoke(context, fired);
}
return KeyEventResult.handled;