AutoComplete.py 9.02 KB
Newer Older
1 2 3 4 5 6 7 8 9
"""AutoComplete.py - An IDLE extension for automatically completing names.

This extension can complete either attribute names of file names. It can pop
a window with all available names, for the user to select from.
"""
import os
import sys
import string

10
from idlelib.configHandler import idleConf
11 12 13 14 15 16 17

# This string includes all chars that may be in an identifier
ID_CHARS = string.ascii_letters + string.digits + "_"

# These constants represent the two different types of completions
COMPLETE_ATTRIBUTES, COMPLETE_FILES = range(1, 2+1)

18 19
from idlelib import AutoCompleteWindow
from idlelib.HyperParser import HyperParser
20 21 22

import __main__

Christian Heimes's avatar
Christian Heimes committed
23 24 25 26
SEPS = os.sep
if os.altsep:  # e.g. '/' on Windows...
    SEPS += os.altsep

27 28 29 30
class AutoComplete:

    menudefs = [
        ('edit', [
31
            ("Show Completions", "<<force-open-completions>>"),
32 33 34 35 36 37 38 39
        ])
    ]

    popupwait = idleConf.GetOption("extensions", "AutoComplete",
                                   "popupwait", type="int", default=0)

    def __init__(self, editwin=None):
        self.editwin = editwin
40 41
        if editwin is None:  # subprocess and test
            return
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
        self.text = editwin.text
        self.autocompletewindow = None

        # id of delayed call, and the index of the text insert when the delayed
        # call was issued. If _delayed_completion_id is None, there is no
        # delayed call.
        self._delayed_completion_id = None
        self._delayed_completion_index = None

    def _make_autocomplete_window(self):
        return AutoCompleteWindow.AutoCompleteWindow(self.text)

    def _remove_autocomplete_window(self, event=None):
        if self.autocompletewindow:
            self.autocompletewindow.hide_window()
            self.autocompletewindow = None

    def force_open_completions_event(self, event):
        """Happens when the user really wants to open a completion list, even
        if a function call is needed.
        """
        self.open_completions(True, False, True)

    def try_open_completions_event(self, event):
        """Happens when it would be nice to open a completion list, but not
67
        really necessary, for example after an dot, so function
68 69 70 71 72 73
        calls won't be made.
        """
        lastchar = self.text.get("insert-1c")
        if lastchar == ".":
            self._open_completions_later(False, False, False,
                                         COMPLETE_ATTRIBUTES)
Christian Heimes's avatar
Christian Heimes committed
74
        elif lastchar in SEPS:
75 76 77 78
            self._open_completions_later(False, False, False,
                                         COMPLETE_FILES)

    def autocomplete_event(self, event):
79
        """Happens when the user wants to complete his word, and if necessary,
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
        open a completion list after that (if there is more than one
        completion)
        """
        if hasattr(event, "mc_state") and event.mc_state:
            # A modifier was pressed along with the tab, continue as usual.
            return
        if self.autocompletewindow and self.autocompletewindow.is_active():
            self.autocompletewindow.complete()
            return "break"
        else:
            opened = self.open_completions(False, True, True)
            if opened:
                return "break"

    def _open_completions_later(self, *args):
        self._delayed_completion_index = self.text.index("insert")
        if self._delayed_completion_id is not None:
            self.text.after_cancel(self._delayed_completion_id)
        self._delayed_completion_id = \
            self.text.after(self.popupwait, self._delayed_open_completions,
                            *args)

    def _delayed_open_completions(self, *args):
        self._delayed_completion_id = None
        if self.text.index("insert") != self._delayed_completion_index:
            return
        self.open_completions(*args)

    def open_completions(self, evalfuncs, complete, userWantsWin, mode=None):
        """Find the completions and create the AutoCompleteWindow.
        Return True if successful (no syntax error or so found).
        if complete is True, then if there's nothing to complete and no
        start of completion, won't open completions and return False.
        If mode is given, will open a completion list only in this mode.
        """
        # Cancel another delayed call, if it exists.
        if self._delayed_completion_id is not None:
            self.text.after_cancel(self._delayed_completion_id)
            self._delayed_completion_id = None

        hp = HyperParser(self.editwin, "insert")
        curline = self.text.get("insert linestart", "insert")
        i = j = len(curline)
        if hp.is_in_string() and (not mode or mode==COMPLETE_FILES):
