Skip to content

Commit 2d6f20f

Browse files
committed
improve completion suggestions inside backticks
Previously the behavior was a little janky: on the first character after a backtick, only identifiers which might _require_ a backtick were offered as suggestions (because, for instance, they matched reserved words). Sometimes, that list would be empty, and no suggestions were offered. Then, after typing a few more characters, rapidfuzz matching kicked in, and more suggestions were offered, with backticks off for the additional rapidfuzz suggestions. Now the behavior is more consistent: if a backtick is typed, _all_ suggestions which could work in that place are offered, with uniform backticks on the suggestions, even if backticks are not required for the given identifier. Of course, how early the suggestions are offered is still dependent on the min_completion_trigger option in ~/.myclirc, so the above paragraph is conditional. Changes * tuck optimization check inside _find_doubled_backticks(), and make the optimization test for double instead of single backticks * remove function unescape_name(), which is unused, and seems wrongly named since it oriented towards strings rather than identifiers * let find_matches() add backticks to all suggestions if backtick quoting is detected at the cursor * recast a variable name in _find_doubled_backticks() Per some comments in the tests, it would be nicer if column names sorted more strongly to the top in the SELECT context, but that is not new to these changes. Another idea could be sorting to the top only those suggestions which require a backtick, when in the backtick context -- something for the future. We also seem to be needlessly suggesting some generic keywords in the SELECT context, but that is also not new to these changes. And if they are going to appear, it seems to make more sense to have them in backticks like the other suggestions, for the sake of uniformity.
1 parent 7a3eaf3 commit 2d6f20f

4 files changed

Lines changed: 337 additions & 34 deletions

File tree

changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Upcoming (TBD)
44
Features
55
---------
66
* `--checkup` now checks for external executables.
7+
* Improve completion suggestions within backticks.
78

89

910
Bug Fixes
@@ -12,6 +13,7 @@ Bug Fixes
1213
* Don't diagnose free-entry sections such as `[favorite_queries]` in `--checkup`.
1314
* When accepting a filename completion, fill in leading `./` if given.
1415

16+
1517
Internal
1618
--------
1719
* Bump `cli_helpers` to non-yanked version.

mycli/packages/completion_engine.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,22 +45,26 @@ def _is_where_or_having(token: Token | None) -> bool:
4545

4646
def _find_doubled_backticks(text: str) -> list[int]:
4747
length = len(text)
48-
doubled_backticks: list[int] = []
48+
doubled_backtick_positions: list[int] = []
4949
backtick = '`'
50+
two_backticks = backtick + backtick
51+
52+
if two_backticks not in text:
53+
return doubled_backtick_positions
5054

5155
for index in range(0, length):
5256
ch = text[index]
5357
if ch != backtick:
5458
index += 1
5559
continue
5660
if index + 1 < length and text[index + 1] == backtick:
57-
doubled_backticks.append(index)
58-
doubled_backticks.append(index + 1)
61+
doubled_backtick_positions.append(index)
62+
doubled_backtick_positions.append(index + 1)
5963
index += 2
6064
continue
6165
index += 1
6266

63-
return doubled_backticks
67+
return doubled_backtick_positions
6468

6569

