Initial revision
diff --git a/Lib/idlelib/EditorWindow.py b/Lib/idlelib/EditorWindow.py
new file mode 100644
index 0000000..9f76ef7
--- /dev/null
+++ b/Lib/idlelib/EditorWindow.py
@@ -0,0 +1,749 @@
+# changes by dscherer@cmu.edu
+#   - created format and run menus
+#   - added silly advice dialog (apologies to Douglas Adams)
+#   - made Python Documentation work on Windows (requires win32api to
+#     do a ShellExecute(); other ways of starting a web browser are awkward)
+
+import sys
+import os
+import string
+import re
+import imp
+from Tkinter import *
+import tkSimpleDialog
+import tkMessageBox
+import idlever
+import WindowList
+from IdleConf import idleconf
+
+# The default tab setting for a Text widget, in average-width characters.
+TK_TABWIDTH_DEFAULT = 8
+
+# 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 <<open-path-browser>>
+
+#$ 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 %s
+
+An Integrated DeveLopment Environment for Python
+
+by Guido van Rossum
+
+This version of IDLE has been modified by David Scherer
+  (dscherer@cmu.edu).  See readme.txt for details.
+""" % idlever.IDLE_VERSION
+
+class EditorWindow:
+
+    from Percolator import Percolator
+    from ColorDelegator import ColorDelegator
+    from UndoDelegator import UndoDelegator
+    from IOBinding import IOBinding
+    import Bindings
+    from Tkinter import Toplevel
+    from MultiStatusBar import MultiStatusBar
+
+    about_title = about_title
+    about_text = about_text
+
+    vars = {}
+
+    def __init__(self, flist=None, filename=None, key=None, root=None):
+        edconf = idleconf.getsection('EditorWindow')
+        coconf = idleconf.getsection('Colors')
+        self.flist = flist
+        root = root or flist.root
+        self.root = root
+        if flist:
+            self.vars = flist.vars
+        self.menubar = Menu(root)
+        self.top = top = self.Toplevel(root, menu=self.menubar)
+        self.vbar = vbar = Scrollbar(top, name='vbar')
+        self.text_frame = text_frame = Frame(top)
+        self.text = text = Text(text_frame, name='text', padx=5,
+                      foreground=coconf.getdef('normal-foreground'),
+                      background=coconf.getdef('normal-background'),
+                      highlightcolor=coconf.getdef('hilite-foreground'),
+                      highlightbackground=coconf.getdef('hilite-background'),
+                      insertbackground=coconf.getdef('cursor-background'),
+                      width=edconf.getint('width'),
+                      height=edconf.getint('height'),
+                      wrap="none")
+
+        self.createmenubar()
+        self.apply_bindings()
+
+        self.top.protocol("WM_DELETE_WINDOW", self.close)
+        self.top.bind("<<close-window>>", self.close_event)
+        text.bind("<<center-insert>>", self.center_insert_event)
+        text.bind("<<help>>", self.help_dialog)
+        text.bind("<<good-advice>>", self.good_advice)
+        text.bind("<<python-docs>>", self.python_docs)
+        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)
+            text.bind("<<open-path-browser>>", self.open_path_browser)
+
+        vbar['command'] = text.yview
+        vbar.pack(side=RIGHT, fill=Y)
+
+        text['yscrollcommand'] = vbar.set
+        text['font'] = edconf.get('font-name'), edconf.get('font-size')
+        text_frame.pack(side=LEFT, fill=BOTH, expand=1)
+        text.pack(side=TOP, fill=BOTH, expand=1)
+        text.focus_set()
+
+        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.io = io = self.IOBinding(self)
+
+        text.undo_block_start = undo.undo_block_start
+        text.undo_block_stop = undo.undo_block_stop
+        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()
+
+        self.load_extensions()
+
+        menu = self.menudict.get('windows')
+        if menu:
+            end = menu.index("end")
+            if end is None:
+                end = -1
+            if end >= 0:
+                menu.add_separator()
+                end = end + 1
+            self.wmenu_end = end
+            WindowList.register_callback(self.postwindowsmenu)
+
+        # Some abstractions so IDLE extensions are cross-IDE
+        self.askyesno = tkMessageBox.askyesno
+        self.askinteger = tkSimpleDialog.askinteger
+        self.showerror = tkMessageBox.showerror
+
+        if self.extensions.has_key('AutoIndent'):
+            self.extensions['AutoIndent'].set_indentation_params(
+                self.ispythonsource(filename))
+        self.set_status_bar()
+
+    def set_status_bar(self):
+        self.status_bar = self.MultiStatusBar(self.text_frame)
+        self.status_bar.set_label('column', 'Col: ?', side=RIGHT)
+        self.status_bar.set_label('line', 'Ln: ?', side=RIGHT)
+        self.status_bar.pack(side=BOTTOM, fill=X)
+        self.text.bind('<KeyRelease>', self.set_line_and_column)
+        self.text.bind('<ButtonRelease>', self.set_line_and_column)
+        self.text.after_idle(self.set_line_and_column)
+
+    def set_line_and_column(self, event=None):
+        line, column = string.split(self.text.index(INSERT), '.')
+        self.status_bar.set_label('column', 'Col: %s' % column)
+        self.status_bar.set_label('line', 'Ln: %s' % line)
+
+    def wakeup(self):
+        if self.top.wm_state() == "iconic":
+            self.top.wm_deiconify()
+        else:
+            self.top.tkraise()
+        self.text.focus_set()
+
+    menu_specs = [
+        ("file", "_File"),
+        ("edit", "_Edit"),
+        ("format", "F_ormat"),
+        ("run", "_Run"),
+        ("windows", "_Windows"),
+        ("help", "_Help"),
+    ]
+
+    def createmenubar(self):
+        mbar = self.menubar
+        self.menudict = menudict = {}
+        for name, label in self.menu_specs:
+            underline, label = prepstr(label)
+            menudict[name] = menu = Menu(mbar, name=name)
+            mbar.add_cascade(label=label, menu=menu, underline=underline)
+        self.fill_menus()
+
+    def postwindowsmenu(self):
+        # Only called when Windows menu exists
+        # XXX Actually, this Just-In-Time updating interferes badly
+        # XXX with the tear-off feature.  It would be better to update
+        # XXX all Windows menus whenever the list of windows changes.
+        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)
+        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 good_advice(self, event=None):
+        tkMessageBox.showinfo('Advice', "Don't Panic!", master=self.text)
+
+    def help_dialog(self, event=None):
+        try:
+            helpfile = os.path.join(os.path.dirname(__file__), self.helpfile)
+        except NameError:
+            helpfile = self.helpfile
+        if self.flist:
+            self.flist.open(helpfile)
+        else:
+            self.io.loadfile(helpfile)
+
+    help_viewer = "netscape -remote 'openurl(%(url)s)' 2>/dev/null || " \
+                  "netscape %(url)s &"
+    help_url = "http://www.python.org/doc/current/"
+
+    def python_docs(self, event=None):
+        if sys.platform=='win32':
+          try:
+            import win32api
+            import ExecBinding
+            doc = os.path.join( os.path.dirname( ExecBinding.pyth_exe ), "doc", "index.html" )
+            win32api.ShellExecute(0, None, doc, None, sys.path[0], 1)
+          except:
+            pass
+        else:
+          cmd = self.help_viewer % {"url": self.help_url}
+          os.system(cmd)
+
+    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:
+            name = ""
+        else:
+            name = string.strip(name)
+        if not name:
+            name = tkSimpleDialog.askstring("Module",
+                     "Enter the name of a Python module\n"
+                     "to search on sys.path and open:",
+                     parent=self.text)
+            if name:
+                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 (NameError, ImportError), msg:
+            tkMessageBox.showerror("Import error", str(msg), parent=self.text)
+            return
+        if type != imp.PY_SOURCE:
+            tkMessageBox.showerror("Unsupported type",
+                "%s is not a source module" % name, parent=self.text)
+            return
+        if f:
+            f.close()
+        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)
+            self.text.focus_set()
+            return None
+        head, tail = os.path.split(filename)
+        base, ext = os.path.splitext(tail)
+        import ClassBrowser
+        ClassBrowser.ClassBrowser(self.flist, base, [head])
+
+    def open_path_browser(self, event=None):
+        import PathBrowser
+        PathBrowser.PathBrowser(self.flist)
+
+    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
+        base, ext = os.path.splitext(os.path.basename(filename))
+        if os.path.normcase(ext) in (".py", ".pyw"):
+            return 1
+        try:
+            f = open(filename)
+            line = f.readline()
+            f.close()
+        except IOError:
+            return 0
+        return line[:2] == '#!' and string.find(line, 'python') >= 0
+
+    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()
+        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):
+        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"
+        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()
+
+    def center(self, mark="insert"):
+        text = self.text
+        top, bot = self.getwindowlines()
+        lineno = self.getlineno(mark)
+        height = bot - top
+        newtop = max(1, lineno - height/2)
+        text.yview(float(newtop))
+
+    def getwindowlines(self):
+        text = self.text
+        top = self.getlineno("@0,0")
+        bot = self.getlineno("@0,65535")
+        if top == bot and text.winfo_height() == 1:
+            # Geometry manager hasn't run yet
+            height = int(text['height'])
+            bot = top + height - 1
+        return top, bot
+
+    def getlineno(self, mark="insert"):
+        text = self.text
+        return int(float(text.index(mark)))
+
+    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.maybesave()
+        if reply != "cancel":
+            self._close()
+        return reply
+
+    def _close(self):
+        WindowList.unregister_callback(self.postwindowsmenu)
+        if self.close_hook:
+            self.close_hook()
+        self.flist = None
+        colorizing = 0
+        self.unload_extensions()
+        self.io.close(); self.io = None
+        self.undo = None # XXX
+        if self.color:
+            colorizing = self.color.colorizing
+            doh = colorizing and self.top
+            self.color.close(doh) # Cancel colorization
+        self.text = None
+        self.vars = None
+        self.per.close(); self.per = None
+        if not colorizing:
+            self.top.destroy()
+
+    def load_extensions(self):
+        self.extensions = {}
+        self.load_standard_extensions()
+
+    def unload_extensions(self):
+        for ins in self.extensions.values():
+            if hasattr(ins, "close"):
+                ins.close()
+        self.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):
+        return idleconf.getextensions()
+
+    def load_extension(self, name):
+        mod = __import__(name, globals(), locals(), [])
+        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.apply_bindings(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.fill_menus(ins.menudefs, keydefs)
+        return ins
+
+    def apply_bindings(self, keydefs=None):
+        if keydefs is None:
+            keydefs = self.Bindings.default_keydefs
+        text = self.text
+        text.keydefs = keydefs
+        for event, keylist in keydefs.items():
+            if keylist:
+                apply(text.event_add, (event,) + tuple(keylist))
+
+    def fill_menus(self, defs=None, keydefs=None):
+        # Fill the menus. Menus that are absent or None in
+        # self.menudict are ignored.
+        if defs is None:
+            defs = self.Bindings.menudefs
+        if keydefs is None:
+            keydefs = self.Bindings.default_keydefs
+        menudict = self.menudict
+        text = self.text
+        for mname, itemlist in defs:
+            menu = menudict.get(mname)
+            if not menu:
+                continue
+            for item in itemlist:
+                if not item:
+                    menu.add_separator()
+                else:
+                    label, event = item
+                    checkbutton = (label[:1] == '!')
+                    if checkbutton:
+                        label = label[1:]
+                    underline, label = prepstr(label)
+                    accelerator = get_accelerator(keydefs, event)
+                    def command(text=text, event=event):
+                        text.event_generate(event)
+                    if checkbutton:
+                        var = self.getrawvar(event, BooleanVar)
+                        menu.add_checkbutton(label=label, underline=underline,
+                            command=command, accelerator=accelerator,
+                            variable=var)
+                    else:
+                        menu.add_command(label=label, underline=underline,
+                            command=command, accelerator=accelerator)
+
+    def getvar(self, name):
+        var = self.getrawvar(name)
+        if var:
+            return var.get()
+
+    def setvar(self, name, value, vartype=None):
+        var = self.getrawvar(name, vartype)
+        if var:
+            var.set(value)
+
+    def getrawvar(self, name, vartype=None):
+        var = self.vars.get(name)
+        if not var and vartype:
+            self.vars[name] = var = vartype(self.text)
+        return var
+
+    # Tk implementations of "virtual text methods" -- each platform
+    # reusing IDLE's support code needs to define these for its GUI's
+    # flavor of widget.
+
+    # Is character at text_index in a Python string?  Return 0 for
+    # "guaranteed no", true for anything else.  This info is expensive
+    # to compute ab initio, but is probably already known by the
+    # platform's colorizer.
+
+    def is_char_in_string(self, text_index):
+        if self.color:
+            # Return true iff colorizer hasn't (re)gotten this far
+            # yet, or the character is tagged as being in a string
+            return self.text.tag_prevrange("TODO", text_index) or \
+                   "STRING" in self.text.tag_names(text_index)
+        else:
+            # The colorizer is missing: assume the worst
+            return 1
+
+    # If a selection is defined in the text widget, return (start,
+    # end) as Tkinter text indices, otherwise return (None, None)
+    def get_selection_indices(self):
+        try:
+            first = self.text.index("sel.first")
+            last = self.text.index("sel.last")
+            return first, last
+        except TclError:
+            return None, None
+
+    # Return the text widget's current view of what a tab stop means
+    # (equivalent width in spaces).
+
+    def get_tabwidth(self):
+        current = self.text['tabs'] or TK_TABWIDTH_DEFAULT
+        return int(current)
+
+    # Set the text widget's current view of what a tab stop means.
+
+    def set_tabwidth(self, newtabwidth):
+        text = self.text
+        if self.get_tabwidth() != newtabwidth:
+            pixels = text.tk.call("font", "measure", text["font"],
+                                  "-displayof", text.master,
+                                  "n" * newtabwith)
+            text.configure(tabs=pixels)
+
+def prepstr(s):
+    # Helper to extract the underscore from a string, e.g.
+    # prepstr("Co_py") returns (2, "Copy").
+    i = string.find(s, '_')
+    if i >= 0:
+        s = s[:i] + s[i+1:]
+    return i, s
+
+
+keynames = {
+ 'bracketleft': '[',
+ 'bracketright': ']',
+ 'slash': '/',
+}
+
+def get_accelerator(keydefs, event):
+    keylist = keydefs.get(event)
+    if not keylist:
+        return ""
+    s = keylist[0]
+    s = re.sub(r"-[a-z]\b", lambda m: string.upper(m.group()), s)
+    s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s)
+    s = re.sub("Key-", "", s)
+    s = re.sub("Cancel","Ctrl-Break",s)   # dscherer@cmu.edu
+    s = re.sub("Control-", "Ctrl-", s)
+    s = re.sub("-", "+", s)
+    s = re.sub("><", " ", s)
+    s = re.sub("<", "", s)
+    s = re.sub(">", "", s)
+    return s
+
+
+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_]')
+    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=root, filename=filename)
+    edit.set_close_hook(root.quit)
+    root.mainloop()
+    root.destroy()
+
+if __name__ == '__main__':
+    test()