Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@fcoury
Copy link
Owner

@fcoury fcoury commented Dec 13, 2025

Summary

  • Transform g into a global goto prefix key with multi-key sequences
  • Add bottom-right hint popup that appears after configurable timeout (like Helix editor)
  • Implement modular key sequence handler for reusable multi-key handling

Key Bindings

Sequence Action
gg Go to first row (grid) / document start (editor)
ge Go to editor
gc Go to connections sidebar (auto-shows if hidden)
gt Go to tables/schema sidebar (auto-shows if hidden)
gr Go to results grid

Features

  • Hint Popup: Press g and wait 500ms to see available options
  • Fast Sequences: Type gg quickly to execute without popup
  • Configurable Timeout: key_sequence_timeout_ms in config (default 500ms)
  • Auto-show Sidebar: gc and gt automatically show sidebar if hidden

Test plan

  • Press g and wait - hint popup should appear in bottom-right
  • Type gg quickly - should go to first row without popup
  • Test ge, gc, gt, gr navigate to correct areas
  • gc and gt should auto-show sidebar if hidden
  • Press g then Esc - should cancel sequence
  • Press g then invalid key - should cancel sequence

Summary by CodeRabbit

  • New Features

    • Multi-key command sequences added (gg, ge, gc, gt, gr) for quick navigation between editor, connections, tables, results, and first-row.
    • Visual key-hint popup shows available follow-up keys after a prefix, with a configurable timeout.
    • Added Ctrl+B to toggle the sidebar.
  • Documentation

    • Help updated with a new "Go To (g prefix)" section documenting the new shortcuts.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 13, 2025

Warning

Rate limit exceeded

@fcoury has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 0 minutes and 25 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 5bb17a4 and b2e0e53.

📒 Files selected for processing (9)
  • crates/tsql/src/app/app.rs (14 hunks)
  • crates/tsql/src/config/keymap.rs (4 hunks)
  • crates/tsql/src/config/schema.rs (2 hunks)
  • crates/tsql/src/ui/grid.rs (2 hunks)
  • crates/tsql/src/ui/help_popup.rs (2 hunks)
  • crates/tsql/src/ui/key_hint_popup.rs (1 hunks)
  • crates/tsql/src/ui/key_sequence.rs (1 hunks)
  • crates/tsql/src/ui/mod.rs (2 hunks)
  • crates/tsql/src/ui/sidebar.rs (1 hunks)

Walkthrough

Adds timed multi-key command handling for Normal mode with a key-sequence handler, UI hint popup, new goto actions, config timeout, and App-level integration to start/complete sequences and render hints. Public APIs exposed for sequence types and popup.

Changes

Cohort / File(s) Summary
Key sequence core & UI
crates/tsql/src/ui/key_sequence.rs, crates/tsql/src/ui/key_hint_popup.rs, crates/tsql/src/ui/mod.rs
New KeySequenceHandler, KeySequenceResult, KeySequenceAction, PendingKey types and tests; new KeyHint + KeyHintPopup widget; ui module exports updated.
App integration
crates/tsql/src/app/app.rs
App gains pub key_sequence: KeySequenceHandler; input handling updated to start/progress/cancel sequences; rendering optionally shows KeyHintPopup; added routing to execute completed sequence actions.
Keymap & config
crates/tsql/src/config/keymap.rs, crates/tsql/src/config/schema.rs
Added Action variants: GotoFirst, GotoEditor, GotoConnections, GotoTables, GotoResults (descriptions and FromStr mappings); added key_sequence_timeout_ms: u64 to KeymapConfig (default 500 ms).
UI updates
crates/tsql/src/ui/grid.rs, crates/tsql/src/ui/help_popup.rs, crates/tsql/src/ui/sidebar.rs
GridKeyResult::GotoFirstRow added; removed direct 'g' handling inside grid; new "Go To (g prefix)" help section with gg/ge/gc/gt/gr; added global Ctrl+B binding; Sidebar::select_first_connection added.
Formatting-only
crates/tsql/src/ui/completion.rs
Whitespace and line-wrapping changes; no behavioral changes.

Sequence Diagram

