1"""AutoComplete.py - An IDLE extension for automatically completing names.
2
3This extension can complete either attribute names of 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 an 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        self.autocompletewindow.show_window(comp_lists,
160                                            "insert-%dc" % len(comp_start),
161                                            complete,
162                                            mode,
163                                            userWantsWin)
164        return True
165
166    def fetch_completions(self, what, mode):
167        """Return a pair of lists of completions for something. The first list
168        is a sublist of the second. Both are sorted.
169
170        If there is a Python subprocess, get the comp. list there.  Otherwise,
171        either fetch_completions() is running in the subprocess itself or it
172        was called in an IDLE EditorWindow before any script had been run.
173
174        The subprocess environment is that of the most recently run script.  If
175        two unrelated modules are being edited some calltips in the current
176        module may be inoperative if the module was not the last to run.
177        """
178        try:
179            rpcclt = self.editwin.flist.pyshell.interp.rpcclt
180        except:
181            rpcclt = None
182        if rpcclt:
183            return rpcclt.remotecall("exec", "get_the_completion_list",
184                                     (what, mode), {})
185        else:
186            if mode == COMPLETE_ATTRIBUTES:
187                if what == "":
188                    namespace = __main__.__dict__.copy()
189                    namespace.update(__main__.__builtins__.__dict__)
190                    bigl = eval("dir()", namespace)
191                    bigl.sort()
192                    if "__all__" in bigl:
193                        smalll = sorted(eval("__all__", namespace))
194                    else:
195                        smalll = [s for s in bigl if s[:1] != '_']
196                else:
197                    try:
198                        entity = self.get_entity(what)
199                        bigl = dir(entity)
200                        bigl.sort()
201                        if "__all__" in bigl:
202                            smalll = sorted(entity.__all__)
203                        else:
204                            smalll = [s for s in bigl if s[:1] != '_']
205                    except:
206                        return [], []
207
208            elif mode == COMPLETE_FILES:
209                if what == "":
210                    what = "."
211                try:
212                    expandedpath = os.path.expanduser(what)
213                    bigl = os.listdir(expandedpath)
214                    bigl.sort()
215                    smalll = [s for s in bigl if s[:1] != '.']
216                except OSError:
217                    return [], []
218
219            if not smalll:
220                smalll = bigl
221            return smalll, bigl
222
223    def get_entity(self, name):
224        """Lookup name in a namespace spanning sys.modules and __main.dict__"""
225        namespace = sys.modules.copy()
226        namespace.update(__main__.__dict__)
227        return eval(name, namespace)
228