Initial checking of Tk-based Python IDE.
Features: text editor with syntax coloring and undo;
subclassed into interactive Python shell which adds history.
diff --git a/Tools/idle/AutoExpand.py b/Tools/idle/AutoExpand.py
new file mode 100644
index 0000000..0d80ce8
--- /dev/null
+++ b/Tools/idle/AutoExpand.py
@@ -0,0 +1,75 @@
+import string
+import re
+
+class AutoExpand:
+    
+    wordchars = string.letters + string.digits + "_"
+
+    def __init__(self, text):
+        self.text = text
+        self.text.wordlist = None
+        self.state = None
+        self.text.bind("<<expand-word>>", self.autoexpand)
+        
+    def autoexpand(self, event):
+        curinsert = self.text.index("insert")
+        curline = self.text.get("insert linestart", "insert lineend")
+        if not self.state:
+            words = self.getwords()
+            index = 0
+        else:
+            words, index, insert, line = self.state
+            if insert != curinsert or line != curline:
+                words = self.getwords()
+                index = 0
+        if not words:
+            self.text.bell()
+            return "break"
+        word = self.getprevword()
+        self.text.delete("insert - %d chars" % len(word), "insert")
+        newword = words[index]
+        index = (index + 1) % len(words)
+        if index == 0:
+            self.text.bell()            # Warn we cycled around
+        self.text.insert("insert", newword)
+        curinsert = self.text.index("insert")
+        curline = self.text.get("insert linestart", "insert lineend")
+        self.state = words, index, curinsert, curline
+        return "break"
+        
+    def getwords(self):
+        word = self.getprevword()
+        if not word:
+            return []
+        before = self.text.get("1.0", "insert wordstart")
+        wbefore = re.findall(r"\b" + word + r"\w+\b", before)
+        del before
+        after = self.text.get("insert wordend", "end")
+        wafter = re.findall(r"\b" + word + r"\w+\b", after)
+        del after
+        if not wbefore and not wafter:
+            return []
+        words = []
+        dict = {}
+        # search backwards through words before
+        wbefore.reverse()
+        for w in wbefore:
+            if dict.get(w):
+                continue
+            words.append(w)
+            dict[w] = w
+        # search onwards through words after
+        for w in wafter:
+            if dict.get(w):
+                continue
+            words.append(w)
+            dict[w] = w
+        words.append(word)
+        return words
+        
+    def getprevword(self):
+        line = self.text.get("insert linestart", "insert")
+        i = len(line)
+        while i > 0 and line[i-1] in self.wordchars:
+            i = i-1
+        return line[i:]
diff --git a/Tools/idle/AutoIndent.py b/Tools/idle/AutoIndent.py
new file mode 100644
index 0000000..d800589
--- /dev/null
+++ b/Tools/idle/AutoIndent.py
@@ -0,0 +1,124 @@
+import string
+
+class AutoIndent:
+
+    def __init__(self, text, prefertabs=0, spaceindent=4*" "):
+        self.text = text
+        self.prefertabs = prefertabs
+        self.spaceindent = spaceindent
+        text.bind("<<newline-and-indent>>", self.autoindent)
+        text.bind("<<indent-region>>", self.indentregion)
+        text.bind("<<dedent-region>>", self.dedentregion)
+        text.bind("<<comment-region>>", self.commentregion)
+        text.bind("<<uncomment-region>>", self.uncommentregion)
+
+    def config(self, **options):
+        for key, value in options.items():
+            if key == 'prefertabs':
+                self.prefertabs = value
+            elif key == 'spaceindent':
+                self.spaceindent = value
+            else:
+                raise KeyError, "bad option name: %s" % `key`
+
+    def autoindent(self, event):
+        text = self.text
+        line = text.get("insert linestart", "insert")
+        i, n = 0, len(line)
+        while i < n and line[i] in " \t":
+            i = i+1
+        indent = line[:i]
+        lastchar = text.get("insert -1c")
+        if lastchar == ":":
+            if not indent:
+                if self.prefertabs:
+                    indent = "\t"
+                else:
+                    indent = self.spaceindent
+            elif indent[-1] == "\t":
+                indent = indent + "\t"
+            else:
+                indent = indent + self.spaceindent
+        text.insert("insert", "\n" + indent)
+        text.see("insert")
+        return "break"
+
+    def indentregion(self, event):
+        head, tail, chars, lines = self.getregion()
+        for pos in range(len(lines)):
+            line = lines[pos]
+            if line:
+                i, n = 0, len(line)
+                while i < n and line[i] in " \t":
+                    i = i+1
+                line = line[:i] + "    " + line[i:]
+                lines[pos] = line
+        self.setregion(head, tail, chars, lines)
+        return "break"
+
+    def dedentregion(self, event):
+        head, tail, chars, lines = self.getregion()
+        for pos in range(len(lines)):
+            line = lines[pos]
+            if line:
+                i, n = 0, len(line)
+                while i < n and line[i] in " \t":
+                    i = i+1
+                indent, line = line[:i], line[i:]
+                if indent:
+                    if indent == "\t" or indent[-2:] == "\t\t":
+                        indent = indent[:-1] + "    "
+                    elif indent[-4:] == "    ":
+                        indent = indent[:-4]
+                    else:
+                        indent = string.expandtabs(indent, 8)
+                        indent = indent[:-4]
+                    line = indent + line
+                lines[pos] = line
+        self.setregion(head, tail, chars, lines)
+        return "break"
+
+    def commentregion(self, event):
+        head, tail, chars, lines = self.getregion()
+        for pos in range(len(lines)):
+            line = lines[pos]
+            if not line:
+                continue
+            lines[pos] = '##' + line
+        self.setregion(head, tail, chars, lines)
+
+    def uncommentregion(self, event):
+        head, tail, chars, lines = self.getregion()
+        for pos in range(len(lines)):
+            line = lines[pos]
+            if not line:
+                continue
+            if line[:2] == '##':
+                line = line[2:]
+            elif line[:1] == '#':
+                line = line[1:]
+            lines[pos] = line
+        self.setregion(head, tail, chars, lines)
+
+    def getregion(self):
+        text = self.text
+        head = text.index("sel.first linestart")
+        tail = text.index("sel.last -1c lineend +1c")
+        if not (head and tail):
+            head = text.index("insert linestart")
+            tail = text.index("insert lineend +1c")
+        chars = text.get(head, tail)
+        lines = string.split(chars, "\n")
+        return head, tail, chars, lines
+
+    def setregion(self, head, tail, chars, lines):
+        text = self.text
+        newchars = string.join(lines, "\n")
+        if newchars == chars:
+            text.bell()
+            return
+        text.tag_remove("sel", "1.0", "end")
+        text.mark_set("insert", head)
+        text.delete(head, tail)
+        text.insert(head, newchars)
+        text.tag_add("sel", head, "insert")
diff --git a/Tools/idle/Bindings.py b/Tools/idle/Bindings.py
new file mode 100644
index 0000000..9e2e791
--- /dev/null
+++ b/Tools/idle/Bindings.py
@@ -0,0 +1,92 @@
+# The first item of each tuple is the virtual event;
+# each of the remaining items is an actual key binding for the event.
+# (This conveniently forms an argument list for event_add().)
+
+win_bindings = [
+    ("<<beginning-of-line>>", "<Control-a>", "<Home>"),
+
+    ("<<expand-word>>", "<Meta-slash>", "<Alt-slash>"),
+
+    ("<<newline-and-indent>>", "<Key-Return>", "<KP_Enter>"),
+    ("<<plain-newline-and-indent>>", "<Control-j>"),
+
+    ("<<interrupt-execution>>", "<Control-c>"),
+    ("<<end-of-file>>", "<Control-d>"),
+
+    ("<<dedent-region>>", "<Control-bracketleft>"),
+    ("<<indent-region>>", "<Control-bracketright>"),
+
+    ("<<comment-region>>", "<Meta-Key-3>", "<Alt-Key-3>"),
+    ("<<uncomment-region>>", "<Meta-Key-4>", "<Alt-Key-4>"),
+
+    ("<<history-previous>>", "<Meta-p>", "<Alt-p>"),
+    ("<<history-next>>", "<Meta-n>", "<Alt-n>"),
+
+    ("<<toggle-auto-coloring>>", "<Control-slash>"),
+
+    ("<<close-all-windows>>", "<Control-q>"),
+    ("<<open-new-window>>", "<Control-n>"),
+    ("<<open-window-from-file>>", "<Control-o>"),
+    ("<<save-window>>", "<Control-s>"),
+    ("<<save-window-as-file>>", "<Control-w>"),
+    ("<<save-copy-of-window-as-file>>", "<Meta-w>"),
+
+    ("<<find>>", "<Control-f>"),
+    ("<<find-next>>", "<F3>"),
+    ("<<find-same>>", "<Control-F3>"),
+    ("<<goto-line>>", "<Alt-g>", "<Meta-g>"),
+
+    ("<<undo>>", "<Control-z>"),
+    ("<<redo>>", "<Control-y>"),
+    ("<<dump-undo-state>>", "<Control-backslash>"),
+]
+
+emacs_bindings = [
+    ("<<beginning-of-line>>", "<Control-a>", "<Home>"),
+    ("<<center-insert>>", "<Control-l>"),
+
+    ("<<expand-word>>", "<Meta-slash>", "<Alt-slash>"),
+
+    ("<<newline-and-indent>>", "<Key-Return>", "<KP_Enter>"),
+    ("<<plain-newline-and-indent>>", "<Control-j>"),
+
+    ("<<interrupt-execution>>", "<Control-c>"),
+    ("<<end-of-file>>", "<Control-d>"),
+
+    ("<<dedent-region>>",
+     "<Meta-bracketleft>", "<Alt-bracketleft>", "<Control-bracketleft>"),
+    ("<<indent-region>>",
+     "<Meta-bracketright>", "<Alt-bracketright>", "<Control-bracketright>"),
+
+    ("<<comment-region>>", "<Meta-Key-3>", "<Alt-Key-3>"),
+    ("<<uncomment-region>>", "<Meta-Key-4>", "<Alt-Key-4>"),
+
+    ("<<history-previous>>", "<Meta-p>", "<Alt-p>"),
+    ("<<history-next>>", "<Meta-n>", "<Alt-n>"),
+
+    ("<<toggle-auto-coloring>>", "<Control-slash>"),
+
+    ("<<close-all-windows>>", "<Control-x><Control-c>"),
+    ("<<close-window>>", "<Control-x><Control-0>"),
+    ("<<open-new-window>>", "<Control-x><Control-n>"),
+    ("<<open-window-from-file>>", "<Control-x><Control-f>"),
+    ("<<save-window>>", "<Control-x><Control-s>"),
+    ("<<save-window-as-file>>", "<Control-x><Control-w>"),
+    ("<<save-copy-of-window-as-file>>", "<Control-x><w>"),
+
+    ("<<find>>", "<Control-u><Control-u><Control-s>"),
+    ("<<find-next>>", "<Control-u><Control-s>"),
+    ("<<find-same>>", "<Control-s>"),
+    ("<<goto-line>>", "<Alt-g>", "<Meta-g>"),
+
+    ("<<undo>>", "<Control-z>"),
+    ("<<redo>>", "<Alt-z>", "<Meta-z>"),
+    ("<<dump-undo-state>>", "<Control-backslash>"),
+]
+
+default_bindings = emacs_bindings
+
+def apply_bindings(text, bindings=default_bindings):
+    event_add = text.event_add
+    for args in bindings:
+        apply(event_add, args)
diff --git a/Tools/idle/ColorDelegator.py b/Tools/idle/ColorDelegator.py
new file mode 100644
index 0000000..0cbf2fb
--- /dev/null
+++ b/Tools/idle/ColorDelegator.py
@@ -0,0 +1,203 @@
+import time
+import string
+import re
+import keyword
+from Tkinter import *
+from Delegator import Delegator
+
+__debug__ = 0
+
+
+def any(name, list):
+    return "(?P<%s>" % name + string.join(list, "|") + ")"
+
+def make_pat():
+    kw = r"\b" + any("KEYWORD", keyword.kwlist) + r"\b"
+    comment = any("COMMENT", [r"#[^\n]*"])
+    sqstring = r"(\b[rR])?'([^'\\\n]|\\.)*'?"
+    dqstring = r'(\b[rR])?"([^"\\\n]|\\.)*"?'
+    sq3string = r"(\b[rR])?'''([^'\\]|\\.|'(?!''))*(''')?"
+    dq3string = r'(\b[rR])?"""([^"\\]|\\.|"(?!""))*(""")?'
+    string = any("STRING", [sq3string, dq3string, sqstring, dqstring])
+    return kw + "|" + comment + "|" + string + "|" + any("SYNC", [r"\n"])
+
+prog = re.compile(make_pat(), re.S)
+idprog = re.compile(r"\s+(\w+)", re.S)
+
+class ColorDelegator(Delegator):
+
+    def __init__(self):
+        Delegator.__init__(self)
+        self.prog = prog
+	self.idprog = idprog
+
+    def setdelegate(self, delegate):
+	if self.delegate is not None:
+            self.unbind("<<toggle-auto-coloring>>")
+        Delegator.setdelegate(self, delegate)
+        if delegate is not None:
+            self.config_colors()
+            self.bind("<<toggle-auto-coloring>>", self.toggle_colorize_event)
+            self.notify_range("1.0", "end")
+
+    def config_colors(self):
+        for tag, cnf in self.tagdefs.items():
+            if cnf:
+                apply(self.tag_configure, (tag,), cnf)
+
+    tagdefs = {
+        "COMMENT":    {"foreground": "#dd0000"},
+        "KEYWORD":    {"foreground": "#ff7700"},
+        "STRING":     {"foreground": "#00aa00"},
+        "DEFINITION": {"foreground": "#0000ff"},
+
+        "SYNC":       {}, #{"background": "#ffff00"},
+        "TODO":       {}, #{"background": "#cccccc"},
+        }
+
+    def insert(self, index, chars, tags=None):
+        index = self.index(index)
+        self.delegate.insert(index, chars, tags)
+        self.notify_range(index, index + "+%dc" % len(chars))
+
+    def delete(self, index1, index2=None):
+        index1 = self.index(index1)
+        self.delegate.delete(index1, index2)
+        self.notify_range(index1)
+
+    after_id = None
+    allow_colorizing = 1
+    colorizing = 0
+
+    def notify_range(self, index1, index2=None):
+        self.tag_add("TODO", index1, index2)
+        if self.after_id:
+            if __debug__: print "colorizing already scheduled"
+            return
+        if self.colorizing:
+            self.stop_colorizing = 1
+	    if __debug__: print "stop colorizing"
+        if self.allow_colorizing:
+	    if __debug__: print "schedule colorizing"
+            self.after_id = self.after(1, self.recolorize)
+
+    def close(self):
+        if self.after_id:
+            after_id = self.after_id
+            self.after_id = None
+            if __debug__: print "cancel scheduled recolorizer"
+            self.after_cancel(after_id)
+        self.allow_colorizing = 0
+        self.stop_colorizing = 1
+
+    def toggle_colorize_event(self, event):
+        if self.after_id:
+            after_id = self.after_id
+            self.after_id = None
+            if __debug__: print "cancel scheduled recolorizer"
+            self.after_cancel(after_id)
+	if self.allow_colorizing and self.colorizing:
+	    if __debug__: print "stop colorizing"
+	    self.stop_colorizing = 1
+	self.allow_colorizing = not self.allow_colorizing
+	if self.allow_colorizing and not self.colorizing:
+	    self.after_id = self.after(1, self.recolorize)
+	if __debug__:
+	    print "auto colorizing turned", self.allow_colorizing and "on" or "off"
+	return "break"
+
+    def recolorize(self):
+        self.after_id = None
+        if not self.delegate:
+            if __debug__: print "no delegate"
+            return
+	if not self.allow_colorizing:
+	    if __debug__: print "auto colorizing is off"
+	    return
+	if self.colorizing:
+	    if __debug__: print "already colorizing"
+            return
+        try:
+	    self.stop_colorizing = 0
+            self.colorizing = 1
+            if __debug__: print "colorizing..."
+            t0 = time.clock()
+            self.recolorize_main()
+            t1 = time.clock()
+            if __debug__: print "%.3f seconds" % (t1-t0)
+        finally:
+            self.colorizing = 0
+        if self.allow_colorizing and self.tag_nextrange("TODO", "1.0"):
+	    if __debug__: print "reschedule colorizing"
+            self.after_id = self.after(1, self.recolorize)
+
+    def recolorize_main(self):
+	next = "1.0"
+	was_ok = is_ok = 0
+	while 1:
+	    item = self.tag_nextrange("TODO", next)
+	    if not item:
+		break
+	    head, tail = item
+	    self.tag_remove("SYNC", head, tail)
+	    item = self.tag_prevrange("SYNC", head)
+	    if item:
+		head = item[1]
+	    else:
+		head = "1.0"
+
+	    chars = ""
+	    mark = head
+	    is_ok = was_ok = 0
+	    while not (was_ok and is_ok):
+		next = self.index(mark + " lineend +1c")
+		was_ok = "SYNC" in self.tag_names(next + "-1c")
+		line = self.get(mark, next)
+		##print head, "get", mark, next, "->", `line`
+		if not line:
+		    return
+		for tag in self.tagdefs.keys():
+		    self.tag_remove(tag, mark, next)
+		chars = chars + line
+		m = self.prog.search(chars)
+		while m:
+		    i, j = m.span()
+		    for key, value in m.groupdict().items():
+			if value:
+			    a, b = m.span(key)
+			    self.tag_add(key,
+					 head + "+%dc" % a,
+					 head + "+%dc" % b)
+			    if value in ("def", "class"):
+				m1 = self.idprog.match(chars, b)
+				if m1:
+				    a, b = m1.span(1)
+				    self.tag_add("DEFINITION",
+						 head + "+%dc" % a,
+						 head + "+%dc" % b)
+		    m = self.prog.search(chars, j)
+		is_ok = "SYNC" in self.tag_names(next + "-1c")
+		mark = next
+		if is_ok:
+		    head = mark
+		    chars = ""
+		self.update()
+		if self.stop_colorizing:
+		    if __debug__: print "colorizing stopped"
+		    return
+
+
+def main():
+    from Percolator import Percolator
+    root = Tk()
+    root.wm_protocol("WM_DELETE_WINDOW", root.quit)
+    text = Text(background="white")
+    text.pack(expand=1, fill="both")
+    text.focus_set()
+    p = Percolator(text)
+    d = ColorDelegator()
+    p.insertfilter(d)
+    root.mainloop()
+
+if __name__ == "__main__":
+    main()
diff --git a/Tools/idle/Delegator.py b/Tools/idle/Delegator.py
new file mode 100644
index 0000000..6125591
--- /dev/null
+++ b/Tools/idle/Delegator.py
@@ -0,0 +1,33 @@
+class Delegator:
+
+    # The cache is only used to be able to change delegates!
+
+    def __init__(self, delegate=None):
+        self.delegate = delegate
+        self.__cache = {}
+
+    def __getattr__(self, name):
+        attr = getattr(self.delegate, name) # May raise AttributeError
+        setattr(self, name, attr)
+        self.__cache[name] = attr
+        return attr
+
+    def resetcache(self):
+        for key in self.__cache.keys():
+            try:
+                delattr(self, key)
+            except AttributeError:
+                pass
+        self.__cache.clear()
+
+    def cachereport(self):
+        keys = self.__cache.keys()
+        keys.sort()
+        print keys
+
+    def setdelegate(self, delegate):
+        self.resetcache()
+        self.delegate = delegate
+
+    def getdelegate(self):
+        return self.delegate
diff --git a/Tools/idle/EditorWindow.py b/Tools/idle/EditorWindow.py
new file mode 100644
index 0000000..2028ed1
--- /dev/null
+++ b/Tools/idle/EditorWindow.py
@@ -0,0 +1,175 @@
+import sys
+import os
+import string
+from Tkinter import *
+
+
+class EditorWindow:
+
+    from Percolator import Percolator
+    from ColorDelegator import ColorDelegator
+    from UndoDelegator import UndoDelegator
+    from IOBinding import IOBinding
+    from SearchBinding import SearchBinding
+    from AutoIndent import AutoIndent
+    from AutoExpand import AutoExpand
+    import Bindings
+
+    def __init__(self, root, filename=None):
+        self.top = top = Toplevel(root)
+        self.vbar = vbar = Scrollbar(top, name='vbar')
+        self.text = text = Text(top, name='text')
+
+        self.Bindings.apply_bindings(text)
+
+        self.top.protocol("WM_DELETE_WINDOW", self.close)
+        self.top.bind("<<close-window>>", self.close_event)
+        self.text.bind("<<center-insert>>", self.center_insert_event)
+
+        vbar['command'] = text.yview
+        vbar.pack(side=RIGHT, fill=Y)
+
+        text['yscrollcommand'] = vbar.set
+        text['background'] = 'white'
+        if sys.platform[:3] == 'win':
+            text['font'] = ("lucida console", 8)
+        text.pack(side=LEFT, fill=BOTH, expand=1)
+        text.focus_set()
+
+        self.auto = auto = self.AutoIndent(text)
+        self.autoex = self.AutoExpand(text)
+        self.per = per = self.Percolator(text)
+        if self.ispythonsource(filename):
+            self.color = color = self.ColorDelegator(); per.insertfilter(color)
+            ##print "Initial colorizer"
+        else:
+            ##print "No initial colorizer"
+            self.color = None
+        self.undo = undo = self.UndoDelegator(); per.insertfilter(undo)
+        self.search = search = self.SearchBinding(undo)
+        self.io = io = self.IOBinding(undo)
+
+        undo.set_saved_change_hook(self.saved_change_hook)
+        io.set_filename_change_hook(self.filename_change_hook)
+
+        if filename:
+            if os.path.exists(filename):
+                io.loadfile(filename)
+            else:
+                io.set_filename(filename)
+
+        self.saved_change_hook()
+
+    def gotoline(self, lineno):
+        if lineno is not None and lineno > 0:
+            self.text.mark_set("insert", "%d.0" % lineno)
+            self.text.tag_remove("sel", "1.0", "end")
+            self.text.tag_add("sel", "insert", "insert +1l")
+            self.center()
+
+    def ispythonsource(self, filename):
+        if not filename:
+            return 1
+        if os.path.normcase(filename[-3:]) == ".py":
+            return 1
+        try:
+            f = open(filename)
+            line = f.readline()
+            f.close()
+        except IOError:
+            return 0
+        return line[:2] == '#!' and string.find(line, 'python') >= 0
+
+    close_hook = None
+
+    def set_close_hook(self, close_hook):
+        self.close_hook = close_hook
+
+    def filename_change_hook(self):
+        self.saved_change_hook()
+        if self.ispythonsource(self.io.filename):
+            self.addcolorizer()
+        else:
+            self.rmcolorizer()
+
+    def addcolorizer(self):
+        if self.color:
+            return
+        ##print "Add colorizer"
+        self.per.removefilter(self.undo)
+        self.color = self.ColorDelegator()
+        self.per.insertfilter(self.color)
+        self.per.insertfilter(self.undo)
+
+    def rmcolorizer(self):
+        if not self.color:
+            return
+        ##print "Remove colorizer"
+        self.per.removefilter(self.undo)
+        self.per.removefilter(self.color)
+        self.color = None
+        self.per.insertfilter(self.undo)
+
+    def saved_change_hook(self):
+        if self.io.filename:
+            title = self.io.filename
+        else:
+            title = "(Untitled)"
+        if not self.undo.get_saved():
+            title = title + " *"
+        self.top.wm_title(title)
+
+    def center_insert_event(self, event):
+        self.center()
+
+    def center(self, mark="insert"):
+        insert = float(self.text.index(mark + " linestart"))
+        end = float(self.text.index("end"))
+        if insert > end-insert:
+            self.text.see("1.0")
+        else:
+            self.text.see("end")
+        self.text.see(mark)
+
+    def close_event(self, event):
+        self.close()
+
+    def close(self):
+        self.top.wm_deiconify()
+        self.top.tkraise()
+        reply = self.io.maybesave()
+        if reply != "cancel":
+            if self.color and self.color.colorizing:
+                self.color.close()
+                self.top.bell()
+                return "cancel"
+            if self.close_hook:
+                self.close_hook()
+            if self.color:
+                self.color.close()          # Cancel colorization
+            self.top.destroy()
+        return reply
+
+
+def fixwordbreaks(root):
+    tk = root.tk
+    tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
+    tk.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]')
+    tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]')
+
+
+def test():
+    root = Tk()
+    fixwordbreaks(root)
+    root.withdraw()
+    if sys.argv[1:]:
+        filename = sys.argv[1]
+    else:
+        filename = None
+    edit = EditorWindow(root, filename)
+    edit.set_close_hook(root.quit)
+    root.mainloop()
+    root.destroy()
+
+if __name__ == '__main__':
+    test()
diff --git a/Tools/idle/FileList.py b/Tools/idle/FileList.py
new file mode 100644
index 0000000..6ffeb30
--- /dev/null
+++ b/Tools/idle/FileList.py
@@ -0,0 +1,169 @@
+import os
+from Tkinter import *
+import tkMessageBox
+
+from EditorWindow import EditorWindow, fixwordbreaks
+from IOBinding import IOBinding
+
+
+class MultiIOBinding(IOBinding):
+
+    def open(self, event):
+        filename = self.askopenfile()
+        if filename:
+            self.flist.open(filename, self.edit)
+        return "break"
+
+
+class MultiEditorWindow(EditorWindow):
+
+    IOBinding = MultiIOBinding
+    from PopupMenu import PopupMenu
+
+    def __init__(self, flist, filename, key):
+        self.flist = flist
+        flist.inversedict[self] = key
+        if key:
+            flist.dict[key] = self
+        EditorWindow.__init__(self, flist.root, filename)
+        self.io.flist = flist
+        self.io.edit = self
+        self.popup = self.PopupMenu(self.text, self.flist)
+        self.text.bind("<<open-new-window>>", self.flist.new_callback)
+        self.text.bind("<<close-all-windows>>", self.flist.close_all_callback)
+
+    def close_hook(self):
+        self.flist.close_edit(self)
+
+    def filename_change_hook(self):
+        self.flist.filename_changed_edit(self)
+        EditorWindow.filename_change_hook(self)
+
+
+class FileList:
+
+    def __init__(self, root):
+        self.root = root
+        self.dict = {}
+        self.inversedict = {}
+
+    def new(self):
+        return self.open(None)
+
+    def open(self, filename, edit=None):
+        if filename:
+            filename = self.canonize(filename)
+            if os.path.isdir(filename):
+                tkMessageBox.showerror(
+                    "Is A Directory",
+                    "The path %s is a directory." % `filename`,
+                    master=self.root)
+                return None
+            key = os.path.normcase(filename)
+            if self.dict.has_key(key):
+                edit = self.dict[key]
+                edit.top.tkraise()
+                edit.top.wm_deiconify()
+                edit.text.focus_set()
+                return edit
+            if not os.path.exists(filename):
+                tkMessageBox.showinfo(
+                    "New File",
+                    "Opening non-existent file %s" % `filename`,
+                    master=self.root)
+            if edit and not edit.io.filename and edit.undo.get_saved():
+                # Reuse existing Untitled window for new file
+                edit.io.loadfile(filename)
+                self.dict[key] = edit
+                self.inversedict[edit] = key
+                edit.top.tkraise()
+                edit.top.wm_deiconify()
+                edit.text.focus_set()
+                return edit
+        else:
+            key = None
+        edit = MultiEditorWindow(self, filename, key)
+        return edit
+
+    def new_callback(self, event):
+        self.new()
+        return "break"
+
+    def close_all_callback(self, event):
+        for edit in self.inversedict.keys():
+            reply = edit.close()
+            if reply == "cancel":
+                break
+        return "break"
+
+    def close_edit(self, edit):
+        try:
+            key = self.inversedict[edit]
+        except KeyError:
+            print "Don't know this EditorWindow object.  (close)"
+            return
+        if key:
+            del self.dict[key]
+        del self.inversedict[edit]
+        if not self.inversedict:
+            self.root.quit()
+
+    def filename_changed_edit(self, edit):
+        edit.saved_change_hook()
+        try:
+            key = self.inversedict[edit]
+        except KeyError:
+            print "Don't know this EditorWindow object.  (rename)"
+            return
+        filename = edit.io.filename
+        if not filename:
+            if key:
+                del self.dict[key]
+            self.inversedict[edit] = None
+            return
+        filename = self.canonize(filename)
+        newkey = os.path.normcase(filename)
+        if newkey == key:
+            return
+        if self.dict.has_key(newkey):
+            conflict = self.dict[newkey]
+            self.inversedict[conflict] = None
+            tkMessageBox.showerror(
+                "Name Conflict",
+                "You now have multiple edit windows open for %s" % `filename`,
+                master=self.root)
+        self.dict[newkey] = edit
+        self.inversedict[edit] = newkey
+        if key:
+            try:
+                del self.dict[key]
+            except KeyError:
+                pass
+
+    def canonize(self, filename):
+        if not os.path.isabs(filename):
+            try:
+                pwd = os.getcwd()
+            except os.error:
+                pass
+            else:
+                filename = os.path.join(pwd, filename)
+        return os.path.normpath(filename)
+
+
+def test():
+    import sys
+    root = Tk()
+    fixwordbreaks(root)
+    root.withdraw()
+    flist = FileList(root)
+    if sys.argv[1:]:
+        for filename in sys.argv[1:]:
+            flist.open(filename)
+    else:
+        flist.new()
+    if flist.inversedict:
+        root.mainloop()
+
+if __name__ == '__main__':
+    test()
diff --git a/Tools/idle/FrameViewer.py b/Tools/idle/FrameViewer.py
new file mode 100644
index 0000000..e5a6051
--- /dev/null
+++ b/Tools/idle/FrameViewer.py
@@ -0,0 +1,38 @@
+from repr import Repr
+from Tkinter import *
+
+class FrameViewer:
+
+    def __init__(self, root, frame):
+        self.root = root
+        self.frame = frame
+        self.top = Toplevel(self.root)
+        self.repr = Repr()
+        self.repr.maxstring = 60
+        self.load_variables()
+        
+    def load_variables(self):
+        row = 0
+        if self.frame.f_locals is not self.frame.f_globals:
+            l = Label(self.top, text="Local Variables",
+                      borderwidth=2, relief="raised")
+            l.grid(row=row, column=0, columnspan=2, sticky="ew")
+            row = self.load_names(self.frame.f_locals, row+1)
+        l = Label(self.top, text="Global Variables",
+                  borderwidth=2, relief="raised")
+        l.grid(row=row, column=0, columnspan=2, sticky="ew")
+        row = self.load_names(self.frame.f_globals, row+1)
+        
+    def load_names(self, dict, row):
+        names = dict.keys()
+        names.sort()
+        for name in names:
+            value = dict[name]
+            svalue = self.repr.repr(value)
+            l = Label(self.top, text=name)
+            l.grid(row=row, column=0, sticky="w")
+            l = Entry(self.top, width=60, borderwidth=0)
+            l.insert(0, svalue)
+            l.grid(row=row, column=1, sticky="w")
+            row = row+1
+        return row
diff --git a/Tools/idle/HelpWindow.py b/Tools/idle/HelpWindow.py
new file mode 100644
index 0000000..a1b13c3
--- /dev/null
+++ b/Tools/idle/HelpWindow.py
@@ -0,0 +1,65 @@
+import os
+import sys
+from Tkinter import *
+
+
+class HelpWindow:
+
+    helpfile = "help.txt"
+    helptitle = "Help Window"
+
+    def __init__(self, root=None):
+        if not root:
+            import Tkinter
+            root = Tkinter._default_root
+        if root:
+            self.top = top = Toplevel(root)
+        else:
+            self.top = top = root = Tk()
+
+        helpfile = self.helpfile
+        if not os.path.exists(helpfile):
+            base = os.path.basename(self.helpfile)
+            for dir in sys.path:
+                fullname = os.path.join(dir, base)
+                if os.path.exists(fullname):
+                    helpfile = fullname
+                    break
+        try:
+            f = open(helpfile)
+            data = f.read()
+            f.close()
+        except IOError, msg:
+            data = "Can't open the help file (%s)" % `helpfile`
+
+        top.protocol("WM_DELETE_WINDOW", self.close_command)
+        top.wm_title(self.helptitle)
+
+        self.close_button = Button(top, text="close",
+                                   command=self.close_command)
+        self.close_button.pack(side="bottom")
+
+        self.vbar = vbar = Scrollbar(top, name="vbar")
+        self.text = text = Text(top)
+
+        vbar["command"] = text.yview
+        text["yscrollcommand"] = vbar.set
+
+        vbar.pack(side="right", fill="y")
+        text.pack(side="left", fill="both", expand=1)
+
+        text.insert("1.0", data)
+
+        text.config(state="disabled")
+        text.see("1.0")
+
+    def close_command(self):
+        self.top.destroy()
+
+
+def main():
+    h = HelpWindow()
+    h.top.mainloop()
+
+if __name__ == "__main__":
+    main()
diff --git a/Tools/idle/History.py b/Tools/idle/History.py
new file mode 100644
index 0000000..0798098
--- /dev/null
+++ b/Tools/idle/History.py
@@ -0,0 +1,73 @@
+import string
+
+class History:
+    
+    def __init__(self, text):
+        self.text = text
+        self.history = []
+        self.history_prefix = None
+        self.history_pointer = None
+        text.bind("<<history-previous>>", self.history_prev)
+        text.bind("<<history-next>>", self.history_next)
+
+    def history_next(self, event):
+        self.history_do(0)
+        return "break"
+
+    def history_prev(self, event):
+        self.history_do(1)
+        return "break"
+
+    def history_do(self, reverse):
+        nhist = len(self.history)
+        pointer = self.history_pointer
+        prefix = self.history_prefix
+        if pointer is not None and prefix is not None:
+            if self.text.compare("insert", "!=", "end-1c") or \
+               self.text.get("iomark", "end-1c") != self.history[pointer]:
+                pointer = prefix = None
+        if pointer is None or prefix is None:
+            prefix = self.text.get("iomark", "end-1c")
+            if reverse:
+                pointer = nhist
+            else:
+                pointer = -1
+        nprefix = len(prefix)
+        while 1:
+            if reverse:
+                pointer = pointer - 1
+            else:
+                pointer = pointer + 1
+            if pointer < 0 or pointer >= nhist:
+                self.text.bell()
+                if self.text.get("iomark", "end-1c") != prefix:
+                    self.text.delete("iomark", "end-1c")
+                    self.text.insert("iomark", prefix)
+                pointer = prefix = None
+                break
+            item = self.history[pointer]
+            if item[:nprefix] == prefix and len(item) > nprefix:
+                self.text.delete("iomark", "end-1c")
+                self.text.insert("iomark", item)
+                break
+        self.text.mark_set("insert", "end-1c")
+        self.text.see("insert")
+        self.text.tag_remove("sel", "1.0", "end")
+        self.history_pointer = pointer
+        self.history_prefix = prefix
+
+    def history_store(self, source):
+        source = string.strip(source)
+        if len(source) > 2:
+            self.history.append(source)
+        self.history_pointer = None
+        self.history_prefix = None
+
+    def recall(self, s):
+        s = string.strip(s)
+        self.text.tag_remove("sel", "1.0", "end")
+        self.text.delete("iomark", "end-1c")
+        self.text.mark_set("insert", "end-1c")
+        self.text.insert("insert", s)
+        self.text.see("insert")
+
diff --git a/Tools/idle/IOBinding.py b/Tools/idle/IOBinding.py
new file mode 100644
index 0000000..0d61afc
--- /dev/null
+++ b/Tools/idle/IOBinding.py
@@ -0,0 +1,158 @@
+import os
+import tkFileDialog
+import tkMessageBox
+
+
+class IOBinding:
+
+    # Calls to non-standard text methods:
+    # reset_undo()
+    # set_saved(1)
+
+    def __init__(self, text):
+        self.text = text
+        self.text.bind("<<open-window-from-file>>", self.open)
+        self.text.bind("<<save-window>>", self.save)
+        self.text.bind("<<save-window-as-file>>", self.save_as)
+        self.text.bind("<<save-copy-of-window-as-file>>", self.save_a_copy)
+
+    filename_change_hook = None
+
+    def set_filename_change_hook(self, hook):
+        self.filename_change_hook = hook
+
+    filename = None
+
+    def set_filename(self, filename):
+        self.filename = filename
+        self.text.set_saved(1)
+        if self.filename_change_hook:
+            self.filename_change_hook()
+
+    def open(self, event):
+        if not self.text.get_saved():
+            reply = self.maybesave()
+            if reply == "cancel":
+                return "break"
+        filename = self.askopenfile()
+        if filename:
+            self.loadfile(filename)
+        return "break"
+
+    def loadfile(self, filename):
+        try:
+            f = open(filename)
+            chars = f.read()
+            f.close()
+        except IOError, msg:
+            tkMessageBox.showerror("I/O Error", str(msg), master=self.text)
+            return 0
+        self.text.delete("1.0", "end")
+        self.set_filename(None)
+        self.text.insert("1.0", chars)
+        self.text.reset_undo()
+        self.set_filename(filename)
+        self.text.mark_set("insert", "1.0")
+        self.text.see("insert")
+        return 1
+
+    def maybesave(self):
+        if self.text.get_saved():
+            return "yes"
+        message = "Do you want to save %s before closing?" % (
+            self.filename or "this untitled document")
+        m = tkMessageBox.Message(
+            title="Save On Close",
+            message=message,
+            icon=tkMessageBox.QUESTION,
+            type=tkMessageBox.YESNOCANCEL,
+            master=self.text)
+        reply = m.show()
+        if reply == "yes":
+            self.save(None)
+            if not self.text.get_saved():
+                reply = "cancel"
+        return reply
+
+    def save(self, event):
+        if not self.filename:
+            self.save_as(event)
+        else:
+            if self.writefile(self.filename):
+                self.text.set_saved(1)
+        return "break"
+
+    def save_as(self, event):
+        filename = self.asksavefile()
+        if filename:
+            if self.writefile(filename):
+                self.set_filename(filename)
+                self.text.set_saved(1)
+        return "break"
+
+    def save_a_copy(self, event):
+        filename = self.asksavefile()
+        if filename:
+            self.writefile(filename)
+        return "break"
+
+    def writefile(self, filename):
+        try:
+            f = open(filename, "w")
+            chars = self.text.get("1.0", "end-1c")
+            f.write(chars)
+            if chars and chars[-1] != "\n":
+                f.write("\n")
+            f.close()
+            ## print "saved to", `filename`
+            return 1
+        except IOError, msg:
+            tkMessageBox.showerror("I/O Error", str(msg),
+                                   master=self.text)
+            return 0
+
+    opendialog = None
+    savedialog = None
+
+    filetypes = [
+        ("Python files", "*.py", "TEXT"),
+        ("All text files", "*", "TEXT"),
+        ("All files", "*"),
+        ]
+
+    def askopenfile(self):
+        dir, base = self.defaultfilename("open")
+        if not self.opendialog:
+            self.opendialog = tkFileDialog.Open(master=self.text,
+                                                filetypes=self.filetypes)
+        return self.opendialog.show(initialdir=dir, initialfile=base)
+
+    def defaultfilename(self, mode="open"):
+        if self.filename:
+            dir, base = os.path.split(self.filename)
+        else:
+            dir = base = ""
+        return dir, base
+
+    def asksavefile(self):
+        dir, base = self.defaultfilename("save")
+        if not self.savedialog:
+            self.savedialog = tkFileDialog.SaveAs(master=self.text,
+                                                  filetypes=self.filetypes)
+        return self.savedialog.show(initialdir=dir, initialfile=base)
+
+
+def test():
+    from Tkinter import *
+    root = Tk()
+    class MyText(Text):
+        def reset_undo(self): pass
+        def set_saved(self, flag): pass
+    text = MyText(root)
+    text.pack()
+    text.focus_set()
+    io = IOBinding(text)
+    root.mainloop()
+
+if __name__ == "__main__":
+    test()
diff --git a/Tools/idle/IdleHistory.py b/Tools/idle/IdleHistory.py
new file mode 100644
index 0000000..0798098
--- /dev/null
+++ b/Tools/idle/IdleHistory.py
@@ -0,0 +1,73 @@
+import string
+
+class History:
+    
+    def __init__(self, text):
+        self.text = text
+        self.history = []
+        self.history_prefix = None
+        self.history_pointer = None
+        text.bind("<<history-previous>>", self.history_prev)
+        text.bind("<<history-next>>", self.history_next)
+
+    def history_next(self, event):
+        self.history_do(0)
+        return "break"
+
+    def history_prev(self, event):
+        self.history_do(1)
+        return "break"
+
+    def history_do(self, reverse):
+        nhist = len(self.history)
+        pointer = self.history_pointer
+        prefix = self.history_prefix
+        if pointer is not None and prefix is not None:
+            if self.text.compare("insert", "!=", "end-1c") or \
+               self.text.get("iomark", "end-1c") != self.history[pointer]:
+                pointer = prefix = None
+        if pointer is None or prefix is None:
+            prefix = self.text.get("iomark", "end-1c")
+            if reverse:
+                pointer = nhist
+            else:
+                pointer = -1
+        nprefix = len(prefix)
+        while 1:
+            if reverse:
+                pointer = pointer - 1
+            else:
+                pointer = pointer + 1
+            if pointer < 0 or pointer >= nhist:
+                self.text.bell()
+                if self.text.get("iomark", "end-1c") != prefix:
+                    self.text.delete("iomark", "end-1c")
+                    self.text.insert("iomark", prefix)
+                pointer = prefix = None
+                break
+            item = self.history[pointer]
+            if item[:nprefix] == prefix and len(item) > nprefix:
+                self.text.delete("iomark", "end-1c")
+                self.text.insert("iomark", item)
+                break
+        self.text.mark_set("insert", "end-1c")
+        self.text.see("insert")
+        self.text.tag_remove("sel", "1.0", "end")
+        self.history_pointer = pointer
+        self.history_prefix = prefix
+
+    def history_store(self, source):
+        source = string.strip(source)
+        if len(source) > 2:
+            self.history.append(source)
+        self.history_pointer = None
+        self.history_prefix = None
+
+    def recall(self, s):
+        s = string.strip(s)
+        self.text.tag_remove("sel", "1.0", "end")
+        self.text.delete("iomark", "end-1c")
+        self.text.mark_set("insert", "end-1c")
+        self.text.insert("insert", s)
+        self.text.see("insert")
+
diff --git a/Tools/idle/Outline.py b/Tools/idle/Outline.py
new file mode 100644
index 0000000..194d058
--- /dev/null
+++ b/Tools/idle/Outline.py
@@ -0,0 +1,46 @@
+from Tkinter import *
+
+class Outline:
+    
+    def __init__(self, root=None):
+        if not root:
+            import Tkinter
+            root = Tkinter._default_root
+        if not root:
+            root = top = Tk()
+        else:
+            top = Toplevel(root)
+        top.wm_title("Outline")
+        self.canvas = canvas = Canvas(top, width=400, height=300,
+                                      borderwidth=2, relief="sunken",
+                                      background="#FFBBBB")
+        canvas.pack(expand=1, fill="both")
+        self.items = []
+    
+    def additem(self, level, open, label):
+        x = 15*level + 5
+        y = 15*len(self.items) + 5
+        if open:
+            id1 = self.canvas.create_polygon(x+3, y+3, x+13, y+3, x+8, y+8,
+                                             outline="black",
+                                             fill="green")
+        else:
+            id1 = self.canvas.create_polygon(x+3, y+4, x+7, y+8, x+3, y+12,
+                                             outline="black",
+                                             fill="red")
+        w = Entry(self.canvas, borderwidth=0, background="#FFBBBB", width=0)
+        w.insert("end", label)
+	id2 = self.canvas.create_window(x+15, y, anchor="nw", window=w)
+        self.items.append((level, open, label, id1, w, id2))
+        
+
+def main():
+    o = Outline()
+    o.additem(0, 1, "hello world")
+    o.additem(1, 0, "sub1")
+    o.additem(1, 1, "sub2")
+    o.additem(2, 0, "sub2.a")
+    o.additem(2, 0, "sub2.b")
+    o.additem(1, 0, "sub3")
+
+main()
diff --git a/Tools/idle/Percolator.py b/Tools/idle/Percolator.py
new file mode 100644
index 0000000..a5f503f
--- /dev/null
+++ b/Tools/idle/Percolator.py
@@ -0,0 +1,77 @@
+from WidgetRedirector import WidgetRedirector
+from Delegator import Delegator
+
+class Percolator:
+
+    def __init__(self, text):
+        # XXX would be nice to inherit from Delegator
+        self.text = text
+        self.redir = WidgetRedirector(text)
+        self.top = self.bottom = Delegator(text)
+        self.bottom.insert = self.redir.register("insert", self.insert)
+        self.bottom.delete = self.redir.register("delete", self.delete)
+        self.filters = []
+
+    def insert(self, index, chars, tags=None):
+        # Could go away if inheriting from Delegator
+        self.top.insert(index, chars, tags)
+
+    def delete(self, index1, index2=None):
+        # Could go away if inheriting from Delegator
+        self.top.delete(index1, index2)
+
+    def insertfilter(self, filter):
+        # Perhaps rename to pushfilter()?
+        assert isinstance(filter, Delegator)
+        assert filter.delegate is None
+        filter.setdelegate(self.top)
+        self.top = filter
+
+    def removefilter(self, filter):
+        # XXX Perhaps should only support popfilter()?
+        assert isinstance(filter, Delegator)
+        assert filter.delegate is not None
+        f = self.top
+        if f is filter:
+            self.top = filter.delegate
+            filter.setdelegate(None)
+        else:
+            while f.delegate is not filter:
+                assert f is not self.bottom
+                f.resetcache()
+                f = f.delegate
+            f.setdelegate(filter.delegate)
+            filter.setdelegate(None)
+
+
+def main():
+    class Tracer(Delegator):
+        def __init__(self, name):
+            self.name = name
+            Delegator.__init__(self, None)
+        def insert(self, *args):
+            print self.name, ": insert", args
+            apply(self.delegate.insert, args)
+        def delete(self, *args):
+            print self.name, ": delete", args
+            apply(self.delegate.delete, args)
+    from Tkinter import *
+    root = Tk()
+    root.wm_protocol("WM_DELETE_WINDOW", root.quit)
+    text = Text()
+    text.pack()
+    text.focus_set()
+    p = Percolator(text)
+    t1 = Tracer("t1")
+    t2 = Tracer("t2")
+    p.insertfilter(t1)
+    p.insertfilter(t2)
+    root.mainloop()
+    p.removefilter(t2)
+    root.mainloop()
+    p.insertfilter(t2)
+    p.removefilter(t1)
+    root.mainloop()
+
+if __name__ == "__main__":
+    main()
diff --git a/Tools/idle/PopupMenu.py b/Tools/idle/PopupMenu.py
new file mode 100644
index 0000000..d08b18d
--- /dev/null
+++ b/Tools/idle/PopupMenu.py
@@ -0,0 +1,86 @@
+import sys
+import re
+
+from Tkinter import *
+
+class PopupMenu:
+
+    def __init__(self, text, flist):
+        self.text = text
+        self.flist = flist
+        self.text.bind("<3>", self.right_menu_event)
+
+    rmenu = None
+
+    def right_menu_event(self, event):
+        if not self.rmenu:
+            self.make_menu()
+        rmenu = self.rmenu
+        self.event = event
+        iswin = sys.platform[:3] == 'win'
+        if iswin:
+            self.text.config(cursor="arrow")
+        rmenu.tk_popup(event.x_root, event.y_root)
+        if iswin:
+            self.text.config(cursor="ibeam")
+
+    def make_menu(self):
+        rmenu = Menu(self.text, tearoff=0)
+        rmenu.add_command(label="Go to line from traceback",
+                          command=self.goto_traceback_line)
+        rmenu.add_command(label="Open stack viewer",
+                          command=self.open_stack_viewer)
+        rmenu.add_command(label="Help", command=self.help)
+        self.rmenu = rmenu
+    
+    file_line_pats = [
+        r'File "([^"]*)", line (\d+)',
+        r'([^\s]+)\((\d+)\)',
+        r'([^\s]+):\s*(\d+):',
+    ]
+    
+    file_line_progs = None
+    
+    def goto_traceback_line(self):
+        if self.file_line_progs is None:
+            l = []
+            for pat in self.file_line_pats:
+                l.append(re.compile(pat))
+            self.file_line_progs = l
+        x, y = self.event.x, self.event.y
+        self.text.mark_set("insert", "@%d,%d" % (x, y))
+        line = self.text.get("insert linestart", "insert lineend")
+        for prog in self.file_line_progs:
+            m = prog.search(line)
+            if m:
+                break
+        else:
+            self.text.bell()
+            return
+        filename, lineno = m.group(1, 2)
+        try:
+            f = open(filename, "r")
+            f.close()
+        except IOError, msg:
+            self.text.bell()
+            return
+        edit = self.flist.open(filename)
+        try:
+            lineno = int(lineno)
+        except ValueError, msg:
+            self.text.bell()
+            return
+        edit.gotoline(lineno)
+    
+    def open_stack_viewer(self):
+        try:
+            sys.last_traceback
+        except:
+            print "No stack trace yet"
+            return
+        from StackViewer import StackViewer
+        sv = StackViewer(self.text._root(), self.flist)
+
+    def help(self):
+        from HelpWindow import HelpWindow
+        HelpWindow()
diff --git a/Tools/idle/PyShell.py b/Tools/idle/PyShell.py
new file mode 100644
index 0000000..053f400
--- /dev/null
+++ b/Tools/idle/PyShell.py
@@ -0,0 +1,475 @@
+#! /usr/bin/env python
+
+import os
+import sys
+import string
+
+import linecache
+from code import InteractiveInterpreter
+
+from Tkinter import *
+import tkMessageBox
+
+from EditorWindow import fixwordbreaks
+from FileList import FileList, MultiEditorWindow, MultiIOBinding
+from ColorDelegator import ColorDelegator
+
+
+class ModifiedIOBinding(MultiIOBinding):
+
+    def defaultfilename(self, mode="open"):
+        if self.filename:
+            return MultiIOBinding.defaultfilename(self, mode)
+        else:
+            try:
+                pwd = os.getcwd()
+            except os.error:
+                pwd = ""
+            return pwd, ""
+
+    def open(self, event):
+        # Override base class method -- don't allow reusing this window
+        filename = self.askopenfile()
+        if filename:
+            self.flist.open(filename)
+        return "break"
+
+    def maybesave(self):
+        # Override base class method -- don't ask any questions
+        if self.text.get_saved():
+            return "yes"
+        else:
+            return "no"
+
+
+class ModifiedColorDelegator(ColorDelegator):
+    
+    def recolorize_main(self):
+        self.tag_remove("TODO", "1.0", "iomark")
+        self.tag_add("SYNC", "1.0", "iomark")
+        ColorDelegator.recolorize_main(self)
+
+
+class ModifiedInterpreter(InteractiveInterpreter):
+    
+    def __init__(self, tkconsole):
+        self.tkconsole = tkconsole
+        InteractiveInterpreter.__init__(self)
+
+    gid = 0
+
+    def runsource(self, source):
+        # Extend base class to stuff the source in the line cache
+        filename = "<console#%d>" % self.gid
+        self.gid = self.gid + 1
+        lines = string.split(source, "\n")
+        linecache.cache[filename] = len(source)+1, 0, lines, filename
+        self.more = 0
+        return InteractiveInterpreter.runsource(self, source, filename)
+
+    def showsyntaxerror(self, filename=None):
+        # Extend base class to color the offending position
+        # (instead of printing it and pointing at it with a caret)
+        text = self.tkconsole.text
+        stuff = self.unpackerror()
+        if not stuff:
+            self.tkconsole.resetoutput()
+            InteractiveInterpreter.showsyntaxerror(self, filename)
+            return
+        msg, lineno, offset, line = stuff
+        if lineno == 1:
+            pos = "iomark + %d chars" % (offset-1)
+        else:
+            pos = "iomark linestart + %d lines + %d chars" % (lineno-1,
+                                                              offset-1)
+        text.tag_add("ERROR", pos)
+        text.see(pos)
+        char = text.get(pos)
+        if char in string.letters + string.digits + "_":
+            text.tag_add("ERROR", pos + " wordstart", pos)
+        self.tkconsole.resetoutput()
+        self.write("SyntaxError: %s\n" % str(msg))
+
+    def unpackerror(self):
+        type, value, tb = sys.exc_info()
+        ok = type == SyntaxError
+        if ok:
+            try:
+                msg, (dummy_filename, lineno, offset, line) = value
+            except:
+                ok = 0
+        if ok:
+            return msg, lineno, offset, line
+        else:
+            return None
+
+    def showtraceback(self):
+        # Extend base class method to reset output properly
+        text = self.tkconsole.text
+        self.tkconsole.resetoutput()
+        InteractiveInterpreter.showtraceback(self)
+
+    def runcode(self, code):
+        # Override base class method
+        try:
+            self.tkconsole.beginexecuting()
+            try:
+                exec code in self.locals
+            except SystemExit:
+                if tkMessageBox.askyesno(
+                    "Exit?",
+                    "Do you want to exit altogether?",
+                    default="yes",
+                    master=self.tkconsole.text):
+                    raise
+                else:
+                    self.showtraceback()
+            except:
+                self.showtraceback()
+        finally:
+            self.tkconsole.endexecuting()
+ 
+    def write(self, s):
+        # Override base class write
+        self.tkconsole.console.write(s)
+       
+
+class PyShell(MultiEditorWindow):
+
+    # Override classes
+    ColorDelegator = ModifiedColorDelegator
+    IOBinding = ModifiedIOBinding
+    
+    # New class
+    from History import History
+
+    def __init__(self, flist=None):
+        self.interp = ModifiedInterpreter(self)
+        if flist is None:
+            root = Tk()
+            fixwordbreaks(root)
+            root.withdraw()
+            flist = FileList(root)
+
+        MultiEditorWindow.__init__(self, flist, None, None)
+        self.config_colors()
+
+        import __builtin__
+        __builtin__.quit = __builtin__.exit = "To exit, type Ctrl-D."
+
+        self.auto.config(prefertabs=1)
+
+        text = self.text
+        text.bind("<<newline-and-indent>>", self.enter_callback)
+        text.bind("<<plain-newline-and-indent>>", self.linefeed_callback)
+        text.bind("<<interrupt-execution>>", self.cancel_callback)
+        text.bind("<<beginning-of-line>>", self.home_callback)
+        text.bind("<<end-of-file>>", self.eof_callback)
+
+        sys.stdout = PseudoFile(self, "stdout")
+        ##sys.stderr = PseudoFile(self, "stderr")
+        sys.stdin = self
+        self.console = PseudoFile(self, "console")
+
+        self.history = self.History(self.text)
+
+    tagdefs = {
+        ##"stdin":   {"background": "yellow"},
+        "stdout":  {"foreground": "blue"},
+        "stderr":  {"foreground": "#007700"},
+        "console": {"foreground": "red"},
+        "ERROR":   {"background": "#FF7777"},
+        None:      {"foreground": "purple"}, # default
+    }
+
+    def config_colors(self):
+        for tag, cnf in self.tagdefs.items():
+            if cnf:
+                if not tag:
+                    apply(self.text.configure, (), cnf)
+                else:
+                    apply(self.text.tag_configure, (tag,), cnf)
+
+    reading = 0
+    executing = 0
+    canceled = 0
+    endoffile = 0
+
+    def beginexecuting(self):
+        # Helper for ModifiedInterpreter
+        self.resetoutput()
+        self.executing = 1
+        self._cancel_check = self.cancel_check
+        ##sys.settrace(self._cancel_check)
+
+    def endexecuting(self):
+        # Helper for ModifiedInterpreter
+        sys.settrace(None)
+        self.executing = 0
+        self.canceled = 0
+
+    def close(self):
+        # Extend base class method
+        if self.executing:
+            # XXX Need to ask a question here
+            if not tkMessageBox.askokcancel(
+                "Cancel?",
+                "The program is still running; do you want to cancel it?",
+                default="ok",
+                master=self.text):
+                return "cancel"
+            self.canceled = 1
+            if self.reading:
+                self.top.quit()
+            return "cancel"
+        reply = MultiEditorWindow.close(self)
+        if reply != "cancel":
+            # Restore std streams
+            sys.stdout = sys.__stdout__
+            sys.stderr = sys.__stderr__
+            sys.stdin = sys.__stdin__
+            # Break cycles
+            self.interp = None
+            self.console = None
+        return reply
+
+    def ispythonsource(self, filename):
+        # Override this so EditorWindow never removes the colorizer
+        return 1
+
+    def saved_change_hook(self):
+        # Override this to get the title right
+        title = "Python Shell"
+        if self.io.filename:
+            title = title + ": " + self.io.filename
+            if not self.undo.get_saved():
+                title = title + " *"
+        self.top.wm_title(title)
+
+    def interact(self):
+        self.resetoutput()
+        self.write("Python %s on %s\n%s\n" %
+                   (sys.version, sys.platform, sys.copyright))
+        try:
+            sys.ps1
+        except AttributeError:
+            sys.ps1 = ">>> "
+        self.showprompt()
+        import Tkinter
+        Tkinter._default_root = None
+        self.top.mainloop()
+
+    def readline(self):
+        save = self.reading
+        try:
+            self.reading = 1
+            self.top.mainloop()
+        finally:
+            self.reading = save
+        line = self.text.get("iomark", "end-1c")
+        self.resetoutput()
+        if self.canceled:
+            self.canceled = 0
+            raise KeyboardInterrupt
+        if self.endoffile:
+            self.endoffile = 0
+            return ""
+        return line
+
+    def cancel_callback(self, event):
+        try:
+            if self.text.compare("sel.first", "!=", "sel.last"):
+                return # Active selection -- always use default binding
+        except:
+            pass
+        if not (self.executing or self.reading):
+            self.resetoutput()
+            self.write("KeyboardInterrupt\n")
+            self.showprompt()
+            return "break"
+        self.endoffile = 0
+        self.canceled = 1
+        if self.reading:
+            self.top.quit()
+        return "break"
+
+    def eof_callback(self, event):
+        if self.executing and not self.reading:
+            return # Let the default binding (delete next char) take over
+        if not (self.text.compare("iomark", "==", "insert") and
+                self.text.compare("insert", "==", "end-1c")):
+            return # Let the default binding (delete next char) take over
+        if not self.executing:
+##             if not tkMessageBox.askokcancel(
+##                 "Exit?",
+##                 "Are you sure you want to exit?",
+##                 default="ok", master=self.text):
+##                 return "break"
+            self.resetoutput()
+            self.close()
+        else:
+            self.canceled = 0
+            self.endoffile = 1
+            self.top.quit()
+        return "break"
+
+    def home_callback(self, event):
+        if event.state != 0 and event.keysym == "Home":
+            return # <Modifier-Home>; fall back to class binding
+        if self.text.compare("iomark", "<=", "insert") and \
+           self.text.compare("insert linestart", "<=", "iomark"):
+            self.text.mark_set("insert", "iomark")
+            self.text.tag_remove("sel", "1.0", "end")
+            self.text.see("insert")
+            return "break"
+
+    def linefeed_callback(self, event):
+        # Insert a linefeed without entering anything (still autoindented)
+        if self.reading:
+            self.text.insert("insert", "\n")
+            self.text.see("insert")
+        else:
+            self.auto.autoindent(event)
+        return "break"
+
+    def enter_callback(self, event):
+        if self.executing and not self.reading:
+            return # Let the default binding (insert '\n') take over
+        # If some text is selected, recall the selection
+        try:
+            sel = self.text.get("sel.first", "sel.last")
+            if sel:
+                self.recall(sel)
+                return "break"
+        except:
+            pass
+        # If we're strictly before the line containing iomark, recall
+        # the current line, less a leading prompt, less leading or
+        # trailing whitespace
+        if self.text.compare("insert", "<", "iomark linestart"):
+            # Check if there's a relevant stdin mark -- if so, use it
+            prev = self.text.tag_prevrange("stdin", "insert")
+            if prev and self.text.compare("insert", "<", prev[1]):
+                self.recall(self.text.get(prev[0], prev[1]))
+                return "break"
+            next = self.text.tag_nextrange("stdin", "insert")
+            if next and self.text.compare("insert lineend", ">=", next[0]):
+                self.recall(self.text.get(next[0], next[1]))
+                return "break"
+            # No stdin mark -- just get the current line
+            self.recall(self.text.get("insert linestart", "insert lineend"))
+            return "break"
+        # If we're anywhere in the current input (including in the
+        # prompt) but not at the very end, move the cursor to the end.
+        if self.text.compare("insert", "<", "end-1c"):
+            self.text.mark_set("insert", "end-1c")
+            self.text.see("insert")
+            return "break"
+        # OK, we're already at the end -- insert a newline and run it.
+        if self.reading:
+            self.text.insert("insert", "\n")
+            self.text.see("insert")
+        else:
+            self.auto.autoindent(event)
+        self.text.tag_add("stdin", "iomark", "end-1c")
+        self.text.update_idletasks()
+        if self.reading:
+            self.top.quit() # Break out of recursive mainloop() in raw_input()
+        else:
+            self.runit()
+        return "break"
+
+    def recall(self, s):
+        if self.history:
+            self.history.recall(s)
+
+    def runit(self):
+        line = self.text.get("iomark", "end-1c")
+        # Strip off last newline and surrounding whitespace.
+        # (To allow you to hit return twice to end a statement.)
+        i = len(line)
+        while i > 0 and line[i-1] in " \t":
+            i = i-1
+        if i > 0 and line[i-1] == "\n":
+            i = i-1
+        while i > 0 and line[i-1] in " \t":
+            i = i-1
+        line = line[:i]
+        more = self.interp.runsource(line)
+        if not more:
+            self.showprompt()
+
+    def cancel_check(self, frame, what, args,
+                     dooneevent=tkinter.dooneevent,
+                     dontwait=tkinter.DONT_WAIT):
+        # Hack -- use the debugger hooks to be able to handle events
+        # and interrupt execution at any time.
+        # This slows execution down quite a bit, so you may want to
+        # disable this (by not calling settrace() in runcode() above)
+        # for full-bore (uninterruptable) speed.
+        # XXX This should become a user option.
+        if self.canceled:
+            return
+        dooneevent(dontwait)
+        if self.canceled:
+            self.canceled = 0
+            raise KeyboardInterrupt
+        return self._cancel_check
+
+    def showprompt(self):
+        self.resetoutput()
+        try:
+            s = str(sys.ps1)
+        except:
+            s = ""
+        self.console.write(s)
+        self.text.mark_set("insert", "end-1c")
+
+    def resetoutput(self):
+        source = self.text.get("iomark", "end-1c")
+        if self.history:
+            self.history.history_store(source)
+        if self.text.get("end-2c") != "\n":
+            self.text.insert("end-1c", "\n")
+        self.text.mark_set("iomark", "end-1c")
+        sys.stdout.softspace = 0
+
+    def write(self, s):
+        # Overrides base class write
+        self.console.write(s)
+
+class PseudoFile:
+
+    def __init__(self, interp, tags):
+        self.interp = interp
+        self.text = interp.text
+        self.tags = tags
+
+    def write(self, s):
+        self.text.mark_gravity("iomark", "right")
+        self.text.insert("iomark", str(s), self.tags)
+        self.text.mark_gravity("iomark", "left")
+        self.text.see("iomark")
+        self.text.update()
+        if self.interp.canceled:
+            self.interp.canceled = 0
+            raise KeyboardInterrupt
+
+    def writelines(self, l):
+        map(self.write, l)
+
+
+def main():
+    global flist, root
+    root = Tk()
+    fixwordbreaks(root)
+    root.withdraw()
+    flist = FileList(root)
+    if sys.argv[1:]:
+        for filename in sys.argv[1:]:
+            flist.open(filename)
+    t = PyShell(flist)
+    t.interact()
+
+if __name__ == "__main__":
+    main()
diff --git a/Tools/idle/README b/Tools/idle/README
new file mode 100644
index 0000000..ed3d7c5
--- /dev/null
+++ b/Tools/idle/README
@@ -0,0 +1,79 @@
+BUGS:
+    
+- when there's a selection, typing ^X will delete the selection!
+  (cause: ^X is a binding for cut ;-( )
+
+TO DO:
+
+- restructure state sensitive code to avoid testing flags all the time
+- integrated debugger
+- object browser
+- save some user state (e.g. window and cursor positions, bindings)
+
+- menu bar
+- make backups when saving
+- check file mtimes at various points
+- interface with RCS/CVS/Perforce ???
+- more search options: case [in]sensitive, fwd/back, string/regex
+- global query replace
+- incremental search
+- more emacsisms:
+  - reindent, reformat text etc.
+  - M-[, M-] to move by paragraphs
+  - smart stuff with whitespace around Return
+- status bar?
+- better help?
+
+Details:
+    
+- when there's a selection, left/right arrow should go to either
+  end of the selection
+
+Structural problems:
+
+- too much knowledge in FileList about EditorWindow (for example)
+
+======================================================================
+
+Comparison to PTUI
+------------------
+
+- PTUI's shell is worse:
+  no coloring;
+  no editing of multi-line commands;
+  ^P seems to permanently remove some text from the buffer
+
+- PTUI's undo is worse:
+  no redo;
+  one char at a time
+
+- PTUI's framework is better:
+  status line
+  menu bar
+  buffer menu
+  (not sure if I like the toolbar)
+
+- PTUI's GUI is a tad ugly:
+  I don't like the multiple buffers in one window model
+
+- PTUI's help is better (HTML!)
+
+- PTUI's search/replace is better (more features)
+
+- PTUI's auto indent is better
+  (understands that "if a: # blah, blah" opens a block)
+
+- PTUI's key bindings are a bit weird (DEL to dedent a line!?!?!?)
+
+- PTUI's fontify is faster but synchronous (and still too slow);
+  also doesn't do as good a job if editing affects lines far below
+
+- PTUI has more bells and whistles:
+  open multiple
+  append
+  zap tabs
+  fontify (you could argue it's not needed in my code)
+  comment/uncomment
+  modularize
+  examine
+  go
diff --git a/Tools/idle/SearchBinding.py b/Tools/idle/SearchBinding.py
new file mode 100644
index 0000000..6abe5e0
--- /dev/null
+++ b/Tools/idle/SearchBinding.py
@@ -0,0 +1,84 @@
+import re
+import tkSimpleDialog
+import tkMessageBox
+
+class SearchBinding:
+	
+	def __init__(self, text):
+		self.text = text
+		self.pat = ""
+		self.prog = None
+		self.text.bind("<<find>>", self.find_event)
+		self.text.bind("<<find-next>>", self.find_next_event)
+		self.text.bind("<<find-same>>", self.find_same_event)
+		self.text.bind("<<goto-line>>", self.goto_line_event)
+	
+	def find_event(self, event):
+		default = self.text.get("self.first", "sel.last") or self.pat
+		new = tkSimpleDialog.askstring("Find",
+			"Regular Expression:",
+			initialvalue=default,
+			parent=self.text)
+		if not new:
+			return "break"
+		self.pat = new
+		try:
+			self.prog = re.compile(self.pat)
+		except re.error, msg:
+			tkMessageBox.showerror("RE error", str(msg),
+			                       master=self.text)
+			return "break"
+		return self.find_next_event(event)
+	
+	def find_same_event(self, event):
+		pat = self.text.get("sel.first", "sel.last")
+		if not pat:
+			return self.find_event(event)
+		self.pat = re.escape(pat)
+		self.prog = None
+		try:
+			self.prog = re.compile(self.pat)
+		except re.error, msg:
+			tkMessageBox.showerror("RE error", str(message),
+			                       master=self.text)
+			return "break"
+		self.text.mark_set("insert", "sel.last")
+		return self.find_next_event(event)
+
+	def find_next_event(self, event):
+		if not self.pat:
+			return self.find_event(event)
+		if not self.prog:
+			self.text.bell()
+			##print "No program"
+			return "break"
+		self.text.mark_set("find", "insert")
+		while 1:
+			chars = self.text.get("find", "find lineend +1c")
+			##print "Searching", `chars`
+			if not chars:
+				self.text.bell()
+				##print "end of buffer"
+				break
+			m = self.prog.search(chars)
+			if m:
+				i, j = m.span()
+				self.text.mark_set("insert", "find +%dc" % j)
+				self.text.mark_set("find", "find +%dc" % i)
+				self.text.tag_remove("sel", "1.0", "end")
+				self.text.tag_add("sel", "find", "insert")
+				self.text.see("insert")
+				break
+			self.text.mark_set("find", "find lineend +1c")
+		return "break"
+	
+	def goto_line_event(self, event):
+		lineno = tkSimpleDialog.askinteger("Goto",
+						   "Go to line number:")
+		if lineno is None:
+			return "break"
+		if lineno <= 0:
+			self.text.bell()
+			return "break"
+		self.text.mark_set("insert", "%d.0" % lineno)
+		self.text.see("insert")
diff --git a/Tools/idle/StackViewer.py b/Tools/idle/StackViewer.py
new file mode 100644
index 0000000..a59668a
--- /dev/null
+++ b/Tools/idle/StackViewer.py
@@ -0,0 +1,288 @@
+import string
+import sys
+import os
+from Tkinter import *
+import linecache
+from repr import Repr
+
+
+class StackViewer:
+
+    def __init__(self, root=None, flist=None):
+        self.flist = flist
+        # Create root and/or toplevel window
+        if not root:
+            import Tkinter
+            root = Tkinter._default_root
+        if not root:
+            root = top = Tk()
+        else:
+            top = Toplevel(root)
+        self.root = root
+        self.top = top
+        top.wm_title("Stack viewer")
+        # Create top frame, with scrollbar and listbox
+        self.topframe = Frame(top)
+        self.topframe.pack(fill="both", expand=1)
+        self.vbar = Scrollbar(self.topframe, name="vbar")
+        self.vbar.pack(side="right", fill="y")
+        self.listbox = Listbox(self.topframe, exportselection=0, 
+                               takefocus=1, width=60)
+        self.listbox.pack(expand=1, fill="both")
+        # Tie listbox and scrollbar together
+        self.vbar["command"] = self.listbox.yview
+        self.listbox["yscrollcommand"] = self.vbar.set
+        # Bind events to the list box
+        self.listbox.bind("<ButtonRelease-1>", self.click_event)
+	self.listbox.bind("<Double-ButtonRelease-1>", self.double_click_event)
+        self.listbox.bind("<ButtonPress-3>", self.popup_event)
+        self.listbox.bind("<Key-Up>", self.up_event)
+        self.listbox.bind("<Key-Down>", self.down_event)
+        # Load the stack
+        linecache.checkcache()
+        stack = getstack()
+        self.load_stack(stack)
+
+    def load_stack(self, stack):
+        self.stack = stack
+        l = self.listbox
+        l.delete(0, END)
+        if len(stack) > 10:
+            l["height"] = 10
+            self.topframe.pack(expand=1)
+        else:
+            l["height"] = len(stack)
+            self.topframe.pack(expand=0)
+        for frame, lineno in stack:
+            try:
+                modname = frame.f_globals["__name__"]
+            except:
+                modname = "?"
+            code = frame.f_code
+            filename = code.co_filename
+            funcname = code.co_name
+            sourceline = linecache.getline(filename, lineno)
+            sourceline = string.strip(sourceline)
+            if funcname in ("?", "", None):
+                item = "%s, line %d: %s" % (modname, lineno, sourceline)
+            else:
+                item = "%s.%s(), line %d: %s" % (modname, funcname,
+                                                 lineno, sourceline)
+            l.insert(END, item)
+        l.focus_set()
+        l.selection_clear(0, "end")
+        l.activate("end")
+        l.see("end")
+
+    rmenu = None
+
+    def click_event(self, event):
+        self.listbox.activate("@%d,%d" % (event.x, event.y))
+        self.show_stack_frame()
+        return "break"
+
+    def popup_event(self, event):
+        if not self.rmenu:
+            self.make_menu()
+        rmenu = self.rmenu
+        self.event = event
+        self.listbox.activate("@%d,%d" % (event.x, event.y))
+        rmenu.tk_popup(event.x_root, event.y_root)
+
+    def make_menu(self):
+        rmenu = Menu(self.top, tearoff=0)
+        rmenu.add_command(label="Go to source line",
+                          command=self.goto_source_line)
+        rmenu.add_command(label="Show stack frame",
+                          command=self.show_stack_frame)
+        self.rmenu = rmenu
+
+    def goto_source_line(self):
+        index = self.listbox.index("active")
+        self.show_source(index)
+    
+    def show_stack_frame(self):
+        index = self.listbox.index("active")
+        self.show_frame(index)
+
+    def double_click_event(self, event):
+        index = self.listbox.index("active")
+        self.show_source(index)
+        return "break"
+    
+    def up_event(self, event):
+        index = self.listbox.index("active") - 1
+        if index < 0:
+            self.top.bell()
+            return "break"
+        self.show_frame(index)
+        return "break"
+        
+    def down_event(self, event):
+        index = self.listbox.index("active") + 1
+        if index >= len(self.stack):
+            self.top.bell()
+            return "break"
+        self.show_frame(index)
+        return "break"
+    
+    def show_source(self, index):
+        if not 0 <= index < len(self.stack):
+            self.top.bell()
+            return
+        frame, lineno = self.stack[index]
+        code = frame.f_code
+        filename = code.co_filename
+        if not self.flist:
+	    self.top.bell()
+	    return
+        if not os.path.exists(filename):
+	    self.top.bell()
+	    return
+        edit = self.flist.open(filename)
+        edit.gotoline(lineno)
+    
+    localsframe = None
+    localsviewer = None
+    localsdict = None
+    globalsframe = None
+    globalsviewer = None
+    globalsdict = None
+    curframe = None
+
+    def show_frame(self, index):
+        if not 0 <= index < len(self.stack):
+            self.top.bell()
+            return
+        self.listbox.selection_clear(0, "end")
+        self.listbox.selection_set(index)
+        self.listbox.activate(index)
+        self.listbox.see(index)
+        self.listbox.focus_set()
+        frame, lineno = self.stack[index]
+        if frame is self.curframe:
+            return
+        self.curframe = None
+        if frame.f_globals is not self.globalsdict:
+            self.show_globals(frame)
+        self.show_locals(frame)
+        self.curframe = frame
+    
+    def show_globals(self, frame):
+        title = "Global Variables"
+        if frame.f_globals.has_key("__name__"):
+            try:
+                name = str(frame.f_globals["__name__"]) + ""
+            except:
+                name = ""
+            if name:
+                title = title + " in module " + name
+        self.globalsdict = None
+        if self.globalsviewer:
+            self.globalsviewer.close()
+        self.globalsviewer = None
+        if not self.globalsframe:
+            self.globalsframe = Frame(self.top)
+        self.globalsdict = frame.f_globals
+        self.globalsviewer = NamespaceViewer(
+            self.globalsframe,
+            title,
+            self.globalsdict)
+        self.globalsframe.pack(fill="both", side="bottom")
+    
+    def show_locals(self, frame):
+        self.localsdict = None
+        if self.localsviewer:
+            self.localsviewer.close()
+        self.localsviewer = None
+        if frame.f_locals is not frame.f_globals:
+            title = "Local Variables"
+            code = frame.f_code
+            funcname = code.co_name
+            if funcname not in ("?", "", None):
+                title = title + " in " + funcname
+            if not self.localsframe:
+                self.localsframe = Frame(self.top)
+            self.localsdict = frame.f_locals
+            self.localsviewer = NamespaceViewer(
+                self.localsframe,
+                title,
+                self.localsdict)
+            self.localsframe.pack(fill="both", side="top")
+        else:
+            if self.localsframe:
+                self.localsframe.forget()
+
+
+def getstack(t=None, f=None):
+    if t is None:
+        t = sys.last_traceback
+    stack = []
+    if t and t.tb_frame is f:
+        t = t.tb_next
+    while f is not None:
+        stack.append((f, f.f_lineno))
+        if f is self.botframe:
+	    break
+        f = f.f_back
+    stack.reverse()
+    while t is not None:
+        stack.append((t.tb_frame, t.tb_lineno))
+        t = t.tb_next
+    return stack
+
+
+class NamespaceViewer:
+    
+    def __init__(self, frame, title, dict):
+        width = 0
+        height = 20*len(dict) # XXX 20 == observed height of Entry widget
+        self.frame = frame
+        self.title = title
+        self.dict = dict
+        self.repr = Repr()
+        self.repr.maxstring = 60
+        self.repr.maxother = 60
+        self.label = Label(frame, text=title, borderwidth=2, relief="groove")
+        self.label.pack(fill="x")
+        self.vbar = vbar = Scrollbar(frame, name="vbar")
+        vbar.pack(side="right", fill="y")
+        self.canvas = canvas = Canvas(frame,
+                                      height=min(300, max(40, height)),
+                                      scrollregion=(0, 0, width, height))
+        canvas.pack(side="left", fill="both", expand=1)
+        vbar["command"] = canvas.yview
+        canvas["yscrollcommand"] = vbar.set
+        self.subframe = subframe = Frame(canvas)
+        self.sfid = canvas.create_window(0, 0, window=subframe, anchor="nw")
+        names = dict.keys()
+        names.sort()
+        row = 0
+        for name in names:
+            value = dict[name]
+            svalue = self.repr.repr(value) # repr(value)
+            l = Label(subframe, text=name)
+            l.grid(row=row, column=0, sticky="nw")
+##            l = Label(subframe, text=svalue, justify="l", wraplength=300)
+            l = Entry(subframe, width=0, borderwidth=0)
+            l.insert(0, svalue)
+##            l["state"] = "disabled"
+            l.grid(row=row, column=1, sticky="nw")
+            row = row+1
+        frame.update_idletasks() # Alas!
+        width = subframe.winfo_reqwidth()
+        height = subframe.winfo_reqheight()
+        canvas["scrollregion"] = (0, 0, width, height)
+        if height > 300:
+            canvas["height"] = 300
+            frame.pack(expand=1)
+        else:
+            canvas["height"] = height
+            frame.pack(expand=0)
+
+    def close(self):
+        for c in self.subframe, self.label, self.vbar, self.canvas:
+            try:
+                c.destroy()
+            except:
+                pass
diff --git a/Tools/idle/UndoDelegator.py b/Tools/idle/UndoDelegator.py
new file mode 100644
index 0000000..ee49651
--- /dev/null
+++ b/Tools/idle/UndoDelegator.py
@@ -0,0 +1,269 @@
+import sys
+import string
+from Tkinter import *
+from Delegator import Delegator
+
+
+class UndoDelegator(Delegator):
+
+    max_undo = 1000
+
+    def __init__(self):
+        Delegator.__init__(self)
+        self.reset_undo()
+    
+    def setdelegate(self, delegate):
+        if self.delegate is not None:
+            self.unbind("<<undo>>")
+            self.unbind("<<redo>>")
+            self.unbind("<<dump-undo-state>>")
+        Delegator.setdelegate(self, delegate)
+        if delegate is not None:
+            self.bind("<<undo>>", self.undo_event)
+            self.bind("<<redo>>", self.redo_event)
+            self.bind("<<dump-undo-state>>", self.dump_event)
+
+    def dump_event(self, event):
+        from pprint import pprint
+        pprint(self.undolist[:self.pointer])
+        print "pointer:", self.pointer,
+        print "saved:", self.saved,
+        print "can_merge:", self.can_merge,
+        print "get_saved():", self.get_saved()
+        pprint(self.undolist[self.pointer:])
+        return "break"
+
+    def reset_undo(self):
+        self.was_saved = -1
+        self.pointer = 0
+        self.undolist = []
+        self.set_saved(1)
+
+    def set_saved(self, flag):
+        if flag:
+            self.saved = self.pointer
+        else:
+            self.saved = -1
+        self.can_merge = 0
+        self.check_saved()
+
+    def get_saved(self):
+        return self.saved == self.pointer
+
+    saved_change_hook = None
+
+    def set_saved_change_hook(self, hook):
+        self.saved_change_hook = hook
+
+    was_saved = -1
+
+    def check_saved(self):
+        is_saved = self.get_saved()
+        if is_saved != self.was_saved:
+            self.was_saved = is_saved
+            if self.saved_change_hook:
+                self.saved_change_hook()
+
+    def insert(self, index, chars, tags=None):
+        self.addcmd(InsertCommand(index, chars, tags))
+
+    def delete(self, index1, index2=None):
+        self.addcmd(DeleteCommand(index1, index2))
+
+    def addcmd(self, cmd):
+        cmd.do(self.delegate)
+        if self.can_merge and self.pointer > 0:
+            lastcmd = self.undolist[self.pointer-1]
+            if lastcmd.merge(cmd):
+                return
+        self.undolist[self.pointer:] = [cmd]
+        if self.saved > self.pointer:
+            self.saved = -1
+        self.pointer = self.pointer + 1
+        if len(self.undolist) > self.max_undo:
+            ##print "truncating undo list"
+            del self.undolist[0]
+            self.pointer = self.pointer - 1
+            if self.saved >= 0:
+                self.saved = self.saved - 1
+        self.can_merge = 1
+        self.check_saved()
+
+    def undo_event(self, event):
+        if self.pointer == 0:
+            self.bell()
+            return "break"
+        cmd = self.undolist[self.pointer - 1]
+        cmd.undo(self.delegate)
+        self.pointer = self.pointer - 1
+        self.can_merge = 0
+        self.check_saved()
+        return "break"
+
+    def redo_event(self, event):
+        if self.pointer >= len(self.undolist):
+            self.bell()
+            return "break"
+        cmd = self.undolist[self.pointer]
+        cmd.redo(self.delegate)
+        self.pointer = self.pointer + 1
+        self.can_merge = 0
+        self.check_saved()
+        return "break"
+
+
+class Command:
+
+    # Base class for Undoable commands
+
+    tags = None
+
+    def __init__(self, index1, index2, chars, tags=None):
+        self.marks_before = {}
+        self.marks_after = {}
+        self.index1 = index1
+        self.index2 = index2
+        self.chars = chars
+        if tags:
+            self.tags = tags
+
+    def __repr__(self):
+        s = self.__class__.__name__
+        t = (self.index1, self.index2, self.chars, self.tags)
+        if self.tags is None:
+            t = t[:-1]
+        return s + `t`
+
+    def do(self, text):
+        pass
+
+    def redo(self, text):
+        pass
+
+    def undo(self, text):
+        pass
+
+    def merge(self, cmd):
+        return 0
+
+    def save_marks(self, text):
+        marks = {}
+        for name in text.mark_names():
+            if name != "insert" and name != "current":
+                marks[name] = text.index(name)
+        return marks
+
+    def set_marks(self, text, marks):
+        for name, index in marks.items():
+            text.mark_set(name, index)
+
+
+class InsertCommand(Command):
+
+    # Undoable insert command
+
+    def __init__(self, index1, chars, tags=None):
+        Command.__init__(self, index1, None, chars, tags)
+
+    def do(self, text):
+        self.marks_before = self.save_marks(text)
+        self.index1 = text.index(self.index1)
+        if text.compare(self.index1, ">", "end-1c"):
+            # Insert before the final newline
+            self.index1 = text.index("end-1c")
+        text.insert(self.index1, self.chars, self.tags)
+        self.index2 = text.index("%s+%dc" % (self.index1, len(self.chars)))
+        self.marks_after = self.save_marks(text)
+        ##sys.__stderr__.write("do: %s\n" % self)
+
+    def redo(self, text):
+        text.mark_set('insert', self.index1)
+        text.insert(self.index1, self.chars, self.tags)
+        self.set_marks(text, self.marks_after)
+        text.see('insert')
+        ##sys.__stderr__.write("redo: %s\n" % self)
+
+    def undo(self, text):
+        text.mark_set('insert', self.index1)
+        text.delete(self.index1, self.index2)
+        self.set_marks(text, self.marks_before)
+        text.see('insert')
+        ##sys.__stderr__.write("undo: %s\n" % self)
+
+    def merge(self, cmd):
+        if self.__class__ is not cmd.__class__:
+            return 0
+        if self.index2 != cmd.index1:
+            return 0
+        if self.tags != cmd.tags:
+            return 0
+        if len(cmd.chars) != 1:
+            return 0
+        if self.chars and \
+           self.classify(self.chars[-1]) != self.classify(cmd.chars):
+            return 0
+        self.index2 = cmd.index2
+        self.chars = self.chars + cmd.chars
+        return 1
+
+    alphanumeric = string.letters + string.digits + "_"
+
+    def classify(self, c):
+        if c in self.alphanumeric:
+            return "alphanumeric"
+        if c == "\n":
+            return "newline"
+        return "punctuation"
+
+
+class DeleteCommand(Command):
+
+    # Undoable delete command
+
+    def __init__(self, index1, index2=None):
+        Command.__init__(self, index1, index2, None, None)
+
+    def do(self, text):
+        self.marks_before = self.save_marks(text)
+        self.index1 = text.index(self.index1)
+        if self.index2:
+            self.index2 = text.index(self.index2)
+        else:
+            self.index2 = text.index(self.index1 + " +1c")
+        if text.compare(self.index2, ">", "end-1c"):
+            # Don't delete the final newline
+            self.index2 = text.index("end-1c")
+        self.chars = text.get(self.index1, self.index2)
+        text.delete(self.index1, self.index2)
+        self.marks_after = self.save_marks(text)
+        ##sys.__stderr__.write("do: %s\n" % self)
+
+    def redo(self, text):
+        text.mark_set('insert', self.index1)
+        text.delete(self.index1, self.index2)
+        self.set_marks(text, self.marks_after)
+        text.see('insert')
+        ##sys.__stderr__.write("redo: %s\n" % self)
+
+    def undo(self, text):
+        text.mark_set('insert', self.index1)
+        text.insert(self.index1, self.chars)
+        self.set_marks(text, self.marks_before)
+        text.see('insert')
+        ##sys.__stderr__.write("undo: %s\n" % self)
+
+
+def main():
+    from Percolator import Percolator
+    root = Tk()
+    root.wm_protocol("WM_DELETE_WINDOW", root.quit)
+    text = Text()
+    text.pack()
+    text.focus_set()
+    p = Percolator(text)
+    d = UndoDelegator()
+    p.insertfilter(d)
+    root.mainloop()
+
+if __name__ == "__main__":
+    main()
diff --git a/Tools/idle/WidgetRedirector.py b/Tools/idle/WidgetRedirector.py
new file mode 100644
index 0000000..45f86f7
--- /dev/null
+++ b/Tools/idle/WidgetRedirector.py
@@ -0,0 +1,84 @@
+from Tkinter import *
+
+
+class WidgetRedirector:
+
+    """Support for redirecting arbitrary widget subcommands."""
+
+    def __init__(self, widget):
+        self.dict = {}
+        self.widget = widget
+        self.tk = tk = widget.tk
+        w = widget._w
+        self.orig = w + "_orig"
+        tk.call("rename", w, self.orig)
+        tk.createcommand(w, self.dispatch)
+
+    def __repr__(self):
+        return "WidgetRedirector(%s<%s>)" % (self.widget.__class__.__name__,
+                                             self.widget._w)
+
+    def __del__(self):
+        self.close()
+
+    def close(self):
+        self.dict = {}
+        widget = self.widget; del self.widget
+        orig = self.orig; del self.orig
+        tk = widget.tk
+        w = widget._w
+        tk.deletecommand(w)
+        tk.call("rename", w, orig)
+
+    def register(self, name, function):
+        if self.dict.has_key(name):
+            previous = function
+        else:
+            previous = OriginalCommand(self, name)
+        self.dict[name] = function
+        setattr(self.widget, name, function)
+        return previous
+
+    def dispatch(self, cmd, *args):
+        m = self.dict.get(cmd)
+        try:
+            if m:
+                return apply(m, args)
+            else:
+                return self.tk.call((self.orig, cmd) + args)
+        except TclError:
+            return ""
+
+
+class OriginalCommand:
+
+    def __init__(self, redir, name):
+        self.redir = redir
+        self.name = name
+        self.tk = redir.tk
+        self.orig = redir.orig
+        self.tk_call = self.tk.call
+        self.orig_and_name = (self.orig, self.name)
+
+    def __repr__(self):
+        return "OriginalCommand(%s, %s)" % (`self.redir`, `self.name`)
+
+    def __call__(self, *args):
+        return self.tk_call(self.orig_and_name + args)
+
+
+def main():
+    root = Tk()
+    text = Text()
+    text.pack()
+    text.focus_set()
+    redir = WidgetRedirector(text)
+    global orig_insert
+    def my_insert(*args):
+        print "insert", args
+        apply(orig_insert, args)
+    orig_insert = redir.register("insert", my_insert)
+    root.mainloop()
+
+if __name__ == "__main__":
+    main()
diff --git a/Tools/idle/help.txt b/Tools/idle/help.txt
new file mode 100644
index 0000000..6dbf2fa
--- /dev/null
+++ b/Tools/idle/help.txt
@@ -0,0 +1,60 @@
+Windows and files:
+
+	^X ^N creates new empty text editor window
+	^X ^C closes all windows
+	Alt-F4 or ^X ^0 (that's control-x-control-zero) closes current window
+	^X ^D opens a file from dialog box
+	^X ^S saves to current file
+	^X ^W saves to file from dialog box
+	^X w save a copy to file from dialog box
+
+Navigation:
+
+	Arrow keys and Page Up/Down to move around
+	Home/End go to begin/end of line
+	Control-Home/End go to begin/end of file
+	Some Emacs bindings may also work, e.g. ^A/^E
+
+Searching: all searches are forward from the cursor without
+wrap-around, case sensitive, Perl-style regular expression matches
+
+	^S without a selection opens search dialog box
+	^S with a selection searches for selected text
+	^U ^S repeats last search
+	Alt-G opens dialog box to go to a specific line
+
+Editing:
+
+	Backspace deletes left of cursor, Delete right of cursor
+	Cut and paste use platform's conventions
+	^[ or Alt-[ left-shifts (dedents) the current line or selection
+	^] or Alt-] right-shifts (indents) the current line or selection
+	Alt-/ expands last word you type (like Emacs dabbrev)
+
+Undo:
+
+	^Z undoes last change; repeat to undo more
+	Alt-Z redoes last undone change; repeat to redo more
+
+Console window:
+
+	^C interrupts executing command
+	^D sends end-of-file; closes console if typed at >>> prompt
+
+	If you get a traceback, right-click on any line listing a
+	filename and line number and select "Go to line from
+	traceback" to open that file and go to the indicated line
+
+Python syntax colors: the coloring is applied in a background thread
+
+	Keywords	orange
+	Strings		green
+	Comments	red
+	Definitions	blue
+
+Console colors:
+
+	Console output	red
+	stdout		blue
+	stderr		dark green
+	stdin		purple
diff --git a/Tools/idle/idle.pyw b/Tools/idle/idle.pyw
new file mode 100644
index 0000000..3c06e05
--- /dev/null
+++ b/Tools/idle/idle.pyw
@@ -0,0 +1,3 @@
+#! /usr/bin/env python
+import PyShell
+PyShell.main()