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
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Features
---------
* Show purpose in tabular `--checkup` output.
* Add optional dependencies to `--checkup`.
* Improve completions for `/command`s.


Documentation
Expand Down
2 changes: 1 addition & 1 deletion mycli/client_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def register_special_commands(self) -> None:
"rehash",
"/rehash",
"Refresh auto-completions.",
arg_type=ArgType.NO_QUERY,
arg_type=ArgType.NO_ARGUMENT,
aliases=[SpecialCommandAlias("\\#", case_sensitive=False)],
)
special.register_special_command(
Expand Down
30 changes: 25 additions & 5 deletions mycli/packages/completion_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ def _keyword_suggestions() -> list[Suggestion]:
return [{'type': 'keyword'}]


def _special_suggestions() -> list[Suggestion]:
return [{'type': 'special'}]


def _keyword_and_special_suggestions() -> list[Suggestion]:
return [{'type': 'keyword'}, {'type': 'special'}]

Expand Down Expand Up @@ -144,8 +148,11 @@ def _emit_none_token(_ctx: SuggestContext) -> list[Suggestion]:
return _keyword_suggestions()


def _emit_blank_token(_ctx: SuggestContext) -> list[Suggestion]:
return _keyword_and_special_suggestions()
def _emit_blank_token(ctx: SuggestContext) -> list[Suggestion]:
if ctx.text_before_cursor.startswith('/'):
return _special_suggestions()
else:
return _keyword_and_special_suggestions()


def _emit_star(_ctx: SuggestContext) -> list[Suggestion]:
Expand Down Expand Up @@ -793,7 +800,8 @@ def suggest_special(text: str) -> list[dict[str, Any]]:
{"type": "view", "schema": []},
{"type": "schema"},
]
elif cmd.lower() in [

if cmd.lower() in [
r'\.',
r'/.',
'source',
Expand All @@ -806,14 +814,16 @@ def suggest_special(text: str) -> list[dict[str, Any]]:
'/tee',
]:
return [{"type": "file_name"}]

# todo: why is \edit case-sensitive?
elif cmd in [
if cmd in [
r'\e',
'/e',
r'\edit',
'/edit',
]:
return [{"type": "file_name"}]

if cmd in ["\\llm", "/llm", "\\ai", "/ai"]:
return [{"type": "llm"}]

Expand All @@ -828,7 +838,17 @@ def suggest_special(text: str) -> list[dict[str, Any]]:
return []
return [{'type': 'special_subcommand', 'subcommands': list(DSN_SUBCOMMANDS)}]

return [{"type": "keyword"}, {"type": "special"}]
if cmd.lower() in [
'help',
'/help',
r'\help',
'?',
'/?',
r'\?',
]:
return [{"type": "keyword"}, {"type": "special"}]

return []


def suggest_based_on_last_token(
Expand Down
8 changes: 4 additions & 4 deletions mycli/packages/special/iocommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def is_show_warnings_enabled() -> bool:
'warnings',
'/warnings',
'Enable automatic warnings display.',
arg_type=ArgType.NO_QUERY,
arg_type=ArgType.NO_ARGUMENT,
case_sensitive=True,
aliases=[SpecialCommandAlias('\\W', case_sensitive=True)],
)
Expand All @@ -113,7 +113,7 @@ def enable_show_warnings() -> Generator[SQLResult, None, None]:
'nowarnings',
'/nowarnings',
'Disable automatic warnings display.',
arg_type=ArgType.NO_QUERY,
arg_type=ArgType.NO_ARGUMENT,
case_sensitive=True,
aliases=[SpecialCommandAlias('\\w', case_sensitive=True)],
)
Expand Down Expand Up @@ -152,7 +152,7 @@ def set_pager(arg: str, **_) -> list[SQLResult]:
"nopager",
"/nopager",
"Disable pager; print to stdout.",
arg_type=ArgType.NO_QUERY,
arg_type=ArgType.NO_ARGUMENT,
case_sensitive=True,
aliases=[SpecialCommandAlias("\\n", case_sensitive=True)],
)
Expand All @@ -165,7 +165,7 @@ def disable_pager() -> list[SQLResult]:
"\\timing",
"/timing",
"Toggle timing of queries.",
arg_type=ArgType.NO_QUERY,
arg_type=ArgType.NO_ARGUMENT,
case_sensitive=True,
aliases=[SpecialCommandAlias("\\t", case_sensitive=True)],
)
Expand Down
22 changes: 11 additions & 11 deletions mycli/packages/special/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@


