jpayne@69: """Complete either attribute names or file names. jpayne@69: jpayne@69: Either on demand or after a user-selected delay after a key character, jpayne@69: pop up a list of candidates. jpayne@69: """ jpayne@69: import __main__ jpayne@69: import os jpayne@69: import string jpayne@69: import sys jpayne@69: jpayne@69: # Two types of completions; defined here for autocomplete_w import below. jpayne@69: ATTRS, FILES = 0, 1 jpayne@69: from idlelib import autocomplete_w jpayne@69: from idlelib.config import idleConf jpayne@69: from idlelib.hyperparser import HyperParser jpayne@69: jpayne@69: # Tuples passed to open_completions. jpayne@69: # EvalFunc, Complete, WantWin, Mode jpayne@69: FORCE = True, False, True, None # Control-Space. jpayne@69: TAB = False, True, True, None # Tab. jpayne@69: TRY_A = False, False, False, ATTRS # '.' for attributes. jpayne@69: TRY_F = False, False, False, FILES # '/' in quotes for file name. jpayne@69: jpayne@69: # This string includes all chars that may be in an identifier. jpayne@69: # TODO Update this here and elsewhere. jpayne@69: ID_CHARS = string.ascii_letters + string.digits + "_" jpayne@69: jpayne@69: SEPS = f"{os.sep}{os.altsep if os.altsep else ''}" jpayne@69: TRIGGERS = f".{SEPS}" jpayne@69: jpayne@69: class AutoComplete: jpayne@69: jpayne@69: def __init__(self, editwin=None): jpayne@69: self.editwin = editwin jpayne@69: if editwin is not None: # not in subprocess or no-gui test jpayne@69: self.text = editwin.text jpayne@69: self.autocompletewindow = None jpayne@69: # id of delayed call, and the index of the text insert when jpayne@69: # the delayed call was issued. If _delayed_completion_id is jpayne@69: # None, there is no delayed call. jpayne@69: self._delayed_completion_id = None jpayne@69: self._delayed_completion_index = None jpayne@69: jpayne@69: @classmethod jpayne@69: def reload(cls): jpayne@69: cls.popupwait = idleConf.GetOption( jpayne@69: "extensions", "AutoComplete", "popupwait", type="int", default=0) jpayne@69: jpayne@69: def _make_autocomplete_window(self): # Makes mocking easier. jpayne@69: return autocomplete_w.AutoCompleteWindow(self.text) jpayne@69: jpayne@69: def _remove_autocomplete_window(self, event=None): jpayne@69: if self.autocompletewindow: jpayne@69: self.autocompletewindow.hide_window() jpayne@69: self.autocompletewindow = None jpayne@69: jpayne@69: def force_open_completions_event(self, event): jpayne@69: "(^space) Open completion list, even if a function call is needed." jpayne@69: self.open_completions(FORCE) jpayne@69: return "break" jpayne@69: jpayne@69: def autocomplete_event(self, event): jpayne@69: "(tab) Complete word or open list if multiple options." jpayne@69: if hasattr(event, "mc_state") and event.mc_state or\ jpayne@69: not self.text.get("insert linestart", "insert").strip(): jpayne@69: # A modifier was pressed along with the tab or jpayne@69: # there is only previous whitespace on this line, so tab. jpayne@69: return None jpayne@69: if self.autocompletewindow and self.autocompletewindow.is_active(): jpayne@69: self.autocompletewindow.complete() jpayne@69: return "break" jpayne@69: else: jpayne@69: opened = self.open_completions(TAB) jpayne@69: return "break" if opened else None jpayne@69: jpayne@69: def try_open_completions_event(self, event=None): jpayne@69: "(./) Open completion list after pause with no movement." jpayne@69: lastchar = self.text.get("insert-1c") jpayne@69: if lastchar in TRIGGERS: jpayne@69: args = TRY_A if lastchar == "." else TRY_F jpayne@69: self._delayed_completion_index = self.text.index("insert") jpayne@69: if self._delayed_completion_id is not None: jpayne@69: self.text.after_cancel(self._delayed_completion_id) jpayne@69: self._delayed_completion_id = self.text.after( jpayne@69: self.popupwait, self._delayed_open_completions, args) jpayne@69: jpayne@69: def _delayed_open_completions(self, args): jpayne@69: "Call open_completions if index unchanged." jpayne@69: self._delayed_completion_id = None jpayne@69: if self.text.index("insert") == self._delayed_completion_index: jpayne@69: self.open_completions(args) jpayne@69: jpayne@69: def open_completions(self, args): jpayne@69: """Find the completions and create the AutoCompleteWindow. jpayne@69: Return True if successful (no syntax error or so found). jpayne@69: If complete is True, then if there's nothing to complete and no jpayne@69: start of completion, won't open completions and return False. jpayne@69: If mode is given, will open a completion list only in this mode. jpayne@69: """ jpayne@69: evalfuncs, complete, wantwin, mode = args jpayne@69: # Cancel another delayed call, if it exists. jpayne@69: if self._delayed_completion_id is not None: jpayne@69: self.text.after_cancel(self._delayed_completion_id) jpayne@69: self._delayed_completion_id = None jpayne@69: jpayne@69: hp = HyperParser(self.editwin, "insert") jpayne@69: curline = self.text.get("insert linestart", "insert") jpayne@69: i = j = len(curline) jpayne@69: if hp.is_in_string() and (not mode or mode==FILES): jpayne@69: # Find the beginning of the string. jpayne@69: # fetch_completions will look at the file system to determine jpayne@69: # whether the string value constitutes an actual file name jpayne@69: # XXX could consider raw strings here and unescape the string jpayne@69: # value if it's not raw. jpayne@69: self._remove_autocomplete_window() jpayne@69: mode = FILES jpayne@69: # Find last separator or string start jpayne@69: while i and curline[i-1] not in "'\"" + SEPS: jpayne@69: i -= 1 jpayne@69: comp_start = curline[i:j] jpayne@69: j = i jpayne@69: # Find string start jpayne@69: while i and curline[i-1] not in "'\"": jpayne@69: i -= 1 jpayne@69: comp_what = curline[i:j] jpayne@69: elif hp.is_in_code() and (not mode or mode==ATTRS): jpayne@69: self._remove_autocomplete_window() jpayne@69: mode = ATTRS jpayne@69: while i and (curline[i-1] in ID_CHARS or ord(curline[i-1]) > 127): jpayne@69: i -= 1 jpayne@69: comp_start = curline[i:j] jpayne@69: if i and curline[i-1] == '.': # Need object with attributes. jpayne@69: hp.set_index("insert-%dc" % (len(curline)-(i-1))) jpayne@69: comp_what = hp.get_expression() jpayne@69: if (not comp_what or jpayne@69: (not evalfuncs and comp_what.find('(') != -1)): jpayne@69: return None jpayne@69: else: jpayne@69: comp_what = "" jpayne@69: else: jpayne@69: return None jpayne@69: jpayne@69: if complete and not comp_what and not comp_start: jpayne@69: return None jpayne@69: comp_lists = self.fetch_completions(comp_what, mode) jpayne@69: if not comp_lists[0]: jpayne@69: return None jpayne@69: self.autocompletewindow = self._make_autocomplete_window() jpayne@69: return not self.autocompletewindow.show_window( jpayne@69: comp_lists, "insert-%dc" % len(comp_start), jpayne@69: complete, mode, wantwin) jpayne@69: jpayne@69: def fetch_completions(self, what, mode): jpayne@69: """Return a pair of lists of completions for something. The first list jpayne@69: is a sublist of the second. Both are sorted. jpayne@69: jpayne@69: If there is a Python subprocess, get the comp. list there. Otherwise, jpayne@69: either fetch_completions() is running in the subprocess itself or it jpayne@69: was called in an IDLE EditorWindow before any script had been run. jpayne@69: jpayne@69: The subprocess environment is that of the most recently run script. If jpayne@69: two unrelated modules are being edited some calltips in the current jpayne@69: module may be inoperative if the module was not the last to run. jpayne@69: """ jpayne@69: try: jpayne@69: rpcclt = self.editwin.flist.pyshell.interp.rpcclt jpayne@69: except: jpayne@69: rpcclt = None jpayne@69: if rpcclt: jpayne@69: return rpcclt.remotecall("exec", "get_the_completion_list", jpayne@69: (what, mode), {}) jpayne@69: else: jpayne@69: if mode == ATTRS: jpayne@69: if what == "": jpayne@69: namespace = {**__main__.__builtins__.__dict__, jpayne@69: **__main__.__dict__} jpayne@69: bigl = eval("dir()", namespace) jpayne@69: bigl.sort() jpayne@69: if "__all__" in bigl: jpayne@69: smalll = sorted(eval("__all__", namespace)) jpayne@69: else: jpayne@69: smalll = [s for s in bigl if s[:1] != '_'] jpayne@69: else: jpayne@69: try: jpayne@69: entity = self.get_entity(what) jpayne@69: bigl = dir(entity) jpayne@69: bigl.sort() jpayne@69: if "__all__" in bigl: jpayne@69: smalll = sorted(entity.__all__) jpayne@69: else: jpayne@69: smalll = [s for s in bigl if s[:1] != '_'] jpayne@69: except: jpayne@69: return [], [] jpayne@69: jpayne@69: elif mode == FILES: jpayne@69: if what == "": jpayne@69: what = "." jpayne@69: try: jpayne@69: expandedpath = os.path.expanduser(what) jpayne@69: bigl = os.listdir(expandedpath) jpayne@69: bigl.sort() jpayne@69: smalll = [s for s in bigl if s[:1] != '.'] jpayne@69: except OSError: jpayne@69: return [], [] jpayne@69: jpayne@69: if not smalll: jpayne@69: smalll = bigl jpayne@69: return smalll, bigl jpayne@69: jpayne@69: def get_entity(self, name): jpayne@69: "Lookup name in a namespace spanning sys.modules and __main.dict__." jpayne@69: return eval(name, {**sys.modules, **__main__.__dict__}) jpayne@69: jpayne@69: jpayne@69: AutoComplete.reload() jpayne@69: jpayne@69: if __name__ == '__main__': jpayne@69: from unittest import main jpayne@69: main('idlelib.idle_test.test_autocomplete', verbosity=2)