124 125 126 127 128
            # Find the beginning of the string
            # fetch_completions will look at the file system to determine whether the
            # string value constitutes an actual file name
            # XXX could consider raw strings here and unescape the string value if it's
            # not raw.
129 130
            self._remove_autocomplete_window()
            mode = COMPLETE_FILES
131 132
            # Find last separator or string start
            while i and curline[i-1] not in "'\"" + SEPS:
133 134 135
                i -= 1
            comp_start = curline[i:j]
            j = i
136 137
            # Find string start
            while i and curline[i-1] not in "'\"":
138 139 140 141 142
                i -= 1
            comp_what = curline[i:j]
        elif hp.is_in_code() and (not mode or mode==COMPLETE_ATTRIBUTES):
            self._remove_autocomplete_window()
            mode = COMPLETE_ATTRIBUTES
143
            while i and (curline[i-1] in ID_CHARS or ord(curline[i-1]) > 127):
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
                i -= 1
            comp_start = curline[i:j]
            if i and curline[i-1] == '.':
                hp.set_index("insert-%dc" % (len(curline)-(i-1)))
                comp_what = hp.get_expression()
                if not comp_what or \
                   (not evalfuncs and comp_what.find('(') != -1):
                    return
            else:
                comp_what = ""
        else:
            return

        if complete and not comp_what and not comp_start:
            return
        comp_lists = self.fetch_completions(comp_what, mode)
        if not comp_lists[0]:
            return
        self.autocompletewindow = self._make_autocomplete_window()
        self.autocompletewindow.show_window(comp_lists,
                                            "insert-%dc" % len(comp_start),
                                            complete,
                                            mode,
                                            userWantsWin)
        return True

    def fetch_completions(self, what, mode):
        """Return a pair of lists of completions for something. The first list
        is a sublist of the second. Both are sorted.

        If there is a Python subprocess, get the comp. list there.  Otherwise,
        either fetch_completions() is running in the subprocess itself or it
        was called in an IDLE EditorWindow before any script had been run.

        The subprocess environment is that of the most recently run script.  If
        two unrelated modules are being edited some calltips in the current
        module may be inoperative if the module was not the last to run.
        """
        try:
            rpcclt = self.editwin.flist.pyshell.interp.rpcclt
        except:
            rpcclt = None
        if rpcclt:
            return rpcclt.remotecall("exec", "get_the_completion_list",
                                     (what, mode), {})
        else:
            if mode == COMPLETE_ATTRIBUTES:
                if what == "":
                    namespace = __main__.__dict__.copy()
                    namespace.update(__main__.__builtins__.__dict__)
                    bigl = eval("dir()", namespace)
                    bigl.sort()
                    if "__all__" in bigl:
197
                        smalll = sorted(eval("__all__", namespace))
198
                    else:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
199
                        smalll = [s for s in bigl if s[:1] != '_']
200 201 202 203 204 205
                else:
                    try:
                        entity = self.get_entity(what)
                        bigl = dir(entity)
                        bigl.sort()
                        if "__all__" in bigl:
206
                            smalll = sorted(entity.__all__)
207
                        else:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
208
                            smalll = [s for s in bigl if s[:1] != '_']
209 210 211 212 213 214 215 216 217 218
                    except:
                        return [], []

            elif mode == COMPLETE_FILES:
                if what == "":
                    what = "."
                try:
                    expandedpath = os.path.expanduser(what)
                    bigl = os.listdir(expandedpath)
                    bigl.sort()
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
219
                    smalll = [s for s in bigl if s[:1] != '.']
220 221 222 223 224 225 226 227 228 229 230 231
                except OSError:
                    return [], []

            if not smalll:
                smalll = bigl
            return smalll, bigl

    def get_entity(self, name):
        """Lookup name in a namespace spanning sys.modules and __main.dict__"""
        namespace = sys.modules.copy()
        namespace.update(__main__.__dict__)
        return eval(name, namespace)