Index: AutoComplete.py =================================================================== RCS file: AutoComplete.py diff -N AutoComplete.py --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ AutoComplete.py 10 Jul 2005 08:05:29 -0000 1.2.2.2 @@ -0,0 +1,226 @@ +"""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 + +from configHandler import idleConf + +import AutoCompleteWindow +from HyperParser import HyperParser + +import __main__ + +# This string includes all chars that may be in a file name (without a path +# separator) +FILENAME_CHARS = string.ascii_letters + string.digits + os.curdir + "._~#$:-" +# 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) + +class AutoComplete: + + menudefs = [ + ('edit', [ + ("Show completions", "<>"), + ]) + ] + + popupwait = idleConf.GetOption("extensions", "AutoComplete", + "popupwait", type="int", default=0) + + def __init__(self, editwin=None): + if editwin == None: # subprocess and test + self.editwin = None + return + self.editwin = editwin + 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 + really neccesary, for example after an dot, so function + calls won't be made. + """ + lastchar = self.text.get("insert-1c") + if lastchar == ".": + self._open_completions_later(False, False, False, + COMPLETE_ATTRIBUTES) + elif lastchar == os.sep: + self._open_completions_later(False, False, False, + COMPLETE_FILES) + + def autocomplete_event(self, event): + """Happens when the user wants to complete his word, and if neccesary, + 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): + self._remove_autocomplete_window() + mode = COMPLETE_FILES + while i and curline[i-1] in FILENAME_CHARS: + i -= 1 + comp_start = curline[i:j] + j = i + while i and curline[i-1] in FILENAME_CHARS+os.sep: + 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 + while i and curline[i-1] in ID_CHARS: + 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: + smalll = eval("__all__", namespace) + smalll.sort() + else: + smalll = filter(lambda s: s[:1] != '_', bigl) + else: + try: + entity = self.get_entity(what) + bigl = dir(entity) + bigl.sort() + if "__all__" in bigl: + smalll = entity.__all__ + smalll.sort() + else: + smalll = filter(lambda s: s[:1] != '_', bigl) + except: + return [], [] + + elif mode == COMPLETE_FILES: + if what == "": + what = "." + try: + expandedpath = os.path.expanduser(what) + bigl = os.listdir(expandedpath) + bigl.sort() + smalll = filter(lambda s: s[:1] != '.', bigl) + 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) Index: AutoCompleteWindow.py =================================================================== RCS file: AutoCompleteWindow.py diff -N AutoCompleteWindow.py --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ AutoCompleteWindow.py 10 Jul 2005 07:30:18 -0000 1.3.2.1 @@ -0,0 +1,395 @@ +""" +An auto-completion window for IDLE, used by the AutoComplete extension +""" +from Tkinter import * +from MultiCall import MC_SHIFT +import AutoComplete + +HIDE_VIRTUAL_EVENT_NAME = "<>" +HIDE_SEQUENCES = ("", "") +KEYPRESS_VIRTUAL_EVENT_NAME = "<>" +# We need to bind event beyond so that the function will be called +# before the default specific IDLE function +KEYPRESS_SEQUENCES = ("", "", "", + "", "", "", "") +KEYRELEASE_VIRTUAL_EVENT_NAME = "<>" +KEYRELEASE_SEQUENCE = "" +LISTUPDATE_SEQUENCE = "" +WINCONFIG_SEQUENCE = "" +DOUBLECLICK_SEQUENCE = "" + +class AutoCompleteWindow: + + def __init__(self, widget): + # The widget (Text) on which we place the AutoCompleteWindow + self.widget = widget + # The widgets we create + self.autocompletewindow = self.listbox = self.scrollbar = None + # The default foreground and background of a selection. Saved because + # they are changed to the regular colors of list items when the + # completion start is not a prefix of the selected completion + self.origselforeground = self.origselbackground = None + # The list of completions + self.completions = None + # A list with more completions, or None + self.morecompletions = None + # The completion mode. Either AutoComplete.COMPLETE_ATTRIBUTES or + # AutoComplete.COMPLETE_FILES + self.mode = None + # The current completion start, on the text box (a string) + self.start = None + # The index of the start of the completion + self.startindex = None + # The last typed start, used so that when the selection changes, + # the new start will be as close as possible to the last typed one. + self.lasttypedstart = None + # Do we have an indication that the user wants the completion window + # (for example, he clicked the list) + self.userwantswindow = None + # event ids + self.hideid = self.keypressid = self.listupdateid = self.winconfigid \ + = self.keyreleaseid = self.doubleclickid = None + + def _change_start(self, newstart): + i = 0 + while i < len(self.start) and i < len(newstart) and \ + self.start[i] == newstart[i]: + i += 1 + if i < len(self.start): + self.widget.delete("%s+%dc" % (self.startindex, i), + "%s+%dc" % (self.startindex, len(self.start))) + if i < len(newstart): + self.widget.insert("%s+%dc" % (self.startindex, i), + newstart[i:]) + self.start = newstart + + def _binary_search(self, s): + """Find the first index in self.completions where completions[i] is + greater or equal to s, or the last index if there is no such + one.""" + i = 0; j = len(self.completions) + while j > i: + m = (i + j) // 2 + if self.completions[m] >= s: + j = m + else: + i = m + 1 + return min(i, len(self.completions)-1) + + def _complete_string(self, s): + """Assuming that s is the prefix of a string in self.completions, + return the longest string which is a prefix of all the strings which + s is a prefix of them. If s is not a prefix of a string, return s.""" + first = self._binary_search(s) + if self.completions[first][:len(s)] != s: + # There is not even one completion which s is a prefix of. + return s + # Find the end of the range of completions where s is a prefix of. + i = first + 1 + j = len(self.completions) + while j > i: + m = (i + j) // 2 + if self.completions[m][:len(s)] != s: + j = m + else: + i = m + 1 + last = i-1 + + # We should return the maximum prefix of first and last + i = len(s) + while len(self.completions[first]) > i and \ + len(self.completions[last]) > i and \ + self.completions[first][i] == self.completions[last][i]: + i += 1 + return self.completions[first][:i] + + def _selection_changed(self): + """Should be called when the selection of the Listbox has changed. + Updates the Listbox display and calls _change_start.""" + cursel = int(self.listbox.curselection()[0]) + + self.listbox.see(cursel) + + lts = self.lasttypedstart + selstart = self.completions[cursel] + if self._binary_search(lts) == cursel: + newstart = lts + else: + i = 0 + while i < len(lts) and i < len(selstart) and lts[i] == selstart[i]: + i += 1 + while cursel > 0 and selstart[:i] <= self.completions[cursel-1]: + i += 1 + newstart = selstart[:i] + self._change_start(newstart) + + if self.completions[cursel][:len(self.start)] == self.start: + # start is a prefix of the selected completion + self.listbox.configure(selectbackground=self.origselbackground, + selectforeground=self.origselforeground) + else: + self.listbox.configure(selectbackground=self.listbox.cget("bg"), + selectforeground=self.listbox.cget("fg")) + # If there are more completions, show them, and call me again. + if self.morecompletions: + self.completions = self.morecompletions + self.morecompletions = None + self.listbox.delete(0, END) + for item in self.completions: + self.listbox.insert(END, item) + self.listbox.select_set(self._binary_search(self.start)) + self._selection_changed() + + def show_window(self, comp_lists, index, complete, mode, userWantsWin): + """Show the autocomplete list, bind events. + If complete is True, complete the text, and if there is exactly one + matching completion, don't open a list.""" + # Handle the start we already have + self.completions, self.morecompletions = comp_lists + self.mode = mode + self.startindex = self.widget.index(index) + self.start = self.widget.get(self.startindex, "insert") + if complete: + completed = self._complete_string(self.start) + self._change_start(completed) + i = self._binary_search(completed) + if self.completions[i] == completed and \ + (i == len(self.completions)-1 or + self.completions[i+1][:len(completed)] != completed): + # There is exactly one matching completion + return + self.userwantswindow = userWantsWin + self.lasttypedstart = self.start + + # Put widgets in place + self.autocompletewindow = acw = Toplevel(self.widget) + # Put it in a position so that it is not seen. + acw.wm_geometry("+10000+10000") + # Make it float + acw.wm_overrideredirect(1) + try: + # This command is only needed and available on Tk >= 8.4.0 for OSX + # Without it, call tips intrude on the typing process by grabbing + # the focus. + acw.tk.call("::tk::unsupported::MacWindowStyle", "style", acw._w, + "help", "noActivates") + except TclError: + pass + self.scrollbar = scrollbar = Scrollbar(acw, orient=VERTICAL) + self.listbox = listbox = Listbox(acw, yscrollcommand=scrollbar.set, + exportselection=False, bg="white") + for item in self.completions: + listbox.insert(END, item) + self.origselforeground = listbox.cget("selectforeground") + self.origselbackground = listbox.cget("selectbackground") + scrollbar.config(command=listbox.yview) + scrollbar.pack(side=RIGHT, fill=Y) + listbox.pack(side=LEFT, fill=BOTH, expand=True) + + # Initialize the listbox selection + self.listbox.select_set(self._binary_search(self.start)) + self._selection_changed() + + # bind events + self.hideid = self.widget.bind(HIDE_VIRTUAL_EVENT_NAME, + self.hide_event) + for seq in HIDE_SEQUENCES: + self.widget.event_add(HIDE_VIRTUAL_EVENT_NAME, seq) + self.keypressid = self.widget.bind(KEYPRESS_VIRTUAL_EVENT_NAME, + self.keypress_event) + for seq in KEYPRESS_SEQUENCES: + self.widget.event_add(KEYPRESS_VIRTUAL_EVENT_NAME, seq) + self.keyreleaseid = self.widget.bind(KEYRELEASE_VIRTUAL_EVENT_NAME, + self.keyrelease_event) + self.widget.event_add(KEYRELEASE_VIRTUAL_EVENT_NAME,KEYRELEASE_SEQUENCE) + self.listupdateid = listbox.bind(LISTUPDATE_SEQUENCE, + self.listupdate_event) + self.winconfigid = acw.bind(WINCONFIG_SEQUENCE, self.winconfig_event) + self.doubleclickid = listbox.bind(DOUBLECLICK_SEQUENCE, + self.doubleclick_event) + + def winconfig_event(self, event): + if not self.is_active(): + return + # Position the completion list window + acw = self.autocompletewindow + self.widget.see(self.startindex) + x, y, cx, cy = self.widget.bbox(self.startindex) + acw.wm_geometry("+%d+%d" % (x + self.widget.winfo_rootx(), + y + self.widget.winfo_rooty() \ + -acw.winfo_height())) + + + def hide_event(self, event): + if not self.is_active(): + return + self.hide_window() + + def listupdate_event(self, event): + if not self.is_active(): + return + self.userwantswindow = True + self._selection_changed() + + def doubleclick_event(self, event): + # Put the selected completion in the text, and close the list + cursel = int(self.listbox.curselection()[0]) + self._change_start(self.completions[cursel]) + self.hide_window() + + def keypress_event(self, event): + if not self.is_active(): + return + keysym = event.keysym + if hasattr(event, "mc_state"): + state = event.mc_state + else: + state = 0 + + if (len(keysym) == 1 or keysym in ("underscore", "BackSpace") + or (self.mode==AutoComplete.COMPLETE_FILES and keysym in + ("period", "minus"))) \ + and not (state & ~MC_SHIFT): + # Normal editing of text + if len(keysym) == 1: + self._change_start(self.start + keysym) + elif keysym == "underscore": + self._change_start(self.start + '_') + elif keysym == "period": + self._change_start(self.start + '.') + elif keysym == "minus": + self._change_start(self.start + '-') + else: + # keysym == "BackSpace" + if len(self.start) == 0: + self.hide_window() + return + self._change_start(self.start[:-1]) + self.lasttypedstart = self.start + self.listbox.select_clear(0, int(self.listbox.curselection()[0])) + self.listbox.select_set(self._binary_search(self.start)) + self._selection_changed() + return "break" + + elif keysym == "Return" and not state: + # If start is a prefix of the selection, or there was an indication + # that the user used the completion window, put the selected + # completion in the text, and close the list. + # Otherwise, close the window and let the event through. + cursel = int(self.listbox.curselection()[0]) + if self.completions[cursel][:len(self.start)] == self.start or \ + self.userwantswindow: + self._change_start(self.completions[cursel]) + self.hide_window() + return "break" + else: + self.hide_window() + return + + elif (self.mode == AutoComplete.COMPLETE_ATTRIBUTES and keysym in + ("period", "space", "parenleft", "parenright", "bracketleft", + "bracketright")) or \ + (self.mode == AutoComplete.COMPLETE_FILES and keysym in + ("slash", "backslash", "quotedbl", "apostrophe")) \ + and not (state & ~MC_SHIFT): + # If start is a prefix of the selection, but is not '' when + # completing file names, put the whole + # selected completion. Anyway, close the list. + cursel = int(self.listbox.curselection()[0]) + if self.completions[cursel][:len(self.start)] == self.start \ + and (self.mode==AutoComplete.COMPLETE_ATTRIBUTES or self.start): + self._change_start(self.completions[cursel]) + self.hide_window() + return + + elif keysym in ("Home", "End", "Prior", "Next", "Up", "Down") and \ + not state: + # Move the selection in the listbox + self.userwantswindow = True + cursel = int(self.listbox.curselection()[0]) + if keysym == "Home": + newsel = 0 + elif keysym == "End": + newsel = len(self.completions)-1 + elif keysym in ("Prior", "Next"): + jump = self.listbox.nearest(self.listbox.winfo_height()) - \ + self.listbox.nearest(0) + if keysym == "Prior": + newsel = max(0, cursel-jump) + else: + assert keysym == "Next" + newsel = min(len(self.completions)-1, cursel+jump) + elif keysym == "Up": + newsel = max(0, cursel-1) + else: + assert keysym == "Down" + newsel = min(len(self.completions)-1, cursel+1) + self.listbox.select_clear(cursel) + self.listbox.select_set(newsel) + self._selection_changed() + return "break" + + elif (keysym == "Tab" and not state): + # The user wants a completion, but it is handled by AutoComplete + # (not AutoCompleteWindow), so ignore. + self.userwantswindow = True + return + + elif reduce(lambda x, y: x or y, + [keysym.find(s) != -1 for s in ("Shift", "Control", "Alt", + "Meta", "Command", "Option") + ]): + # A modifier key, so ignore + return + + else: + # Unknown event, close the window and let it through. + self.hide_window() + return + + def keyrelease_event(self, event): + if not self.is_active(): + return + if self.widget.index("insert") != \ + self.widget.index("%s+%dc" % (self.startindex, len(self.start))): + # If we didn't catch an event which moved the insert, close window + self.hide_window() + + def is_active(self): + return self.autocompletewindow is not None + + def complete(self): + self._change_start(self._complete_string(self.start)) + # The selection doesn't change. + + def hide_window(self): + if not self.is_active(): + return + + # unbind events + for seq in HIDE_SEQUENCES: + self.widget.event_delete(HIDE_VIRTUAL_EVENT_NAME, seq) + self.widget.unbind(HIDE_VIRTUAL_EVENT_NAME, self.hideid) + self.hideid = None + for seq in KEYPRESS_SEQUENCES: + self.widget.event_delete(KEYPRESS_VIRTUAL_EVENT_NAME, seq) + self.widget.unbind(KEYPRESS_VIRTUAL_EVENT_NAME, self.keypressid) + self.keypressid = None + self.widget.event_delete(KEYRELEASE_VIRTUAL_EVENT_NAME, + KEYRELEASE_SEQUENCE) + self.widget.unbind(KEYRELEASE_VIRTUAL_EVENT_NAME, self.keyreleaseid) + self.keyreleaseid = None + self.listbox.unbind(LISTUPDATE_SEQUENCE, self.listupdateid) + self.listupdateid = None + self.autocompletewindow.unbind(WINCONFIG_SEQUENCE, self.winconfigid) + self.winconfigid = None + + # destroy widgets + self.scrollbar.destroy() + self.scrollbar = None + self.listbox.destroy() + self.listbox = None + self.autocompletewindow.destroy() + self.autocompletewindow = None + + Index: CallTipWindow.py =================================================================== RCS file: /var/lib/cvs/mydlelib/CallTipWindow.py,v retrieving revision 1.1.1.1 retrieving revision 1.1.1.1.2.1 diff -u -r1.1.1.1 -r1.1.1.1.2.1 --- CallTipWindow.py 28 Feb 2004 18:00:04 -0000 1.1.1.1 +++ CallTipWindow.py 10 Jul 2005 07:30:18 -0000 1.1.1.1.2.1 @@ -6,33 +6,65 @@ """ from Tkinter import * +HIDE_VIRTUAL_EVENT_NAME = "<>" +HIDE_SEQUENCES = ("", "") +CHECKHIDE_VIRTUAL_EVENT_NAME = "<>" +CHECKHIDE_SEQUENCES = ("", "") +CHECKHIDE_TIME = 100 # miliseconds + +MARK_RIGHT = "calltipwindowregion_right" + class CallTip: def __init__(self, widget): self.widget = widget - self.tipwindow = None - self.id = None - self.x = self.y = 0 - - def showtip(self, text): - " Display text in calltip window" + self.tipwindow = self.label = None + self.parenline = self.parencol = None + self.lastline = None + self.hideid = self.checkhideid = None + + def position_window(self): + """Check if needs to reposition the window, and if so - do it.""" + curline = int(self.widget.index("insert").split('.')[0]) + if curline == self.lastline: + return + self.lastline = curline + self.widget.see("insert") + if curline == self.parenline: + box = self.widget.bbox("%d.%d" % (self.parenline, + self.parencol)) + else: + box = self.widget.bbox("%d.0" % curline) + if not box: + box = list(self.widget.bbox("insert")) + # align to left of window + box[0] = 0 + box[2] = 0 + x = box[0] + self.widget.winfo_rootx() + 2 + y = box[1] + box[3] + self.widget.winfo_rooty() + self.tipwindow.wm_geometry("+%d+%d" % (x, y)) + + def showtip(self, text, parenleft, parenright): + """Show the calltip, bind events which will close it and reposition it. + """ # truncate overly long calltip if len(text) >= 79: text = text[:75] + ' ...' self.text = text if self.tipwindow or not self.text: return - self.widget.see("insert") - x, y, cx, cy = self.widget.bbox("insert") - x = x + self.widget.winfo_rootx() + 2 - y = y + cy + self.widget.winfo_rooty() + + self.widget.mark_set(MARK_RIGHT, parenright) + self.parenline, self.parencol = map( + int, self.widget.index(parenleft).split(".")) + self.tipwindow = tw = Toplevel(self.widget) + self.position_window() # XXX 12 Dec 2002 KBK The following command has two effects: It removes # the calltip window border (good) but also causes (at least on # Linux) the calltip to show as a top level window, burning through # any other window dragged over it. Also, shows on all viewports! tw.wm_overrideredirect(1) - tw.wm_geometry("+%d+%d" % (x, y)) try: # This command is only needed and available on Tk >= 8.4.0 for OSX # Without it, call tips intrude on the typing process by grabbing @@ -41,16 +73,66 @@ "help", "noActivates") except TclError: pass - label = Label(tw, text=self.text, justify=LEFT, - background="#ffffe0", relief=SOLID, borderwidth=1, - font = self.widget['font']) - label.pack() + self.label = Label(tw, text=self.text, justify=LEFT, + background="#ffffe0", relief=SOLID, borderwidth=1, + font = self.widget['font']) + self.label.pack() + + self.checkhideid = self.widget.bind(CHECKHIDE_VIRTUAL_EVENT_NAME, + self.checkhide_event) + for seq in CHECKHIDE_SEQUENCES: + self.widget.event_add(CHECKHIDE_VIRTUAL_EVENT_NAME, seq) + self.widget.after(CHECKHIDE_TIME, self.checkhide_event) + self.hideid = self.widget.bind(HIDE_VIRTUAL_EVENT_NAME, + self.hide_event) + for seq in HIDE_SEQUENCES: + self.widget.event_add(HIDE_VIRTUAL_EVENT_NAME, seq) + + def checkhide_event(self, event=None): + if not self.tipwindow: + # If the event was triggered by the same event that unbinded + # this function, the function will be called nevertheless, + # so do nothing in this case. + return + curline, curcol = map(int, self.widget.index("insert").split('.')) + if curline < self.parenline or \ + (curline == self.parenline and curcol <= self.parencol) or \ + self.widget.compare("insert", ">", MARK_RIGHT): + self.hidetip() + else: + self.position_window() + self.widget.after(CHECKHIDE_TIME, self.checkhide_event) + + def hide_event(self, event): + if not self.tipwindow: + # See the explanation in checkhide_event. + return + self.hidetip() def hidetip(self): - tw = self.tipwindow + if not self.tipwindow: + return + + for seq in CHECKHIDE_SEQUENCES: + self.widget.event_delete(CHECKHIDE_VIRTUAL_EVENT_NAME, seq) + self.widget.unbind(CHECKHIDE_VIRTUAL_EVENT_NAME, self.checkhideid) + self.checkhideid = None + for seq in HIDE_SEQUENCES: + self.widget.event_delete(HIDE_VIRTUAL_EVENT_NAME, seq) + self.widget.unbind(HIDE_VIRTUAL_EVENT_NAME, self.hideid) + self.hideid = None + + self.label.destroy() + self.label = None + self.tipwindow.destroy() self.tipwindow = None - if tw: - tw.destroy() + + self.widget.mark_unset(MARK_RIGHT) + self.parenline = self.parencol = self.lastline = None + + def is_active(self): + return bool(self.tipwindow) + ############################### Index: CallTips.py =================================================================== RCS file: /var/lib/cvs/mydlelib/CallTips.py,v retrieving revision 1.1.1.2 retrieving revision 1.1.1.2.2.1 diff -u -r1.1.1.2 -r1.1.1.2.2.1 --- CallTips.py 10 Jun 2004 20:32:38 -0000 1.1.1.2 +++ CallTips.py 10 Jul 2005 07:30:18 -0000 1.1.1.2.2.1 @@ -3,21 +3,21 @@ Call Tips are floating windows which display function, class, and method parameter and docstring information when you type an opening parenthesis, and which disappear when you type a closing parenthesis. - -Future plans include extending the functionality to include class attributes. - """ import sys -import string import types import CallTipWindow +from HyperParser import HyperParser import __main__ class CallTips: menudefs = [ + ('edit', [ + ("Show call tip", "<>"), + ]) ] def __init__(self, editwin=None): @@ -36,51 +36,47 @@ # See __init__ for usage return CallTipWindow.CallTip(self.text) - def _remove_calltip_window(self): + def _remove_calltip_window(self, event=None): if self.calltip: self.calltip.hidetip() self.calltip = None - def paren_open_event(self, event): - self._remove_calltip_window() - name = self.get_name_at_cursor() - arg_text = self.fetch_tip(name) - if arg_text: - self.calltip_start = self.text.index("insert") - self.calltip = self._make_calltip_window() - self.calltip.showtip(arg_text) - return "" #so the event is handled normally. - - def paren_close_event(self, event): - # Now just hides, but later we should check if other - # paren'd expressions remain open. - self._remove_calltip_window() - return "" #so the event is handled normally. + def force_open_calltip_event(self, event): + """Happens when the user really wants to open a CallTip, even if a + function call is needed. + """ + self.open_calltip(True) - def check_calltip_cancel_event(self, event): - if self.calltip: - # If we have moved before the start of the calltip, - # or off the calltip line, then cancel the tip. - # (Later need to be smarter about multi-line, etc) - if self.text.compare("insert", "<=", self.calltip_start) or \ - self.text.compare("insert", ">", self.calltip_start - + " lineend"): - self._remove_calltip_window() - return "" #so the event is handled normally. + def try_open_calltip_event(self, event): + """Happens when it would be nice to open a CallTip, but not really + neccesary, for example after an opening bracket, so function calls + won't be made. + """ + self.open_calltip(False) - def calltip_cancel_event(self, event): - self._remove_calltip_window() - return "" #so the event is handled normally. + def refresh_calltip_event(self, event): + """If there is already a calltip window, check if it is still needed, + and if so, reload it. + """ + if self.calltip and self.calltip.is_active(): + self.open_calltip(False) - __IDCHARS = "._" + string.ascii_letters + string.digits + def open_calltip(self, evalfuncs): + self._remove_calltip_window() - def get_name_at_cursor(self): - idchars = self.__IDCHARS - str = self.text.get("insert linestart", "insert") - i = len(str) - while i and str[i-1] in idchars: - i -= 1 - return str[i:] + hp = HyperParser(self.editwin, "insert") + sur_paren = hp.get_surrounding_brackets('(') + if not sur_paren: + return + hp.set_index(sur_paren[0]) + name = hp.get_expression() + if not name or (not evalfuncs and name.find('(') != -1): + return + arg_text = self.fetch_tip(name) + if not arg_text: + return + self.calltip = self._make_calltip_window() + self.calltip.showtip(arg_text, sur_paren[0], sur_paren[1]) def fetch_tip(self, name): """Return the argument list and docstring of a function or class @@ -127,7 +123,7 @@ return None def get_arg_text(ob): - "Get a string describing the arguments for the given object" + """Get a string describing the arguments for the given object""" argText = "" if ob is not None: argOffset = 0 @@ -150,7 +146,7 @@ try: realArgs = fob.func_code.co_varnames[argOffset:fob.func_code.co_argcount] defaults = fob.func_defaults or [] - defaults = list(map(lambda name: "=%s" % name, defaults)) + defaults = list(map(lambda name: "=%s" % repr(name), defaults)) defaults = [""] * (len(realArgs)-len(defaults)) + defaults items = map(lambda arg, dflt: arg+dflt, realArgs, defaults) if fob.func_code.co_flags & 0x4: Index: EditorWindow.py =================================================================== RCS file: /var/lib/cvs/mydlelib/EditorWindow.py,v retrieving revision 1.1.1.6 retrieving revision 1.1.1.6.2.1 diff -u -r1.1.1.6 -r1.1.1.6.2.1 --- EditorWindow.py 4 Jul 2005 16:00:31 -0000 1.1.1.6 +++ EditorWindow.py 10 Jul 2005 07:30:18 -0000 1.1.1.6.2.1 @@ -6,6 +6,7 @@ from Tkinter import * import tkSimpleDialog import tkMessageBox +from MultiCall import MultiCallCreator import webbrowser import idlever @@ -89,7 +90,8 @@ self.vbar = vbar = Scrollbar(top, name='vbar') self.text_frame = text_frame = Frame(top) self.width = idleConf.GetOption('main','EditorWindow','width') - self.text = text = Text(text_frame, name='text', padx=5, wrap='none', + self.text = text = MultiCallCreator(Text)( + text_frame, name='text', padx=5, wrap='none', foreground=idleConf.GetHighlight(currentTheme, 'normal',fgBg='fg'), background=idleConf.GetHighlight(currentTheme, @@ -264,8 +266,9 @@ self.status_bar.set_label('column', 'Col: ?', side=RIGHT) self.status_bar.set_label('line', 'Ln: ?', side=RIGHT) self.status_bar.pack(side=BOTTOM, fill=X) - self.text.bind('', self.set_line_and_column) - self.text.bind('', self.set_line_and_column) + self.text.bind("<>", self.set_line_and_column) + self.text.event_add("<>", + "", "") self.text.after_idle(self.set_line_and_column) def set_line_and_column(self, event=None): @@ -355,6 +358,9 @@ return "break" def copy(self,event): + if not self.text.tag_ranges("sel"): + # There is no selection, so do nothing and maybe interrupt. + return self.text.event_generate("<>") return "break" @@ -557,14 +563,28 @@ idleConf.GetOption('main','EditorWindow','font-size'), fontWeight)) - def ResetKeybindings(self): - "Update the keybindings if they are changed" + def RemoveKeybindings(self): + "Remove the keybindings before they are changed." # Called from configDialog.py self.Bindings.default_keydefs=idleConf.GetCurrentKeySet() keydefs = self.Bindings.default_keydefs for event, keylist in keydefs.items(): - self.text.event_delete(event) + self.text.event_delete(event, *keylist) + for extensionName in self.get_standard_extension_names(): + keydefs = idleConf.GetExtensionBindings(extensionName) + if keydefs: + for event, keylist in keydefs.items(): + self.text.event_delete(event, *keylist) + + def ApplyKeybindings(self): + "Update the keybindings after they are changed" + # Called from configDialog.py + self.Bindings.default_keydefs=idleConf.GetCurrentKeySet() self.apply_bindings() + for extensionName in self.get_standard_extension_names(): + keydefs = idleConf.GetExtensionBindings(extensionName) + if keydefs: + self.apply_bindings(keydefs) #update menu accelerators menuEventDict={} for menu in self.Bindings.menudefs: @@ -1064,17 +1084,28 @@ # open/close first need to find the last stmt lno = index2line(text.index('insert')) y = PyParse.Parser(self.indentwidth, self.tabwidth) - for context in self.num_context_lines: - startat = max(lno - context, 1) - startatindex = repr(startat) + ".0" + if not self.context_use_ps1: + for context in self.num_context_lines: + startat = max(lno - context, 1) + startatindex = `startat` + ".0" + rawtext = text.get(startatindex, "insert") + y.set_str(rawtext) + bod = y.find_good_parse_start( + self.context_use_ps1, + self._build_char_in_string_func(startatindex)) + if bod is not None or startat == 1: + break + y.set_lo(bod or 0) + else: + r = text.tag_prevrange("console", "insert") + if r: + startatindex = r[1] + else: + startatindex = "1.0" rawtext = text.get(startatindex, "insert") y.set_str(rawtext) - bod = y.find_good_parse_start( - self.context_use_ps1, - self._build_char_in_string_func(startatindex)) - if bod is not None or startat == 1: - break - y.set_lo(bod or 0) + y.set_lo(0) + c = y.get_continuation_type() if c != PyParse.C_NONE: # The current stmt hasn't ended yet. Index: HyperParser.py =================================================================== RCS file: HyperParser.py diff -N HyperParser.py --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ HyperParser.py 10 Jul 2005 07:30:18 -0000 1.2.2.1 @@ -0,0 +1,242 @@ +""" +HyperParser +=========== +This module defines the HyperParser class, which provides advanced parsing +abilities for the ParenMatch and other extensions. +The HyperParser uses PyParser. PyParser is intended mostly to give information +on the proper indentation of code. HyperParser gives some information on the +structure of code, used by extensions to help the user. +""" + +import string +import keyword +import PyParse + +class HyperParser: + + def __init__(self, editwin, index): + """Initialize the HyperParser to analyze the surroundings of the given + index. + """ + + self.editwin = editwin + self.text = text = editwin.text + + parser = PyParse.Parser(editwin.indentwidth, editwin.tabwidth) + + def index2line(index): + return int(float(index)) + lno = index2line(text.index(index)) + + if not editwin.context_use_ps1: + for context in editwin.num_context_lines: + startat = max(lno - context, 1) + startatindex = `startat` + ".0" + stopatindex = "%d.end" % lno + # We add the newline because PyParse requires a newline at end. + # We add a space so that index won't be at end of line, so that + # its status will be the same as the char before it, if should. + parser.set_str(text.get(startatindex, stopatindex)+' \n') + bod = parser.find_good_parse_start( + editwin._build_char_in_string_func(startatindex)) + if bod is not None or startat == 1: + break + parser.set_lo(bod or 0) + else: + r = text.tag_prevrange("console", index) + if r: + startatindex = r[1] + else: + startatindex = "1.0" + stopatindex = "%d.end" % lno + # We add the newline because PyParse requires a newline at end. + # We add a space so that index won't be at end of line, so that + # its status will be the same as the char before it, if should. + parser.set_str(text.get(startatindex, stopatindex)+' \n') + parser.set_lo(0) + + # We want what the parser has, except for the last newline and space. + self.rawtext = parser.str[:-2] + # As far as I can see, parser.str preserves the statement we are in, + # so that stopatindex can be used to synchronize the string with the + # text box indices. + self.stopatindex = stopatindex + self.bracketing = parser.get_last_stmt_bracketing() + # find which pairs of bracketing are openers. These always correspond + # to a character of rawtext. + self.isopener = [i>0 and self.bracketing[i][1] > self.bracketing[i-1][1] + for i in range(len(self.bracketing))] + + self.set_index(index) + + def set_index(self, index): + """Set the index to which the functions relate. Note that it must be + in the same statement. + """ + indexinrawtext = \ + len(self.rawtext) - len(self.text.get(index, self.stopatindex)) + if indexinrawtext < 0: + raise ValueError("The index given is before the analyzed statement") + self.indexinrawtext = indexinrawtext + # find the rightmost bracket to which index belongs + self.indexbracket = 0 + while self.indexbracket < len(self.bracketing)-1 and \ + self.bracketing[self.indexbracket+1][0] < self.indexinrawtext: + self.indexbracket += 1 + if self.indexbracket < len(self.bracketing)-1 and \ + self.bracketing[self.indexbracket+1][0] == self.indexinrawtext and \ + not self.isopener[self.indexbracket+1]: + self.indexbracket += 1 + + def is_in_string(self): + """Is the index given to the HyperParser is in a string?""" + # The bracket to which we belong should be an opener. + # If it's an opener, it has to have a character. + return self.isopener[self.indexbracket] and \ + self.rawtext[self.bracketing[self.indexbracket][0]] in ('"', "'") + + def is_in_code(self): + """Is the index given to the HyperParser is in a normal code?""" + return not self.isopener[self.indexbracket] or \ + self.rawtext[self.bracketing[self.indexbracket][0]] not in \ + ('#', '"', "'") + + def get_surrounding_brackets(self, openers='([{', mustclose=False): + """If the index given to the HyperParser is surrounded by a bracket + defined in openers (or at least has one before it), return the + indices of the opening bracket and the closing bracket (or the + end of line, whichever comes first). + If it is not surrounded by brackets, or the end of line comes before + the closing bracket and mustclose is True, returns None. + """ + bracketinglevel = self.bracketing[self.indexbracket][1] + before = self.indexbracket + while not self.isopener[before] or \ + self.rawtext[self.bracketing[before][0]] not in openers or \ + self.bracketing[before][1] > bracketinglevel: + before -= 1 + if before < 0: + return None + bracketinglevel = min(bracketinglevel, self.bracketing[before][1]) + after = self.indexbracket + 1 + while after < len(self.bracketing) and \ + self.bracketing[after][1] >= bracketinglevel: + after += 1 + + beforeindex = self.text.index("%s-%dc" % + (self.stopatindex, len(self.rawtext)-self.bracketing[before][0])) + if after >= len(self.bracketing) or \ + self.bracketing[after][0] > len(self.rawtext): + if mustclose: + return None + afterindex = self.stopatindex + else: + # We are after a real char, so it is a ')' and we give the index + # before it. + afterindex = self.text.index("%s-%dc" % + (self.stopatindex, + len(self.rawtext)-(self.bracketing[after][0]-1))) + + return beforeindex, afterindex + + # This string includes all chars that may be in a white space + _whitespace_chars = " \t\n\\" + # This string includes all chars that may be in an identifier + _id_chars = string.ascii_letters + string.digits + "_" + # This string includes all chars that may be the first char of an identifier + _id_first_chars = string.ascii_letters + "_" + + # Given a string and pos, return the number of chars in the identifier + # which ends at pos, or 0 if there is no such one. Saved words are not + # identifiers. + def _eat_identifier(self, str, limit, pos): + i = pos + while i > limit and str[i-1] in self._id_chars: + i -= 1 + if i < pos and (str[i] not in self._id_first_chars or \ + keyword.iskeyword(str[i:pos])): + i = pos + return pos - i + + def get_expression(self): + """Return a string with the Python expression which ends at the given + index, which is empty if there is no real one. + """ + if not self.is_in_code(): + raise ValueError("get_expression should only be called if index "\ + "is inside a code.") + + rawtext = self.rawtext + bracketing = self.bracketing + + brck_index = self.indexbracket + brck_limit = bracketing[brck_index][0] + pos = self.indexinrawtext + + last_identifier_pos = pos + postdot_phase = True + + while 1: + # Eat whitespaces, comments, and if postdot_phase is False - one dot + while 1: + if pos>brck_limit and rawtext[pos-1] in self._whitespace_chars: + # Eat a whitespace + pos -= 1 + elif not postdot_phase and \ + pos > brck_limit and rawtext[pos-1] == '.': + # Eat a dot + pos -= 1 + postdot_phase = True + # The next line will fail if we are *inside* a comment, but we + # shouldn't be. + elif pos == brck_limit and brck_index > 0 and \ + rawtext[bracketing[brck_index-1][0]] == '#': + # Eat a comment + brck_index -= 2 + brck_limit = bracketing[brck_index][0] + pos = bracketing[brck_index+1][0] + else: + # If we didn't eat anything, quit. + break + + if not postdot_phase: + # We didn't find a dot, so the expression end at the last + # identifier pos. + break + + ret = self._eat_identifier(rawtext, brck_limit, pos) + if ret: + # There is an identifier to eat + pos = pos - ret + last_identifier_pos = pos + # Now, in order to continue the search, we must find a dot. + postdot_phase = False + # (the loop continues now) + + elif pos == brck_limit: + # We are at a bracketing limit. If it is a closing bracket, + # eat the bracket, otherwise, stop the search. + level = bracketing[brck_index][1] + while brck_index > 0 and bracketing[brck_index-1][1] > level: + brck_index -= 1 + if bracketing[brck_index][0] == brck_limit: + # We were not at the end of a closing bracket + break + pos = bracketing[brck_index][0] + brck_index -= 1 + brck_limit = bracketing[brck_index][0] + last_identifier_pos = pos + if rawtext[pos] in "([": + # [] and () may be used after an identifier, so we + # continue. postdot_phase is True, so we don't allow a dot. + pass + else: + # We can't continue after other types of brackets + break + + else: + # We've found an operator or something. + break + + return rawtext[last_identifier_pos:self.indexinrawtext] + Index: MultiCall.py =================================================================== RCS file: MultiCall.py diff -N MultiCall.py --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ MultiCall.py 10 Jul 2005 07:30:18 -0000 1.2.2.1 @@ -0,0 +1,404 @@ +""" +MultiCall - a class which inherits its methods from a Tkinter widget (Text, for +example), but enables multiple calls of functions per virtual event - all +matching events will be called, not only the most specific one. This is done +by wrapping the event functions - event_add, event_delete and event_info. +MultiCall recognizes only a subset of legal event sequences. Sequences which +are not recognized are treated by the original Tk handling mechanism. A +more-specific event will be called before a less-specific event. + +The recognized sequences are complete one-event sequences (no emacs-style +Ctrl-X Ctrl-C, no shortcuts like <3>), for all types of events. +Key/Button Press/Release events can have modifiers. +The recognized modifiers are Shift, Control, Option and Command for Mac, and +Control, Alt, Shift, Meta/M for other platforms. + +For all events which were handled by MultiCall, a new member is added to the +event instance passed to the binded functions - mc_type. This is one of the +event type constants defined in this module (such as MC_KEYPRESS). +For Key/Button events (which are handled by MultiCall and may receive +modifiers), another member is added - mc_state. This member gives the state +of the recognized modifiers, as a combination of the modifier constants +also defined in this module (for example, MC_SHIFT). +Using these members is absolutely portable. + +The order by which events are called is defined by these rules: +1. A more-specific event will be called before a less-specific event. +2. A recently-binded event will be called before a previously-binded event, + unless this conflicts with the first rule. +Each function will be called at most once for each event. +""" + +import sys +import os +import string +import re +import Tkinter + +# the event type constants, which define the meaning of mc_type +MC_KEYPRESS=0; MC_KEYRELEASE=1; MC_BUTTONPRESS=2; MC_BUTTONRELEASE=3; +MC_ACTIVATE=4; MC_CIRCULATE=5; MC_COLORMAP=6; MC_CONFIGURE=7; +MC_DEACTIVATE=8; MC_DESTROY=9; MC_ENTER=10; MC_EXPOSE=11; MC_FOCUSIN=12; +MC_FOCUSOUT=13; MC_GRAVITY=14; MC_LEAVE=15; MC_MAP=16; MC_MOTION=17; +MC_MOUSEWHEEL=18; MC_PROPERTY=19; MC_REPARENT=20; MC_UNMAP=21; MC_VISIBILITY=22; +# the modifier state constants, which define the meaning of mc_state +MC_SHIFT = 1<<0; MC_CONTROL = 1<<2; MC_ALT = 1<<3; MC_META = 1<<5 +MC_OPTION = 1<<6; MC_COMMAND = 1<<7 + +# define the list of modifiers, to be used in complex event types. +if sys.platform == "darwin" and sys.executable.count(".app"): + _modifiers = (("Shift",), ("Control",), ("Option",), ("Command",)) + _modifier_masks = (MC_SHIFT, MC_CONTROL, MC_OPTION, MC_COMMAND) +else: + _modifiers = (("Control",), ("Alt",), ("Shift",), ("Meta", "M")) + _modifier_masks = (MC_CONTROL, MC_ALT, MC_SHIFT, MC_META) + +# a dictionary to map a modifier name into its number +_modifier_names = dict([(name, number) + for number in range(len(_modifiers)) + for name in _modifiers[number]]) + +# A binder is a class which binds functions to one type of event. It has two +# methods: bind and unbind, which get a function and a parsed sequence, as +# returned by _parse_sequence(). There are two types of binders: +# _SimpleBinder handles event types with no modifiers and no detail. +# No Python functions are called when no events are binded. +# _ComplexBinder handles event types with modifiers and a detail. +# A Python function is called each time an event is generated. + +class _SimpleBinder: + def __init__(self, type, widget, widgetinst): + self.type = type + self.sequence = '<'+_types[type][0]+'>' + self.widget = widget + self.widgetinst = widgetinst + self.bindedfuncs = [] + self.handlerid = None + + def bind(self, triplet, func): + if not self.handlerid: + def handler(event, l = self.bindedfuncs, mc_type = self.type): + event.mc_type = mc_type + wascalled = {} + for i in range(len(l)-1, -1, -1): + func = l[i] + if func not in wascalled: + wascalled[func] = True + r = func(event) + if r: + return r + self.handlerid = self.widget.bind(self.widgetinst, + self.sequence, handler) + self.bindedfuncs.append(func) + + def unbind(self, triplet, func): + self.bindedfuncs.remove(func) + if not self.bindedfuncs: + self.widget.unbind(self.widgetinst, self.sequence, self.handlerid) + self.handlerid = None + + def __del__(self): + if self.handlerid: + self.widget.unbind(self.widgetinst, self.sequence, self.handlerid) + +# An int in range(1 << len(_modifiers)) represents a combination of modifiers +# (if the least significent bit is on, _modifiers[0] is on, and so on). +# _state_subsets gives for each combination of modifiers, or *state*, +# a list of the states which are a subset of it. This list is ordered by the +# number of modifiers is the state - the most specific state comes first. +_states = range(1 << len(_modifiers)) +_state_names = [reduce(lambda x, y: x + y, + [_modifiers[i][0]+'-' for i in range(len(_modifiers)) + if (1 << i) & s], + "") + for s in _states] +_state_subsets = map(lambda i: filter(lambda j: not (j & (~i)), _states), + _states) +for l in _state_subsets: + l.sort(lambda a, b, nummod = lambda x: len(filter(lambda i: (1<' + self.handlerids.append((seq, self.widget.bind(self.widgetinst, + seq, handler))) + + def bind(self, triplet, func): + if not self.bindedfuncs.has_key(triplet[2]): + self.bindedfuncs[triplet[2]] = [[] for s in _states] + for s in _states: + lists = [ self.bindedfuncs[detail][i] + for detail in (triplet[2], None) + for i in _state_subsets[s] ] + handler = self.__create_handler(lists, self.type, + _state_codes[s]) + seq = "<%s%s-%s>"% (_state_names[s], self.typename, triplet[2]) + self.handlerids.append((seq, self.widget.bind(self.widgetinst, + seq, handler))) + doit = lambda: self.bindedfuncs[triplet[2]][triplet[0]].append(func) + if not self.ishandlerrunning: + doit() + else: + self.doafterhandler.append(doit) + + def unbind(self, triplet, func): + doit = lambda: self.bindedfuncs[triplet[2]][triplet[0]].remove(func) + if not self.ishandlerrunning: + doit() + else: + self.doafterhandler.append(doit) + + def __del__(self): + for seq, id in self.handlerids: + self.widget.unbind(self.widgetinst, seq, id) + +# define the list of event types to be handled by MultiEvent. the order is +# compatible with the definition of event type constants. +_types = ( + ("KeyPress", "Key"), ("KeyRelease",), ("ButtonPress", "Button"), + ("ButtonRelease",), ("Activate",), ("Circulate",), ("Colormap",), + ("Configure",), ("Deactivate",), ("Destroy",), ("Enter",), ("Expose",), + ("FocusIn",), ("FocusOut",), ("Gravity",), ("Leave",), ("Map",), + ("Motion",), ("MouseWheel",), ("Property",), ("Reparent",), ("Unmap",), + ("Visibility",), +) + +# which binder should be used for every event type? +_binder_classes = (_ComplexBinder,) * 4 + (_SimpleBinder,) * (len(_types)-4) + +# A dictionary to map a type name into its number +_type_names = dict([(name, number) + for number in range(len(_types)) + for name in _types[number]]) + +_keysym_re = re.compile(r"^\w+$") +_button_re = re.compile(r"^[1-5]$") +def _parse_sequence(sequence): + """Get a string which should describe an event sequence. If it is + successfully parsed as one, return a tuple containing the state (as an int), + the event type (as an index of _types), and the detail - None if none, or a + string if there is one. If the parsing is unsuccessful, return None. + """ + if not sequence or sequence[0] != '<' or sequence[-1] != '>': + return None + words = string.split(sequence[1:-1], '-') + + modifiers = 0 + while words and words[0] in _modifier_names: + modifiers |= 1 << _modifier_names[words[0]] + del words[0] + + if words and words[0] in _type_names: + type = _type_names[words[0]] + del words[0] + else: + return None + + if _binder_classes[type] is _SimpleBinder: + if modifiers or words: + return None + else: + detail = None + else: + # _ComplexBinder + if type in [_type_names[s] for s in ("KeyPress", "KeyRelease")]: + type_re = _keysym_re + else: + type_re = _button_re + + if not words: + detail = None + elif len(words) == 1 and type_re.match(words[0]): + detail = words[0] + else: + return None + + return modifiers, type, detail + +def _triplet_to_sequence(triplet): + if triplet[2]: + return '<'+_state_names[triplet[0]]+_types[triplet[1]][0]+'-'+ \ + triplet[2]+'>' + else: + return '<'+_state_names[triplet[0]]+_types[triplet[1]][0]+'>' + +_multicall_dict = {} +def MultiCallCreator(widget): + """Return a MultiCall class which inherits its methods from the + given widget class (for example, Tkinter.Text). This is used + instead of a templating mechanism. + """ + if widget in _multicall_dict: + return _multicall_dict[widget] + + class MultiCall (widget): + assert issubclass(widget, Tkinter.Misc) + + def __init__(self, *args, **kwargs): + apply(widget.__init__, (self,)+args, kwargs) + # a dictionary which maps a virtual event to a tuple with: + # 0. the function binded + # 1. a list of triplets - the sequences it is binded to + self.__eventinfo = {} + self.__binders = [_binder_classes[i](i, widget, self) + for i in range(len(_types))] + + def bind(self, sequence=None, func=None, add=None): + #print "bind(%s, %s, %s) called." % (sequence, func, add) + if type(sequence) is str and len(sequence) > 2 and \ + sequence[:2] == "<<" and sequence[-2:] == ">>": + if sequence in self.__eventinfo: + ei = self.__eventinfo[sequence] + if ei[0] is not None: + for triplet in ei[1]: + self.__binders[triplet[1]].unbind(triplet, ei[0]) + ei[0] = func + if ei[0] is not None: + for triplet in ei[1]: + self.__binders[triplet[1]].bind(triplet, func) + else: + self.__eventinfo[sequence] = [func, []] + return widget.bind(self, sequence, func, add) + + def unbind(self, sequence, funcid=None): + if type(sequence) is str and len(sequence) > 2 and \ + sequence[:2] == "<<" and sequence[-2:] == ">>" and \ + sequence in self.__eventinfo: + func, triplets = self.__eventinfo[sequence] + if func is not None: + for triplet in triplets: + self.__binders[triplet[1]].unbind(triplet, func) + self.__eventinfo[sequence][0] = None + return widget.unbind(self, sequence, funcid) + + def event_add(self, virtual, *sequences): + #print "event_add(%s,%s) was called"%(repr(virtual),repr(sequences)) + if virtual not in self.__eventinfo: + self.__eventinfo[virtual] = [None, []] + + func, triplets = self.__eventinfo[virtual] + for seq in sequences: + triplet = _parse_sequence(seq) + if triplet is None: + #print >> sys.stderr, "Seq. %s was added by Tkinter."%seq + widget.event_add(self, virtual, seq) + else: + if func is not None: + self.__binders[triplet[1]].bind(triplet, func) + triplets.append(triplet) + + def event_delete(self, virtual, *sequences): + func, triplets = self.__eventinfo[virtual] + for seq in sequences: + triplet = _parse_sequence(seq) + if triplet is None: + #print >> sys.stderr, "Seq. %s was deleted by Tkinter."%seq + widget.event_delete(self, virtual, seq) + else: + if func is not None: + self.__binders[triplet[1]].unbind(triplet, func) + triplets.remove(triplet) + + def event_info(self, virtual=None): + if virtual is None or virtual not in self.__eventinfo: + return widget.event_info(self, virtual) + else: + return tuple(map(_triplet_to_sequence, + self.__eventinfo[virtual][1])) + \ + widget.event_info(self, virtual) + + def __del__(self): + for virtual in self.__eventinfo: + func, triplets = self.__eventinfo[virtual] + if func: + for triplet in triplets: + self.__binders[triplet[1]].unbind(triplet, func) + + + _multicall_dict[widget] = MultiCall + return MultiCall + +if __name__ == "__main__": + # Test + root = Tkinter.Tk() + text = MultiCallCreator(Tkinter.Text)(root) + text.pack() + def bindseq(seq, n=[0]): + def handler(event): + print seq + text.bind("<>"%n[0], handler) + text.event_add("<>"%n[0], seq) + n[0] += 1 + bindseq("") + bindseq("") + bindseq("") + bindseq("") + bindseq("") + bindseq("") + bindseq("") + bindseq("") + bindseq("") + bindseq("") + bindseq("") + root.mainloop() Index: ParenMatch.py =================================================================== RCS file: /var/lib/cvs/mydlelib/ParenMatch.py,v retrieving revision 1.1.1.1 retrieving revision 1.1.1.1.2.1 diff -u -r1.1.1.1 -r1.1.1.1.2.1 --- ParenMatch.py 28 Feb 2004 18:00:04 -0000 1.1.1.1 +++ ParenMatch.py 10 Jul 2005 07:30:18 -0000 1.1.1.1.2.1 @@ -3,17 +3,14 @@ When you hit a right paren, the cursor should move briefly to the left paren. Paren here is used generically; the matching applies to parentheses, square brackets, and curly braces. - -WARNING: This extension will fight with the CallTips extension, -because they both are interested in the KeyRelease-parenright event. -We'll have to fix IDLE to do something reasonable when two or more -extensions what to capture the same event. """ -import PyParse -from EditorWindow import EditorWindow, index2line +from HyperParser import HyperParser from configHandler import idleConf +keysym_opener = {"parenright":'(', "bracketright":'[', "braceright":'{'} +CHECK_DELAY = 100 # miliseconds + class ParenMatch: """Highlight matching parentheses @@ -31,7 +28,6 @@ expression from the left paren to the right paren. TODO: - - fix interaction with CallTips - extend IDLE with configuration dialog to change options - implement rest of Emacs highlight styles (see below) - print mismatch warning in IDLE status window @@ -41,7 +37,11 @@ to the right of a right paren. I don't know how to do that in Tk, so I haven't bothered. """ - menudefs = [] + menudefs = [ + ('edit', [ + ("Show surrounding parens", "<>"), + ]) + ] STYLE = idleConf.GetOption('extensions','ParenMatch','style', default='expression') FLASH_DELAY = idleConf.GetOption('extensions','ParenMatch','flash-delay', @@ -50,14 +50,36 @@ BELL = idleConf.GetOption('extensions','ParenMatch','bell', type='bool',default=1) + RESTORE_VIRTUAL_EVENT_NAME = "<>" + # We want the restore event be called before the usual return and + # backspace events. + RESTORE_SEQUENCES = ("", "", + "", "") + def __init__(self, editwin): self.editwin = editwin self.text = editwin.text - self.finder = LastOpenBracketFinder(editwin) + # Bind the check-restore event to the function restore_event, + # so that we can then use activate_restore (which calls event_add) + # and deactivate_restore (which calls event_delete). + editwin.text.bind(self.RESTORE_VIRTUAL_EVENT_NAME, + self.restore_event) self.counter = 0 - self._restore = None + self.is_restore_active = 0 self.set_style(self.STYLE) + def activate_restore(self): + if not self.is_restore_active: + for seq in self.RESTORE_SEQUENCES: + self.text.event_add(self.RESTORE_VIRTUAL_EVENT_NAME, seq) + self.is_restore_active = True + + def deactivate_restore(self): + if self.is_restore_active: + for seq in self.RESTORE_SEQUENCES: + self.text.event_delete(self.RESTORE_VIRTUAL_EVENT_NAME, seq) + self.is_restore_active = False + def set_style(self, style): self.STYLE = style if style == "default": @@ -67,23 +89,38 @@ self.create_tag = self.create_tag_expression self.set_timeout = self.set_timeout_none - def flash_open_paren_event(self, event): - index = self.finder.find(keysym_type(event.keysym)) - if index is None: + def flash_paren_event(self, event): + indices = HyperParser(self.editwin, "insert").get_surrounding_brackets() + if indices is None: self.warn_mismatched() return - self._restore = 1 - self.create_tag(index) + self.activate_restore() + self.create_tag(indices) + self.set_timeout_last() + + def paren_closed_event(self, event): + # If it was a shortcut and not really a closing paren, quit. + if self.text.get("insert-1c") not in (')',']','}'): + return + hp = HyperParser(self.editwin, "insert-1c") + if not hp.is_in_code(): + return + indices = hp.get_surrounding_brackets(keysym_opener[event.keysym], True) + if indices is None: + self.warn_mismatched() + return + self.activate_restore() + self.create_tag(indices) self.set_timeout() - def check_restore_event(self, event=None): - if self._restore: - self.text.tag_delete("paren") - self._restore = None + def restore_event(self, event=None): + self.text.tag_delete("paren") + self.deactivate_restore() + self.counter += 1 # disable the last timer, if there is one. def handle_restore_timer(self, timer_count): - if timer_count + 1 == self.counter: - self.check_restore_event() + if timer_count == self.counter: + self.restore_event() def warn_mismatched(self): if self.BELL: @@ -92,87 +129,44 @@ # any one of the create_tag_XXX methods can be used depending on # the style - def create_tag_default(self, index): + def create_tag_default(self, indices): """Highlight the single paren that matches""" - self.text.tag_add("paren", index) + self.text.tag_add("paren", indices[0]) self.text.tag_config("paren", self.HILITE_CONFIG) - def create_tag_expression(self, index): + def create_tag_expression(self, indices): """Highlight the entire expression""" - self.text.tag_add("paren", index, "insert") + if self.text.get(indices[1]) in (')', ']', '}'): + rightindex = indices[1]+"+1c" + else: + rightindex = indices[1] + self.text.tag_add("paren", indices[0], rightindex) self.text.tag_config("paren", self.HILITE_CONFIG) # any one of the set_timeout_XXX methods can be used depending on # the style def set_timeout_none(self): - """Highlight will remain until user input turns it off""" - pass + """Highlight will remain until user input turns it off + or the insert has moved""" + # After CHECK_DELAY, call a function which disables the "paren" tag + # if the event is for the most recent timer and the insert has changed, + # or schedules another call for itself. + self.counter += 1 + def callme(callme, self=self, c=self.counter, + index=self.text.index("insert")): + if index != self.text.index("insert"): + self.handle_restore_timer(c) + else: + self.editwin.text_frame.after(CHECK_DELAY, callme, callme) + self.editwin.text_frame.after(CHECK_DELAY, callme, callme) def set_timeout_last(self): """The last highlight created will be removed after .5 sec""" # associate a counter with an event; only disable the "paren" # tag if the event is for the most recent timer. + self.counter += 1 self.editwin.text_frame.after(self.FLASH_DELAY, lambda self=self, c=self.counter: \ self.handle_restore_timer(c)) - self.counter = self.counter + 1 - -def keysym_type(ks): - # Not all possible chars or keysyms are checked because of the - # limited context in which the function is used. - if ks == "parenright" or ks == "(": - return "paren" - if ks == "bracketright" or ks == "[": - return "bracket" - if ks == "braceright" or ks == "{": - return "brace" - -class LastOpenBracketFinder: - num_context_lines = EditorWindow.num_context_lines - indentwidth = EditorWindow.indentwidth - tabwidth = EditorWindow.tabwidth - context_use_ps1 = EditorWindow.context_use_ps1 - - def __init__(self, editwin): - self.editwin = editwin - self.text = editwin.text - def _find_offset_in_buf(self, lno): - y = PyParse.Parser(self.indentwidth, self.tabwidth) - for context in self.num_context_lines: - startat = max(lno - context, 1) - startatindex = repr(startat) + ".0" - # rawtext needs to contain everything up to the last - # character, which was the close paren. the parser also - # requires that the last line ends with "\n" - rawtext = self.text.get(startatindex, "insert")[:-1] + "\n" - y.set_str(rawtext) - bod = y.find_good_parse_start( - self.context_use_ps1, - self._build_char_in_string_func(startatindex)) - if bod is not None or startat == 1: - break - y.set_lo(bod or 0) - i = y.get_last_open_bracket_pos() - return i, y.str - - def find(self, right_keysym_type): - """Return the location of the last open paren""" - lno = index2line(self.text.index("insert")) - i, buf = self._find_offset_in_buf(lno) - if i is None \ - or keysym_type(buf[i]) != right_keysym_type: - return None - lines_back = buf[i:].count("\n") - 1 - # subtract one for the "\n" added to please the parser - upto_open = buf[:i] - j = upto_open.rfind("\n") + 1 # offset of column 0 of line - offset = i - j - return "%d.%d" % (lno - lines_back, offset) - - def _build_char_in_string_func(self, startindex): - def inner(offset, startindex=startindex, - icis=self.editwin.is_char_in_string): - return icis(startindex + "%dc" % offset) - return inner Index: PyParse.py =================================================================== RCS file: /var/lib/cvs/mydlelib/PyParse.py,v retrieving revision 1.1.1.1 retrieving revision 1.1.1.1.2.1 diff -u -r1.1.1.1 -r1.1.1.1.2.1 --- PyParse.py 28 Feb 2004 18:00:04 -0000 1.1.1.1 +++ PyParse.py 10 Jul 2005 07:30:18 -0000 1.1.1.1.2.1 @@ -13,9 +13,7 @@ _synchre = re.compile(r""" ^ [ \t]* - (?: if - | for - | while + (?: while | else | def | return @@ -144,29 +142,11 @@ # This will be reliable iff given a reliable is_char_in_string # function, meaning that when it says "no", it's absolutely # guaranteed that the char is not in a string. - # - # Ack, hack: in the shell window this kills us, because there's - # no way to tell the differences between output, >>> etc and - # user input. Indeed, IDLE's first output line makes the rest - # look like it's in an unclosed paren!: - # Python 1.5.2 (#0, Apr 13 1999, ... - def find_good_parse_start(self, use_ps1, is_char_in_string=None, + def find_good_parse_start(self, is_char_in_string=None, _synchre=_synchre): str, pos = self.str, None - if use_ps1: - # shell window - ps1 = '\n' + sys.ps1 - i = str.rfind(ps1) - if i >= 0: - pos = i + len(ps1) - # make it look like there's a newline instead - # of ps1 at the start -- hacking here once avoids - # repeated hackery later - self.str = str[:pos-1] + '\n' + str[pos:] - return pos - # File window -- real work. if not is_char_in_string: # no clue -- make the caller pass everything return None @@ -355,6 +335,11 @@ # Creates: # self.stmt_start, stmt_end # slice indices of last interesting stmt + # self.stmt_bracketing + # the bracketing structure of the last interesting stmt; + # for example, for the statement "say(boo) or die", stmt_bracketing + # will be [(0, 0), (3, 1), (8, 0)]. Strings and comments are + # treated as brackets, for the matter. # self.lastch # last non-whitespace character before optional trailing # comment @@ -396,6 +381,7 @@ lastch = "" stack = [] # stack of open bracket indices push_stack = stack.append + bracketing = [(p, 0)] while p < q: # suck up all except ()[]{}'"#\\ m = _chew_ordinaryre(str, p, q) @@ -416,6 +402,7 @@ if ch in "([{": push_stack(p) + bracketing.append((p, len(stack))) lastch = ch p = p+1 continue @@ -425,6 +412,7 @@ del stack[-1] lastch = ch p = p+1 + bracketing.append((p, len(stack))) continue if ch == '"' or ch == "'": @@ -435,14 +423,18 @@ # strings to a couple of characters per line. study1 # also needed to keep track of newlines, and we don't # have to. + bracketing.append((p, len(stack)+1)) lastch = ch p = _match_stringre(str, p, q).end() + bracketing.append((p, len(stack))) continue if ch == '#': # consume comment and trailing newline + bracketing.append((p, len(stack)+1)) p = str.find('\n', p, q) + 1 assert p > 0 + bracketing.append((p, len(stack))) continue assert ch == '\\' @@ -458,6 +450,7 @@ self.lastch = lastch if stack: self.lastopenbracketpos = stack[-1] + self.stmt_bracketing = tuple(bracketing) # Assuming continuation is C_BRACKET, return the number # of spaces the next line should be indented. @@ -582,3 +575,12 @@ def get_last_open_bracket_pos(self): self._study2() return self.lastopenbracketpos + + # the structure of the bracketing of the last interesting statement, + # in the format defined in _study2, or None if the text didn't contain + # anything + stmt_bracketing = None + + def get_last_stmt_bracketing(self): + self._study2() + return self.stmt_bracketing Index: PyShell.py =================================================================== RCS file: /var/lib/cvs/mydlelib/PyShell.py,v retrieving revision 1.1.1.5 retrieving revision 1.1.1.5.2.1 diff -u -r1.1.1.5 -r1.1.1.5.2.1 --- PyShell.py 4 Jul 2005 16:00:30 -0000 1.1.1.5 +++ PyShell.py 10 Jul 2005 07:44:36 -0000 1.1.1.5.2.1 @@ -1092,11 +1092,12 @@ self.recall(self.text.get(next[0], next[1]), event) return "break" # No stdin mark -- just get the current line, less any prompt - line = self.text.get("insert linestart", "insert lineend") - last_line_of_prompt = sys.ps1.split('\n')[-1] - if line.startswith(last_line_of_prompt): - line = line[len(last_line_of_prompt):] - self.recall(line, event) + indices = self.text.tag_nextrange("console", "insert linestart") + if indices and \ + self.text.compare(indices[0], "<=", "insert linestart"): + self.recall(self.text.get(indices[1], "insert lineend"), event) + else: + self.recall(self.text.get("insert linestart", "insert lineend"), event) return "break" # If we're between the beginning of the line and the iomark, i.e. # in the prompt area, move to the end of the prompt Index: config-extensions.def =================================================================== RCS file: /var/lib/cvs/mydlelib/config-extensions.def,v retrieving revision 1.1.1.2 retrieving revision 1.1.1.2.2.2 diff -u -r1.1.1.2 -r1.1.1.2.2.2 --- config-extensions.def 10 Jun 2004 20:32:38 -0000 1.1.1.2 +++ config-extensions.def 10 Jul 2005 08:05:29 -0000 1.1.1.2.2.2 @@ -52,22 +52,30 @@ [CallTips] enable=1 +[CallTips_cfgBindings] +force-open-calltip= [CallTips_bindings] -paren-open= -paren-close= -check-calltip-cancel= -calltip-cancel= +try-open-calltip= +refresh-calltip= [ParenMatch] -enable=0 +enable=1 style= expression flash-delay= 500 bell= 1 -hilite-foreground= black -hilite-background= #43cd80 +[ParenMatch_cfgBindings] +flash-paren= [ParenMatch_bindings] -flash-open-paren= -check-restore= +paren-closed= + +[AutoComplete] +enable=1 +popupwait=0 +[AutoComplete_cfgBindings] +force-open-completions= +[AutoComplete_bindings] +autocomplete= +try-open-completions= [CodeContext] enable=1 Index: configDialog.py =================================================================== RCS file: /var/lib/cvs/mydlelib/configDialog.py,v retrieving revision 1.1.1.6 retrieving revision 1.1.1.6.2.1 diff -u -r1.1.1.6 -r1.1.1.6.2.1 --- configDialog.py 4 Jul 2005 16:00:30 -0000 1.1.1.6 +++ configDialog.py 10 Jul 2005 07:30:18 -0000 1.1.1.6.2.1 @@ -1106,6 +1106,13 @@ idleConf.userCfg[configType].Save() self.ResetChangedItems() #clear the changed items dict + def DeactivateCurrentConfig(self): + #Before a config is saved, some cleanup of current + #config must be done - remove the previous keybindings + winInstances=self.parent.instance_dict.keys() + for instance in winInstances: + instance.RemoveKeybindings() + def ActivateConfigChanges(self): "Dynamically apply configuration changes" winInstances=self.parent.instance_dict.keys() @@ -1113,7 +1120,7 @@ instance.ResetColorizer() instance.ResetFont() instance.set_notabs_indentwidth() - instance.ResetKeybindings() + instance.ApplyKeybindings() instance.reset_help_menu_entries() def Cancel(self): @@ -1124,6 +1131,7 @@ self.destroy() def Apply(self): + self.DeactivateCurrentConfig() self.SaveAllChangedConfigs() self.ActivateConfigChanges() Index: run.py =================================================================== RCS file: /var/lib/cvs/mydlelib/run.py,v retrieving revision 1.1.1.3 retrieving revision 1.1.1.3.2.1 diff -u -r1.1.1.3 -r1.1.1.3.2.1 --- run.py 4 Jul 2005 16:00:31 -0000 1.1.1.3 +++ run.py 10 Jul 2005 07:30:18 -0000 1.1.1.3.2.1 @@ -9,6 +9,8 @@ import Queue import CallTips +import AutoComplete + import RemoteDebugger import RemoteObjectBrowser import StackViewer @@ -275,6 +277,7 @@ self.rpchandler = rpchandler self.locals = __main__.__dict__ self.calltip = CallTips.CallTips() + self.autocomplete = AutoComplete.AutoComplete() def runcode(self, code): try: @@ -305,6 +308,9 @@ def get_the_calltip(self, name): return self.calltip.fetch_tip(name) + def get_the_completion_list(self, what, mode): + return self.autocomplete.fetch_completions(what, mode) + def stackviewer(self, flist_oid=None): if self.usr_exc_info: typ, val, tb = self.usr_exc_info