sequenceDiagram
    participant User
    participant App
    participant KeySequenceHandler
    participant KeyHintPopup
    participant UI

    User->>App: press 'g' (first key)
    App->>KeySequenceHandler: process_first_key('g')
    KeySequenceHandler-->>App: Started(PendingKey::G)
    App->>App: record pending, start timeout

    alt timeout reached
        App->>KeySequenceHandler: should_show_hint?
        KeySequenceHandler-->>App: true
        App->>KeyHintPopup: new(PendingKey::G)
        App->>UI: render KeyHintPopup
        UI-->>User: show hints (gg, ge, gc, gt, gr)
    end

    User->>App: press second key (e.g., 'g' or 'e')
    App->>KeySequenceHandler: process_second_key('g'/'e'/...)
    KeySequenceHandler-->>App: Completed(KeySequenceAction::Goto*)
    App->>App: execute_key_sequence_action(...)
    App->>UI: update focus/navigation
    KeySequenceHandler-->>App: cleared pending state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay attention to timeout/timing tests and Instant usage in crates/tsql/src/ui/key_sequence.rs
  • Verify App integration in crates/tsql/src/app/app.rs covers all new goto actions in relevant UI contexts
  • Ensure removal of in-grid 'g' handling and GridKeyResult::GotoFirstRow signal are consistently handled
  • Validate popup rendering/area calculations in crates/tsql/src/ui/key_hint_popup.rs

Poem

🐰 I nudge a key, then wait a beat,

gg or ge make journeys fleet,
Hints appear when time is thin,
A rabbit's hop — and navigation's in,
Quick sequences, soft and neat.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main feature: adding a global goto key (g) with a hint popup for multi-key sequences.
Docstring Coverage ✅ Passed Docstring coverage is 98.15% which is sufficient. The required threshold is 80.00%.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/tsql/src/app/app.rs (1)

1449-1467: Esc must cancel self.key_sequence too (otherwise sequences can get “stuck”).
Currently Esc clears self.pending_key but not self.key_sequence, so “g then Esc” can leave a pending sequence behind.

@@
         if key.code == KeyCode::Esc && key.modifiers == KeyModifiers::NONE {
             if self.db.running {
                 self.cancel_query();
                 return false;
             }
 
+            // Cancel any pending multi-key sequence (e.g., started with 'g')
+            self.key_sequence.cancel();
+
             self.help_popup = None;
             self.search.close();
             self.command.close();
             self.completion.close();
             self.cell_editor.close();
             self.history_picker = None;
             self.connection_picker = None;
             self.pending_key = None;
             self.last_error = None;
             self.mode = Mode::Normal;
             return false;
         }
🧹 Nitpick comments (7)
crates/tsql/src/config/schema.rs (1)

131-132: LGTM!

The configurable timeout with a sensible 500ms default is well-designed. The u64 type is appropriate for millisecond durations.

Consider adding bounds validation in the future if users report issues with extreme timeout values (e.g., <50ms causing hint flicker or >5000ms feeling unresponsive).

Also applies to: 154-154

crates/tsql/src/config/keymap.rs (2)

101-107: Avoid long-term drift between config::Action::Goto* and ui::KeySequenceAction::* (same conceptual actions).
Right now the PR introduces two parallel “goto action” enums. Consider converging on a single source of truth (e.g., KeySequenceAction -> Action conversion, or reuse Action directly) to prevent future mismatches in behavior/descriptions.

Also applies to: 178-182, 285-290


934-956: Add coverage for the new goto_* FromStr branches.
The test_action_from_str test doesn’t exercise the new variants, so regressions in config parsing could slip in.

@@
     fn test_action_from_str() {
@@
         // Test case insensitivity
         assert_eq!("MOVE_UP".parse::<Action>().unwrap(), Action::MoveUp);
         assert_eq!("Move_Up".parse::<Action>().unwrap(), Action::MoveUp);
+
+        // Goto sequences
+        assert_eq!("goto_first".parse::<Action>().unwrap(), Action::GotoFirst);
+        assert_eq!("goto_editor".parse::<Action>().unwrap(), Action::GotoEditor);
+        assert_eq!(
+            "goto_connections".parse::<Action>().unwrap(),
+            Action::GotoConnections
+        );
+        assert_eq!("goto_tables".parse::<Action>().unwrap(), Action::GotoTables);
+        assert_eq!("goto_results".parse::<Action>().unwrap(), Action::GotoResults);
     }
crates/tsql/src/ui/key_hint_popup.rs (1)

31-38: Keep hint copy aligned with actual actions (and consider reusing Action::description()).
Right now the popup shows abbreviated strings (“tables”, “first row”) while config descriptions are richer (“tables/schema sidebar”, “first row/document start”). Reusing the existing description source (or at least aligning wording) will reduce UI drift.

Also applies to: 114-127

crates/tsql/src/app/app.rs (1)

