summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Lib/idlelib/AutoComplete.py226
-rw-r--r--Lib/idlelib/AutoCompleteWindow.py393
-rw-r--r--Lib/idlelib/CallTipWindow.py116
-rw-r--r--Lib/idlelib/CallTips.py84
-rw-r--r--Lib/idlelib/EditorWindow.py61
-rw-r--r--Lib/idlelib/HyperParser.py241
-rw-r--r--Lib/idlelib/MultiCall.py404
-rw-r--r--Lib/idlelib/ParenMatch.py169
-rw-r--r--Lib/idlelib/PyParse.py46
-rw-r--r--Lib/idlelib/PyShell.py11
-rw-r--r--Lib/idlelib/config-extensions.def26
-rw-r--r--Lib/idlelib/configDialog.py10
-rw-r--r--Lib/idlelib/run.py6
13 files changed, 1592 insertions, 201 deletions
diff --git a/Lib/idlelib/AutoComplete.py b/Lib/idlelib/AutoComplete.py
new file mode 100644
index 0000000..7085386
--- /dev/null
+++ b/Lib/idlelib/AutoComplete.py
@@ -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", "<<force-open-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)
diff --git a/Lib/idlelib/AutoCompleteWindow.py b/Lib/idlelib/AutoCompleteWindow.py
new file mode 100644
index 0000000..d8bbff4
--- /dev/null
+++ b/Lib/idlelib/AutoCompleteWindow.py
@@ -0,0 +1,393 @@
+"""
+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 = "<<autocompletewindow-hide>>"
+HIDE_SEQUENCES = ("<FocusOut>", "<ButtonPress>")
+KEYPRESS_VIRTUAL_EVENT_NAME = "<<autocompletewindow-keypress>>"
+# We need to bind event beyond <Key> so that the function will be called
+# before the default specific IDLE function
+KEYPRESS_SEQUENCES = ("<Key>", "<Key-BackSpace>", "<Key-Return>",
+ "<Key-Up>", "<Key-Down>", "<Key-Home>", "<Key-End>")
+KEYRELEASE_VIRTUAL_EVENT_NAME = "<<autocompletewindow-keyrelease>>"
+KEYRELEASE_SEQUENCE = "<KeyRelease>"
+LISTUPDATE_SEQUENCE = "<ButtonRelease>"
+WINCONFIG_SEQUENCE = "<Configure>"
+DOUBLECLICK_SEQUENCE = "<Double-ButtonRelease>"
+
+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
diff --git a/Lib/idlelib/CallTipWindow.py b/Lib/idlelib/CallTipWindow.py
index 990d96e..09d6414 100644
--- a/Lib/idlelib/CallTipWindow.py
+++ b/Lib/idlelib/CallTipWindow.py
@@ -6,33 +6,65 @@ Used by the CallTips IDLE extension.
"""
from Tkinter import *
+HIDE_VIRTUAL_EVENT_NAME = "<<caltipwindow-hide>>"
+HIDE_SEQUENCES = ("<Key-Escape>", "<FocusOut>")
+CHECKHIDE_VIRTUAL_EVENT_NAME = "<<calltipwindow-checkhide>>"
+CHECKHIDE_SEQUENCES = ("<KeyRelease>", "<ButtonRelease>")
+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
+ 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):
- " Display text in calltip window"
+ 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 @@ class CallTip:
"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)
+
###############################
diff --git a/Lib/idlelib/CallTips.py b/Lib/idlelib/CallTips.py
index 97d9746..47a1d55 100644
--- a/Lib/idlelib/CallTips.py
+++ b/Lib/idlelib/CallTips.py
@@ -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", "<<force-open-calltip>>"),
+ ])
]
def __init__(self, editwin=None):
@@ -36,51 +36,47 @@ class CallTips:
# 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 calltip_cancel_event(self, event):
- 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 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 @@ def _find_constructor(class_ob):
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 @@ def get_arg_text(ob):
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:
diff --git a/Lib/idlelib/EditorWindow.py b/Lib/idlelib/EditorWindow.py
index cc38122..1cd496a 100644
--- a/Lib/idlelib/EditorWindow.py
+++ b/Lib/idlelib/EditorWindow.py
@@ -6,6 +6,7 @@ from itertools import count
from Tkinter import *
import tkSimpleDialog
import tkMessageBox
+from MultiCall import MultiCallCreator
import webbrowser
import idlever
@@ -89,7 +90,8 @@ class EditorWindow(object):
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 @@ class EditorWindow(object):
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('<KeyRelease>', self.set_line_and_column)
- self.text.bind('<ButtonRelease>', self.set_line_and_column)
+ self.text.bind("<<set-line-and-column>>", self.set_line_and_column)
+ self.text.event_add("<<set-line-and-column>>",
+ "<KeyRelease>", "<ButtonRelease>")
self.text.after_idle(self.set_line_and_column)
def set_line_and_column(self, event=None):
@@ -355,6 +358,9 @@ class EditorWindow(object):
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("<<Copy>>")
return "break"
@@ -557,14 +563,28 @@ class EditorWindow(object):
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 @@ class EditorWindow(object):
# 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.
diff --git a/Lib/idlelib/HyperParser.py b/Lib/idlelib/HyperParser.py
new file mode 100644
index 0000000..519de74
--- /dev/null
+++ b/Lib/idlelib/HyperParser.py
@@ -0,0 +1,241 @@
+"""
+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]
diff --git a/Lib/idlelib/MultiCall.py b/Lib/idlelib/MultiCall.py
new file mode 100644
index 0000000..ea8b140
--- /dev/null
+++ b/Lib/idlelib/MultiCall.py
@@ -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<<i) & x,
+ range(len(_modifiers)))):
+ nummod(b) - nummod(a))
+# _state_codes gives for each state, the portable code to be passed as mc_state
+_state_codes = [reduce(lambda x, y: x | y,
+ [_modifier_masks[i] for i in range(len(_modifiers))
+ if (1 << i) & s],
+ 0)
+ for s in _states]
+
+class _ComplexBinder:
+ # This class binds many functions, and only unbinds them when it is deleted.
+ # self.handlerids is the list of seqs and ids of binded handler functions.
+ # The binded functions sit in a dictionary of lists of lists, which maps
+ # a detail (or None) and a state into a list of functions.
+ # When a new detail is discovered, handlers for all the possible states
+ # are binded.
+
+ def __create_handler(self, lists, mc_type, mc_state):
+ def handler(event, lists = lists,
+ mc_type = mc_type, mc_state = mc_state,
+ ishandlerrunning = self.ishandlerrunning,
+ doafterhandler = self.doafterhandler):
+ ishandlerrunning[:] = [True]
+ event.mc_type = mc_type
+ event.mc_state = mc_state
+ wascalled = {}
+ r = None
+ for l in lists:
+ for i in range(len(l)-1, -1, -1):
+ func = l[i]
+ if func not in wascalled:
+ wascalled[func] = True
+ r = l[i](event)
+ if r:
+ break
+ if r:
+ break
+ ishandlerrunning[:] = []
+ # Call all functions in doafterhandler and remove them from list
+ while doafterhandler:
+ doafterhandler.pop()()
+ if r:
+ return r
+ return handler
+
+ def __init__(self, type, widget, widgetinst):
+ self.type = type
+ self.typename = _types[type][0]
+ self.widget = widget
+ self.widgetinst = widgetinst
+ self.bindedfuncs = {None: [[] for s in _states]}
+ self.handlerids = []
+ # we don't want to change the lists of functions while a handler is
+ # running - it will mess up the loop and anyway, we usually want the
+ # change to happen from the next event. So we have a list of functions
+ # for the handler to run after it finishes calling the binded functions.
+ # It calls them only once.
+ # ishandlerrunning is a list. An empty one means no, otherwise - yes.
+ # this is done so that it would be mutable.
+ self.ishandlerrunning = []
+ self.doafterhandler = []
+ for s in _states:
+ lists = [self.bindedfuncs[None][i] for i in _state_subsets[s]]
+ handler = self.__create_handler(lists, type, _state_codes[s])
+ seq = '<'+_state_names[s]+self.typename+'>'
+ 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("<<handler%d>>"%n[0], handler)
+ text.event_add("<<handler%d>>"%n[0], seq)
+ n[0] += 1
+ bindseq("<Key>")
+ bindseq("<Control-Key>")
+ bindseq("<Alt-Key-a>")
+ bindseq("<Control-Key-a>")
+ bindseq("<Alt-Control-Key-a>")
+ bindseq("<Key-b>")
+ bindseq("<Control-Button-1>")
+ bindseq("<Alt-Button-1>")
+ bindseq("<FocusOut>")
+ bindseq("<Enter>")
+ bindseq("<Leave>")
+ root.mainloop()
diff --git a/Lib/idlelib/ParenMatch.py b/Lib/idlelib/ParenMatch.py
index 407f468..673aee2 100644
--- a/Lib/idlelib/ParenMatch.py
+++ b/Lib/idlelib/ParenMatch.py
@@ -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 @@ class ParenMatch:
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 @@ class ParenMatch:
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", "<<flash-paren>>"),
+ ])
+ ]
STYLE = idleConf.GetOption('extensions','ParenMatch','style',
default='expression')
FLASH_DELAY = idleConf.GetOption('extensions','ParenMatch','flash-delay',
@@ -50,14 +50,36 @@ class ParenMatch:
BELL = idleConf.GetOption('extensions','ParenMatch','bell',
type='bool',default=1)
+ RESTORE_VIRTUAL_EVENT_NAME = "<<parenmatch-check-restore>>"
+ # We want the restore event be called before the usual return and
+ # backspace events.
+ RESTORE_SEQUENCES = ("<KeyPress>", "<ButtonPress>",
+ "<Key-Return>", "<Key-BackSpace>")
+
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 @@ class ParenMatch:
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,43 @@ class ParenMatch:
# 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
diff --git a/Lib/idlelib/PyParse.py b/Lib/idlelib/PyParse.py
index 1bf4919..1a9db67 100644
--- a/Lib/idlelib/PyParse.py
+++ b/Lib/idlelib/PyParse.py
@@ -14,9 +14,7 @@ if 0: # for throwaway debugging output
_synchre = re.compile(r"""
^
[ \t]*
- (?: if
- | for
- | while
+ (?: while
| else
| def
| return
@@ -145,29 +143,11 @@ class Parser:
# 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
@@ -363,6 +343,11 @@ class Parser:
# 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
@@ -404,6 +389,7 @@ class Parser:
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)
@@ -424,6 +410,7 @@ class Parser:
if ch in "([{":
push_stack(p)
+ bracketing.append((p, len(stack)))
lastch = ch
p = p+1
continue
@@ -433,6 +420,7 @@ class Parser:
del stack[-1]
lastch = ch
p = p+1
+ bracketing.append((p, len(stack)))
continue
if ch == '"' or ch == "'":
@@ -443,14 +431,18 @@ class Parser:
# 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 == '\\'
@@ -466,6 +458,7 @@ class Parser:
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.
@@ -590,3 +583,12 @@ class Parser:
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
diff --git a/Lib/idlelib/PyShell.py b/Lib/idlelib/PyShell.py
index 5034417..f81091b 100644
--- a/Lib/idlelib/PyShell.py
+++ b/Lib/idlelib/PyShell.py
@@ -1091,11 +1091,12 @@ class PyShell(OutputWindow):
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
diff --git a/Lib/idlelib/config-extensions.def b/Lib/idlelib/config-extensions.def
index 4a4055f..8b3f90c 100644
--- a/Lib/idlelib/config-extensions.def
+++ b/Lib/idlelib/config-extensions.def
@@ -52,22 +52,30 @@ check-module=<Alt-Key-x>
[CallTips]
enable=1
+[CallTips_cfgBindings]
+force-open-calltip=<Control-Key-backslash>
[CallTips_bindings]
-paren-open=<Key-parenleft>
-paren-close=<Key-parenright>
-check-calltip-cancel=<KeyRelease>
-calltip-cancel=<ButtonPress> <Key-Escape>
+try-open-calltip=<KeyRelease-parenleft>
+refresh-calltip=<KeyRelease-parenright> <KeyRelease-0>
[ParenMatch]
-enable=0
+enable=1
style= expression
flash-delay= 500
bell= 1
-hilite-foreground= black
-hilite-background= #43cd80
+[ParenMatch_cfgBindings]
+flash-paren=<Control-Key-0>
[ParenMatch_bindings]
-flash-open-paren=<KeyRelease-parenright> <KeyRelease-bracketright> <KeyRelease-braceright>
-check-restore=<KeyPress>
+paren-closed=<KeyRelease-parenright> <KeyRelease-bracketright> <KeyRelease-braceright>
+
+[AutoComplete]
+enable=1
+popupwait=0
+[AutoComplete_cfgBindings]
+force-open-completions=<Control-Key-space>
+[AutoComplete_bindings]
+autocomplete=<Key-Tab>
+try-open-completions=<KeyRelease-period> <KeyRelease-slash> <KeyRelease-backslash>
[CodeContext]
enable=1
diff --git a/Lib/idlelib/configDialog.py b/Lib/idlelib/configDialog.py
index 63bcae2..2d8835c 100644
--- a/Lib/idlelib/configDialog.py
+++ b/Lib/idlelib/configDialog.py
@@ -1106,6 +1106,13 @@ class ConfigDialog(Toplevel):
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 @@ class ConfigDialog(Toplevel):
instance.ResetColorizer()
instance.ResetFont()
instance.set_notabs_indentwidth()
- instance.ResetKeybindings()
+ instance.ApplyKeybindings()
instance.reset_help_menu_entries()
def Cancel(self):
@@ -1124,6 +1131,7 @@ class ConfigDialog(Toplevel):
self.destroy()
def Apply(self):
+ self.DeactivateCurrentConfig()
self.SaveAllChangedConfigs()
self.ActivateConfigChanges()
diff --git a/Lib/idlelib/run.py b/Lib/idlelib/run.py
index 8adb6f1..ae810c4 100644
--- a/Lib/idlelib/run.py
+++ b/Lib/idlelib/run.py
@@ -9,6 +9,8 @@ import threading
import Queue
import CallTips
+import AutoComplete
+
import RemoteDebugger
import RemoteObjectBrowser
import StackViewer
@@ -275,6 +277,7 @@ class Executive(object):
self.rpchandler = rpchandler
self.locals = __main__.__dict__
self.calltip = CallTips.CallTips()
+ self.autocomplete = AutoComplete.AutoComplete()
def runcode(self, code):
try:
@@ -305,6 +308,9 @@ class Executive(object):
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