Checking in IDLE 0.2.

Much has changed -- too much, in fact, to write down.
The big news is that there's a standard way to write IDLE extensions;
see extend.txt.  Some sample extensions have been provided, and
some existing code has been converted to extensions.  Probably the
biggest new user feature is a new search dialog with more options,
search and replace, and even search in files (grep).

This is exactly as downloaded from my laptop after returning
from the holidays -- it hasn't even been tested on Unix yet.
diff --git a/Tools/idle/EditorWindow.py b/Tools/idle/EditorWindow.py
index 24b62dc..b4c9156 100644
--- a/Tools/idle/EditorWindow.py
+++ b/Tools/idle/EditorWindow.py
@@ -5,15 +5,70 @@
 from Tkinter import *
 import tkSimpleDialog
 import tkMessageBox
+import idlever
+
+# File menu
+
+#$ event <<open-module>>
+#$ win <Alt-m>
+#$ unix <Control-x><Control-m>
+
+#$ event <<open-class-browser>>
+#$ win <Alt-c>
+#$ unix <Control-x><Control-b>
+
+#$ event <<close-window>>
+#$ unix <Control-x><Control-0>
+#$ unix <Control-x><Key-0>
+#$ win <Alt-F4>
+
+# Edit menu
+
+#$ event <<Copy>>
+#$ win <Control-c>
+#$ unix <Alt-w>
+
+#$ event <<Cut>>
+#$ win <Control-x>
+#$ unix <Control-w>
+
+#$ event <<Paste>>
+#$ win <Control-v>
+#$ unix <Control-y>
+
+#$ event <<select-all>>
+#$ win <Alt-a>
+#$ unix <Alt-a>
+
+# Help menu
+
+#$ event <<help>>
+#$ win <F1>
+#$ unix <F1>
+
+#$ event <<about-idle>>
+
+# Events without menu entries
+
+#$ event <<remove-selection>>
+#$ win <Escape>
+
+#$ event <<center-insert>>
+#$ win <Control-l>
+#$ unix <Control-l>
+
+#$ event <<do-nothing>>
+#$ unix <Control-x>
+
 
 about_title = "About IDLE"
 about_text = """\
-IDLE 0.1
+IDLE %s
 
-A not totally unintegrated development environment for Python
+An Integrated DeveLopment Environment for Python
 
 by Guido van Rossum
-"""
+""" % idlever.IDLE_VERSION
 
 class EditorWindow:
 
@@ -21,44 +76,52 @@
     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
-    
-    about_title = about_title
-    about_text = about_text    
+    from Tkinter import Toplevel
 
-    def __init__(self, root, filename=None):
+    about_title = about_title
+    about_text = about_text
+
+    def __init__(self, flist=None, filename=None, key=None, root=None):
+        self.flist = flist
+        root = root or flist.root
         self.root = root
         self.menubar = Menu(root)
-        self.top = top = Toplevel(root, menu=self.menubar)
+        self.top = top = self.Toplevel(root, menu=self.menubar)
         self.vbar = vbar = Scrollbar(top, name='vbar')
-        self.text = text = Text(top, name='text')
+        self.text = text = Text(top, name='text', padx=5,
+                                background="white", wrap="none")
 
         self.createmenubar()
         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)
-        self.text.bind("<<help>>", self.help_dialog)
-        self.text.bind("<<about-idle>>", self.about_dialog)
-        self.text.bind("<<open-module>>", self.open_module)
-        self.text.bind("<<do-nothing>>", lambda event: "break")
+        text.bind("<<center-insert>>", self.center_insert_event)
+        text.bind("<<help>>", self.help_dialog)
+        text.bind("<<about-idle>>", self.about_dialog)
+        text.bind("<<open-module>>", self.open_module)
+        text.bind("<<do-nothing>>", lambda event: "break")
+        text.bind("<<select-all>>", self.select_all)
+        text.bind("<<remove-selection>>", self.remove_selection)
+        text.bind("<3>", self.right_menu_event)
+        if flist:
+            flist.inversedict[self] = key
+            if key:
+                flist.dict[key] = self
+            text.bind("<<open-new-window>>", self.flist.new_callback)
+            text.bind("<<close-all-windows>>", self.flist.close_all_callback)
+            text.bind("<<open-class-browser>>", self.open_class_browser)
 
         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)
@@ -67,8 +130,7 @@
             ##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)
+        self.io = io = self.IOBinding(self)
 
         undo.set_saved_change_hook(self.saved_change_hook)
         io.set_filename_change_hook(self.filename_change_hook)
@@ -81,9 +143,29 @@
 
         self.saved_change_hook()
 
+        self.load_extensions()
+
+        menu = self.menudict.get('windows')
+        if menu:
+            menu.configure(tearoff=0)
+            end = menu.index("end")
+            if end is None:
+                end = -1
+            if end >= 0:
+                menu.add_separator()
+                end = end + 1
+            self.wmenu_end = end
+            menu.configure(postcommand=self.postwindowsmenu)
+
+    def wakeup(self):
+        self.top.tkraise()
+        self.top.wm_deiconify()
+        self.text.focus_set()
+
     menu_specs = [
         ("file", "_File"),
         ("edit", "_Edit"),
+        ("windows", "_Windows"),
         ("help", "_Help"),
     ]
 
@@ -96,15 +178,78 @@
             mbar.add_cascade(label=label, menu=menu, underline=underline)
         self.Bindings.fill_menus(self.text, mdict)
 
+    def postwindowsmenu(self):
+        # Only called when Windows menu exists
+        menu = self.menudict['windows']
+        end = menu.index("end")
+        if end is None:
+            end = -1
+        if end > self.wmenu_end:
+            menu.delete(self.wmenu_end+1, end)
+        import WindowList
+        WindowList.add_windows_to_menu(menu)
+
+    rmenu = None
+
+    def right_menu_event(self, event):
+        self.text.tag_remove("sel", "1.0", "end")
+        self.text.mark_set("insert", "@%d,%d" % (event.x, event.y))
+        if not self.rmenu:
+            self.make_rmenu()
+        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")
+
+    rmenu_specs = [
+        # ("Label", "<<virtual-event>>"), ...
+        ("Close", "<<close-window>>"), # Example
+    ]
+
+    def make_rmenu(self):
+        rmenu = Menu(self.text, tearoff=0)
+        for label, eventname in self.rmenu_specs:
+            def command(text=self.text, eventname=eventname):
+                text.event_generate(eventname)
+            rmenu.add_command(label=label, command=command)
+        self.rmenu = rmenu
+
     def about_dialog(self, event=None):
         tkMessageBox.showinfo(self.about_title, self.about_text,
                               master=self.text)
 
+    helpfile = "help.txt"
+
     def help_dialog(self, event=None):
-        from HelpWindow import HelpWindow
-        HelpWindow(root=self.root)
-    
+        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
+        if self.flist:
+            self.flist.open(helpfile)
+        else:
+            self.io.loadfile(helpfile)
+
+    def select_all(self, event=None):
+        self.text.tag_add("sel", "1.0", "end-1c")
+        self.text.mark_set("insert", "1.0")
+        self.text.see("insert")
+        return "break"
+
+    def remove_selection(self, event=None):
+        self.text.tag_remove("sel", "1.0", "end")
+        self.text.see("insert")
+
     def open_module(self, event=None):
+        # XXX Shouldn't this be in IOBinding or in FileList?
         try:
             name = self.text.get("sel.first", "sel.last")
         except TclError:
@@ -120,6 +265,8 @@
                 name = string.strip(name)
             if not name:
                 return
+        # XXX Ought to support package syntax
+        # XXX Ought to insert current file's directory in front of path
         try:
             (f, file, (suffix, mode, type)) = imp.find_module(name)
         except ImportError, msg:
@@ -131,7 +278,26 @@
             return
         if f:
             f.close()
-        self.flist.open(file, self)
+        if self.flist:
+            self.flist.open(file)
+        else:
+            self.io.loadfile(file)
+
+    def open_class_browser(self, event=None):
+        filename = self.io.filename
+        if not filename:
+            tkMessageBox.showerror(
+                "No filename",
+                "This buffer has no associated filename",
+                master=self.text)
+            return None
+        head, tail = os.path.split(filename)
+        base, ext = os.path.splitext(tail)
+        import pyclbr
+        if pyclbr._modules.has_key(base):
+            del pyclbr._modules[base]
+        import ClassBrowser
+        ClassBrowser.ClassBrowser(self.flist, base, [head])
 
     def gotoline(self, lineno):
         if lineno is not None and lineno > 0:
@@ -143,7 +309,8 @@
     def ispythonsource(self, filename):
         if not filename:
             return 1
-        if os.path.normcase(filename[-3:]) == ".py":
+        base, ext = os.path.splitext(os.path.basename(filename))
+        if os.path.normcase(ext) in (".py", ".pyw"):
             return 1
         try:
             f = open(filename)
@@ -153,12 +320,16 @@
             return 0
         return line[:2] == '#!' and string.find(line, 'python') >= 0
 
-    close_hook = None
+    def close_hook(self):
+        if self.flist:
+            self.flist.close_edit(self)
 
     def set_close_hook(self, close_hook):
         self.close_hook = close_hook
 
     def filename_change_hook(self):
+        if self.flist:
+            self.flist.filename_changed_edit(self)
         self.saved_change_hook()
         if self.ispythonsource(self.io.filename):
             self.addcolorizer()
@@ -184,13 +355,40 @@
         self.per.insertfilter(self.undo)
 
     def saved_change_hook(self):
-        if self.io.filename:
-            title = self.io.filename
+        short = self.short_title()
+        long = self.long_title()
+        if short and long:
+            title = short + " - " + long
+        elif short:
+            title = short
+        elif long:
+            title = long
         else:
-            title = "(Untitled)"
-        if not self.undo.get_saved():
-            title = title + " *"
+            title = "Untitled"
+        icon = short or long or title
+        if not self.get_saved():
+            title = "*%s*" % title
+            icon = "*%s" % icon
         self.top.wm_title(title)
+        self.top.wm_iconname(icon)
+
+    def get_saved(self):
+        return self.undo.get_saved()
+
+    def set_saved(self, flag):
+        self.undo.set_saved(flag)
+
+    def reset_undo(self):
+        self.undo.reset_undo()
+
+    def short_title(self):
+        filename = self.io.filename
+        if filename:
+            filename = os.path.basename(filename)
+        return filename
+
+    def long_title(self):
+        return self.io.filename or ""
 
     def center_insert_event(self, event):
         self.center()
@@ -207,10 +405,14 @@
     def close_event(self, event):
         self.close()
 
+    def maybesave(self):
+        if self.io:
+            return self.io.maybesave()
+
     def close(self):
         self.top.wm_deiconify()
         self.top.tkraise()
-        reply = self.io.maybesave()
+        reply = self.maybesave()
         if reply != "cancel":
             if self.color and self.color.colorizing:
                 self.color.close()
@@ -223,8 +425,59 @@
             self.top.destroy()
         return reply
 
+    def load_extensions(self):
+        self.extensions = {}
+        self.load_standard_extensions()
+
+    def load_standard_extensions(self):
+        for name in self.get_standard_extension_names():
+            try:
+                self.load_extension(name)
+            except:
+                print "Failed to load extension", `name`
+                import traceback
+                traceback.print_exc()
+
+    def get_standard_extension_names(self):
+        import extend
+        return extend.standard
+
+    def load_extension(self, name):
+        mod = __import__(name)
+        cls = getattr(mod, name)
+        ins = cls(self)
+        self.extensions[name] = ins
+        kdnames = ["keydefs"]
+        if sys.platform == 'win32':
+            kdnames.append("windows_keydefs")
+        elif sys.platform == 'mac':
+            kdnames.append("mac_keydefs")
+        else:
+            kdnames.append("unix_keydefs")
+        keydefs = {}
+        for kdname in kdnames:
+            if hasattr(ins, kdname):
+                keydefs.update(getattr(ins, kdname))
+        if keydefs:
+            self.Bindings.apply_bindings(self.text, keydefs)
+            for vevent in keydefs.keys():
+                methodname = string.replace(vevent, "-", "_")
+                while methodname[:1] == '<':
+                    methodname = methodname[1:]
+                while methodname[-1:] == '>':
+                    methodname = methodname[:-1]
+                methodname = methodname + "_event"
+                if hasattr(ins, methodname):
+                    self.text.bind(vevent, getattr(ins, methodname))
+        if hasattr(ins, "menudefs"):
+            self.Bindings.fill_menus(self.text, self. menudict,
+                                     ins.menudefs, keydefs)
+        return ins
+
 
 def fixwordbreaks(root):
+    # Make sure that Tk's double-click and next/previous word
+    # operations use our definition of a word (i.e. an identifier)
     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_]')
@@ -239,7 +492,7 @@
         filename = sys.argv[1]
     else:
         filename = None
-    edit = EditorWindow(root, filename)
+    edit = EditorWindow(root=root, filename=filename)
     edit.set_close_hook(root.quit)
     root.mainloop()
     root.destroy()