17db96d56Sopenharmony_ci"""Complete either attribute names or file names.
27db96d56Sopenharmony_ci
37db96d56Sopenharmony_ciEither on demand or after a user-selected delay after a key character,
47db96d56Sopenharmony_cipop up a list of candidates.
57db96d56Sopenharmony_ci"""
67db96d56Sopenharmony_ciimport __main__
77db96d56Sopenharmony_ciimport keyword
87db96d56Sopenharmony_ciimport os
97db96d56Sopenharmony_ciimport string
107db96d56Sopenharmony_ciimport sys
117db96d56Sopenharmony_ci
127db96d56Sopenharmony_ci# Modified keyword list is used in fetch_completions.
137db96d56Sopenharmony_cicompletion_kwds = [s for s in keyword.kwlist
147db96d56Sopenharmony_ci                     if s not in {'True', 'False', 'None'}]  # In builtins.
157db96d56Sopenharmony_cicompletion_kwds.extend(('match', 'case'))  # Context keywords.
167db96d56Sopenharmony_cicompletion_kwds.sort()
177db96d56Sopenharmony_ci
187db96d56Sopenharmony_ci# Two types of completions; defined here for autocomplete_w import below.
197db96d56Sopenharmony_ciATTRS, FILES = 0, 1
207db96d56Sopenharmony_cifrom idlelib import autocomplete_w
217db96d56Sopenharmony_cifrom idlelib.config import idleConf
227db96d56Sopenharmony_cifrom idlelib.hyperparser import HyperParser
237db96d56Sopenharmony_ci
247db96d56Sopenharmony_ci# Tuples passed to open_completions.
257db96d56Sopenharmony_ci#       EvalFunc, Complete, WantWin, Mode
267db96d56Sopenharmony_ciFORCE = True,     False,    True,    None   # Control-Space.
277db96d56Sopenharmony_ciTAB   = False,    True,     True,    None   # Tab.
287db96d56Sopenharmony_ciTRY_A = False,    False,    False,   ATTRS  # '.' for attributes.
297db96d56Sopenharmony_ciTRY_F = False,    False,    False,   FILES  # '/' in quotes for file name.
307db96d56Sopenharmony_ci
317db96d56Sopenharmony_ci# This string includes all chars that may be in an identifier.
327db96d56Sopenharmony_ci# TODO Update this here and elsewhere.
337db96d56Sopenharmony_ciID_CHARS = string.ascii_letters + string.digits + "_"
347db96d56Sopenharmony_ci
357db96d56Sopenharmony_ciSEPS = f"{os.sep}{os.altsep if os.altsep else ''}"
367db96d56Sopenharmony_ciTRIGGERS = f".{SEPS}"
377db96d56Sopenharmony_ci
387db96d56Sopenharmony_ciclass AutoComplete:
397db96d56Sopenharmony_ci
407db96d56Sopenharmony_ci    def __init__(self, editwin=None, tags=None):
417db96d56Sopenharmony_ci        self.editwin = editwin
427db96d56Sopenharmony_ci        if editwin is not None:   # not in subprocess or no-gui test
437db96d56Sopenharmony_ci            self.text = editwin.text
447db96d56Sopenharmony_ci        self.tags = tags
457db96d56Sopenharmony_ci        self.autocompletewindow = None
467db96d56Sopenharmony_ci        # id of delayed call, and the index of the text insert when
477db96d56Sopenharmony_ci        # the delayed call was issued. If _delayed_completion_id is
487db96d56Sopenharmony_ci        # None, there is no delayed call.
497db96d56Sopenharmony_ci        self._delayed_completion_id = None
507db96d56Sopenharmony_ci        self._delayed_completion_index = None
517db96d56Sopenharmony_ci
527db96d56Sopenharmony_ci    @classmethod
537db96d56Sopenharmony_ci    def reload(cls):
547db96d56Sopenharmony_ci        cls.popupwait = idleConf.GetOption(
557db96d56Sopenharmony_ci            "extensions", "AutoComplete", "popupwait", type="int", default=0)
567db96d56Sopenharmony_ci
577db96d56Sopenharmony_ci    def _make_autocomplete_window(self):  # Makes mocking easier.
587db96d56Sopenharmony_ci        return autocomplete_w.AutoCompleteWindow(self.text, tags=self.tags)
597db96d56Sopenharmony_ci
607db96d56Sopenharmony_ci    def _remove_autocomplete_window(self, event=None):
617db96d56Sopenharmony_ci        if self.autocompletewindow:
627db96d56Sopenharmony_ci            self.autocompletewindow.hide_window()
637db96d56Sopenharmony_ci            self.autocompletewindow = None
647db96d56Sopenharmony_ci
657db96d56Sopenharmony_ci    def force_open_completions_event(self, event):
667db96d56Sopenharmony_ci        "(^space) Open completion list, even if a function call is needed."
677db96d56Sopenharmony_ci        self.open_completions(FORCE)
687db96d56Sopenharmony_ci        return "break"
697db96d56Sopenharmony_ci
707db96d56Sopenharmony_ci    def autocomplete_event(self, event):
717db96d56Sopenharmony_ci        "(tab) Complete word or open list if multiple options."
727db96d56Sopenharmony_ci        if hasattr(event, "mc_state") and event.mc_state or\
737db96d56Sopenharmony_ci                not self.text.get("insert linestart", "insert").strip():
747db96d56Sopenharmony_ci            # A modifier was pressed along with the tab or
757db96d56Sopenharmony_ci            # there is only previous whitespace on this line, so tab.
767db96d56Sopenharmony_ci            return None
777db96d56Sopenharmony_ci        if self.autocompletewindow and self.autocompletewindow.is_active():
787db96d56Sopenharmony_ci            self.autocompletewindow.complete()
797db96d56Sopenharmony_ci            return "break"
807db96d56Sopenharmony_ci        else:
817db96d56Sopenharmony_ci            opened = self.open_completions(TAB)
827db96d56Sopenharmony_ci            return "break" if opened else None
837db96d56Sopenharmony_ci
847db96d56Sopenharmony_ci    def try_open_completions_event(self, event=None):
857db96d56Sopenharmony_ci        "(./) Open completion list after pause with no movement."
867db96d56Sopenharmony_ci        lastchar = self.text.get("insert-1c")
877db96d56Sopenharmony_ci        if lastchar in TRIGGERS:
887db96d56Sopenharmony_ci            args = TRY_A if lastchar == "." else TRY_F
897db96d56Sopenharmony_ci            self._delayed_completion_index = self.text.index("insert")
907db96d56Sopenharmony_ci            if self._delayed_completion_id is not None:
917db96d56Sopenharmony_ci                self.text.after_cancel(self._delayed_completion_id)
927db96d56Sopenharmony_ci            self._delayed_completion_id = self.text.after(
937db96d56Sopenharmony_ci                self.popupwait, self._delayed_open_completions, args)
947db96d56Sopenharmony_ci
957db96d56Sopenharmony_ci    def _delayed_open_completions(self, args):
967db96d56Sopenharmony_ci        "Call open_completions if index unchanged."
977db96d56Sopenharmony_ci        self._delayed_completion_id = None
987db96d56Sopenharmony_ci        if self.text.index("insert") == self._delayed_completion_index:
997db96d56Sopenharmony_ci            self.open_completions(args)
1007db96d56Sopenharmony_ci
1017db96d56Sopenharmony_ci    def open_completions(self, args):
1027db96d56Sopenharmony_ci        """Find the completions and create the AutoCompleteWindow.
1037db96d56Sopenharmony_ci        Return True if successful (no syntax error or so found).
1047db96d56Sopenharmony_ci        If complete is True, then if there's nothing to complete and no
1057db96d56Sopenharmony_ci        start of completion, won't open completions and return False.
1067db96d56Sopenharmony_ci        If mode is given, will open a completion list only in this mode.
1077db96d56Sopenharmony_ci        """
1087db96d56Sopenharmony_ci        evalfuncs, complete, wantwin, mode = args
1097db96d56Sopenharmony_ci        # Cancel another delayed call, if it exists.
1107db96d56Sopenharmony_ci        if self._delayed_completion_id is not None:
1117db96d56Sopenharmony_ci            self.text.after_cancel(self._delayed_completion_id)
1127db96d56Sopenharmony_ci            self._delayed_completion_id = None
1137db96d56Sopenharmony_ci
1147db96d56Sopenharmony_ci        hp = HyperParser(self.editwin, "insert")
1157db96d56Sopenharmony_ci        curline = self.text.get("insert linestart", "insert")
1167db96d56Sopenharmony_ci        i = j = len(curline)
1177db96d56Sopenharmony_ci        if hp.is_in_string() and (not mode or mode==FILES):
1187db96d56Sopenharmony_ci            # Find the beginning of the string.
1197db96d56Sopenharmony_ci            # fetch_completions will look at the file system to determine
1207db96d56Sopenharmony_ci            # whether the string value constitutes an actual file name
1217db96d56Sopenharmony_ci            # XXX could consider raw strings here and unescape the string
1227db96d56Sopenharmony_ci            # value if it's not raw.
1237db96d56Sopenharmony_ci            self._remove_autocomplete_window()
1247db96d56Sopenharmony_ci            mode = FILES
1257db96d56Sopenharmony_ci            # Find last separator or string start
1267db96d56Sopenharmony_ci            while i and curline[i-1] not in "'\"" + SEPS:
1277db96d56Sopenharmony_ci                i -= 1
1287db96d56Sopenharmony_ci            comp_start = curline[i:j]
1297db96d56Sopenharmony_ci            j = i
1307db96d56Sopenharmony_ci            # Find string start
1317db96d56Sopenharmony_ci            while i and curline[i-1] not in "'\"":
1327db96d56Sopenharmony_ci                i -= 1
1337db96d56Sopenharmony_ci            comp_what = curline[i:j]
1347db96d56Sopenharmony_ci        elif hp.is_in_code() and (not mode or mode==ATTRS):
1357db96d56Sopenharmony_ci            self._remove_autocomplete_window()
1367db96d56Sopenharmony_ci            mode = ATTRS
1377db96d56Sopenharmony_ci            while i and (curline[i-1] in ID_CHARS or ord(curline[i-1]) > 127):
1387db96d56Sopenharmony_ci                i -= 1
1397db96d56Sopenharmony_ci            comp_start = curline[i:j]
1407db96d56Sopenharmony_ci            if i and curline[i-1] == '.':  # Need object with attributes.
1417db96d56Sopenharmony_ci                hp.set_index("insert-%dc" % (len(curline)-(i-1)))
1427db96d56Sopenharmony_ci                comp_what = hp.get_expression()
1437db96d56Sopenharmony_ci                if (not comp_what or
1447db96d56Sopenharmony_ci                   (not evalfuncs and comp_what.find('(') != -1)):
1457db96d56Sopenharmony_ci                    return None
1467db96d56Sopenharmony_ci            else:
1477db96d56Sopenharmony_ci                comp_what = ""
1487db96d56Sopenharmony_ci        else:
1497db96d56Sopenharmony_ci            return None
1507db96d56Sopenharmony_ci
1517db96d56Sopenharmony_ci        if complete and not comp_what and not comp_start:
1527db96d56Sopenharmony_ci            return None
1537db96d56Sopenharmony_ci        comp_lists = self.fetch_completions(comp_what, mode)
1547db96d56Sopenharmony_ci        if not comp_lists[0]:
1557db96d56Sopenharmony_ci            return None
1567db96d56Sopenharmony_ci        self.autocompletewindow = self._make_autocomplete_window()
1577db96d56Sopenharmony_ci        return not self.autocompletewindow.show_window(
1587db96d56Sopenharmony_ci                comp_lists, "insert-%dc" % len(comp_start),
1597db96d56Sopenharmony_ci                complete, mode, wantwin)
1607db96d56Sopenharmony_ci
1617db96d56Sopenharmony_ci    def fetch_completions(self, what, mode):
1627db96d56Sopenharmony_ci        """Return a pair of lists of completions for something. The first list
1637db96d56Sopenharmony_ci        is a sublist of the second. Both are sorted.
1647db96d56Sopenharmony_ci
1657db96d56Sopenharmony_ci        If there is a Python subprocess, get the comp. list there.  Otherwise,
1667db96d56Sopenharmony_ci        either fetch_completions() is running in the subprocess itself or it
1677db96d56Sopenharmony_ci        was called in an IDLE EditorWindow before any script had been run.
1687db96d56Sopenharmony_ci
1697db96d56Sopenharmony_ci        The subprocess environment is that of the most recently run script.  If
1707db96d56Sopenharmony_ci        two unrelated modules are being edited some calltips in the current
1717db96d56Sopenharmony_ci        module may be inoperative if the module was not the last to run.
1727db96d56Sopenharmony_ci        """
1737db96d56Sopenharmony_ci        try:
1747db96d56Sopenharmony_ci            rpcclt = self.editwin.flist.pyshell.interp.rpcclt
1757db96d56Sopenharmony_ci        except:
1767db96d56Sopenharmony_ci            rpcclt = None
1777db96d56Sopenharmony_ci        if rpcclt:
1787db96d56Sopenharmony_ci            return rpcclt.remotecall("exec", "get_the_completion_list",
1797db96d56Sopenharmony_ci                                     (what, mode), {})
1807db96d56Sopenharmony_ci        else:
1817db96d56Sopenharmony_ci            if mode == ATTRS:
1827db96d56Sopenharmony_ci                if what == "":  # Main module names.
1837db96d56Sopenharmony_ci                    namespace = {**__main__.__builtins__.__dict__,
1847db96d56Sopenharmony_ci                                 **__main__.__dict__}
1857db96d56Sopenharmony_ci                    bigl = eval("dir()", namespace)
1867db96d56Sopenharmony_ci                    bigl.extend(completion_kwds)
1877db96d56Sopenharmony_ci                    bigl.sort()
1887db96d56Sopenharmony_ci                    if "__all__" in bigl:
1897db96d56Sopenharmony_ci                        smalll = sorted(eval("__all__", namespace))
1907db96d56Sopenharmony_ci                    else:
1917db96d56Sopenharmony_ci                        smalll = [s for s in bigl if s[:1] != '_']
1927db96d56Sopenharmony_ci                else:
1937db96d56Sopenharmony_ci                    try:
1947db96d56Sopenharmony_ci                        entity = self.get_entity(what)
1957db96d56Sopenharmony_ci                        bigl = dir(entity)
1967db96d56Sopenharmony_ci                        bigl.sort()
1977db96d56Sopenharmony_ci                        if "__all__" in bigl:
1987db96d56Sopenharmony_ci                            smalll = sorted(entity.__all__)
1997db96d56Sopenharmony_ci                        else:
2007db96d56Sopenharmony_ci                            smalll = [s for s in bigl if s[:1] != '_']
2017db96d56Sopenharmony_ci                    except:
2027db96d56Sopenharmony_ci                        return [], []
2037db96d56Sopenharmony_ci
2047db96d56Sopenharmony_ci            elif mode == FILES:
2057db96d56Sopenharmony_ci                if what == "":
2067db96d56Sopenharmony_ci                    what = "."
2077db96d56Sopenharmony_ci                try:
2087db96d56Sopenharmony_ci                    expandedpath = os.path.expanduser(what)
2097db96d56Sopenharmony_ci                    bigl = os.listdir(expandedpath)
2107db96d56Sopenharmony_ci                    bigl.sort()
2117db96d56Sopenharmony_ci                    smalll = [s for s in bigl if s[:1] != '.']
2127db96d56Sopenharmony_ci                except OSError:
2137db96d56Sopenharmony_ci                    return [], []
2147db96d56Sopenharmony_ci
2157db96d56Sopenharmony_ci            if not smalll:
2167db96d56Sopenharmony_ci                smalll = bigl
2177db96d56Sopenharmony_ci            return smalll, bigl
2187db96d56Sopenharmony_ci
2197db96d56Sopenharmony_ci    def get_entity(self, name):
2207db96d56Sopenharmony_ci        "Lookup name in a namespace spanning sys.modules and __main.dict__."
2217db96d56Sopenharmony_ci        return eval(name, {**sys.modules, **__main__.__dict__})
2227db96d56Sopenharmony_ci
2237db96d56Sopenharmony_ci
2247db96d56Sopenharmony_ciAutoComplete.reload()
2257db96d56Sopenharmony_ci
2267db96d56Sopenharmony_ciif __name__ == '__main__':
2277db96d56Sopenharmony_ci    from unittest import main
2287db96d56Sopenharmony_ci    main('idlelib.idle_test.test_autocomplete', verbosity=2)
229