1919from mycli .packages .special .main import COMMANDS as SPECIAL_COMMANDS
2020
2121_logger = logging .getLogger (__name__ )
22+ _CASE_CHANGE_PAT = re .compile ('(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])' )
2223
2324
2425class Fuzziness (IntEnum ):
@@ -1173,8 +1174,135 @@ def reset_completions(self) -> None:
11731174 }
11741175 self .all_completions = set (self .keywords + self .functions )
11751176
1176- @staticmethod
1177+ def maybe_quote_identifier (self , item : str ) -> str :
1178+ if item .startswith ('`' ):
1179+ return item
1180+ if item == '*' :
1181+ return item
1182+ return '`' + item + '`'
1183+
1184+ def quote_collection_if_needed (
1185+ self ,
1186+ text : str ,
1187+ collection : Collection [Any ],
1188+ text_before_cursor : str ,
1189+ ) -> Collection [Any ]:
1190+ # checking text.startswith() first is an optimization; is_inside_quotes() covers more cases
1191+ if text .startswith ('`' ) or is_inside_quotes (text_before_cursor , len (text_before_cursor )) == 'backtick' :
1192+ return [self .maybe_quote_identifier (x ) if isinstance (x , str ) else x for x in collection ]
1193+ return collection
1194+
1195+ def word_parts_match (
1196+ self ,
1197+ text_parts : list [str ],
1198+ item_parts : list [str ],
1199+ ) -> bool :
1200+ occurrences = 0
1201+ for text_part in text_parts :
1202+ for item_part in item_parts :
1203+ if item_part .startswith (text_part ):
1204+ occurrences += 1
1205+ break
1206+ return occurrences >= len (text_parts )
1207+
1208+ def find_fuzzy_match (
1209+ self ,
1210+ item : str ,
1211+ pattern : re .Pattern [str ],
1212+ under_words_text : list [str ],
1213+ case_words_text : list [str ],
1214+ ) -> int | None :
1215+ if pattern .search (item .lower ()):
1216+ return Fuzziness .REGEX
1217+
1218+ under_words_item = [x for x in item .lower ().split ('_' ) if x ]
1219+ if self .word_parts_match (under_words_text , under_words_item ):
1220+ return Fuzziness .UNDER_WORDS
1221+
1222+ case_words_item = re .split (_CASE_CHANGE_PAT , item )
1223+ if self .word_parts_match (case_words_text , case_words_item ):
1224+ return Fuzziness .CAMEL_CASE
1225+
1226+ return None
1227+
1228+ def find_fuzzy_matches (
1229+ self ,
1230+ last : str ,
1231+ text : str ,
1232+ collection : Collection [Any ],
1233+ ) -> list [tuple [str , int ]]:
1234+ completions : list [tuple [str , int ]] = []
1235+ regex = '.{0,3}?' .join (map (re .escape , text ))
1236+ pattern = re .compile (f'({ regex } )' )
1237+ under_words_text = [x for x in text .split ('_' ) if x ]
1238+ case_words_text = re .split (_CASE_CHANGE_PAT , last )
1239+
1240+ for item in collection :
1241+ fuzziness = self .find_fuzzy_match (item , pattern , under_words_text , case_words_text )
1242+ if fuzziness is not None :
1243+ completions .append ((item , fuzziness ))
1244+
1245+ if len (text ) >= 4 :
1246+ rapidfuzz_matches = rapidfuzz .process .extract (
1247+ text ,
1248+ collection ,
1249+ scorer = rapidfuzz .fuzz .WRatio ,
1250+ # todo: maybe make our own processor which only does case-folding
1251+ # because underscores are valuable info
1252+ processor = rapidfuzz .utils .default_process ,
1253+ limit = 20 ,
1254+ score_cutoff = 75 ,
1255+ )
1256+ for item , _score , _type in rapidfuzz_matches :
1257+ if len (item ) < len (text ) / 1.5 :
1258+ continue
1259+ if item in completions :
1260+ continue
1261+ completions .append ((item , Fuzziness .RAPIDFUZZ ))
1262+
1263+ return completions
1264+
1265+ def find_perfect_matches (
1266+ self ,
1267+ text : str ,
1268+ collection : Collection [Any ],
1269+ start_only : bool ,
1270+ ) -> list [tuple [str , int ]]:
1271+ completions : list [tuple [str , int ]] = []
1272+ match_end_limit = len (text ) if start_only else None
1273+ for item in collection :
1274+ match_point = item .lower ().find (text , 0 , match_end_limit )
1275+ if match_point >= 0 :
1276+ completions .append ((item , Fuzziness .PERFECT ))
1277+ return completions
1278+
1279+ def resolve_casing (
1280+ self ,
1281+ casing : str | None ,
1282+ last : str ,
1283+ ) -> str | None :
1284+ if casing != 'auto' :
1285+ return casing
1286+ return 'lower' if last and (last [0 ].islower () or last [- 1 ].islower ()) else 'upper'
1287+
1288+ def apply_casing (
1289+ self ,
1290+ completions : list [tuple [str , int ]],
1291+ casing : str | None ,
1292+ ) -> Generator [tuple [str , int ], None , None ]:
1293+ if casing is None :
1294+ return (completion for completion in completions )
1295+
1296+ def apply_case (tup : tuple [str , int ]) -> tuple [str , int ]:
1297+ kw , fuzziness = tup
1298+ if casing == 'upper' :
1299+ return (kw .upper (), fuzziness )
1300+ return (kw .lower (), fuzziness )
1301+
1302+ return (apply_case (completion ) for completion in completions )
1303+
11771304 def find_matches (
1305+ self ,
11781306 orig_text : str ,
11791307 collection : Collection ,
11801308 start_only : bool = False ,
@@ -1195,96 +1323,17 @@ def find_matches(
11951323 yields prompt_toolkit Completion instances for any matches found
11961324 in the collection of available completions.
11971325 """
1198- last = last_word (orig_text , include = " most_punctuations" )
1326+ last = last_word (orig_text , include = ' most_punctuations' )
11991327 text = last .lower ()
1200- # unicode support not possible without adding the regex dependency
1201- case_change_pat = re .compile ("(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])" )
1202-
1203- completions : list [tuple [str , int ]] = []
1204-
1205- def maybe_quote_identifier (item : str ) -> str :
1206- if item .startswith ('`' ):
1207- return item
1208- if item == '*' :
1209- return item
1210- return '`' + item + '`'
1211-
1212- # checking text.startswith() first is an optimization; is_inside_quotes() covers more cases
1213- if text .startswith ('`' ) or is_inside_quotes (text_before_cursor , len (text_before_cursor )) == 'backtick' :
1214- quoted_collection : Collection [Any ] = [maybe_quote_identifier (x ) if isinstance (x , str ) else x for x in collection ]
1215- else :
1216- quoted_collection = collection
1328+ quoted_collection = self .quote_collection_if_needed (text , collection , text_before_cursor )
12171329
12181330 if fuzzy :
1219- regex = ".{0,3}?" .join (map (re .escape , text ))
1220- pat = re .compile (f'({ regex } )' )
1221- under_words_text = [x for x in text .split ('_' ) if x ]
1222- case_words_text = re .split (case_change_pat , last )
1223-
1224- for item in quoted_collection :
1225- r = pat .search (item .lower ())
1226- if r :
1227- completions .append ((item , Fuzziness .REGEX ))
1228- continue
1229-
1230- under_words_item = [x for x in item .lower ().split ('_' ) if x ]
1231- occurrences = 0
1232- for elt_word in under_words_text :
1233- for elt_item in under_words_item :
1234- if elt_item .startswith (elt_word ):
1235- occurrences += 1
1236- break
1237- if occurrences >= len (under_words_text ):
1238- completions .append ((item , Fuzziness .UNDER_WORDS ))
1239- continue
1240-
1241- case_words_item = re .split (case_change_pat , item )
1242- occurrences = 0
1243- for elt_word in case_words_text :
1244- for elt_item in case_words_item :
1245- if elt_item .startswith (elt_word ):
1246- occurrences += 1
1247- break
1248- if occurrences >= len (case_words_text ):
1249- completions .append ((item , Fuzziness .CAMEL_CASE ))
1250- continue
1251-
1252- if len (text ) >= 4 :
1253- rapidfuzz_matches = rapidfuzz .process .extract (
1254- text ,
1255- quoted_collection ,
1256- scorer = rapidfuzz .fuzz .WRatio ,
1257- # todo: maybe make our own processor which only does case-folding
1258- # because underscores are valuable info
1259- processor = rapidfuzz .utils .default_process ,
1260- limit = 20 ,
1261- score_cutoff = 75 ,
1262- )
1263- for elt in rapidfuzz_matches :
1264- item , _score , _type = elt
1265- if len (item ) < len (text ) / 1.5 :
1266- continue
1267- if item in completions :
1268- continue
1269- completions .append ((item , Fuzziness .RAPIDFUZZ ))
1270-
1331+ completions = self .find_fuzzy_matches (last , text , quoted_collection )
12711332 else :
1272- match_end_limit = len (text ) if start_only else None
1273- for item in quoted_collection :
1274- match_point = item .lower ().find (text , 0 , match_end_limit )
1275- if match_point >= 0 :
1276- completions .append ((item , Fuzziness .PERFECT ))
1277-
1278- if casing == "auto" :
1279- casing = "lower" if last and (last [0 ].islower () or last [- 1 ].islower ()) else "upper"
1280-
1281- def apply_case (tup : tuple [str , int ]) -> tuple [str , int ]:
1282- kw , fuzziness = tup
1283- if casing == "upper" :
1284- return (kw .upper (), fuzziness )
1285- return (kw .lower (), fuzziness )
1333+ completions = self .find_perfect_matches (text , quoted_collection , start_only )
12861334
1287- return (x if casing is None else apply_case (x ) for x in completions )
1335+ casing = self .resolve_casing (casing , last )
1336+ return self .apply_casing (completions , casing )
12881337
12891338 def get_completions (
12901339 self ,
0 commit comments