keymap: support bare-modifier double-tap chords; double-Shift → quick-open (T-341)

The chord matcher couldn't represent a bare or double-tapped modifier:
KeyChord.parse required a base key, so `shift shift` failed, and JetBrains
"Search Everywhere" (double-Shift) was unbindable.

Design decision: search-everywhere aliases clide's existing quick-open
finder (not a new overlay) — bound across all four presets per the user.

Changes:
- KeyChord: a bare modifier name (`shift`, `ctrl`, `cmd`, …) parses as a
  modifier-free chord on that modifier's logical key, so parseSequence(
  'shift shift') yields a two-chord double-tap. Adds KeyChord.bareModifier
  and modifierForLogicalKey.
- ModifierTapTracker: headless, clock-injected double-tap detector. A bare
  modifier never forms a single chord; an intervening key breaks the gesture.
- app.dart global handler feeds bare-modifier KeyDowns to the tracker and,
  on a double-tap, resolves the 2-chord sequence via the new
  KeymapService.resolveSequence. The existing single-chord path is untouched
  (zero behavioural risk to normal keys).
- Presets: default/vim/vscode/jetbrains add `shift shift` → quickOpen.open.
  jetbrains header updated (the gesture is now expressible).

Tests: bare-modifier parse/equality/round-trip; tracker window/reset/
different-modifier/consume; each shipped preset resolves double-Shift to
QuickOpenIntent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 13:26:54 +02:00
co-authored by Claude Opus 4.8
parent c1b78d2845
commit 430189d714
15 changed files with 312 additions and 12 deletions
+10
View File
@@ -157,6 +157,16 @@ class KeymapService extends ChangeNotifier {
return km.resolve(chord, _scope);
}
/// Resolve a complete chord [sequence] (e.g. a double-tapped modifier,
/// `[shift, shift]`) against the active keymap and current scope. Returns
/// the bound intent only on an exact full-sequence match, else null.
/// Used by the global handler's double-tap detector (T-341).
Intent? resolveSequence(List<KeyChord> sequence) {
final km = _active;
if (km == null) return null;
return km.match(sequence, _scope).exact;
}
/// Set a named scope flag. Producers should call this when their
/// state changes so when-clauses re-evaluate correctly. Notifies
/// listeners when the value actually changes.