vim normal-mode navigation in non-editor panes (T-406)
The structural T-403 child: make vim normal mode mean navigation in panes that were mouse-only. The passive global key path can't run multi-chord sequences (D-82), so each pane hosts its own SequenceMatcher — factored into a reusable PaneKeyNav that resolves the live keymap and dispatches nav.* intents while a pane holds focus under the vim preset. - nav.* intents (down/up/pageDown/pageUp/top/bottom/expandOrRight/ collapseOrLeft/activate) — preset-neutral; vim.yaml binds j/k/ctrl+d/ctrl+u/ gg/G/l/h/[o,enter] under `vim.normal && !editor.focused`. - The editor publishes an `editor.focused` scope flag from its focus node, so the same keys stay buffer motions while the editor is focused and become nav when a pane is — resolved by file order + the guard (no change to the editor motion bindings). - File tree: a flattened visible-index selection cursor in FileTreeController (j/k move, h collapse-or-out, l expand-or-into, o/enter open), with a focus ring + scroll-into-view. - Conversation: j/k line-scroll, ctrl+d/u half-page, gg top, G bottom — G re-arms follow-tail. Foundation for T-404/T-405/T-407, which build on the per-pane matcher and the editor.focused guard. Git panel + ticket board list nav deferred to a follow-up (the ticket says lists can trail). Tests: keymap resolution under both scopes, PaneKeyNav dispatch, the controller selection model, and end-to-end key-driven nav in both panes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,8 @@ import 'package:clide/builtin/claude/src/prompt_card.dart';
|
||||
import 'package:clide/builtin/claude/src/transcript_reader.dart';
|
||||
import 'package:clide/builtin/claude/src/workflow_run.dart';
|
||||
import 'package:clide/kernel/src/facade.dart';
|
||||
import 'package:clide/kernel/src/keymap/intents.dart';
|
||||
import 'package:clide/kernel/src/keymap/pane_key_nav.dart';
|
||||
import 'package:clide/kernel/src/syntax/language_map.dart';
|
||||
import 'package:clide/kernel/src/theme/controller.dart';
|
||||
import 'package:clide/kernel/src/theme/tokens.dart';
|
||||
@@ -387,10 +389,48 @@ class _ConversationViewState extends State<ConversationView> {
|
||||
return list;
|
||||
},
|
||||
);
|
||||
return ColoredBox(
|
||||
final body = ColoredBox(
|
||||
color: tokens.panelBackground,
|
||||
child: widget.wrapInSelectionArea ? ClideSelectionArea(child: sized) : sized,
|
||||
);
|
||||
// Vim nav scrolls the conversation while this region holds focus under the
|
||||
// vim preset (T-406): j/k by a line, ctrl+d/u by half a viewport, gg/G to
|
||||
// the ends — G also re-arms follow-tail so new output keeps it pinned.
|
||||
return PaneKeyNav(onNav: _onNav, child: body);
|
||||
}
|
||||
|
||||
/// One "line" of scroll for j/k — a few text rows' worth.
|
||||
static const double _lineScroll = 48;
|
||||
|
||||
void _onNav(NavIntent intent, int count) {
|
||||
if (!_scroll.hasClients) return;
|
||||
final p = _scroll.position;
|
||||
final half = p.viewportDimension / 2;
|
||||
switch (intent) {
|
||||
case NavDownIntent():
|
||||
_scrollBy(_lineScroll * count);
|
||||
case NavUpIntent():
|
||||
_scrollBy(-_lineScroll * count);
|
||||
case NavPageDownIntent():
|
||||
_scrollBy(half);
|
||||
case NavPageUpIntent():
|
||||
_scrollBy(-half);
|
||||
case NavTopIntent():
|
||||
_scroll.jumpTo(0);
|
||||
_atBottom = false;
|
||||
case NavBottomIntent():
|
||||
_scroll.jumpTo(p.maxScrollExtent);
|
||||
_atBottom = true; // re-arm follow-tail (T-297)
|
||||
case NavExpandOrRightIntent() || NavCollapseOrLeftIntent() || NavActivateIntent():
|
||||
break; // a reader pane has no expand/activate semantics
|
||||
}
|
||||
}
|
||||
|
||||
void _scrollBy(double delta) {
|
||||
final p = _scroll.position;
|
||||
final target = (p.pixels + delta).clamp(0.0, p.maxScrollExtent);
|
||||
_scroll.jumpTo(target);
|
||||
_atBottom = (p.maxScrollExtent - target) <= _bottomEpsilon;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user