Skip to content

Commit 43ed8e3

Browse files
authored
Merge pull request #1581 from dbcli/RW/better-suggest-inside-backquotes
Improve completion suggestions inside backticks
2 parents 7a3eaf3 + 2d6f20f commit 43ed8e3

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)