From 735fbb8ecdd3096acd4a3c9fccb282d063b6463b Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Sat, 14 Feb 2026 06:14:33 -0500 Subject: [PATCH] optionally defer completions until N characters have been typed. A strong effort is taken for efficiency on reading the trailing characters, since this prompt_toolkit Filter will run on every keystroke. Though the cost of running the Pygments lexer on every keystroke surely dwarfs this. If fewer than N characters have been typed, the suggestions can still be summoned by control-space (does not advance into the candidates) or tab (does immediately advance into the candidates). The motivation is to both reduce distractions and to reduce lag when typing. There is another way to do this by passing thresholds into `mycli/sqlcompleter.py`, but it doesn't preserve the ability to summon completions when below the trigger threshold. --- changelog.md | 1 + mycli/main.py | 40 ++++++++++++++++++++++++++++++++++++++-- mycli/myclirc | 4 ++++ test/myclirc | 4 ++++ 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 941f1610..1111072a 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ Features * Add many CLI flags to startup tips. * Accept all special commands without trailing semicolons in multi-line mode. * Add prompt format strings for socket connections. +* Optionally defer auto-completions until a minimum number of characters is typed. Bug Fixes diff --git a/mycli/main.py b/mycli/main.py index 7bd1b38e..92173d04 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -31,11 +31,12 @@ import click from configobj import ConfigObj import keyring +from prompt_toolkit.application.current import get_app from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.completion import Completion, DynamicCompleter from prompt_toolkit.document import Document from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode -from prompt_toolkit.filters import HasFocus, IsDone +from prompt_toolkit.filters import Condition, HasFocus, IsDone from prompt_toolkit.formatted_text import ANSI, AnyFormattedText from prompt_toolkit.key_binding.bindings.named_commands import register as prompt_register from prompt_toolkit.key_binding.key_processor import KeyPressEvent @@ -84,6 +85,36 @@ SUPPORT_INFO = "Home: http://mycli.net\nBug tracker: https://github.com/dbcli/mycli/issues" DEFAULT_WIDTH = 80 DEFAULT_HEIGHT = 25 +MIN_COMPLETION_TRIGGER = 1 + + +@Condition +def complete_while_typing_filter() -> bool: + """Whether enough characters have been typed to trigger completion. + + Written in a verbose way, with a string slice, for efficiency.""" + if MIN_COMPLETION_TRIGGER <= 1: + return True + app = get_app() + text = app.current_buffer.text.lstrip() + text_len = len(text) + if text_len < MIN_COMPLETION_TRIGGER: + return False + last_word = text[-MIN_COMPLETION_TRIGGER:] + if len(last_word) == text_len: + return text_len >= MIN_COMPLETION_TRIGGER + if text[:6].lower() in ['source', r'\.']: + # Different word characters for paths; see comment below. + # In fact, it might be nice if paths had a different threshold. + return not bool(re.search(r'[\s!-,:-@\[-^\{\}-]', last_word)) + else: + # This is "whitespace and all punctuation except underscore and backtick" + # acting as word breaks, but it would be neat if we could complete differently + # when inside a backtick, accepting all legal characters towards the trigger + # limit. We would have to parse the statement, or at least go back more + # characters, costing performance. This still works within a backtick! So + # long as there are three trailing non-punctuation characters. + return not bool(re.search(r'[\s!-/:-@\[-^\{-~]', last_word)) class MyCli: @@ -122,6 +153,8 @@ def __init__( warn: bool | None = None, myclirc: str = "~/.myclirc", ) -> None: + global MIN_COMPLETION_TRIGGER + self.sqlexecute = sqlexecute self.logfile = logfile self.defaults_suffix = defaults_suffix @@ -222,6 +255,9 @@ def __init__( ) self._completer_lock = threading.Lock() + self.min_completion_trigger = c["main"].as_int("min_completion_trigger") + MIN_COMPLETION_TRIGGER = self.min_completion_trigger + # Register custom special commands. self.register_special_commands() @@ -1147,7 +1183,7 @@ def one_iteration(text: str | None = None) -> None: completer=DynamicCompleter(lambda: self.completer), history=history, auto_suggest=AutoSuggestFromHistory(), - complete_while_typing=True, + complete_while_typing=complete_while_typing_filter, multiline=cli_is_multiline(self), style=style_factory(self.syntax_style, self.cli_style), include_default_pygments_style=False, diff --git a/mycli/myclirc b/mycli/myclirc index 52912e5d..ab021eca 100644 --- a/mycli/myclirc +++ b/mycli/myclirc @@ -9,6 +9,10 @@ show_warnings = False # possible completions will be listed. smart_completion = True +# Minimum characters typed before offering completion suggestions. +# Suggestion: 3. +min_completion_trigger = 1 + # Multi-line mode allows breaking up the sql statements into multiple lines. If # this is set to True, then the end of the statements must have a semi-colon. # If this is set to False then sql statements can't be split into multiple diff --git a/test/myclirc b/test/myclirc index a66e6406..f3e3bbd2 100644 --- a/test/myclirc +++ b/test/myclirc @@ -9,6 +9,10 @@ show_warnings = False # possible completions will be listed. smart_completion = True +# Minimum characters typed before offering completion suggestions. +# Suggestion: 3. +min_completion_trigger = 1 + # Multi-line mode allows breaking up the sql statements into multiple lines. If # this is set to True, then the end of the statements must have a semi-colon. # If this is set to False then sql statements can't be split into multiple