6670
@functools.lru_cache(maxsize=128)
@@ -76,8 +80,7 @@ def is_inside_quotes(text: str, pos: int) -> Literal[False, 'single', 'double',
7680
backslash = '\\'
7781

7882
# scanning the string twice seems to be needed to handle doubled backticks
79-
if backtick in text:
80-
doubled_backtick_positions = _find_doubled_backticks(text)
83+
doubled_backtick_positions = _find_doubled_backticks(text)
8184

8285
length = len(text)
8386
if pos < 0:

mycli/sqlcompleter.py

Lines changed: 120 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from pygments.lexers._mysql_builtins import MYSQL_DATATYPES, MYSQL_FUNCTIONS, MYSQL_KEYWORDS
1212
import rapidfuzz
1313

14-
from mycli.packages.completion_engine import suggest_type
14+
from mycli.packages.completion_engine import is_inside_quotes, suggest_type
1515
from mycli.packages.filepaths import complete_path, parse_path, suggest_path
1616
from mycli.packages.parseutils import extract_columns_from_select, last_word
1717
from mycli.packages.special import llm
@@ -810,13 +810,6 @@ def escape_name(self, name: str) -> str:
810810

811811
return name
812812

813-
def unescape_name(self, name: str) -> str:
814-
"""Unquote a string."""
815-
if name and name[0] == '"' and name[-1] == '"':
816-
name = name[1:-1]
817-
818-
return name
819-
820813
def escaped_names(self, names: Collection[str]) -> list[str]:
821814
return [self.escape_name(name) for name in names]
822815

@@ -974,6 +967,7 @@ def find_matches(
974967
start_only: bool = False,
975968
fuzzy: bool = True,
976969
casing: str | None = None,
970+
text_before_cursor: str = '',
977971
) -> Generator[tuple[str, int], None, None]:
978972
"""Find completion matches for the given text.
979973
@@ -995,13 +989,26 @@ def find_matches(
995989

996990
completions: list[tuple[str, int]] = []
997991

992+
def maybe_quote_identifier(item: str) -> str:
993+
if item.startswith('`'):
994+
return item
995+
if item == '*':
996+
return item
997+
return '`' + item + '`'
998+
999+
# checking text.startswith() first is an optimization; is_inside_quotes() covers more cases
1000+
if text.startswith('`') or is_inside_quotes(text_before_cursor, len(text_before_cursor)) == 'backtick':
1001+
quoted_collection: Collection[Any] = [maybe_quote_identifier(x) if isinstance(x, str) else x for x in collection]
1002+
else:
1003+
quoted_collection = collection
1004+
9981005
if fuzzy:
9991006
regex = ".{0,3}?".join(map(re.escape, text))
10001007
pat = re.compile(f'({regex})')
10011008
under_words_text = [x for x in text.split('_') if x]
10021009
case_words_text = re.split(case_change_pat, last)
10031010

1004-
for item in collection:
1011+
for item in quoted_collection:
10051012
r = pat.search(item.lower())
10061013
if r:
10071014
completions.append((item, Fuzziness.REGEX))
@@ -1032,7 +1039,7 @@ def find_matches(
10321039
if len(text) >= 4:
10331040
rapidfuzz_matches = rapidfuzz.process.extract(
10341041
text,
1035-
collection,
1042+
quoted_collection,
10361043
scorer=rapidfuzz.fuzz.WRatio,
10371044
# todo: maybe make our own processor which only does case-folding
10381045
# because underscores are valuable info
@@ -1050,7 +1057,7 @@ def find_matches(
10501057

10511058
else:
10521059
match_end_limit = len(text) if start_only else None
1053-
for item in collection:
1060+
for item in quoted_collection:
10541061
match_point = item.lower().find(text, 0, match_end_limit)
10551062
if match_point >= 0:
10561063
completions.append((item, Fuzziness.PERFECT))
@@ -1083,7 +1090,13 @@ def get_completions(
10831090
# If smart_completion is off then match any word that starts with
10841091
# 'word_before_cursor'.
10851092
if not smart_completion:
1086-
matches = self.find_matches(word_before_cursor, self.all_completions, start_only=True, fuzzy=False)
1093+
matches = self.find_matches(
1094+
word_before_cursor,
1095+
self.all_completions,
1096+
start_only=True,
1097+
fuzzy=False,
1098+
text_before_cursor=document.text_before_cursor,
1099+
)
10871100
return (Completion(x[0], -len(text_for_len)) for x in matches)
10881101

10891102
completions: list[tuple[str, int, int]] = []
@@ -1110,13 +1123,21 @@ def get_completions(
11101123
# showing all columns. So make them unique and sort them.
11111124
scoped_cols = sorted(set(scoped_cols), key=lambda s: s.strip('`'))
11121125

1113-
cols = self.find_matches(word_before_cursor, scoped_cols)
1126+
cols = self.find_matches(
1127+
word_before_cursor,
1128+
scoped_cols,
1129+
text_before_cursor=document.text_before_cursor,
1130+
)
11141131
completions.extend([(*x, rank) for x in cols])
11151132

11161133
elif suggestion["type"] == "function":
11171134
# suggest user-defined functions using substring matching
11181135
funcs = self.populate_schema_objects(suggestion["schema"], "functions")
1119-
user_funcs = self.find_matches(word_before_cursor, funcs)
1136+
user_funcs = self.find_matches(
1137+
word_before_cursor,
1138+
funcs,
1139+
text_before_cursor=document.text_before_cursor,
1140+
)
11201141
completions.extend([(*x, rank) for x in user_funcs])
11211142

11221143
# suggest hardcoded functions using startswith matching only if
@@ -1125,13 +1146,22 @@ def get_completions(
11251146
# eg: SELECT * FROM users u WHERE u.
11261147
if not suggestion["schema"]:
11271148
predefined_funcs = self.find_matches(
1128-
word_before_cursor, self.functions, start_only=True, fuzzy=False, casing=self.keyword_casing
1149+
word_before_cursor,
1150+
self.functions,
1151+
start_only=True,
1152+
fuzzy=False,
1153+
casing=self.keyword_casing,
1154+
text_before_cursor=document.text_before_cursor,
11291155
)
11301156
completions.extend([(*x, rank) for x in predefined_funcs])
11311157

11321158
elif suggestion["type"] == "procedure":
11331159
procs = self.populate_schema_objects(suggestion["schema"], "procedures")
1134-
procs_m = self.find_matches(word_before_cursor, procs)
1160+
procs_m = self.find_matches(
1161+
word_before_cursor,
1162+
procs,
1163+
text_before_cursor=document.text_before_cursor,
1164+
)
11351165
completions.extend([(*x, rank) for x in procs_m])
11361166

11371167
elif suggestion["type"] == "table":
@@ -1144,53 +1174,107 @@ def get_completions(
11441174
tables = self.populate_schema_objects(suggestion["schema"], "tables", columns)
11451175
else:
11461176
tables = self.populate_schema_objects(suggestion["schema"], "tables")
1147-
tables_m = self.find_matches(word_before_cursor, tables)
1177+
tables_m = self.find_matches(
1178+
word_before_cursor,
1179+
tables,
1180+
text_before_cursor=document.text_before_cursor,
1181+
)
11481182
completions.extend([(*x, rank) for x in tables_m])
11491183

11501184
elif suggestion["type"] == "view":
11511185
views = self.populate_schema_objects(suggestion["schema"], "views")
1152-
views_m = self.find_matches(word_before_cursor, views)
1186+
views_m = self.find_matches(
1187+
word_before_cursor,
1188+
views,
1189+
text_before_cursor=document.text_before_cursor,
1190+
)
11531191
completions.extend([(*x, rank) for x in views_m])
11541192

11551193
elif suggestion["type"] == "alias":
11561194
aliases = suggestion["aliases"]
1157-
aliases_m = self.find_matches(word_before_cursor, aliases)
1195+
aliases_m = self.find_matches(
1196+
word_before_cursor,
1197+
aliases,
1198+
text_before_cursor=document.text_before_cursor,
1199+
)
11581200
completions.extend([(*x, rank) for x in aliases_m])
11591201

11601202
elif suggestion["type"] == "database":
1161-
dbs_m = self.find_matches(word_before_cursor, self.databases)
1203+
dbs_m = self.find_matches(
1204+
word_before_cursor,
1205+
self.databases,
1206+
text_before_cursor=document.text_before_cursor,
1207+
)
11621208
completions.extend([(*x, rank) for x in dbs_m])
11631209

11641210
elif suggestion["type"] == "keyword":
1165-
keywords_m = self.find_matches(word_before_cursor, self.keywords, casing=self.keyword_casing)
1211+
keywords_m = self.find_matches(
1212+
word_before_cursor,
1213+
self.keywords,
1214+
casing=self.keyword_casing,
1215+
text_before_cursor=document.text_before_cursor,
1216+
)
11661217
completions.extend([(*x, rank) for x in keywords_m])
11671218

11681219
elif suggestion["type"] == "show":
11691220
show_items_m = self.find_matches(
1170-
word_before_cursor, self.show_items, start_only=False, fuzzy=True, casing=self.keyword_casing
1221+
word_before_cursor,
1222+
self.show_items,
1223+
start_only=False,
1224+
fuzzy=True,
1225+
casing=self.keyword_casing,
1226+
text_before_cursor=document.text_before_cursor,
11711227
)
11721228
completions.extend([(*x, rank) for x in show_items_m])
11731229

11741230
elif suggestion["type"] == "change":
1175-
change_items_m = self.find_matches(word_before_cursor, self.change_items, start_only=False, fuzzy=True)
1231+
change_items_m = self.find_matches(
1232+
word_before_cursor,
1233+
self.change_items,
1234+
start_only=False,
1235+
fuzzy=True,
1236+
text_before_cursor=document.text_before_cursor,
1237+
)
11761238
completions.extend([(*x, rank) for x in change_items_m])
11771239

11781240
elif suggestion["type"] == "user":
1179-
users_m = self.find_matches(word_before_cursor, self.users, start_only=False, fuzzy=True)
1241+
users_m = self.find_matches(
1242+
word_before_cursor,
1243+
self.users,
1244+
start_only=False,
1245+
fuzzy=True,
1246+
text_before_cursor=document.text_before_cursor,
1247+
)
11801248
completions.extend([(*x, rank) for x in users_m])
11811249

11821250
elif suggestion["type"] == "special":
1183-
special_m = self.find_matches(word_before_cursor, self.special_commands, start_only=True, fuzzy=False)
1251+
special_m = self.find_matches(
1252+
word_before_cursor,
1253+
self.special_commands,
1254+
start_only=True,
1255+
fuzzy=False,
1256+
text_before_cursor=document.text_before_cursor,
1257+
)
11841258
# specials are special, and go early in the candidates, first if possible
11851259
completions.extend([(*x, 0) for x in special_m])
11861260

11871261
elif suggestion["type"] == "favoritequery":
11881262
if hasattr(FavoriteQueries, 'instance') and hasattr(FavoriteQueries.instance, 'list'):
1189-
queries_m = self.find_matches(word_before_cursor, FavoriteQueries.instance.list(), start_only=False, fuzzy=True)
1263+
queries_m = self.find_matches(
1264+
word_before_cursor,
1265+
FavoriteQueries.instance.list(),
1266+
start_only=False,
1267+
fuzzy=True,
1268+
text_before_cursor=document.text_before_cursor,
1269+
)
11901270
completions.extend([(*x, rank) for x in queries_m])
11911271

11921272
elif suggestion["type"] == "table_format":
1193-
formats_m = self.find_matches(word_before_cursor, self.table_formats)
1273+
formats_m = self.find_matches(
1274+
word_before_cursor,
1275+
self.table_formats,
1276+
text_before_cursor=document.text_before_cursor,
1277+
)
11941278
completions.extend([(*x, rank) for x in formats_m])
11951279

11961280
elif suggestion["type"] == "file_name":
@@ -1210,6 +1294,7 @@ def get_completions(
12101294
possible_entries,
12111295
start_only=False,
12121296
fuzzy=True,
1297+
text_before_cursor=document.text_before_cursor,
12131298
)
12141299
completions.extend([(*x, rank) for x in subcommands_m])
12151300
elif suggestion["type"] == "enum_value":
@@ -1220,7 +1305,14 @@ def get_completions(
12201305
)
12211306
if enum_values:
12221307
quoted_values = [self._quote_sql_string(value) for value in enum_values]
1223-
completions = [(*x, rank) for x in self.find_matches(word_before_cursor, quoted_values)]
1308+
completions = [
1309+
(*x, rank)
1310+
for x in self.find_matches(
1311+
word_before_cursor,
1312+
quoted_values,
1313+
text_before_cursor=document.text_before_cursor,
1314+
)
1315+
]
12241316
break
12251317

12261318
def completion_sort_key(item: tuple[str, int, int], text_for_len: str):

0 commit comments

Comments
 (0)