1"""Complete either attribute names or file names.
2
3Either on demand or after a user-selected delay after a key character,
4pop up a list of candidates.
5"""
6import __main__
7import keyword
8import os
9import string
10import sys
11
12# Two types of completions; defined here for autocomplete_w import below.
13ATTRS, FILES = 0, 1
14from idlelib import autocomplete_w
15from idlelib.config import idleConf
16from idlelib.hyperparser import HyperParser
17
18# Tuples passed to open_completions.
19#       EvalFunc, Complete, WantWin, Mode
20FORCE = True,     False,    True,    None   # Control-Space.
21TAB   = False,    True,     True,    None   # Tab.
22TRY_A = False,    False,    False,   ATTRS  # '.' for attributes.
23TRY_F = False,    False,    False,   FILES  # '/' in quotes for file name.
24
25# This string includes all chars that may be in an identifier.
26# TODO Update this here and elsewhere.
27ID_CHARS = string.ascii_letters + string.digits + "_"
28
29SEPS = f"{os.sep}{os.altsep if os.altsep else ''}"
30TRIGGERS = f".{SEPS}"
31
32class AutoComplete:
33
34    def __init__(self, editwin=None):
35        self.editwin = editwin
36        if editwin is not None:   # not in subprocess or no-gui test
37            self.text = editwin.text
38        self.autocompletewindow = None
39        # id of delayed call, and the index of the text insert when
40        # the delayed call was issued. If _delayed_completion_id is
41        # None, there is no delayed call.
42        self._delayed_completion_id = None
43        self._delayed_completion_index = None
44
45    @classmethod
46    def reload(cls):
47        cls.popupwait = idleConf.GetOption(
48            "extensions", "AutoComplete", "popupwait", type="int", default=0)
49
50    def _make_autocomplete_window(self):  # Makes mocking easier.
51        return autocomplete_w.AutoCompleteWindow(self.text)
52
53    def _remove_autocomplete_window(self, event=None):
54        if self.autocompletewindow:
55            self.autocompletewindow.hide_window()
56            self.autocompletewindow = None
57
58    def force_open_completions_event(self, event):
59        "(^space) Open completion list, even if a function call is needed."
60        self.open_completions(FORCE)
61        return "break"
62
63    def autocomplete_event(self, event):
64        "(tab) Complete word or open list if multiple options."
65        if hasattr(event, "mc_state") and event.mc_state or\
66                not self.text.get("insert linestart", "insert").strip():
67            # A modifier was pressed along with the tab or
68            # there is only previous whitespace on this line, so tab.
69            return None
70        if self.autocompletewindow and self.autocompletewindow.is_active():
71            self.autocompletewindow.complete()
72            return "break"
73        else:
74            opened = self.open_completions(TAB)
75            return "break" if opened else None
76
77    def try_open_completions_event(self, event=None):
78        "(./) Open completion list after pause with no movement."
79        lastchar = self.text.get("insert-1c")
80        if lastchar in TRIGGERS:
81            args = TRY_A if lastchar == "." else TRY_F
82            self._delayed_completion_index = self.text.index("insert")
83            if self._delayed_completion_id is not None:
84                self.text.after_cancel(self._delayed_completion_id)
85            self._delayed_completion_id = self.text.after(
86                self.popupwait, self._delayed_open_completions, args)
87
88    def _delayed_open_completions(self, args):
89        "Call open_completions if index unchanged."
90        self._delayed_completion_id = None
91        if self.text.index("insert") == self._delayed_completion_index:
92            self.open_completions(args)
93
94    def open_completions(self, args):
95        """Find the completions and create the AutoCompleteWindow.
96        Return True if successful (no syntax error or so found).
97        If complete is True, then if there's nothing to complete and no
98        start of completion, won't open completions and return False.
99        If mode is given, will open a completion list only in this mode.
100        """
101        evalfuncs, complete, wantwin, mode = args
102        # Cancel another delayed call, if it exists.
103        if self._delayed_completion_id is not None:
104            self.text.after_cancel(self._delayed_completion_id)
105            self._delayed_completion_id = None
106
107        hp = HyperParser(self.editwin, "insert")
108        curline = self.text.get("insert linestart", "insert")
109        i = j = len(curline)
110        if hp.is_in_string() and (not mode or mode==FILES):
111            # Find the beginning of the string.
112            # fetch_completions will look at the file system to determine
113            # whether the string value constitutes an actual file name
114            # XXX could consider raw strings here and unescape the string
115            # value if it's not raw.
116            self._remove_autocomplete_window()
117            mode = FILES
118            # Find last separator or string start
119            while i and curline[i-1] not in "'\"" + SEPS:
120                i -= 1
121            comp_start = curline[i:j]
122            j = i
123            # Find string start
124            while i and curline[i-1] not in "'\"":
125                i -= 1
126            comp_what = curline[i:j]
127        elif hp.is_in_code() and (not mode or mode==ATTRS):
128            self._remove_autocomplete_window()
129            mode = ATTRS
130            while i and (curline[i-1] in ID_CHARS or ord(curline[i-1]) > 127):
131                i -= 1
132            comp_start = curline[i:j]
133            if i and curline[i-1] == '.':  # Need object with attributes.
134                hp.set_index("insert-%dc" % (len(curline)-(i-1)))
135                comp_what = hp.get_expression()
136                if (not comp_what or
137                   (not evalfuncs and comp_what.find('(') != -1)):
138                    return None
139            else:
140                comp_what = ""
141        else:
142            return None
143
144        if complete and not comp_what and not comp_start:
145            return None
146        comp_lists = self.fetch_completions(comp_what, mode)
147        if not comp_lists[0]:
148            return None
149        self.autocompletewindow = self._make_autocomplete_window()
150        return not self.autocompletewindow.show_window(
151                comp_lists, "insert-%dc" % len(comp_start),
152                complete, mode, wantwin)
153
154    def fetch_completions(self, what, mode):
155        """Return a pair of lists of completions for something. The first list
156        is a sublist of the second. Both are sorted.
157
158        If there is a Python subprocess, get the comp. list there.  Otherwise,
159        either fetch_completions() is running in the subprocess itself or it
160        was called in an IDLE EditorWindow before any script had been run.
161
162        The subprocess environment is that of the most recently run script.  If
163        two unrelated modules are being edited some calltips in the current
164        module may be inoperative if the module was not the last to run.
165        """
166        try:
167            rpcclt = self.editwin.flist.pyshell.interp.rpcclt
168        except:
169            rpcclt = None
170        if rpcclt:
171            return rpcclt.remotecall("exec", "get_the_completion_list",
172                                     (what, mode), {})
173        else:
174            if mode == ATTRS:
175                if what == "":  # Main module names.
176                    namespace = {**__main__.__builtins__.__dict__,
177                                 **__main__.__dict__}
178                    bigl = eval("dir()", namespace)
179                    kwds = (s for s in keyword.kwlist
180                            if s not in {'True', 'False', 'None'})
181                    bigl.extend(kwds)
182                    bigl.sort()
183                    if "__all__" in bigl:
184                        smalll = sorted(eval("__all__", namespace))
185                    else:
186                        smalll = [s for s in bigl if s[:1] != '_']
187                else:
188                    try:
189                        entity = self.get_entity(what)
190                        bigl = dir(entity)
191                        bigl.sort()
192                        if "__all__" in bigl:
193                            smalll = sorted(entity.__all__)
194                        else:
195                            smalll = [s for s in bigl if s[:1] != '_']
196                    except:
197                        return [], []
198
199            elif mode == FILES:
200                if what == "":
201                    what = "."
202                try:
203                    expandedpath = os.path.expanduser(what)
204                    bigl = os.listdir(expandedpath)
205                    bigl.sort()
206                    smalll = [s for s in bigl if s[:1] != '.']
207                except OSError:
208                    return [], []
209
210            if not smalll:
211                smalll = bigl
212            return smalll, bigl
213
214    def get_entity(self, name):
215        "Lookup name in a namespace spanning sys.modules and __main.dict__."
216        return eval(name, {**sys.modules, **__main__.__dict__})
217
218
219AutoComplete.reload()
220
221if __name__ == '__main__':
222    from unittest import main
223    main('idlelib.idle_test.test_autocomplete', verbosity=2)
224