class ArgType(Enum):
NO_QUERY = 0
NO_ARGUMENT = 0
PARSED_QUERY = 1
RAW_QUERY = 2

Expand Down Expand Up @@ -205,7 +205,7 @@ def execute(cur: Cursor, sql: str) -> list[SQLResult]:
if command.lower().startswith(("help", "/help", "\\?", "/?", "?")) and arg:
return show_keyword_help(cur=cur, arg=arg)

if special_cmd.arg_type == ArgType.NO_QUERY:
if special_cmd.arg_type == ArgType.NO_ARGUMENT:
return special_cmd.handler()
elif special_cmd.arg_type == ArgType.PARSED_QUERY:
return special_cmd.handler(cur=cur, arg=arg, command_verbosity=(command_verbosity == CommandVerbosity.VERBOSE))
Expand All @@ -219,7 +219,7 @@ def execute(cur: Cursor, sql: str) -> list[SQLResult]:
"help",
"/help [term]",
"Show this table, or search for help on a term.",
arg_type=ArgType.NO_QUERY,
arg_type=ArgType.NO_ARGUMENT,
aliases=[SpecialCommandAlias("\\?", case_sensitive=False), SpecialCommandAlias("?", case_sensitive=False)],
)
def show_help(*_args) -> list[SQLResult]:
Expand Down Expand Up @@ -283,7 +283,7 @@ def show_keyword_help(cur: Cursor, arg: str) -> list[SQLResult]:
return _show_mysql_help(cur, keyword)


@special_command('\\bug', '/bug', 'File a bug on GitHub.', arg_type=ArgType.NO_QUERY)
@special_command('\\bug', '/bug', 'File a bug on GitHub.', arg_type=ArgType.NO_ARGUMENT)
def file_bug(*_args) -> list[SQLResult]:
webbrowser.open_new_tab(ISSUES_URL)
return [SQLResult(status=f'{ISSUES_URL} — press "New Issue"')]
Expand All @@ -293,14 +293,14 @@ def file_bug(*_args) -> list[SQLResult]:
"exit",
"/exit",
"Exit.",
arg_type=ArgType.NO_QUERY,
arg_type=ArgType.NO_ARGUMENT,
aliases=[SpecialCommandAlias("\\q", case_sensitive=False)],
)
@special_command(
"quit",
"/quit",
"Quit.",
arg_type=ArgType.NO_QUERY,
arg_type=ArgType.NO_ARGUMENT,
aliases=[SpecialCommandAlias("\\q", case_sensitive=False)],
)
def quit_(*_args):
Expand All @@ -311,38 +311,38 @@ def quit_(*_args):
"\\edit",
"/edit <filename> | <query>\\edit",
"Edit query with editor (uses $VISUAL or $EDITOR).",
arg_type=ArgType.NO_QUERY,
arg_type=ArgType.NO_ARGUMENT,
case_sensitive=True,
aliases=[SpecialCommandAlias("\\e", case_sensitive=True)],
)
@special_command(
"\\clip",
"/clip | <query>\\clip",
"Copy query to the system clipboard.",
arg_type=ArgType.NO_QUERY,
arg_type=ArgType.NO_ARGUMENT,
case_sensitive=True,
)
@special_command(
"\\G",
"<query>\\G",
"Display query results vertically.",
arg_type=ArgType.NO_QUERY,
arg_type=ArgType.NO_ARGUMENT,
case_sensitive=True,
backslash_only=True,
)
@special_command(
"\\g",
"<query>\\g",
"Display query results (mnemonic: go).",
arg_type=ArgType.NO_QUERY,
arg_type=ArgType.NO_ARGUMENT,
case_sensitive=True,
backslash_only=True,
)
@special_command(
"\\x",
"<query>\\x",
"Display query results in an explorer rather than a pager.",
arg_type=ArgType.NO_QUERY,
arg_type=ArgType.NO_ARGUMENT,
case_sensitive=True,
backslash_only=True,
)
Expand Down
37 changes: 33 additions & 4 deletions test/pytests/test_completion_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import pytest
import sqlparse
from sqlparse import tokens
from sqlparse.sql import Statement, Token

