Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 3 additions & 35 deletions mycli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,8 @@
from configobj import ConfigObj
import keyring
from prompt_toolkit import print_formatted_text
from prompt_toolkit.application.current import get_app
from prompt_toolkit.completion import Completion
from prompt_toolkit.document import Document
from prompt_toolkit.filters import Condition
from prompt_toolkit.formatted_text import (
ANSI,
HTML,
Expand Down Expand Up @@ -65,6 +63,7 @@
ISSUES_URL,
REPO_URL,
)
from mycli.main_modes import repl as repl_package
from mycli.main_modes.batch import (
main_batch_from_stdin,
main_batch_with_progress_bar,
Expand Down Expand Up @@ -93,39 +92,9 @@
sqlparse.engine.grouping.MAX_GROUPING_DEPTH = None # type: ignore[assignment]
sqlparse.engine.grouping.MAX_GROUPING_TOKENS = None # type: ignore[assignment]

MIN_COMPLETION_TRIGGER = 1
EMPTY_PASSWORD_FLAG_SENTINEL = -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 IntOrStringClickParamType(click.ParamType):
name = 'text' # display as TEXT in helpdoc

Expand Down Expand Up @@ -179,8 +148,6 @@ 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
Expand Down Expand Up @@ -291,7 +258,8 @@ 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
# a hack, pending a better way to handle settings and state
repl_package.MIN_COMPLETION_TRIGGER = self.min_completion_trigger
self.last_prompt_message = ANSI('')
self.last_custom_toolbar_message = ANSI('')

Expand Down
35 changes: 33 additions & 2 deletions mycli/main_modes/repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@

import click
import prompt_toolkit
from prompt_toolkit.application.current import get_app
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, ThreadedAutoSuggest
from prompt_toolkit.completion import DynamicCompleter
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,
)
Expand Down Expand Up @@ -66,6 +67,7 @@


SUPPORT_INFO = f"Home: {HOME_URL}\nBug tracker: {ISSUES_URL}"
MIN_COMPLETION_TRIGGER = 1


def _main_module():
Expand All @@ -80,6 +82,35 @@ class ReplState:
mutating: bool = False


@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))


def _create_history(mycli: 'MyCli') -> FileHistoryWithTimestamp | None:
history_file = os.path.expanduser(os.environ.get('MYCLI_HISTFILE', mycli.config.get('history_file', '~/.mycli-history')))
if dir_path_exists(history_file):
Expand Down Expand Up @@ -307,7 +338,7 @@ def _build_prompt_session(
complete_in_thread=True,
history=history,
auto_suggest=ThreadedAutoSuggest(AutoSuggestFromHistory()),
complete_while_typing=_main_module().complete_while_typing_filter,
complete_while_typing=complete_while_typing_filter,
multiline=cli_is_multiline(mycli),
style=style_factory_ptoolkit(mycli.syntax_style, mycli.cli_style),
include_default_pygments_style=False,
Expand Down
30 changes: 30 additions & 0 deletions test/pytests/test_main_modes_repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,36 @@ def patch_repl_runtime_defaults(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(repl_mode, 'is_mutating', lambda status: False)


def test_complete_while_typing_filter_covers_threshold_and_word_rules(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(repl_mode, 'MIN_COMPLETION_TRIGGER', 3)
monkeypatch.setattr(repl_mode, 'get_app', lambda: SimpleNamespace(current_buffer=SimpleNamespace(text='ab')))
assert repl_mode.complete_while_typing_filter() is False

monkeypatch.setattr(repl_mode, 'get_app', lambda: SimpleNamespace(current_buffer=SimpleNamespace(text='abc')))
assert repl_mode.complete_while_typing_filter() is True

monkeypatch.setattr(repl_mode, 'get_app', lambda: SimpleNamespace(current_buffer=SimpleNamespace(text='source xyz')))
assert repl_mode.complete_while_typing_filter() is True

monkeypatch.setattr(repl_mode, 'get_app', lambda: SimpleNamespace(current_buffer=SimpleNamespace(text='source x/')))
assert repl_mode.complete_while_typing_filter() is False

monkeypatch.setattr(repl_mode, 'get_app', lambda: SimpleNamespace(current_buffer=SimpleNamespace(text='\\. abc')))
assert repl_mode.complete_while_typing_filter() is True

monkeypatch.setattr(repl_mode, 'get_app', lambda: SimpleNamespace(current_buffer=SimpleNamespace(text='\\. a/')))
assert repl_mode.complete_while_typing_filter() is False

monkeypatch.setattr(repl_mode, 'get_app', lambda: SimpleNamespace(current_buffer=SimpleNamespace(text='select abc')))
assert repl_mode.complete_while_typing_filter() is True

monkeypatch.setattr(repl_mode, 'get_app', lambda: SimpleNamespace(current_buffer=SimpleNamespace(text='select a!')))
assert repl_mode.complete_while_typing_filter() is False

monkeypatch.setattr(repl_mode, 'MIN_COMPLETION_TRIGGER', 1)
assert repl_mode.complete_while_typing_filter() is True


def test_repl_main_module_and_create_history(monkeypatch: pytest.MonkeyPatch) -> None:
cli = make_repl_cli()
monkeypatch.setenv('MYCLI_HISTFILE', '~/override-history')
Expand Down
24 changes: 0 additions & 24 deletions test/pytests/test_main_regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,30 +532,6 @@ def __init__(self) -> None:
assert mycli.llm_prompt_section_truncate == 0


def test_complete_while_typing_filter_covers_source_and_sql_word_rules(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(main, 'MIN_COMPLETION_TRIGGER', 3)
monkeypatch.setattr(main, 'get_app', lambda: SimpleNamespace(current_buffer=SimpleNamespace(text='ab')))
assert main.complete_while_typing_filter() is False

monkeypatch.setattr(main, 'get_app', lambda: SimpleNamespace(current_buffer=SimpleNamespace(text='abc')))
assert main.complete_while_typing_filter() is True

monkeypatch.setattr(main, 'get_app', lambda: SimpleNamespace(current_buffer=SimpleNamespace(text='source xyz')))
assert main.complete_while_typing_filter() is True

monkeypatch.setattr(main, 'get_app', lambda: SimpleNamespace(current_buffer=SimpleNamespace(text='source x/')))
assert main.complete_while_typing_filter() is False

monkeypatch.setattr(main, 'get_app', lambda: SimpleNamespace(current_buffer=SimpleNamespace(text='select abc')))
assert main.complete_while_typing_filter() is True

monkeypatch.setattr(main, 'get_app', lambda: SimpleNamespace(current_buffer=SimpleNamespace(text='select a!')))
assert main.complete_while_typing_filter() is False

monkeypatch.setattr(main, 'MIN_COMPLETION_TRIGGER', 1)
assert main.complete_while_typing_filter() is True


def test_int_or_string_click_param_type_accepts_and_rejects_values() -> None:
param_type = main.IntOrStringClickParamType()

Expand Down
Loading