Kaydet (Commit) b1754455 authored tarafından Kurt B. Kaiser's avatar Kurt B. Kaiser

Merge IDLE-syntax-branch r39668:41449 into trunk

A    idlelib/AutoCompleteWindow.py
A    idlelib/AutoComplete.py
A    idlelib/HyperParser.py
M    idlelib/PyShell.py
M    idlelib/ParenMatch.py
M    idlelib/configDialog.py
M    idlelib/EditorWindow.py
M    idlelib/PyParse.py
M    idlelib/CallTips.py
M    idlelib/CallTipWindow.py
M    idlelib/run.py
M    idlelib/config-extensions.def
A    idlelib/MultiCall.py
üst c85c74cd
"""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)
This diff is collapsed.
...@@ -6,33 +6,65 @@ Used by the CallTips IDLE extension. ...@@ -6,33 +6,65 @@ Used by the CallTips IDLE extension.
""" """
from Tkinter import * 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: class CallTip:
def __init__(self, widget): def __init__(self, widget):
self.widget = widget self.widget = widget
self.tipwindow = None self.tipwindow = self.label = None
self.id = None self.parenline = self.parencol = None
self.x = self.y = 0 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): def showtip(self, text, parenleft, parenright):
" Display text in calltip window" """Show the calltip, bind events which will close it and reposition it.
"""
# truncate overly long calltip # truncate overly long calltip
if len(text) >= 79: if len(text) >= 79:
text = text[:75] + ' ...' text = text[:75] + ' ...'
self.text = text self.text = text
if self.tipwindow or not self.text: if self.tipwindow or not self.text:
return return
self.widget.see("insert")
x, y, cx, cy = self.widget.bbox("insert") self.widget.mark_set(MARK_RIGHT, parenright)
x = x + self.widget.winfo_rootx() + 2 self.parenline, self.parencol = map(
y = y + cy + self.widget.winfo_rooty() int, self.widget.index(parenleft).split("."))
self.tipwindow = tw = Toplevel(self.widget) self.tipwindow = tw = Toplevel(self.widget)
self.position_window()
# XXX 12 Dec 2002 KBK The following command has two effects: It removes # XXX 12 Dec 2002 KBK The following command has two effects: It removes
# the calltip window border (good) but also causes (at least on # the calltip window border (good) but also causes (at least on
# Linux) the calltip to show as a top level window, burning through # Linux) the calltip to show as a top level window, burning through
# any other window dragged over it. Also, shows on all viewports! # any other window dragged over it. Also, shows on all viewports!
tw.wm_overrideredirect(1) tw.wm_overrideredirect(1)
tw.wm_geometry("+%d+%d" % (x, y))
try: try:
# This command is only needed and available on Tk >= 8.4.0 for OSX # 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 # Without it, call tips intrude on the typing process by grabbing
...@@ -41,16 +73,66 @@ class CallTip: ...@@ -41,16 +73,66 @@ class CallTip:
"help", "noActivates") "help", "noActivates")
except TclError: except TclError:
pass pass
label = Label(tw, text=self.text, justify=LEFT, self.label = Label(tw, text=self.text, justify=LEFT,
background="#ffffe0", relief=SOLID, borderwidth=1, background="#ffffe0", relief=SOLID, borderwidth=1,
font = self.widget['font']) font = self.widget['font'])
label.pack() 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): 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 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)
############################### ###############################
......
...@@ -3,21 +3,21 @@ ...@@ -3,21 +3,21 @@
Call Tips are floating windows which display function, class, and method Call Tips are floating windows which display function, class, and method
parameter and docstring information when you type an opening parenthesis, and parameter and docstring information when you type an opening parenthesis, and
which disappear when you type a closing parenthesis. which disappear when you type a closing parenthesis.
Future plans include extending the functionality to include class attributes.
""" """
import sys import sys
import string
import types import types
import CallTipWindow import CallTipWindow
from HyperParser import HyperParser
import __main__ import __main__
class CallTips: class CallTips:
menudefs = [ menudefs = [
('edit', [
("Show call tip", "<<force-open-calltip>>"),
])
] ]
def __init__(self, editwin=None): def __init__(self, editwin=None):
...@@ -36,51 +36,47 @@ class CallTips: ...@@ -36,51 +36,47 @@ class CallTips:
# See __init__ for usage # See __init__ for usage
return CallTipWindow.CallTip(self.text) return CallTipWindow.CallTip(self.text)
def _remove_calltip_window(self): def _remove_calltip_window(self, event=None):
if self.calltip: if self.calltip:
self.calltip.hidetip() self.calltip.hidetip()
self.calltip = None self.calltip = None
def paren_open_event(self, event): def force_open_calltip_event(self, event):
self._remove_calltip_window() """Happens when the user really wants to open a CallTip, even if a
name = self.get_name_at_cursor() function call is needed.
arg_text = self.fetch_tip(name) """
if arg_text: self.open_calltip(True)
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 check_calltip_cancel_event(self, event): def try_open_calltip_event(self, event):
if self.calltip: """Happens when it would be nice to open a CallTip, but not really
# If we have moved before the start of the calltip, neccesary, for example after an opening bracket, so function calls
# or off the calltip line, then cancel the tip. won't be made.
# (Later need to be smarter about multi-line, etc) """
if self.text.compare("insert", "<=", self.calltip_start) or \ self.open_calltip(False)
self.text.compare("insert", ">", self.calltip_start
+ " lineend"): def refresh_calltip_event(self, event):
self._remove_calltip_window() """If there is already a calltip window, check if it is still needed,
return "" #so the event is handled normally. and if so, reload it.
"""
def calltip_cancel_event(self, event): if self.calltip and self.calltip.is_active():
self._remove_calltip_window() self.open_calltip(False)
return "" #so the event is handled normally.
__IDCHARS = "._" + string.ascii_letters + string.digits def open_calltip(self, evalfuncs):
self._remove_calltip_window()
def get_name_at_cursor(self): hp = HyperParser(self.editwin, "insert")
idchars = self.__IDCHARS sur_paren = hp.get_surrounding_brackets('(')
str = self.text.get("insert linestart", "insert") if not sur_paren:
i = len(str) return
while i and str[i-1] in idchars: hp.set_index(sur_paren[0])
i -= 1 name = hp.get_expression()
return str[i:] 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): def fetch_tip(self, name):
"""Return the argument list and docstring of a function or class """Return the argument list and docstring of a function or class
...@@ -127,7 +123,7 @@ def _find_constructor(class_ob): ...@@ -127,7 +123,7 @@ def _find_constructor(class_ob):
return None return None
def get_arg_text(ob): 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 = "" argText = ""
if ob is not None: if ob is not None:
argOffset = 0 argOffset = 0
...@@ -150,7 +146,7 @@ def get_arg_text(ob): ...@@ -150,7 +146,7 @@ def get_arg_text(ob):
try: try:
realArgs = fob.func_code.co_varnames[argOffset:fob.func_code.co_argcount] realArgs = fob.func_code.co_varnames[argOffset:fob.func_code.co_argcount]
defaults = fob.func_defaults or [] 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 defaults = [""] * (len(realArgs)-len(defaults)) + defaults
items = map(lambda arg, dflt: arg+dflt, realArgs, defaults) items = map(lambda arg, dflt: arg+dflt, realArgs, defaults)
if fob.func_code.co_flags & 0x4: if fob.func_code.co_flags & 0x4:
......
...@@ -6,6 +6,7 @@ from itertools import count ...@@ -6,6 +6,7 @@ from itertools import count
from Tkinter import * from Tkinter import *
import tkSimpleDialog import tkSimpleDialog
import tkMessageBox import tkMessageBox
from MultiCall import MultiCallCreator
import webbrowser import webbrowser
import idlever import idlever
...@@ -89,7 +90,8 @@ class EditorWindow(object): ...@@ -89,7 +90,8 @@ class EditorWindow(object):
self.vbar = vbar = Scrollbar(top, name='vbar') self.vbar = vbar = Scrollbar(top, name='vbar')
self.text_frame = text_frame = Frame(top) self.text_frame = text_frame = Frame(top)
self.width = idleConf.GetOption('main','EditorWindow','width') 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, foreground=idleConf.GetHighlight(currentTheme,
'normal',fgBg='fg'), 'normal',fgBg='fg'),
background=idleConf.GetHighlight(currentTheme, background=idleConf.GetHighlight(currentTheme,
...@@ -264,8 +266,9 @@ class EditorWindow(object): ...@@ -264,8 +266,9 @@ class EditorWindow(object):
self.status_bar.set_label('column', 'Col: ?', side=RIGHT) self.status_bar.set_label('column', 'Col: ?', side=RIGHT)
self.status_bar.set_label('line', 'Ln: ?', side=RIGHT) self.status_bar.set_label('line', 'Ln: ?', side=RIGHT)
self.status_bar.pack(side=BOTTOM, fill=X) self.status_bar.pack(side=BOTTOM, fill=X)
self.text.bind('<KeyRelease>', self.set_line_and_column) self.text.bind("<<set-line-and-column>>", self.set_line_and_column)
self.text.bind('<ButtonRelease>', self.set_line_and_column) self.text.event_add("<<set-line-and-column>>",
"<KeyRelease>", "<ButtonRelease>")
self.text.after_idle(self.set_line_and_column) self.text.after_idle(self.set_line_and_column)
def set_line_and_column(self, event=None): def set_line_and_column(self, event=None):
...@@ -355,6 +358,9 @@ class EditorWindow(object): ...@@ -355,6 +358,9 @@ class EditorWindow(object):
return "break" return "break"
def copy(self,event): 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>>") self.text.event_generate("<<Copy>>")
return "break" return "break"
...@@ -557,14 +563,28 @@ class EditorWindow(object): ...@@ -557,14 +563,28 @@ class EditorWindow(object):
idleConf.GetOption('main','EditorWindow','font-size'), idleConf.GetOption('main','EditorWindow','font-size'),
fontWeight)) fontWeight))
def ResetKeybindings(self): def RemoveKeybindings(self):
"Update the keybindings if they are changed" "Remove the keybindings before they are changed."
# Called from configDialog.py # Called from configDialog.py
self.Bindings.default_keydefs=idleConf.GetCurrentKeySet() self.Bindings.default_keydefs=idleConf.GetCurrentKeySet()
keydefs = self.Bindings.default_keydefs keydefs = self.Bindings.default_keydefs
for event, keylist in keydefs.items(): 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() self.apply_bindings()
for extensionName in self.get_standard_extension_names():
keydefs = idleConf.GetExtensionBindings(extensionName)
if keydefs:
self.apply_bindings(keydefs)
#update menu accelerators #update menu accelerators
menuEventDict={} menuEventDict={}
for menu in self.Bindings.menudefs: for menu in self.Bindings.menudefs:
...@@ -1064,17 +1084,28 @@ class EditorWindow(object): ...@@ -1064,17 +1084,28 @@ class EditorWindow(object):
# open/close first need to find the last stmt # open/close first need to find the last stmt
lno = index2line(text.index('insert')) lno = index2line(text.index('insert'))
y = PyParse.Parser(self.indentwidth, self.tabwidth) y = PyParse.Parser(self.indentwidth, self.tabwidth)
for context in self.num_context_lines: if not self.context_use_ps1:
startat = max(lno - context, 1) for context in self.num_context_lines:
startatindex = repr(startat) + ".0" 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") rawtext = text.get(startatindex, "insert")
y.set_str(rawtext) y.set_str(rawtext)
bod = y.find_good_parse_start( y.set_lo(0)
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)
c = y.get_continuation_type() c = y.get_continuation_type()
if c != PyParse.C_NONE: if c != PyParse.C_NONE:
# The current stmt hasn't ended yet. # The current stmt hasn't ended yet.
......
This diff is collapsed.
This diff is collapsed.
...@@ -3,17 +3,14 @@ ...@@ -3,17 +3,14 @@
When you hit a right paren, the cursor should move briefly to the left When you hit a right paren, the cursor should move briefly to the left
paren. Paren here is used generically; the matching applies to paren. Paren here is used generically; the matching applies to
parentheses, square brackets, and curly braces. 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 HyperParser import HyperParser
from EditorWindow import EditorWindow, index2line
from configHandler import idleConf from configHandler import idleConf
keysym_opener = {"parenright":'(', "bracketright":'[', "braceright":'{'}
CHECK_DELAY = 100 # miliseconds
class ParenMatch: class ParenMatch:
"""Highlight matching parentheses """Highlight matching parentheses
...@@ -31,7 +28,6 @@ class ParenMatch: ...@@ -31,7 +28,6 @@ class ParenMatch:
expression from the left paren to the right paren. expression from the left paren to the right paren.
TODO: TODO:
- fix interaction with CallTips
- extend IDLE with configuration dialog to change options - extend IDLE with configuration dialog to change options
- implement rest of Emacs highlight styles (see below) - implement rest of Emacs highlight styles (see below)
- print mismatch warning in IDLE status window - print mismatch warning in IDLE status window
...@@ -41,7 +37,11 @@ class ParenMatch: ...@@ -41,7 +37,11 @@ class ParenMatch:
to the right of a right paren. I don't know how to do that in Tk, to the right of a right paren. I don't know how to do that in Tk,
so I haven't bothered. so I haven't bothered.
""" """
menudefs = [] menudefs = [
('edit', [
("Show surrounding parens", "<<flash-paren>>"),
])
]
STYLE = idleConf.GetOption('extensions','ParenMatch','style', STYLE = idleConf.GetOption('extensions','ParenMatch','style',
default='expression') default='expression')
FLASH_DELAY = idleConf.GetOption('extensions','ParenMatch','flash-delay', FLASH_DELAY = idleConf.GetOption('extensions','ParenMatch','flash-delay',
...@@ -50,14 +50,36 @@ class ParenMatch: ...@@ -50,14 +50,36 @@ class ParenMatch:
BELL = idleConf.GetOption('extensions','ParenMatch','bell', BELL = idleConf.GetOption('extensions','ParenMatch','bell',
type='bool',default=1) 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): def __init__(self, editwin):
self.editwin = editwin self.editwin = editwin
self.text = editwin.text 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.counter = 0
self._restore = None self.is_restore_active = 0
self.set_style(self.STYLE) 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): def set_style(self, style):
self.STYLE = style self.STYLE = style
if style == "default": if style == "default":
...@@ -67,23 +89,38 @@ class ParenMatch: ...@@ -67,23 +89,38 @@ class ParenMatch:
self.create_tag = self.create_tag_expression self.create_tag = self.create_tag_expression
self.set_timeout = self.set_timeout_none self.set_timeout = self.set_timeout_none
def flash_open_paren_event(self, event): def flash_paren_event(self, event):
index = self.finder.find(keysym_type(event.keysym)) indices = HyperParser(self.editwin, "insert").get_surrounding_brackets()
if index is None: if indices is None:
self.warn_mismatched() self.warn_mismatched()
return return
self._restore = 1 self.activate_restore()
self.create_tag(index) 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() self.set_timeout()
def check_restore_event(self, event=None): def restore_event(self, event=None):
if self._restore: self.text.tag_delete("paren")
self.text.tag_delete("paren") self.deactivate_restore()
self._restore = None self.counter += 1 # disable the last timer, if there is one.
def handle_restore_timer(self, timer_count): def handle_restore_timer(self, timer_count):
if timer_count + 1 == self.counter: if timer_count == self.counter:
self.check_restore_event() self.restore_event()
def warn_mismatched(self): def warn_mismatched(self):
if self.BELL: if self.BELL:
...@@ -92,87 +129,43 @@ class ParenMatch: ...@@ -92,87 +129,43 @@ class ParenMatch:
# any one of the create_tag_XXX methods can be used depending on # any one of the create_tag_XXX methods can be used depending on
# the style # the style
def create_tag_default(self, index): def create_tag_default(self, indices):
"""Highlight the single paren that matches""" """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) self.text.tag_config("paren", self.HILITE_CONFIG)
def create_tag_expression(self, index): def create_tag_expression(self, indices):
"""Highlight the entire expression""" """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) self.text.tag_config("paren", self.HILITE_CONFIG)
# any one of the set_timeout_XXX methods can be used depending on # any one of the set_timeout_XXX methods can be used depending on
# the style # the style
def set_timeout_none(self): def set_timeout_none(self):
"""Highlight will remain until user input turns it off""" """Highlight will remain until user input turns it off
pass 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): def set_timeout_last(self):
"""The last highlight created will be removed after .5 sec""" """The last highlight created will be removed after .5 sec"""
# associate a counter with an event; only disable the "paren" # associate a counter with an event; only disable the "paren"
# tag if the event is for the most recent timer. # tag if the event is for the most recent timer.
self.counter += 1
self.editwin.text_frame.after(self.FLASH_DELAY, self.editwin.text_frame.after(self.FLASH_DELAY,
lambda self=self, c=self.counter: \ lambda self=self, c=self.counter: \
self.handle_restore_timer(c)) 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
...@@ -14,9 +14,7 @@ if 0: # for throwaway debugging output ...@@ -14,9 +14,7 @@ if 0: # for throwaway debugging output
_synchre = re.compile(r""" _synchre = re.compile(r"""
^ ^
[ \t]* [ \t]*
(?: if (?: while
| for
| while
| else | else
| def | def
| return | return
...@@ -145,29 +143,11 @@ class Parser: ...@@ -145,29 +143,11 @@ class Parser:
# This will be reliable iff given a reliable is_char_in_string # This will be reliable iff given a reliable is_char_in_string
# function, meaning that when it says "no", it's absolutely # function, meaning that when it says "no", it's absolutely
# guaranteed that the char is not in a string. # 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): _synchre=_synchre):
str, pos = self.str, None 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: if not is_char_in_string:
# no clue -- make the caller pass everything # no clue -- make the caller pass everything
return None return None
...@@ -363,6 +343,11 @@ class Parser: ...@@ -363,6 +343,11 @@ class Parser:
# Creates: # Creates:
# self.stmt_start, stmt_end # self.stmt_start, stmt_end
# slice indices of last interesting stmt # 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 # self.lastch
# last non-whitespace character before optional trailing # last non-whitespace character before optional trailing
# comment # comment
...@@ -404,6 +389,7 @@ class Parser: ...@@ -404,6 +389,7 @@ class Parser:
lastch = "" lastch = ""
stack = [] # stack of open bracket indices stack = [] # stack of open bracket indices
push_stack = stack.append push_stack = stack.append
bracketing = [(p, 0)]
while p < q: while p < q:
# suck up all except ()[]{}'"#\\ # suck up all except ()[]{}'"#\\
m = _chew_ordinaryre(str, p, q) m = _chew_ordinaryre(str, p, q)
...@@ -424,6 +410,7 @@ class Parser: ...@@ -424,6 +410,7 @@ class Parser:
if ch in "([{": if ch in "([{":
push_stack(p) push_stack(p)
bracketing.append((p, len(stack)))
lastch = ch lastch = ch
p = p+1 p = p+1
continue continue
...@@ -433,6 +420,7 @@ class Parser: ...@@ -433,6 +420,7 @@ class Parser:
del stack[-1] del stack[-1]
lastch = ch lastch = ch
p = p+1 p = p+1
bracketing.append((p, len(stack)))
continue continue
if ch == '"' or ch == "'": if ch == '"' or ch == "'":
...@@ -443,14 +431,18 @@ class Parser: ...@@ -443,14 +431,18 @@ class Parser:
# strings to a couple of characters per line. study1 # strings to a couple of characters per line. study1
# also needed to keep track of newlines, and we don't # also needed to keep track of newlines, and we don't
# have to. # have to.
bracketing.append((p, len(stack)+1))
lastch = ch lastch = ch
p = _match_stringre(str, p, q).end() p = _match_stringre(str, p, q).end()
bracketing.append((p, len(stack)))
continue continue
if ch == '#': if ch == '#':
# consume comment and trailing newline # consume comment and trailing newline
bracketing.append((p, len(stack)+1))
p = str.find('\n', p, q) + 1 p = str.find('\n', p, q) + 1
assert p > 0 assert p > 0
bracketing.append((p, len(stack)))
continue continue
assert ch == '\\' assert ch == '\\'
...@@ -466,6 +458,7 @@ class Parser: ...@@ -466,6 +458,7 @@ class Parser:
self.lastch = lastch self.lastch = lastch
if stack: if stack:
self.lastopenbracketpos = stack[-1] self.lastopenbracketpos = stack[-1]
self.stmt_bracketing = tuple(bracketing)
# Assuming continuation is C_BRACKET, return the number # Assuming continuation is C_BRACKET, return the number
# of spaces the next line should be indented. # of spaces the next line should be indented.
...@@ -590,3 +583,12 @@ class Parser: ...@@ -590,3 +583,12 @@ class Parser:
def get_last_open_bracket_pos(self): def get_last_open_bracket_pos(self):
self._study2() self._study2()
return self.lastopenbracketpos 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
...@@ -1091,11 +1091,12 @@ class PyShell(OutputWindow): ...@@ -1091,11 +1091,12 @@ class PyShell(OutputWindow):
self.recall(self.text.get(next[0], next[1]), event) self.recall(self.text.get(next[0], next[1]), event)
return "break" return "break"
# No stdin mark -- just get the current line, less any prompt # No stdin mark -- just get the current line, less any prompt
line = self.text.get("insert linestart", "insert lineend") indices = self.text.tag_nextrange("console", "insert linestart")
last_line_of_prompt = sys.ps1.split('\n')[-1] if indices and \
if line.startswith(last_line_of_prompt): self.text.compare(indices[0], "<=", "insert linestart"):
line = line[len(last_line_of_prompt):] self.recall(self.text.get(indices[1], "insert lineend"), event)
self.recall(line, event) else:
self.recall(self.text.get("insert linestart", "insert lineend"), event)
return "break" return "break"
# If we're between the beginning of the line and the iomark, i.e. # 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 # in the prompt area, move to the end of the prompt
......
...@@ -52,22 +52,30 @@ check-module=<Alt-Key-x> ...@@ -52,22 +52,30 @@ check-module=<Alt-Key-x>
[CallTips] [CallTips]
enable=1 enable=1
[CallTips_cfgBindings]
force-open-calltip=<Control-Key-backslash>
[CallTips_bindings] [CallTips_bindings]
paren-open=<Key-parenleft> try-open-calltip=<KeyRelease-parenleft>
paren-close=<Key-parenright> refresh-calltip=<KeyRelease-parenright> <KeyRelease-0>
check-calltip-cancel=<KeyRelease>
calltip-cancel=<ButtonPress> <Key-Escape>
[ParenMatch] [ParenMatch]
enable=0 enable=1
style= expression style= expression
flash-delay= 500 flash-delay= 500
bell= 1 bell= 1
hilite-foreground= black [ParenMatch_cfgBindings]
hilite-background= #43cd80 flash-paren=<Control-Key-0>
[ParenMatch_bindings] [ParenMatch_bindings]
flash-open-paren=<KeyRelease-parenright> <KeyRelease-bracketright> <KeyRelease-braceright> paren-closed=<KeyRelease-parenright> <KeyRelease-bracketright> <KeyRelease-braceright>
check-restore=<KeyPress>
[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] [CodeContext]
enable=1 enable=1
......
...@@ -1106,6 +1106,13 @@ class ConfigDialog(Toplevel): ...@@ -1106,6 +1106,13 @@ class ConfigDialog(Toplevel):
idleConf.userCfg[configType].Save() idleConf.userCfg[configType].Save()
self.ResetChangedItems() #clear the changed items dict 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): def ActivateConfigChanges(self):
"Dynamically apply configuration changes" "Dynamically apply configuration changes"
winInstances=self.parent.instance_dict.keys() winInstances=self.parent.instance_dict.keys()
...@@ -1113,7 +1120,7 @@ class ConfigDialog(Toplevel): ...@@ -1113,7 +1120,7 @@ class ConfigDialog(Toplevel):
instance.ResetColorizer() instance.ResetColorizer()
instance.ResetFont() instance.ResetFont()
instance.set_notabs_indentwidth() instance.set_notabs_indentwidth()
instance.ResetKeybindings() instance.ApplyKeybindings()
instance.reset_help_menu_entries() instance.reset_help_menu_entries()
def Cancel(self): def Cancel(self):
...@@ -1124,6 +1131,7 @@ class ConfigDialog(Toplevel): ...@@ -1124,6 +1131,7 @@ class ConfigDialog(Toplevel):
self.destroy() self.destroy()
def Apply(self): def Apply(self):
self.DeactivateCurrentConfig()
self.SaveAllChangedConfigs() self.SaveAllChangedConfigs()
self.ActivateConfigChanges() self.ActivateConfigChanges()
......
...@@ -9,6 +9,8 @@ import threading ...@@ -9,6 +9,8 @@ import threading
import Queue import Queue
import CallTips import CallTips
import AutoComplete
import RemoteDebugger import RemoteDebugger
import RemoteObjectBrowser import RemoteObjectBrowser
import StackViewer import StackViewer
...@@ -275,6 +277,7 @@ class Executive(object): ...@@ -275,6 +277,7 @@ class Executive(object):
self.rpchandler = rpchandler self.rpchandler = rpchandler
self.locals = __main__.__dict__ self.locals = __main__.__dict__
self.calltip = CallTips.CallTips() self.calltip = CallTips.CallTips()
self.autocomplete = AutoComplete.AutoComplete()
def runcode(self, code): def runcode(self, code):
try: try:
...@@ -305,6 +308,9 @@ class Executive(object): ...@@ -305,6 +308,9 @@ class Executive(object):
def get_the_calltip(self, name): def get_the_calltip(self, name):
return self.calltip.fetch_tip(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): def stackviewer(self, flist_oid=None):
if self.usr_exc_info: if self.usr_exc_info:
typ, val, tb = self.usr_exc_info typ, val, tb = self.usr_exc_info
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment