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