from mycli.packages import completion_engine, special
from mycli.packages.completion_engine import (
Expand Down Expand Up @@ -428,9 +430,16 @@ def test_emit_none_token():
assert _emit_none_token(context) == [{'type': 'keyword'}]


def test_emit_blank_token():
context = _build_suggest_context('', '', None, '', empty_identifier())
assert _emit_blank_token(context) == [{'type': 'keyword'}, {'type': 'special'}]
@pytest.mark.parametrize(
('text_before_cursor', 'expected'),
[
('', [{'type': 'keyword'}, {'type': 'special'}]),
('/', [{'type': 'special'}]),
],
)
def test_emit_blank_token(text_before_cursor, expected):
context = _build_suggest_context('', text_before_cursor, None, '', empty_identifier())
assert _emit_blank_token(context) == expected


def test_emit_star():
Expand Down Expand Up @@ -816,6 +825,25 @@ def fake_parse(text: str):
assert suggestions == [{'type': 'special'}]


def test_suggest_type_handles_whitespace_only_statement(monkeypatch):
whitespace_statement = Statement([Token(tokens.Text.Whitespace, ' ')])
monkeypatch.setattr(completion_engine.sqlparse, 'parse', lambda _text: [whitespace_statement])
monkeypatch.setattr(completion_engine, 'suggest_based_on_last_token', lambda *_args: [{'type': 'fallback'}])

assert suggest_type(' ', ' ') == [{'type': 'fallback'}]


def test_suggest_type_handles_parser_results_shorter_than_cursor(monkeypatch):
statements = [
Statement([Token(tokens.Keyword, 'SELECT')]),
Statement([Token(tokens.Keyword, 'SELECT')]),
]
monkeypatch.setattr(completion_engine.sqlparse, 'parse', lambda _text: statements)
monkeypatch.setattr(completion_engine, 'suggest_based_on_last_token', lambda *_args: [{'type': 'fallback'}])

assert suggest_type('long cursor text', 'long cursor text') == [{'type': 'fallback'}]


@pytest.mark.parametrize(
('text', 'expected'),
[
Expand Down Expand Up @@ -848,7 +876,8 @@ def fake_parse(text: str):
('/dsn delete prod ', []),
('/dsn show', []),
('/dsn help ', []),
('pager ', [{'type': 'keyword'}, {'type': 'special'}]),
('/help ', [{'type': 'keyword'}, {'type': 'special'}]),
('/pager ', []),
],
)
def test_suggest_special(text, expected):
Expand Down
6 changes: 3 additions & 3 deletions test/pytests/test_special_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def test_execute_raises_when_case_sensitive_exact_lookup_falls_back_to_lowercase
'Camel',
'Camel',
'Description',
arg_type=special_main.ArgType.NO_QUERY,
arg_type=special_main.ArgType.NO_ARGUMENT,
hidden=False,
case_sensitive=True,
aliases=None,
Expand All @@ -204,7 +204,7 @@ def handler() -> list[SQLResult]:
'demo',
'demo',
'Description',
arg_type=special_main.ArgType.NO_QUERY,
arg_type=special_main.ArgType.NO_ARGUMENT,
)

assert special_main.execute(cast(Any, None), 'demo') == [SQLResult(status='ok')]
Expand All @@ -224,7 +224,7 @@ def handler() -> list[SQLResult]:
'demo',
'demo',
'Description',
arg_type=special_main.ArgType.NO_QUERY,
arg_type=special_main.ArgType.NO_ARGUMENT,
)

assert special_main.execute(cast(Any, None), 'DEMO') == [SQLResult(status='ok')]
Expand Down
Loading