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