From 83c790cc99fe38160b7b7742da58c4b3ae3f7164 Mon Sep 17 00:00:00 2001 From: Peter Adams <18162810+Maxteabag@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:00:02 +0200 Subject: [PATCH] Honor autocomplete actions rebound to Tab Autocomplete key bindings should keep working when users assign Tab to navigation or closing rather than acceptance. Dispatch the active binding once through Textual's guarded action system before falling back to literal tab insertion. Constraint: QueryTextArea intercepts Tab before normal key routing Rejected: Yield the event to bubbling | Textual handlers can process navigation twice Confidence: high Scope-risk: narrow Directive: Keep custom autocomplete bindings ahead of literal Tab insertion Tested: 15 focused tests; full suite 1716 passed and 646 skipped Not-tested: Three unrelated baseline failures reproduced on origin/main Related: #267 --- sqlit/shared/ui/widgets_text_area.py | 17 +++++++--- tests/ui/test_query_tab_insert.py | 47 ++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/sqlit/shared/ui/widgets_text_area.py b/sqlit/shared/ui/widgets_text_area.py index 13475d5e..71475d85 100644 --- a/sqlit/shared/ui/widgets_text_area.py +++ b/sqlit/shared/ui/widgets_text_area.py @@ -149,16 +149,23 @@ async def _on_key(self, event: Key) -> None: if event.key == "tab": if not self._is_insert_mode(): return - # If the autocomplete dropdown is open, let the app's key router - # accept the suggestion only when Tab is actually bound to the - # autocomplete_accept action. Otherwise Tab should insert a tab. + # If the autocomplete dropdown is open, let the app's action system + # handle Tab when the autocomplete keymap claims it. Otherwise Tab + # should insert a tab character. app = cast("AutocompleteProtocol", self.app) if getattr(app, "_autocomplete_visible", False): from sqlit.core.keymap import get_keymap keymap = get_keymap() - if "tab" in keymap.keys_for_action("autocomplete_accept"): - return + for binding in keymap.get_action_keys(): + if ( + binding.key == event.key + and binding.context == "autocomplete" + and await self.app.run_action(binding.action) + ): + event.prevent_default() + event.stop() + return self._push_undo_if_changed() tab = self._tab_insert_string() selection = self.selection diff --git a/tests/ui/test_query_tab_insert.py b/tests/ui/test_query_tab_insert.py index 146ed3b5..0d1300cf 100644 --- a/tests/ui/test_query_tab_insert.py +++ b/tests/ui/test_query_tab_insert.py @@ -150,3 +150,50 @@ async def test_tab_inserts_when_autocomplete_accept_is_rebound(self) -> None: assert app.query_input.text == "sel\t" finally: reset_keymap() + + @pytest.mark.asyncio + async def test_tab_honors_other_autocomplete_action_rebinding(self) -> None: + app = _make_app() + + try: + async with app.run_test(size=(100, 35)) as pilot: + app.action_focus_query() + await pilot.press("i") + await pilot.pause() + + defaults = DefaultKeymapProvider() + action_keys = [ + binding + for binding in defaults.get_action_keys() + if not ( + binding.key == "tab" + and binding.context == "autocomplete" + ) + ] + action_keys.append( + ActionKeyDef("tab", "autocomplete_next", "autocomplete") + ) + set_keymap( + FileBasedKeymapProvider( + "rebound-autocomplete-next", + defaults.get_leader_commands(), + action_keys, + ) + ) + + app.query_input.text = "s" + app.query_input.cursor_location = (0, 1) + app._show_autocomplete( + ["select", "set", "session", "sequence", "security"], + "s", + ) + await pilot.pause() + assert app.autocomplete_dropdown.selected_index == 0 + + await pilot.press("tab") + await pilot.pause() + + assert app.query_input.text == "s" + assert app.autocomplete_dropdown.selected_index == 1 + finally: + reset_keymap()