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