1"""AutoComplete.py - An IDLE extension for automatically completing names.
2
3This extension can complete either attribute names or file names. It can pop
4a window with all available names, for the user to select from.
5"""
6import os
7import sys
8import string
9
10from idlelib.configHandler import idleConf
11
12# This string includes all chars that may be in a file name (without a path
13# separator)
14FILENAME_CHARS = string.ascii_letters + string.digits + os.curdir + "._~#$:-"
15# This string includes all chars that may be in an identifier
16ID_CHARS = string.ascii_letters + string.digits + "_"
17
18# These constants represent the two different types of completions
19COMPLETE_ATTRIBUTES, COMPLETE_FILES = range(1, 2+1)
20
21from idlelib import AutoCompleteWindow
22from idlelib.HyperParser import HyperParser
23
24import __main__
25
26SEPS = os.sep
27if os.altsep:  # e.g. '/' on Windows...
28    SEPS += os.altsep
29
30class AutoComplete:
31
32    menudefs = [
33        ('edit', [
34            ("Show Completions", "<<force-open-completions>>"),
35        ])
36    ]
37
38    popupwait = idleConf.GetOption("extensions", "AutoComplete",
39                                   "popupwait", type="int", default=0)
40
41    def __init__(self, editwin=None):
42        self.editwin = editwin
43        if editwin is None:  # subprocess and test
44            return
45        self.text = editwin.text
46        self.autocompletewindow = None
47
48        # id of delayed call, and the index of the text insert when the delayed
49        # call was issued. If _delayed_completion_id is None, there is no
50        # delayed call.
51        self._delayed_completion_id = None
52        self._delayed_completion_index = None
53
54    def _make_autocomplete_window(self):
55        return AutoCompleteWindow.AutoCompleteWindow(self.text)
56
57    def _remove_autocomplete_window(self, event=None):
58        if self.autocompletewindow:
59            self.autocompletewindow.hide_window()
60            self.autocompletewindow = None
61
62    def force_open_completions_event(self, event):
63        """Happens when the user really wants to open a completion list, even
64        if a function call is needed.
65        """
66        self.open_completions(True, False, True)
67
68    def try_open_completions_event(self, event):
69        """Happens when it would be nice to open a completion list, but not
70        really necessary, for example after a dot, so function
71        calls won't be made.
72        """
73        lastchar = self.text.get("insert-1c")
74        if lastchar == ".":
75            self._open_completions_later(False, False, False,
76                                         COMPLETE_ATTRIBUTES)
77        elif lastchar in SEPS:
78            self._open_completions_later(False, False, False,
79                                         COMPLETE_FILES)
80
81    def autocomplete_event(self, event):
82        """Happens when the user wants to complete his word, and if necessary,
83        open a completion list after that (if there is more than one
84        completion)
85        """
86        if hasattr(event, "mc_state") and event.mc_state:
87            # A modifier was pressed along with the tab, continue as usual.
88            return
89        if self.autocompletewindow and self.autocompletewindow.is_active():
90            self.autocompletewindow.complete()
91            return "break"
92        else:
93            opened = self.open_completions(False, True, True)
94            if opened:
95                return "break"
96
97    def _open_completions_later(self, *args):
98        self._delayed_completion_index = self.text.index("insert")
99        if self._delayed_completion_id is not None:
100            self.text.after_cancel(self._delayed_completion_id)
101        self._delayed_completion_id = \
102            self.text.after(self.popupwait, self._delayed_open_completions,
103                            *args)
104
105    def _delayed_open_completions(self, *args):
106        self._delayed_completion_id = None
107        if self.text.index("insert") != self._delayed_completion_index:
108            return
109        self.open_completions(*args)
110
111    def open_completions(self, evalfuncs, complete, userWantsWin, mode=None):
112        """Find the completions and create the AutoCompleteWindow.
113        Return True if successful (no syntax error or so found).
114        if complete is True, then if there's nothing to complete and no
115        start of completion, won't open completions and return False.
116        If mode is given, will open a completion list only in this mode.
117        """
118        # Cancel another delayed call, if it exists.
119        if self._delayed_completion_id is not None:
120            self.text.after_cancel(self._delayed_completion_id)
121            self._delayed_completion_id = None
122
123        hp = HyperParser(self.editwin, "insert")
124        curline = self.text.get("insert linestart", "insert")
125        i = j = len(curline)
126        if hp.is_in_string() and (not mode or mode==COMPLETE_FILES):
127            self._remove_autocomplete_window()
128            mode = COMPLETE_FILES
129            while i and curline[i-1] in FILENAME_CHARS:
130                i -= 1
131            comp_start = curline[i:j]
132            j = i
133            while i and curline[i-1] in FILENAME_CHARS + SEPS:
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:
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
148            else:
149                comp_what = ""
150        else:
151            return
152
153        if complete and not comp_what and not comp_start:
154            return
155        comp_lists = self.fetch_completions(comp_what, mode)
156        if not comp_lists[0]:
157            return
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
227if __name__ == '__main__':
228    from unittest import main
229    main('idlelib.idle_test.test_autocomplete', verbosity=2)
230