1361-1368: Either mark hint_shown (or remove it) to keep KeySequenceHandler state coherent.
KeySequenceHandler has hint_shown, but the app never calls mark_hint_shown(), and should_show_hint() doesn’t consult it—so the flag is currently dead state.

@@
                 if self.key_sequence.should_show_hint() {
                     if let Some(pending_key) = self.key_sequence.pending() {
                         let hint_popup = KeyHintPopup::new(pending_key);
                         hint_popup.render(frame, size);
+                        self.key_sequence.mark_hint_shown();
                     }
                 }
crates/tsql/src/ui/key_sequence.rs (2)

88-109: Make hint_shown meaningful (or delete it) by gating should_show_hint().
Right now hint_shown doesn’t affect anything, so it’s confusing state. If you keep it, should_show_hint() should likely return false once hint_shown is true.

@@
     pub fn should_show_hint(&self) -> bool {
-        if let (Some(_), Some(since)) = (self.pending, self.pending_since) {
-            since.elapsed() >= Duration::from_millis(self.timeout_ms)
+        if self.hint_shown {
+            return false;
+        }
+        if let (Some(_), Some(since)) = (self.pending, self.pending_since) {
+            since.elapsed() >= Duration::from_millis(self.timeout_ms)
         } else {
             false
         }
     }

Also applies to: 101-109


173-178: Fix is_waiting() doc (or implement timeout-based auto-cancel) + reduce sleep-test flakiness.

  • The is_waiting() comment says it considers timeout, but it doesn’t.
  • test_should_show_hint_after_timeout using sleep(20ms) can be flaky; bump the margin.
@@
-    /// Check if there's a pending key and timeout hasn't been reached.
-    /// Useful for deciding whether to wait for more input.
+    /// Returns true if there is a pending key sequence.
     pub fn is_waiting(&self) -> bool {
         self.pending.is_some()
     }
@@
     fn test_should_show_hint_after_timeout() {
         let mut handler = KeySequenceHandler::new(10); // 10ms timeout for quick test
@@
-        sleep(Duration::from_millis(20));
+        sleep(Duration::from_millis(50));
         assert!(handler.should_show_hint());
     }

Also applies to: 286-295

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0b81bef and 5f04d97.

📒 Files selected for processing (10)
  • crates/tsql/src/app/app.rs (14 hunks)
  • crates/tsql/src/config/keymap.rs (3 hunks)
  • crates/tsql/src/config/schema.rs (2 hunks)
  • crates/tsql/src/ui/completion.rs (2 hunks)
  • crates/tsql/src/ui/grid.rs (2 hunks)
  • crates/tsql/src/ui/help_popup.rs (2 hunks)
  • crates/tsql/src/ui/key_hint_popup.rs (1 hunks)
  • crates/tsql/src/ui/key_sequence.rs (1 hunks)
  • crates/tsql/src/ui/mod.rs (2 hunks)
  • crates/tsql/src/ui/sidebar.rs (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
crates/tsql/src/ui/key_sequence.rs (2)
crates/tsql/src/config/schema.rs (4)
  • default (45-57)
  • default (83-94)
  • default (114-122)
  • default (151-161)
crates/tsql/src/ui/key_hint_popup.rs (2)
  • new (26-28)
  • new (48-50)
crates/tsql/src/ui/key_hint_popup.rs (1)
crates/tsql/src/ui/key_sequence.rs (2)
  • new (74-81)
  • default (67-69)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test (windows-latest)
🔇 Additional comments (8)
crates/tsql/src/ui/completion.rs (1)

221-221: LGTM!

Formatting-only changes that collapse method chains and constructor calls onto single lines. No behavioral impact.

Also applies to: 249-250

crates/tsql/src/ui/help_popup.rs (2)

76-88: LGTM!

New help section for goto key sequences is well-documented and aligns with the PR objectives. The bindings (gg, ge, gc, gt, gr) accurately describe the implemented functionality.


216-229: LGTM!

GOTO section is appropriately positioned in ALL_SECTIONS after GLOBAL, making it discoverable for users learning the new navigation shortcuts.

crates/tsql/src/ui/mod.rs (1)

11-12: LGTM!

New modules and re-exports follow the existing pattern and properly expose the key sequence infrastructure for use by the app layer.

Also applies to: 33-34

crates/tsql/src/ui/sidebar.rs (2)

241-245: LGTM with minor observation.

The method is clean and follows the existing pattern from connections_up/connections_down. Note that if the connections list is empty, this will set selected_connection = Some(0) which is technically invalid, but get_selected_connection will safely return None in that case. This matches the existing behavior in other methods.


69-70: LGTM!

Formatting adjustments only - no behavioral changes.

Also applies to: 248-251

crates/tsql/src/ui/grid.rs (2)

48-49: LGTM!

The new GotoFirstRow variant correctly signals that this action originates from the gg key sequence and should be handled at the app level, aligning with the new multi-key sequence architecture.


218-220: LGTM!

Correctly removed the 'g' key from the direct first-row navigation. The 'g' key is now a sequence prefix handled at the app level, while Home key remains as a direct shortcut for keyboard navigation.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/tsql/src/app/app.rs (1)

1800-1815: Edge case: connections_down(count) with count == 0.
You guard the “move to schema” path with count > 0, but you still call connections_down(0) in the else. If connections_down assumes count > 0, this could misbehave.

♻️ Duplicate comments (1)
crates/tsql/src/app/app.rs (1)

1582-1630: Clearing legacy pending_key during sequences
Already addressed in this diff and previously called out.

🧹 Nitpick comments (7)
crates/tsql/src/ui/key_sequence.rs (3)

116-136: Deduplicate state reset logic (cancel() vs complete()).
cancel() and complete() do the same thing; it’s easy for them to drift later.

 pub fn cancel(&mut self) {
-    self.pending = None;
-    self.pending_since = None;
-    self.hint_shown = false;
+    self.complete();
 }

137-147: Guard against starting a “first key” while already waiting (API footgun).
Right now process_first_key() will reset the timer/state even if a sequence is already pending; App currently guards this, but the handler API itself doesn’t. Consider making it a no-op (or cancel+restart) when self.pending.is_some().


291-300: Timeout test may be flaky under load.
The sleep(50ms) vs timeout=10ms is probably fine, but CI jitter can still bite. Consider increasing the margin (e.g., timeout 50ms + sleep 200ms) to reduce flakes.

crates/tsql/src/app/app.rs (4)

1361-1378: Compute hint visibility once per tick to avoid “marked shown but not rendered” edge cases.
Because should_show_hint() is time-based and called multiple times per loop, it can flip between calls. Recommend computing once before draw() and reusing the value in both places.

-            terminal.draw(|frame| {
+            let show_key_hint = self.key_sequence.should_show_hint();
+            let pending_key = self.key_sequence.pending();
+
+            terminal.draw(|frame| {
                 let size = frame.area();
                 ...
-                if self.key_sequence.should_show_hint() {
-                    if let Some(pending_key) = self.key_sequence.pending() {
+                if show_key_hint {
+                    if let Some(pending_key) = pending_key {
                         let hint_popup = KeyHintPopup::new(pending_key);
                         hint_popup.render(frame, size);
                     }
                 }
                 ...
             })?;

1375-1378: Tie mark_hint_shown() to the same computed “rendered” condition.
This pairs with the change above.

-            if self.key_sequence.should_show_hint() && !self.key_sequence.is_hint_shown() {
+            if show_key_hint && !self.key_sequence.is_hint_shown() {
                 self.key_sequence.mark_hint_shown();
             }

1582-1630: Clarify/lock in intended behavior for “invalid second key” (fallthrough vs consume).
Currently KeySequenceResult::Cancelled cancels and then lets the second key fall through to normal handling. That can be great (users don’t “lose” the key), but it also means g becomes a hard prefix that always eats the first key even if the user mistypes. Worth confirming this UX is intentional.

(Also: the else if key.code == KeyCode::Esc branch inside this block appears redundant because Esc is handled earlier in on_key().)


1732-1734: Dead check in grid ToggleSidebar branch (focus can’t be Sidebar here).
This branch is under Focus::Grid, so matches!(self.focus, Focus::Sidebar(_)) is always false.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5f04d97 and 5bb17a4.

📒 Files selected for processing (4)
  • crates/tsql/src/app/app.rs (15 hunks)
  • crates/tsql/src/config/keymap.rs (4 hunks)
  • crates/tsql/src/ui/key_hint_popup.rs (1 hunks)
  • crates/tsql/src/ui/key_sequence.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/tsql/src/ui/key_hint_popup.rs
🧰 Additional context used
🧬 Code graph analysis (2)
crates/tsql/src/app/app.rs (3)
crates/tsql/src/config/keymap.rs (2)
  • new (305-307)
  • new (428-432)
crates/tsql/src/ui/key_hint_popup.rs (2)
  • new (27-29)
  • new (49-51)
crates/tsql/src/ui/key_sequence.rs (1)
  • default (67-69)
crates/tsql/src/ui/key_sequence.rs (2)
crates/tsql/src/config/schema.rs (4)
  • default (45-57)
  • default (83-94)
  • default (114-122)
  • default (151-161)
crates/tsql/src/ui/key_hint_popup.rs (2)
  • new (27-29)
  • new (49-51)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test (windows-latest)
🔇 Additional comments (7)
crates/tsql/src/ui/key_sequence.rs (1)

8-52: Nice, minimal public surface for sequences (PendingKey, KeySequenceAction, KeySequenceResult).
This reads cleanly and should be easy to extend beyond g*.

crates/tsql/src/app/app.rs (4)

561-563: App-level integration looks straightforward (configurable timeout + handler stored on App).

Also applies to: 666-688


1461-1463: Cancelling pending sequences on Esc is a good, predictable escape hatch.


3736-3776: execute_key_sequence_action() routing is clean and keeps the key-sequence handler reusable.
Nice touch auto-showing the sidebar for gc/gt.


1773-1777: Fallback GridKeyResult::GotoFirstRow handling is fine as a safety net.

crates/tsql/src/config/keymap.rs (2)

101-108: New action variants are well-named and consistent with snake_case config.


956-969: Tests cover parsing for the new goto_* strings.

Transform 'g' into a global goto prefix key with multi-key sequences:
- gg: Go to first row (grid) / document start (editor)
- ge: Go to editor
- gc: Go to connections sidebar (auto-shows if hidden)
- gt: Go to tables/schema sidebar (auto-shows if hidden)
- gr: Go to results grid

Features:
- Configurable timeout (default 500ms) before showing hint popup
- Bottom-right hint popup displays available options (like Helix editor)
- Modular key_sequence.rs module for reusable multi-key handling
- Works in Normal mode across all focus areas

Configuration:
- key_sequence_timeout_ms in [keymap] section (default: 500)
- Mark hint_shown after rendering popup to track state coherently
- Cancel key_sequence on Esc to prevent stale pending state
- Clear pending_key when starting/processing g sequences to prevent
  operator-pending state from leaking across key sequences
- Use UnicodeWidthStr for proper character width calculation in popup
- Add dimension clamping to prevent popup overflow on small terminals
- Fix should_show_hint() to keep showing hint until sequence ends
- Fix is_waiting() doc comment to match implementation
- Add test coverage for goto_* FromStr parsing
- Increase test sleep margin from 20ms to 50ms to reduce flakiness
The Action::GotoFirst, GotoEditor, GotoConnections, GotoTables, and
GotoResults variants were defined and parseable but had no execution
handlers, causing custom keybindings using these actions to silently
do nothing.

Added handlers in:
- Grid keymap handling (lines 1738-1764): handles goto actions when
  grid is focused in normal mode
- handle_editor_action() (lines 3069-3091): handles goto actions when
  query editor is focused in normal mode
- Insert mode keymap handling (lines 3573-3598): handles goto actions
  when editor is in insert mode

Each handler performs context-appropriate navigation:
- GotoFirst: In grid goes to first row, in editor goes to document start
- GotoEditor: Sets focus to query editor
- GotoConnections: Shows sidebar and focuses connections section
- GotoTables: Shows sidebar and focuses schema/tables section
- GotoResults: Sets focus to results grid

Added test_goto_action_bindings_in_grid_keymap to verify that custom
keybindings for goto_* actions are correctly registered and can be
used via configurable keymaps.
Key sequence improvements (key_sequence.rs):
- Deduplicate cancel() and complete() into shared clear_state() method
- Guard process_first_key() against already-waiting state (cancel+restart)
- Increase test timeout margin (50ms timeout + 200ms sleep) to reduce flakiness

Render loop improvements (app.rs):
- Compute hint visibility once per tick to avoid time-based state flipping
- Use pre-computed show_key_hint and pending_key_for_hint in draw closure
- Remove redundant Esc check (already handled by global Esc handler)
- Add UX comment explaining Cancelled fallthrough behavior
- Remove dead Focus::Sidebar check in Focus::Grid ToggleSidebar branch
@fcoury fcoury force-pushed the fcoury/global-goto-key branch from f525ec4 to b2e0e53 Compare December 14, 2025 00:17
@fcoury fcoury merged commit e71b4bb into master Dec 14, 2025
5 of 6 checks passed
@fcoury fcoury deleted the fcoury/global-goto-key branch December 14, 2025 00:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants