Home | History | Annotate | Download | only in idlelib
      1 import sys
      2 import os
      3 import re
      4 import imp
      5 from Tkinter import *
      6 import tkSimpleDialog
      7 import tkMessageBox
      8 import webbrowser
      9 
     10 from idlelib.MultiCall import MultiCallCreator
     11 from idlelib import idlever
     12 from idlelib import WindowList
     13 from idlelib import SearchDialog
     14 from idlelib import GrepDialog
     15 from idlelib import ReplaceDialog
     16 from idlelib import PyParse
     17 from idlelib.configHandler import idleConf
     18 from idlelib import aboutDialog, textView, configDialog
     19 from idlelib import macosxSupport
     20 
     21 # The default tab setting for a Text widget, in average-width characters.
     22 TK_TABWIDTH_DEFAULT = 8
     23 
     24 def _sphinx_version():
     25     "Format sys.version_info to produce the Sphinx version string used to install the chm docs"
     26     major, minor, micro, level, serial = sys.version_info
     27     release = '%s%s' % (major, minor)
     28     if micro:
     29         release += '%s' % (micro,)
     30     if level == 'candidate':
     31         release += 'rc%s' % (serial,)
     32     elif level != 'final':
     33         release += '%s%s' % (level[0], serial)
     34     return release
     35 
     36 def _find_module(fullname, path=None):
     37     """Version of imp.find_module() that handles hierarchical module names"""
     38 
     39     file = None
     40     for tgt in fullname.split('.'):
     41         if file is not None:
     42             file.close()            # close intermediate files
     43         (file, filename, descr) = imp.find_module(tgt, path)
     44         if descr[2] == imp.PY_SOURCE:
     45             break                   # find but not load the source file
     46         module = imp.load_module(tgt, file, filename, descr)
     47         try:
     48             path = module.__path__
     49         except AttributeError:
     50             raise ImportError, 'No source for module ' + module.__name__
     51     if descr[2] != imp.PY_SOURCE:
     52         # If all of the above fails and didn't raise an exception,fallback
     53         # to a straight import which can find __init__.py in a package.
     54         m = __import__(fullname)
     55         try:
     56             filename = m.__file__
     57         except AttributeError:
     58             pass
     59         else:
     60             file = None
     61             base, ext = os.path.splitext(filename)
     62             if ext == '.pyc':
     63                 ext = '.py'
     64             filename = base + ext
     65             descr = filename, None, imp.PY_SOURCE
     66     return file, filename, descr
     67 
     68 
     69 class HelpDialog(object):
     70 
     71     def __init__(self):
     72         self.parent = None      # parent of help window
     73         self.dlg = None         # the help window iteself
     74 
     75     def display(self, parent, near=None):
     76         """ Display the help dialog.
     77 
     78             parent - parent widget for the help window
     79 
     80             near - a Toplevel widget (e.g. EditorWindow or PyShell)
     81                    to use as a reference for placing the help window
     82         """
     83         if self.dlg is None:
     84             self.show_dialog(parent)
     85         if near:
     86             self.nearwindow(near)
     87 
     88     def show_dialog(self, parent):
     89         self.parent = parent
     90         fn=os.path.join(os.path.abspath(os.path.dirname(__file__)),'help.txt')
     91         self.dlg = dlg = textView.view_file(parent,'Help',fn, modal=False)
     92         dlg.bind('<Destroy>', self.destroy, '+')
     93 
     94     def nearwindow(self, near):
     95         # Place the help dialog near the window specified by parent.
     96         # Note - this may not reposition the window in Metacity
     97         #  if "/apps/metacity/general/disable_workarounds" is enabled
     98         dlg = self.dlg
     99         geom = (near.winfo_rootx() + 10, near.winfo_rooty() + 10)
    100         dlg.withdraw()
    101         dlg.geometry("=+%d+%d" % geom)
    102         dlg.deiconify()
    103         dlg.lift()
    104 
    105     def destroy(self, ev=None):
    106         self.dlg = None
    107         self.parent = None
    108 
    109 helpDialog = HelpDialog()  # singleton instance
    110 
    111 
    112 class EditorWindow(object):
    113     from idlelib.Percolator import Percolator
    114     from idlelib.ColorDelegator import ColorDelegator
    115     from idlelib.UndoDelegator import UndoDelegator
    116     from idlelib.IOBinding import IOBinding, filesystemencoding, encoding
    117     from idlelib import Bindings
    118     from Tkinter import Toplevel
    119     from idlelib.MultiStatusBar import MultiStatusBar
    120 
    121     help_url = None
    122 
    123     def __init__(self, flist=None, filename=None, key=None, root=None):
    124         if EditorWindow.help_url is None:
    125             dochome =  os.path.join(sys.prefix, 'Doc', 'index.html')
    126             if sys.platform.count('linux'):
    127                 # look for html docs in a couple of standard places
    128                 pyver = 'python-docs-' + '%s.%s.%s' % sys.version_info[:3]
    129                 if os.path.isdir('/var/www/html/python/'):  # "python2" rpm
    130                     dochome = '/var/www/html/python/index.html'
    131                 else:
    132                     basepath = '/usr/share/doc/'  # standard location
    133                     dochome = os.path.join(basepath, pyver,
    134                                            'Doc', 'index.html')
    135             elif sys.platform[:3] == 'win':
    136                 chmfile = os.path.join(sys.prefix, 'Doc',
    137                                        'Python%s.chm' % _sphinx_version())
    138                 if os.path.isfile(chmfile):
    139                     dochome = chmfile
    140             elif macosxSupport.runningAsOSXApp():
    141                 # documentation is stored inside the python framework
    142                 dochome = os.path.join(sys.prefix,
    143                         'Resources/English.lproj/Documentation/index.html')
    144             dochome = os.path.normpath(dochome)
    145             if os.path.isfile(dochome):
    146                 EditorWindow.help_url = dochome
    147                 if sys.platform == 'darwin':
    148                     # Safari requires real file:-URLs
    149                     EditorWindow.help_url = 'file://' + EditorWindow.help_url
    150             else:
    151                 EditorWindow.help_url = "http://docs.python.org/%d.%d" % sys.version_info[:2]
    152         currentTheme=idleConf.CurrentTheme()
    153         self.flist = flist
    154         root = root or flist.root
    155         self.root = root
    156         try:
    157             sys.ps1
    158         except AttributeError:
    159             sys.ps1 = '>>> '
    160         self.menubar = Menu(root)
    161         self.top = top = WindowList.ListedToplevel(root, menu=self.menubar)
    162         if flist:
    163             self.tkinter_vars = flist.vars
    164             #self.top.instance_dict makes flist.inversedict available to
    165             #configDialog.py so it can access all EditorWindow instances
    166             self.top.instance_dict = flist.inversedict
    167         else:
    168             self.tkinter_vars = {}  # keys: Tkinter event names
    169                                     # values: Tkinter variable instances
    170             self.top.instance_dict = {}
    171         self.recent_files_path = os.path.join(idleConf.GetUserCfgDir(),
    172                 'recent-files.lst')
    173         self.text_frame = text_frame = Frame(top)
    174         self.vbar = vbar = Scrollbar(text_frame, name='vbar')
    175         self.width = idleConf.GetOption('main','EditorWindow','width', type='int')
    176         text_options = {
    177                 'name': 'text',
    178                 'padx': 5,
    179                 'wrap': 'none',
    180                 'width': self.width,
    181                 'height': idleConf.GetOption('main', 'EditorWindow', 'height', type='int')}
    182         if TkVersion >= 8.5:
    183             # Starting with tk 8.5 we have to set the new tabstyle option
    184             # to 'wordprocessor' to achieve the same display of tabs as in
    185             # older tk versions.
    186             text_options['tabstyle'] = 'wordprocessor'
    187         self.text = text = MultiCallCreator(Text)(text_frame, **text_options)
    188         self.top.focused_widget = self.text
    189 
    190         self.createmenubar()
    191         self.apply_bindings()
    192 
    193         self.top.protocol("WM_DELETE_WINDOW", self.close)
    194         self.top.bind("<<close-window>>", self.close_event)
    195         if macosxSupport.runningAsOSXApp():
    196             # Command-W on editorwindows doesn't work without this.
    197             text.bind('<<close-window>>', self.close_event)
    198             # Some OS X systems have only one mouse button,
    199             # so use control-click for pulldown menus there.
    200             #  (Note, AquaTk defines <2> as the right button if
    201             #   present and the Tk Text widget already binds <2>.)
    202             text.bind("<Control-Button-1>",self.right_menu_event)
    203         else:
    204             # Elsewhere, use right-click for pulldown menus.
    205             text.bind("<3>",self.right_menu_event)
    206         text.bind("<<cut>>", self.cut)
    207         text.bind("<<copy>>", self.copy)
    208         text.bind("<<paste>>", self.paste)
    209         text.bind("<<center-insert>>", self.center_insert_event)
    210         text.bind("<<help>>", self.help_dialog)
    211         text.bind("<<python-docs>>", self.python_docs)
    212         text.bind("<<about-idle>>", self.about_dialog)
    213         text.bind("<<open-config-dialog>>", self.config_dialog)
    214         text.bind("<<open-module>>", self.open_module)
    215         text.bind("<<do-nothing>>", lambda event: "break")
    216         text.bind("<<select-all>>", self.select_all)
    217         text.bind("<<remove-selection>>", self.remove_selection)
    218         text.bind("<<find>>", self.find_event)
    219         text.bind("<<find-again>>", self.find_again_event)
    220         text.bind("<<find-in-files>>", self.find_in_files_event)
    221         text.bind("<<find-selection>>", self.find_selection_event)
    222         text.bind("<<replace>>", self.replace_event)
    223         text.bind("<<goto-line>>", self.goto_line_event)
    224         text.bind("<<smart-backspace>>",self.smart_backspace_event)
    225         text.bind("<<newline-and-indent>>",self.newline_and_indent_event)
    226         text.bind("<<smart-indent>>",self.smart_indent_event)
    227         text.bind("<<indent-region>>",self.indent_region_event)
    228         text.bind("<<dedent-region>>",self.dedent_region_event)
    229         text.bind("<<comment-region>>",self.comment_region_event)
    230         text.bind("<<uncomment-region>>",self.uncomment_region_event)
    231         text.bind("<<tabify-region>>",self.tabify_region_event)
    232         text.bind("<<untabify-region>>",self.untabify_region_event)
    233         text.bind("<<toggle-tabs>>",self.toggle_tabs_event)
    234         text.bind("<<change-indentwidth>>",self.change_indentwidth_event)
    235         text.bind("<Left>", self.move_at_edge_if_selection(0))
    236         text.bind("<Right>", self.move_at_edge_if_selection(1))
    237         text.bind("<<del-word-left>>", self.del_word_left)
    238         text.bind("<<del-word-right>>", self.del_word_right)
    239         text.bind("<<beginning-of-line>>", self.home_callback)
    240 
    241         if flist:
    242             flist.inversedict[self] = key
    243             if key:
    244                 flist.dict[key] = self
    245             text.bind("<<open-new-window>>", self.new_callback)
    246             text.bind("<<close-all-windows>>", self.flist.close_all_callback)
    247             text.bind("<<open-class-browser>>", self.open_class_browser)
    248             text.bind("<<open-path-browser>>", self.open_path_browser)
    249 
    250         self.set_status_bar()
    251         vbar['command'] = text.yview
    252         vbar.pack(side=RIGHT, fill=Y)
    253         text['yscrollcommand'] = vbar.set
    254         fontWeight = 'normal'
    255         if idleConf.GetOption('main', 'EditorWindow', 'font-bold', type='bool'):
    256             fontWeight='bold'
    257         text.config(font=(idleConf.GetOption('main', 'EditorWindow', 'font'),
    258                           idleConf.GetOption('main', 'EditorWindow',
    259                                              'font-size', type='int'),
    260                           fontWeight))
    261         text_frame.pack(side=LEFT, fill=BOTH, expand=1)
    262         text.pack(side=TOP, fill=BOTH, expand=1)
    263         text.focus_set()
    264 
    265         # usetabs true  -> literal tab characters are used by indent and
    266         #                  dedent cmds, possibly mixed with spaces if
    267         #                  indentwidth is not a multiple of tabwidth,
    268         #                  which will cause Tabnanny to nag!
    269         #         false -> tab characters are converted to spaces by indent
    270         #                  and dedent cmds, and ditto TAB keystrokes
    271         # Although use-spaces=0 can be configured manually in config-main.def,
    272         # configuration of tabs v. spaces is not supported in the configuration
    273         # dialog.  IDLE promotes the preferred Python indentation: use spaces!
    274         usespaces = idleConf.GetOption('main', 'Indent', 'use-spaces', type='bool')
    275         self.usetabs = not usespaces
    276 
    277         # tabwidth is the display width of a literal tab character.
    278         # CAUTION:  telling Tk to use anything other than its default
    279         # tab setting causes it to use an entirely different tabbing algorithm,
    280         # treating tab stops as fixed distances from the left margin.
    281         # Nobody expects this, so for now tabwidth should never be changed.
    282         self.tabwidth = 8    # must remain 8 until Tk is fixed.
    283 
    284         # indentwidth is the number of screen characters per indent level.
    285         # The recommended Python indentation is four spaces.
    286         self.indentwidth = self.tabwidth
    287         self.set_notabs_indentwidth()
    288 
    289         # If context_use_ps1 is true, parsing searches back for a ps1 line;
    290         # else searches for a popular (if, def, ...) Python stmt.
    291         self.context_use_ps1 = False
    292 
    293         # When searching backwards for a reliable place to begin parsing,
    294         # first start num_context_lines[0] lines back, then
    295         # num_context_lines[1] lines back if that didn't work, and so on.
    296         # The last value should be huge (larger than the # of lines in a
    297         # conceivable file).
    298         # Making the initial values larger slows things down more often.
    299         self.num_context_lines = 50, 500, 5000000
    300 
    301         self.per = per = self.Percolator(text)
    302 
    303         self.undo = undo = self.UndoDelegator()
    304         per.insertfilter(undo)
    305         text.undo_block_start = undo.undo_block_start
    306         text.undo_block_stop = undo.undo_block_stop
    307         undo.set_saved_change_hook(self.saved_change_hook)
    308 
    309         # IOBinding implements file I/O and printing functionality
    310         self.io = io = self.IOBinding(self)
    311         io.set_filename_change_hook(self.filename_change_hook)
    312 
    313         # Create the recent files submenu
    314         self.recent_files_menu = Menu(self.menubar)
    315         self.menudict['file'].insert_cascade(3, label='Recent Files',
    316                                              underline=0,
    317                                              menu=self.recent_files_menu)
    318         self.update_recent_files_list()
    319 
    320         self.color = None # initialized below in self.ResetColorizer
    321         if filename:
    322             if os.path.exists(filename) and not os.path.isdir(filename):
    323                 io.loadfile(filename)
    324             else:
    325                 io.set_filename(filename)
    326         self.ResetColorizer()
    327         self.saved_change_hook()
    328 
    329         self.set_indentation_params(self.ispythonsource(filename))
    330 
    331         self.load_extensions()
    332 
    333         menu = self.menudict.get('windows')
    334         if menu:
    335             end = menu.index("end")
    336             if end is None:
    337                 end = -1
    338             if end >= 0:
    339                 menu.add_separator()
    340                 end = end + 1
    341             self.wmenu_end = end
    342             WindowList.register_callback(self.postwindowsmenu)
    343 
    344         # Some abstractions so IDLE extensions are cross-IDE
    345         self.askyesno = tkMessageBox.askyesno
    346         self.askinteger = tkSimpleDialog.askinteger
    347         self.showerror = tkMessageBox.showerror
    348 
    349     def _filename_to_unicode(self, filename):
    350         """convert filename to unicode in order to display it in Tk"""
    351         if isinstance(filename, unicode) or not filename:
    352             return filename
    353         else:
    354             try:
    355                 return filename.decode(self.filesystemencoding)
    356             except UnicodeDecodeError:
    357                 # XXX
    358                 try:
    359                     return filename.decode(self.encoding)
    360                 except UnicodeDecodeError:
    361                     # byte-to-byte conversion
    362                     return filename.decode('iso8859-1')
    363 
    364     def new_callback(self, event):
    365         dirname, basename = self.io.defaultfilename()
    366         self.flist.new(dirname)
    367         return "break"
    368 
    369     def home_callback(self, event):
    370         if (event.state & 4) != 0 and event.keysym == "Home":
    371             # state&4==Control. If <Control-Home>, use the Tk binding.
    372             return
    373         if self.text.index("iomark") and \
    374            self.text.compare("iomark", "<=", "insert lineend") and \
    375            self.text.compare("insert linestart", "<=", "iomark"):
    376             # In Shell on input line, go to just after prompt
    377             insertpt = int(self.text.index("iomark").split(".")[1])
    378         else:
    379             line = self.text.get("insert linestart", "insert lineend")
    380             for insertpt in xrange(len(line)):
    381                 if line[insertpt] not in (' ','\t'):
    382                     break
    383             else:
    384                 insertpt=len(line)
    385         lineat = int(self.text.index("insert").split('.')[1])
    386         if insertpt == lineat:
    387             insertpt = 0
    388         dest = "insert linestart+"+str(insertpt)+"c"
    389         if (event.state&1) == 0:
    390             # shift was not pressed
    391             self.text.tag_remove("sel", "1.0", "end")
    392         else:
    393             if not self.text.index("sel.first"):
    394                 self.text.mark_set("my_anchor", "insert")  # there was no previous selection
    395             else:
    396                 if self.text.compare(self.text.index("sel.first"), "<", self.text.index("insert")):
    397                     self.text.mark_set("my_anchor", "sel.first") # extend back
    398                 else:
    399                     self.text.mark_set("my_anchor", "sel.last") # extend forward
    400             first = self.text.index(dest)
    401             last = self.text.index("my_anchor")
    402             if self.text.compare(first,">",last):
    403                 first,last = last,first
    404             self.text.tag_remove("sel", "1.0", "end")
    405             self.text.tag_add("sel", first, last)
    406         self.text.mark_set("insert", dest)
    407         self.text.see("insert")
    408         return "break"
    409 
    410     def set_status_bar(self):
    411         self.status_bar = self.MultiStatusBar(self.top)
    412         if macosxSupport.runningAsOSXApp():
    413             # Insert some padding to avoid obscuring some of the statusbar
    414             # by the resize widget.
    415             self.status_bar.set_label('_padding1', '    ', side=RIGHT)
    416         self.status_bar.set_label('column', 'Col: ?', side=RIGHT)
    417         self.status_bar.set_label('line', 'Ln: ?', side=RIGHT)
    418         self.status_bar.pack(side=BOTTOM, fill=X)
    419         self.text.bind("<<set-line-and-column>>", self.set_line_and_column)
    420         self.text.event_add("<<set-line-and-column>>",
    421                             "<KeyRelease>", "<ButtonRelease>")
    422         self.text.after_idle(self.set_line_and_column)
    423 
    424     def set_line_and_column(self, event=None):
    425         line, column = self.text.index(INSERT).split('.')
    426         self.status_bar.set_label('column', 'Col: %s' % column)
    427         self.status_bar.set_label('line', 'Ln: %s' % line)
    428 
    429     menu_specs = [
    430         ("file", "_File"),
    431         ("edit", "_Edit"),
    432         ("format", "F_ormat"),
    433         ("run", "_Run"),
    434         ("options", "_Options"),
    435         ("windows", "_Windows"),
    436         ("help", "_Help"),
    437     ]
    438 
    439     if macosxSupport.runningAsOSXApp():
    440         del menu_specs[-3]
    441         menu_specs[-2] = ("windows", "_Window")
    442 
    443 
    444     def createmenubar(self):
    445         mbar = self.menubar
    446         self.menudict = menudict = {}
    447         for name, label in self.menu_specs:
    448             underline, label = prepstr(label)
    449             menudict[name] = menu = Menu(mbar, name=name)
    450             mbar.add_cascade(label=label, menu=menu, underline=underline)
    451 
    452         if macosxSupport.isCarbonAquaTk(self.root):
    453             # Insert the application menu
    454             menudict['application'] = menu = Menu(mbar, name='apple')
    455             mbar.add_cascade(label='IDLE', menu=menu)
    456 
    457         self.fill_menus()
    458         self.base_helpmenu_length = self.menudict['help'].index(END)
    459         self.reset_help_menu_entries()
    460 
    461     def postwindowsmenu(self):
    462         # Only called when Windows menu exists
    463         menu = self.menudict['windows']
    464         end = menu.index("end")
    465         if end is None:
    466             end = -1
    467         if end > self.wmenu_end:
    468             menu.delete(self.wmenu_end+1, end)
    469         WindowList.add_windows_to_menu(menu)
    470 
    471     rmenu = None
    472 
    473     def right_menu_event(self, event):
    474         self.text.mark_set("insert", "@%d,%d" % (event.x, event.y))
    475         if not self.rmenu:
    476             self.make_rmenu()
    477         rmenu = self.rmenu
    478         self.event = event
    479         iswin = sys.platform[:3] == 'win'
    480         if iswin:
    481             self.text.config(cursor="arrow")
    482 
    483         for item in self.rmenu_specs:
    484             try:
    485                 label, eventname, verify_state = item
    486             except ValueError: # see issue1207589
    487                 continue
    488 
    489             if verify_state is None:
    490                 continue
    491             state = getattr(self, verify_state)()
    492             rmenu.entryconfigure(label, state=state)
    493 
    494         rmenu.tk_popup(event.x_root, event.y_root)
    495         if iswin:
    496             self.text.config(cursor="ibeam")
    497 
    498     rmenu_specs = [
    499         # ("Label", "<<virtual-event>>", "statefuncname"), ...
    500         ("Close", "<<close-window>>", None), # Example
    501     ]
    502 
    503     def make_rmenu(self):
    504         rmenu = Menu(self.text, tearoff=0)
    505         for item in self.rmenu_specs:
    506             label, eventname = item[0], item[1]
    507             if label is not None:
    508                 def command(text=self.text, eventname=eventname):
    509                     text.event_generate(eventname)
    510                 rmenu.add_command(label=label, command=command)
    511             else:
    512                 rmenu.add_separator()
    513         self.rmenu = rmenu
    514 
    515     def rmenu_check_cut(self):
    516         return self.rmenu_check_copy()
    517 
    518     def rmenu_check_copy(self):
    519         try:
    520             indx = self.text.index('sel.first')
    521         except TclError:
    522             return 'disabled'
    523         else:
    524             return 'normal' if indx else 'disabled'
    525 
    526     def rmenu_check_paste(self):
    527         try:
    528             self.text.tk.call('tk::GetSelection', self.text, 'CLIPBOARD')
    529         except TclError:
    530             return 'disabled'
    531         else:
    532             return 'normal'
    533 
    534     def about_dialog(self, event=None):
    535         aboutDialog.AboutDialog(self.top,'About IDLE')
    536 
    537     def config_dialog(self, event=None):
    538         configDialog.ConfigDialog(self.top,'Settings')
    539 
    540     def help_dialog(self, event=None):
    541         if self.root:
    542             parent = self.root
    543         else:
    544             parent = self.top
    545         helpDialog.display(parent, near=self.top)
    546 
    547     def python_docs(self, event=None):
    548         if sys.platform[:3] == 'win':
    549             try:
    550                 os.startfile(self.help_url)
    551             except WindowsError as why:
    552                 tkMessageBox.showerror(title='Document Start Failure',
    553                     message=str(why), parent=self.text)
    554         else:
    555             webbrowser.open(self.help_url)
    556         return "break"
    557 
    558     def cut(self,event):
    559         self.text.event_generate("<<Cut>>")
    560         return "break"
    561 
    562     def copy(self,event):
    563         if not self.text.tag_ranges("sel"):
    564             # There is no selection, so do nothing and maybe interrupt.
    565             return
    566         self.text.event_generate("<<Copy>>")
    567         return "break"
    568 
    569     def paste(self,event):
    570         self.text.event_generate("<<Paste>>")
    571         self.text.see("insert")
    572         return "break"
    573 
    574     def select_all(self, event=None):
    575         self.text.tag_add("sel", "1.0", "end-1c")
    576         self.text.mark_set("insert", "1.0")
    577         self.text.see("insert")
    578         return "break"
    579 
    580     def remove_selection(self, event=None):
    581         self.text.tag_remove("sel", "1.0", "end")
    582         self.text.see("insert")
    583 
    584     def move_at_edge_if_selection(self, edge_index):
    585         """Cursor move begins at start or end of selection
    586 
    587         When a left/right cursor key is pressed create and return to Tkinter a
    588         function which causes a cursor move from the associated edge of the
    589         selection.
    590 
    591         """
    592         self_text_index = self.text.index
    593         self_text_mark_set = self.text.mark_set
    594         edges_table = ("sel.first+1c", "sel.last-1c")
    595         def move_at_edge(event):
    596             if (event.state & 5) == 0: # no shift(==1) or control(==4) pressed
    597                 try:
    598                     self_text_index("sel.first")
    599                     self_text_mark_set("insert", edges_table[edge_index])
    600                 except TclError:
    601                     pass
    602         return move_at_edge
    603 
    604     def del_word_left(self, event):
    605         self.text.event_generate('<Meta-Delete>')
    606         return "break"
    607 
    608     def del_word_right(self, event):
    609         self.text.event_generate('<Meta-d>')
    610         return "break"
    611 
    612     def find_event(self, event):
    613         SearchDialog.find(self.text)
    614         return "break"
    615 
    616     def find_again_event(self, event):
    617         SearchDialog.find_again(self.text)
    618         return "break"
    619 
    620     def find_selection_event(self, event):
    621         SearchDialog.find_selection(self.text)
    622         return "break"
    623 
    624     def find_in_files_event(self, event):
    625         GrepDialog.grep(self.text, self.io, self.flist)
    626         return "break"
    627 
    628     def replace_event(self, event):
    629         ReplaceDialog.replace(self.text)
    630         return "break"
    631 
    632     def goto_line_event(self, event):
    633         text = self.text
    634         lineno = tkSimpleDialog.askinteger("Goto",
    635                 "Go to line number:",parent=text)
    636         if lineno is None:
    637             return "break"
    638         if lineno <= 0:
    639             text.bell()
    640             return "break"
    641         text.mark_set("insert", "%d.0" % lineno)
    642         text.see("insert")
    643 
    644     def open_module(self, event=None):
    645         # XXX Shouldn't this be in IOBinding or in FileList?
    646         try:
    647             name = self.text.get("sel.first", "sel.last")
    648         except TclError:
    649             name = ""
    650         else:
    651             name = name.strip()
    652         name = tkSimpleDialog.askstring("Module",
    653                  "Enter the name of a Python module\n"
    654                  "to search on sys.path and open:",
    655                  parent=self.text, initialvalue=name)
    656         if name:
    657             name = name.strip()
    658         if not name:
    659             return
    660         # XXX Ought to insert current file's directory in front of path
    661         try:
    662             (f, file, (suffix, mode, type)) = _find_module(name)
    663         except (NameError, ImportError), msg:
    664             tkMessageBox.showerror("Import error", str(msg), parent=self.text)
    665             return
    666         if type != imp.PY_SOURCE:
    667             tkMessageBox.showerror("Unsupported type",
    668                 "%s is not a source module" % name, parent=self.text)
    669             return
    670         if f:
    671             f.close()
    672         if self.flist:
    673             self.flist.open(file)
    674         else:
    675             self.io.loadfile(file)
    676 
    677     def open_class_browser(self, event=None):
    678         filename = self.io.filename
    679         if not filename:
    680             tkMessageBox.showerror(
    681                 "No filename",
    682                 "This buffer has no associated filename",
    683                 master=self.text)
    684             self.text.focus_set()
    685             return None
    686         head, tail = os.path.split(filename)
    687         base, ext = os.path.splitext(tail)
    688         from idlelib import ClassBrowser
    689         ClassBrowser.ClassBrowser(self.flist, base, [head])
    690 
    691     def open_path_browser(self, event=None):
    692         from idlelib import PathBrowser
    693         PathBrowser.PathBrowser(self.flist)
    694 
    695     def gotoline(self, lineno):
    696         if lineno is not None and lineno > 0:
    697             self.text.mark_set("insert", "%d.0" % lineno)
    698             self.text.tag_remove("sel", "1.0", "end")
    699             self.text.tag_add("sel", "insert", "insert +1l")
    700             self.center()
    701 
    702     def ispythonsource(self, filename):
    703         if not filename or os.path.isdir(filename):
    704             return True
    705         base, ext = os.path.splitext(os.path.basename(filename))
    706         if os.path.normcase(ext) in (".py", ".pyw"):
    707             return True
    708         try:
    709             f = open(filename)
    710             line = f.readline()
    711             f.close()
    712         except IOError:
    713             return False
    714         return line.startswith('#!') and line.find('python') >= 0
    715 
    716     def close_hook(self):
    717         if self.flist:
    718             self.flist.unregister_maybe_terminate(self)
    719             self.flist = None
    720 
    721     def set_close_hook(self, close_hook):
    722         self.close_hook = close_hook
    723 
    724     def filename_change_hook(self):
    725         if self.flist:
    726             self.flist.filename_changed_edit(self)
    727         self.saved_change_hook()
    728         self.top.update_windowlist_registry(self)
    729         self.ResetColorizer()
    730 
    731     def _addcolorizer(self):
    732         if self.color:
    733             return
    734         if self.ispythonsource(self.io.filename):
    735             self.color = self.ColorDelegator()
    736         # can add more colorizers here...
    737         if self.color:
    738             self.per.removefilter(self.undo)
    739             self.per.insertfilter(self.color)
    740             self.per.insertfilter(self.undo)
    741 
    742     def _rmcolorizer(self):
    743         if not self.color:
    744             return
    745         self.color.removecolors()
    746         self.per.removefilter(self.color)
    747         self.color = None
    748 
    749     def ResetColorizer(self):
    750         "Update the colour theme"
    751         # Called from self.filename_change_hook and from configDialog.py
    752         self._rmcolorizer()
    753         self._addcolorizer()
    754         theme = idleConf.GetOption('main','Theme','name')
    755         normal_colors = idleConf.GetHighlight(theme, 'normal')
    756         cursor_color = idleConf.GetHighlight(theme, 'cursor', fgBg='fg')
    757         select_colors = idleConf.GetHighlight(theme, 'hilite')
    758         self.text.config(
    759             foreground=normal_colors['foreground'],
    760             background=normal_colors['background'],
    761             insertbackground=cursor_color,
    762             selectforeground=select_colors['foreground'],
    763             selectbackground=select_colors['background'],
    764             )
    765 
    766     def ResetFont(self):
    767         "Update the text widgets' font if it is changed"
    768         # Called from configDialog.py
    769         fontWeight='normal'
    770         if idleConf.GetOption('main','EditorWindow','font-bold',type='bool'):
    771             fontWeight='bold'
    772         self.text.config(font=(idleConf.GetOption('main','EditorWindow','font'),
    773                 idleConf.GetOption('main','EditorWindow','font-size',
    774                                    type='int'),
    775                 fontWeight))
    776 
    777     def RemoveKeybindings(self):
    778         "Remove the keybindings before they are changed."
    779         # Called from configDialog.py
    780         self.Bindings.default_keydefs = keydefs = idleConf.GetCurrentKeySet()
    781         for event, keylist in keydefs.items():
    782             self.text.event_delete(event, *keylist)
    783         for extensionName in self.get_standard_extension_names():
    784             xkeydefs = idleConf.GetExtensionBindings(extensionName)
    785             if xkeydefs:
    786                 for event, keylist in xkeydefs.items():
    787                     self.text.event_delete(event, *keylist)
    788 
    789     def ApplyKeybindings(self):
    790         "Update the keybindings after they are changed"
    791         # Called from configDialog.py
    792         self.Bindings.default_keydefs = keydefs = idleConf.GetCurrentKeySet()
    793         self.apply_bindings()
    794         for extensionName in self.get_standard_extension_names():
    795             xkeydefs = idleConf.GetExtensionBindings(extensionName)
    796             if xkeydefs:
    797                 self.apply_bindings(xkeydefs)
    798         #update menu accelerators
    799         menuEventDict = {}
    800         for menu in self.Bindings.menudefs:
    801             menuEventDict[menu[0]] = {}
    802             for item in menu[1]:
    803                 if item:
    804                     menuEventDict[menu[0]][prepstr(item[0])[1]] = item[1]
    805         for menubarItem in self.menudict.keys():
    806             menu = self.menudict[menubarItem]
    807             end = menu.index(END) + 1
    808             for index in range(0, end):
    809                 if menu.type(index) == 'command':
    810                     accel = menu.entrycget(index, 'accelerator')
    811                     if accel:
    812                         itemName = menu.entrycget(index, 'label')
    813                         event = ''
    814                         if menubarItem in menuEventDict:
    815                             if itemName in menuEventDict[menubarItem]:
    816                                 event = menuEventDict[menubarItem][itemName]
    817                         if event:
    818                             accel = get_accelerator(keydefs, event)
    819                             menu.entryconfig(index, accelerator=accel)
    820 
    821     def set_notabs_indentwidth(self):
    822         "Update the indentwidth if changed and not using tabs in this window"
    823         # Called from configDialog.py
    824         if not self.usetabs:
    825             self.indentwidth = idleConf.GetOption('main', 'Indent','num-spaces',
    826                                                   type='int')
    827 
    828     def reset_help_menu_entries(self):
    829         "Update the additional help entries on the Help menu"
    830         help_list = idleConf.GetAllExtraHelpSourcesList()
    831         helpmenu = self.menudict['help']
    832         # first delete the extra help entries, if any
    833         helpmenu_length = helpmenu.index(END)
    834         if helpmenu_length > self.base_helpmenu_length:
    835             helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length)
    836         # then rebuild them
    837         if help_list:
    838             helpmenu.add_separator()
    839             for entry in help_list:
    840                 cmd = self.__extra_help_callback(entry[1])
    841                 helpmenu.add_command(label=entry[0], command=cmd)
    842         # and update the menu dictionary
    843         self.menudict['help'] = helpmenu
    844 
    845     def __extra_help_callback(self, helpfile):
    846         "Create a callback with the helpfile value frozen at definition time"
    847         def display_extra_help(helpfile=helpfile):
    848             if not helpfile.startswith(('www', 'http')):
    849                 helpfile = os.path.normpath(helpfile)
    850             if sys.platform[:3] == 'win':
    851                 try:
    852                     os.startfile(helpfile)
    853                 except WindowsError as why:
    854                     tkMessageBox.showerror(title='Document Start Failure',
    855                         message=str(why), parent=self.text)
    856             else:
    857                 webbrowser.open(helpfile)
    858         return display_extra_help
    859 
    860     def update_recent_files_list(self, new_file=None):
    861         "Load and update the recent files list and menus"
    862         rf_list = []
    863         if os.path.exists(self.recent_files_path):
    864             rf_list_file = open(self.recent_files_path,'r')
    865             try:
    866                 rf_list = rf_list_file.readlines()
    867             finally:
    868                 rf_list_file.close()
    869         if new_file:
    870             new_file = os.path.abspath(new_file) + '\n'
    871             if new_file in rf_list:
    872                 rf_list.remove(new_file)  # move to top
    873             rf_list.insert(0, new_file)
    874         # clean and save the recent files list
    875         bad_paths = []
    876         for path in rf_list:
    877             if '\0' in path or not os.path.exists(path[0:-1]):
    878                 bad_paths.append(path)
    879         rf_list = [path for path in rf_list if path not in bad_paths]
    880         ulchars = "1234567890ABCDEFGHIJK"
    881         rf_list = rf_list[0:len(ulchars)]
    882         try:
    883             with open(self.recent_files_path, 'w') as rf_file:
    884                 rf_file.writelines(rf_list)
    885         except IOError as err:
    886             if not getattr(self.root, "recentfilelist_error_displayed", False):
    887                 self.root.recentfilelist_error_displayed = True
    888                 tkMessageBox.showerror(title='IDLE Error',
    889                     message='Unable to update Recent Files list:\n%s'
    890                         % str(err),
    891                     parent=self.text)
    892         # for each edit window instance, construct the recent files menu
    893         for instance in self.top.instance_dict.keys():
    894             menu = instance.recent_files_menu
    895             menu.delete(0, END)  # clear, and rebuild:
    896             for i, file_name in enumerate(rf_list):
    897                 file_name = file_name.rstrip()  # zap \n
    898                 # make unicode string to display non-ASCII chars correctly
    899                 ufile_name = self._filename_to_unicode(file_name)
    900                 callback = instance.__recent_file_callback(file_name)
    901                 menu.add_command(label=ulchars[i] + " " + ufile_name,
    902                                  command=callback,
    903                                  underline=0)
    904 
    905     def __recent_file_callback(self, file_name):
    906         def open_recent_file(fn_closure=file_name):
    907             self.io.open(editFile=fn_closure)
    908         return open_recent_file
    909 
    910     def saved_change_hook(self):
    911         short = self.short_title()
    912         long = self.long_title()
    913         if short and long:
    914             title = short + " - " + long
    915         elif short:
    916             title = short
    917         elif long:
    918             title = long
    919         else:
    920             title = "Untitled"
    921         icon = short or long or title
    922         if not self.get_saved():
    923             title = "*%s*" % title
    924             icon = "*%s" % icon
    925         self.top.wm_title(title)
    926         self.top.wm_iconname(icon)
    927 
    928     def get_saved(self):
    929         return self.undo.get_saved()
    930 
    931     def set_saved(self, flag):
    932         self.undo.set_saved(flag)
    933 
    934     def reset_undo(self):
    935         self.undo.reset_undo()
    936 
    937     def short_title(self):
    938         filename = self.io.filename
    939         if filename:
    940             filename = os.path.basename(filename)
    941         # return unicode string to display non-ASCII chars correctly
    942         return self._filename_to_unicode(filename)
    943 
    944     def long_title(self):
    945         # return unicode string to display non-ASCII chars correctly
    946         return self._filename_to_unicode(self.io.filename or "")
    947 
    948     def center_insert_event(self, event):
    949         self.center()
    950 
    951     def center(self, mark="insert"):
    952         text = self.text
    953         top, bot = self.getwindowlines()
    954         lineno = self.getlineno(mark)
    955         height = bot - top
    956         newtop = max(1, lineno - height//2)
    957         text.yview(float(newtop))
    958 
    959     def getwindowlines(self):
    960         text = self.text
    961         top = self.getlineno("@0,0")
    962         bot = self.getlineno("@0,65535")
    963         if top == bot and text.winfo_height() == 1:
    964             # Geometry manager hasn't run yet
    965             height = int(text['height'])
    966             bot = top + height - 1
    967         return top, bot
    968 
    969     def getlineno(self, mark="insert"):
    970         text = self.text
    971         return int(float(text.index(mark)))
    972 
    973     def get_geometry(self):
    974         "Return (width, height, x, y)"
    975         geom = self.top.wm_geometry()
    976         m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom)
    977         tuple = (map(int, m.groups()))
    978         return tuple
    979 
    980     def close_event(self, event):
    981         self.close()
    982 
    983     def maybesave(self):
    984         if self.io:
    985             if not self.get_saved():
    986                 if self.top.state()!='normal':
    987                     self.top.deiconify()
    988                 self.top.lower()
    989                 self.top.lift()
    990             return self.io.maybesave()
    991 
    992     def close(self):
    993         reply = self.maybesave()
    994         if str(reply) != "cancel":
    995             self._close()
    996         return reply
    997 
    998     def _close(self):
    999         if self.io.filename:
   1000             self.update_recent_files_list(new_file=self.io.filename)
   1001         WindowList.unregister_callback(self.postwindowsmenu)
   1002         self.unload_extensions()
   1003         self.io.close()
   1004         self.io = None
   1005         self.undo = None
   1006         if self.color:
   1007             self.color.close(False)
   1008             self.color = None
   1009         self.text = None
   1010         self.tkinter_vars = None
   1011         self.per.close()
   1012         self.per = None
   1013         self.top.destroy()
   1014         if self.close_hook:
   1015             # unless override: unregister from flist, terminate if last window
   1016             self.close_hook()
   1017 
   1018     def load_extensions(self):
   1019         self.extensions = {}
   1020         self.load_standard_extensions()
   1021 
   1022     def unload_extensions(self):
   1023         for ins in self.extensions.values():
   1024             if hasattr(ins, "close"):
   1025                 ins.close()
   1026         self.extensions = {}
   1027 
   1028     def load_standard_extensions(self):
   1029         for name in self.get_standard_extension_names():
   1030             try:
   1031                 self.load_extension(name)
   1032             except:
   1033                 print "Failed to load extension", repr(name)
   1034                 import traceback
   1035                 traceback.print_exc()
   1036 
   1037     def get_standard_extension_names(self):
   1038         return idleConf.GetExtensions(editor_only=True)
   1039 
   1040     def load_extension(self, name):
   1041         try:
   1042             mod = __import__(name, globals(), locals(), [])
   1043         except ImportError:
   1044             print "\nFailed to import extension: ", name
   1045             return
   1046         cls = getattr(mod, name)
   1047         keydefs = idleConf.GetExtensionBindings(name)
   1048         if hasattr(cls, "menudefs"):
   1049             self.fill_menus(cls.menudefs, keydefs)
   1050         ins = cls(self)
   1051         self.extensions[name] = ins
   1052         if keydefs:
   1053             self.apply_bindings(keydefs)
   1054             for vevent in keydefs.keys():
   1055                 methodname = vevent.replace("-", "_")
   1056                 while methodname[:1] == '<':
   1057                     methodname = methodname[1:]
   1058                 while methodname[-1:] == '>':
   1059                     methodname = methodname[:-1]
   1060                 methodname = methodname + "_event"
   1061                 if hasattr(ins, methodname):
   1062                     self.text.bind(vevent, getattr(ins, methodname))
   1063 
   1064     def apply_bindings(self, keydefs=None):
   1065         if keydefs is None:
   1066             keydefs = self.Bindings.default_keydefs
   1067         text = self.text
   1068         text.keydefs = keydefs
   1069         for event, keylist in keydefs.items():
   1070             if keylist:
   1071                 text.event_add(event, *keylist)
   1072 
   1073     def fill_menus(self, menudefs=None, keydefs=None):
   1074         """Add appropriate entries to the menus and submenus
   1075 
   1076         Menus that are absent or None in self.menudict are ignored.
   1077         """
   1078         if menudefs is None:
   1079             menudefs = self.Bindings.menudefs
   1080         if keydefs is None:
   1081             keydefs = self.Bindings.default_keydefs
   1082         menudict = self.menudict
   1083         text = self.text
   1084         for mname, entrylist in menudefs:
   1085             menu = menudict.get(mname)
   1086             if not menu:
   1087                 continue
   1088             for entry in entrylist:
   1089                 if not entry:
   1090                     menu.add_separator()
   1091                 else:
   1092                     label, eventname = entry
   1093                     checkbutton = (label[:1] == '!')
   1094                     if checkbutton:
   1095                         label = label[1:]
   1096                     underline, label = prepstr(label)
   1097                     accelerator = get_accelerator(keydefs, eventname)
   1098                     def command(text=text, eventname=eventname):
   1099                         text.event_generate(eventname)
   1100                     if checkbutton:
   1101                         var = self.get_var_obj(eventname, BooleanVar)
   1102                         menu.add_checkbutton(label=label, underline=underline,
   1103                             command=command, accelerator=accelerator,
   1104                             variable=var)
   1105                     else:
   1106                         menu.add_command(label=label, underline=underline,
   1107                                          command=command,
   1108                                          accelerator=accelerator)
   1109 
   1110     def getvar(self, name):
   1111         var = self.get_var_obj(name)
   1112         if var:
   1113             value = var.get()
   1114             return value
   1115         else:
   1116             raise NameError, name
   1117 
   1118     def setvar(self, name, value, vartype=None):
   1119         var = self.get_var_obj(name, vartype)
   1120         if var:
   1121             var.set(value)
   1122         else:
   1123             raise NameError, name
   1124 
   1125     def get_var_obj(self, name, vartype=None):
   1126         var = self.tkinter_vars.get(name)
   1127         if not var and vartype:
   1128             # create a Tkinter variable object with self.text as master:
   1129             self.tkinter_vars[name] = var = vartype(self.text)
   1130         return var
   1131 
   1132     # Tk implementations of "virtual text methods" -- each platform
   1133     # reusing IDLE's support code needs to define these for its GUI's
   1134     # flavor of widget.
   1135 
   1136     # Is character at text_index in a Python string?  Return 0 for
   1137     # "guaranteed no", true for anything else.  This info is expensive
   1138     # to compute ab initio, but is probably already known by the
   1139     # platform's colorizer.
   1140 
   1141     def is_char_in_string(self, text_index):
   1142         if self.color:
   1143             # Return true iff colorizer hasn't (re)gotten this far
   1144             # yet, or the character is tagged as being in a string
   1145             return self.text.tag_prevrange("TODO", text_index) or \
   1146                    "STRING" in self.text.tag_names(text_index)
   1147         else:
   1148             # The colorizer is missing: assume the worst
   1149             return 1
   1150 
   1151     # If a selection is defined in the text widget, return (start,
   1152     # end) as Tkinter text indices, otherwise return (None, None)
   1153     def get_selection_indices(self):
   1154         try:
   1155             first = self.text.index("sel.first")
   1156             last = self.text.index("sel.last")
   1157             return first, last
   1158         except TclError:
   1159             return None, None
   1160 
   1161     # Return the text widget's current view of what a tab stop means
   1162     # (equivalent width in spaces).
   1163 
   1164     def get_tabwidth(self):
   1165         current = self.text['tabs'] or TK_TABWIDTH_DEFAULT
   1166         return int(current)
   1167 
   1168     # Set the text widget's current view of what a tab stop means.
   1169 
   1170     def set_tabwidth(self, newtabwidth):
   1171         text = self.text
   1172         if self.get_tabwidth() != newtabwidth:
   1173             pixels = text.tk.call("font", "measure", text["font"],
   1174                                   "-displayof", text.master,
   1175                                   "n" * newtabwidth)
   1176             text.configure(tabs=pixels)
   1177 
   1178     # If ispythonsource and guess are true, guess a good value for
   1179     # indentwidth based on file content (if possible), and if
   1180     # indentwidth != tabwidth set usetabs false.
   1181     # In any case, adjust the Text widget's view of what a tab
   1182     # character means.
   1183 
   1184     def set_indentation_params(self, ispythonsource, guess=True):
   1185         if guess and ispythonsource:
   1186             i = self.guess_indent()
   1187             if 2 <= i <= 8:
   1188                 self.indentwidth = i
   1189             if self.indentwidth != self.tabwidth:
   1190                 self.usetabs = False
   1191         self.set_tabwidth(self.tabwidth)
   1192 
   1193     def smart_backspace_event(self, event):
   1194         text = self.text
   1195         first, last = self.get_selection_indices()
   1196         if first and last:
   1197             text.delete(first, last)
   1198             text.mark_set("insert", first)
   1199             return "break"
   1200         # Delete whitespace left, until hitting a real char or closest
   1201         # preceding virtual tab stop.
   1202         chars = text.get("insert linestart", "insert")
   1203         if chars == '':
   1204             if text.compare("insert", ">", "1.0"):
   1205                 # easy: delete preceding newline
   1206                 text.delete("insert-1c")
   1207             else:
   1208                 text.bell()     # at start of buffer
   1209             return "break"
   1210         if  chars[-1] not in " \t":
   1211             # easy: delete preceding real char
   1212             text.delete("insert-1c")
   1213             return "break"
   1214         # Ick.  It may require *inserting* spaces if we back up over a
   1215         # tab character!  This is written to be clear, not fast.
   1216         tabwidth = self.tabwidth
   1217         have = len(chars.expandtabs(tabwidth))
   1218         assert have > 0
   1219         want = ((have - 1) // self.indentwidth) * self.indentwidth
   1220         # Debug prompt is multilined....
   1221         if self.context_use_ps1:
   1222             last_line_of_prompt = sys.ps1.split('\n')[-1]
   1223         else:
   1224             last_line_of_prompt = ''
   1225         ncharsdeleted = 0
   1226         while 1:
   1227             if chars == last_line_of_prompt:
   1228                 break
   1229             chars = chars[:-1]
   1230             ncharsdeleted = ncharsdeleted + 1
   1231             have = len(chars.expandtabs(tabwidth))
   1232             if have <= want or chars[-1] not in " \t":
   1233                 break
   1234         text.undo_block_start()
   1235         text.delete("insert-%dc" % ncharsdeleted, "insert")
   1236         if have < want:
   1237             text.insert("insert", ' ' * (want - have))
   1238         text.undo_block_stop()
   1239         return "break"
   1240 
   1241     def smart_indent_event(self, event):
   1242         # if intraline selection:
   1243         #     delete it
   1244         # elif multiline selection:
   1245         #     do indent-region
   1246         # else:
   1247         #     indent one level
   1248         text = self.text
   1249         first, last = self.get_selection_indices()
   1250         text.undo_block_start()
   1251         try:
   1252             if first and last:
   1253                 if index2line(first) != index2line(last):
   1254                     return self.indent_region_event(event)
   1255                 text.delete(first, last)
   1256                 text.mark_set("insert", first)
   1257             prefix = text.get("insert linestart", "insert")
   1258             raw, effective = classifyws(prefix, self.tabwidth)
   1259             if raw == len(prefix):
   1260                 # only whitespace to the left
   1261                 self.reindent_to(effective + self.indentwidth)
   1262             else:
   1263                 # tab to the next 'stop' within or to right of line's text:
   1264                 if self.usetabs:
   1265                     pad = '\t'
   1266                 else:
   1267                     effective = len(prefix.expandtabs(self.tabwidth))
   1268                     n = self.indentwidth
   1269                     pad = ' ' * (n - effective % n)
   1270                 text.insert("insert", pad)
   1271             text.see("insert")
   1272             return "break"
   1273         finally:
   1274             text.undo_block_stop()
   1275 
   1276     def newline_and_indent_event(self, event):
   1277         text = self.text
   1278         first, last = self.get_selection_indices()
   1279         text.undo_block_start()
   1280         try:
   1281             if first and last:
   1282                 text.delete(first, last)
   1283                 text.mark_set("insert", first)
   1284             line = text.get("insert linestart", "insert")
   1285             i, n = 0, len(line)
   1286             while i < n and line[i] in " \t":
   1287                 i = i+1
   1288             if i == n:
   1289                 # the cursor is in or at leading indentation in a continuation
   1290                 # line; just inject an empty line at the start
   1291                 text.insert("insert linestart", '\n')
   1292                 return "break"
   1293             indent = line[:i]
   1294             # strip whitespace before insert point unless it's in the prompt
   1295             i = 0
   1296             last_line_of_prompt = sys.ps1.split('\n')[-1]
   1297             while line and line[-1] in " \t" and line != last_line_of_prompt:
   1298                 line = line[:-1]
   1299                 i = i+1
   1300             if i:
   1301                 text.delete("insert - %d chars" % i, "insert")
   1302             # strip whitespace after insert point
   1303             while text.get("insert") in " \t":
   1304                 text.delete("insert")
   1305             # start new line
   1306             text.insert("insert", '\n')
   1307 
   1308             # adjust indentation for continuations and block
   1309             # open/close first need to find the last stmt
   1310             lno = index2line(text.index('insert'))
   1311             y = PyParse.Parser(self.indentwidth, self.tabwidth)
   1312             if not self.context_use_ps1:
   1313                 for context in self.num_context_lines:
   1314                     startat = max(lno - context, 1)
   1315                     startatindex = repr(startat) + ".0"
   1316                     rawtext = text.get(startatindex, "insert")
   1317                     y.set_str(rawtext)
   1318                     bod = y.find_good_parse_start(
   1319                               self.context_use_ps1,
   1320                               self._build_char_in_string_func(startatindex))
   1321                     if bod is not None or startat == 1:
   1322                         break
   1323                 y.set_lo(bod or 0)
   1324             else:
   1325                 r = text.tag_prevrange("console", "insert")
   1326                 if r:
   1327                     startatindex = r[1]
   1328                 else:
   1329                     startatindex = "1.0"
   1330                 rawtext = text.get(startatindex, "insert")
   1331                 y.set_str(rawtext)
   1332                 y.set_lo(0)
   1333 
   1334             c = y.get_continuation_type()
   1335             if c != PyParse.C_NONE:
   1336                 # The current stmt hasn't ended yet.
   1337                 if c == PyParse.C_STRING_FIRST_LINE:
   1338                     # after the first line of a string; do not indent at all
   1339                     pass
   1340                 elif c == PyParse.C_STRING_NEXT_LINES:
   1341                     # inside a string which started before this line;
   1342                     # just mimic the current indent
   1343                     text.insert("insert", indent)
   1344                 elif c == PyParse.C_BRACKET:
   1345                     # line up with the first (if any) element of the
   1346                     # last open bracket structure; else indent one
   1347                     # level beyond the indent of the line with the
   1348                     # last open bracket
   1349                     self.reindent_to(y.compute_bracket_indent())
   1350                 elif c == PyParse.C_BACKSLASH:
   1351                     # if more than one line in this stmt already, just
   1352                     # mimic the current indent; else if initial line
   1353                     # has a start on an assignment stmt, indent to
   1354                     # beyond leftmost =; else to beyond first chunk of
   1355                     # non-whitespace on initial line
   1356                     if y.get_num_lines_in_stmt() > 1:
   1357                         text.insert("insert", indent)
   1358                     else:
   1359                         self.reindent_to(y.compute_backslash_indent())
   1360                 else:
   1361                     assert 0, "bogus continuation type %r" % (c,)
   1362                 return "break"
   1363 
   1364             # This line starts a brand new stmt; indent relative to
   1365             # indentation of initial line of closest preceding
   1366             # interesting stmt.
   1367             indent = y.get_base_indent_string()
   1368             text.insert("insert", indent)
   1369             if y.is_block_opener():
   1370                 self.smart_indent_event(event)
   1371             elif indent and y.is_block_closer():
   1372                 self.smart_backspace_event(event)
   1373             return "break"
   1374         finally:
   1375             text.see("insert")
   1376             text.undo_block_stop()
   1377 
   1378     # Our editwin provides a is_char_in_string function that works
   1379     # with a Tk text index, but PyParse only knows about offsets into
   1380     # a string. This builds a function for PyParse that accepts an
   1381     # offset.
   1382 
   1383     def _build_char_in_string_func(self, startindex):
   1384         def inner(offset, _startindex=startindex,
   1385                   _icis=self.is_char_in_string):
   1386             return _icis(_startindex + "+%dc" % offset)
   1387         return inner
   1388 
   1389     def indent_region_event(self, event):
   1390         head, tail, chars, lines = self.get_region()
   1391         for pos in range(len(lines)):
   1392             line = lines[pos]
   1393             if line:
   1394                 raw, effective = classifyws(line, self.tabwidth)
   1395                 effective = effective + self.indentwidth
   1396                 lines[pos] = self._make_blanks(effective) + line[raw:]
   1397         self.set_region(head, tail, chars, lines)
   1398         return "break"
   1399 
   1400     def dedent_region_event(self, event):
   1401         head, tail, chars, lines = self.get_region()
   1402         for pos in range(len(lines)):
   1403             line = lines[pos]
   1404             if line:
   1405                 raw, effective = classifyws(line, self.tabwidth)
   1406                 effective = max(effective - self.indentwidth, 0)
   1407                 lines[pos] = self._make_blanks(effective) + line[raw:]
   1408         self.set_region(head, tail, chars, lines)
   1409         return "break"
   1410 
   1411     def comment_region_event(self, event):
   1412         head, tail, chars, lines = self.get_region()
   1413         for pos in range(len(lines) - 1):
   1414             line = lines[pos]
   1415             lines[pos] = '##' + line
   1416         self.set_region(head, tail, chars, lines)
   1417 
   1418     def uncomment_region_event(self, event):
   1419         head, tail, chars, lines = self.get_region()
   1420         for pos in range(len(lines)):
   1421             line = lines[pos]
   1422             if not line:
   1423                 continue
   1424             if line[:2] == '##':
   1425                 line = line[2:]
   1426             elif line[:1] == '#':
   1427                 line = line[1:]
   1428             lines[pos] = line
   1429         self.set_region(head, tail, chars, lines)
   1430 
   1431     def tabify_region_event(self, event):
   1432         head, tail, chars, lines = self.get_region()
   1433         tabwidth = self._asktabwidth()
   1434         if tabwidth is None: return
   1435         for pos in range(len(lines)):
   1436             line = lines[pos]
   1437             if line:
   1438                 raw, effective = classifyws(line, tabwidth)
   1439                 ntabs, nspaces = divmod(effective, tabwidth)
   1440                 lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]
   1441         self.set_region(head, tail, chars, lines)
   1442 
   1443     def untabify_region_event(self, event):
   1444         head, tail, chars, lines = self.get_region()
   1445         tabwidth = self._asktabwidth()
   1446         if tabwidth is None: return
   1447         for pos in range(len(lines)):
   1448             lines[pos] = lines[pos].expandtabs(tabwidth)
   1449         self.set_region(head, tail, chars, lines)
   1450 
   1451     def toggle_tabs_event(self, event):
   1452         if self.askyesno(
   1453               "Toggle tabs",
   1454               "Turn tabs " + ("on", "off")[self.usetabs] +
   1455               "?\nIndent width " +
   1456               ("will be", "remains at")[self.usetabs] + " 8." +
   1457               "\n Note: a tab is always 8 columns",
   1458               parent=self.text):
   1459             self.usetabs = not self.usetabs
   1460             # Try to prevent inconsistent indentation.
   1461             # User must change indent width manually after using tabs.
   1462             self.indentwidth = 8
   1463         return "break"
   1464 
   1465     # XXX this isn't bound to anything -- see tabwidth comments
   1466 ##     def change_tabwidth_event(self, event):
   1467 ##         new = self._asktabwidth()
   1468 ##         if new != self.tabwidth:
   1469 ##             self.tabwidth = new
   1470 ##             self.set_indentation_params(0, guess=0)
   1471 ##         return "break"
   1472 
   1473     def change_indentwidth_event(self, event):
   1474         new = self.askinteger(
   1475                   "Indent width",
   1476                   "New indent width (2-16)\n(Always use 8 when using tabs)",
   1477                   parent=self.text,
   1478                   initialvalue=self.indentwidth,
   1479                   minvalue=2,
   1480                   maxvalue=16)
   1481         if new and new != self.indentwidth and not self.usetabs:
   1482             self.indentwidth = new
   1483         return "break"
   1484 
   1485     def get_region(self):
   1486         text = self.text
   1487         first, last = self.get_selection_indices()
   1488         if first and last:
   1489             head = text.index(first + " linestart")
   1490             tail = text.index(last + "-1c lineend +1c")
   1491         else:
   1492             head = text.index("insert linestart")
   1493             tail = text.index("insert lineend +1c")
   1494         chars = text.get(head, tail)
   1495         lines = chars.split("\n")
   1496         return head, tail, chars, lines
   1497 
   1498     def set_region(self, head, tail, chars, lines):
   1499         text = self.text
   1500         newchars = "\n".join(lines)
   1501         if newchars == chars:
   1502             text.bell()
   1503             return
   1504         text.tag_remove("sel", "1.0", "end")
   1505         text.mark_set("insert", head)
   1506         text.undo_block_start()
   1507         text.delete(head, tail)
   1508         text.insert(head, newchars)
   1509         text.undo_block_stop()
   1510         text.tag_add("sel", head, "insert")
   1511 
   1512     # Make string that displays as n leading blanks.
   1513 
   1514     def _make_blanks(self, n):
   1515         if self.usetabs:
   1516             ntabs, nspaces = divmod(n, self.tabwidth)
   1517             return '\t' * ntabs + ' ' * nspaces
   1518         else:
   1519             return ' ' * n
   1520 
   1521     # Delete from beginning of line to insert point, then reinsert
   1522     # column logical (meaning use tabs if appropriate) spaces.
   1523 
   1524     def reindent_to(self, column):
   1525         text = self.text
   1526         text.undo_block_start()
   1527         if text.compare("insert linestart", "!=", "insert"):
   1528             text.delete("insert linestart", "insert")
   1529         if column:
   1530             text.insert("insert", self._make_blanks(column))
   1531         text.undo_block_stop()
   1532 
   1533     def _asktabwidth(self):
   1534         return self.askinteger(
   1535             "Tab width",
   1536             "Columns per tab? (2-16)",
   1537             parent=self.text,
   1538             initialvalue=self.indentwidth,
   1539             minvalue=2,
   1540             maxvalue=16)
   1541 
   1542     # Guess indentwidth from text content.
   1543     # Return guessed indentwidth.  This should not be believed unless
   1544     # it's in a reasonable range (e.g., it will be 0 if no indented
   1545     # blocks are found).
   1546 
   1547     def guess_indent(self):
   1548         opener, indented = IndentSearcher(self.text, self.tabwidth).run()
   1549         if opener and indented:
   1550             raw, indentsmall = classifyws(opener, self.tabwidth)
   1551             raw, indentlarge = classifyws(indented, self.tabwidth)
   1552         else:
   1553             indentsmall = indentlarge = 0
   1554         return indentlarge - indentsmall
   1555 
   1556 # "line.col" -> line, as an int
   1557 def index2line(index):
   1558     return int(float(index))
   1559 
   1560 # Look at the leading whitespace in s.
   1561 # Return pair (# of leading ws characters,
   1562 #              effective # of leading blanks after expanding
   1563 #              tabs to width tabwidth)
   1564 
   1565 def classifyws(s, tabwidth):
   1566     raw = effective = 0
   1567     for ch in s:
   1568         if ch == ' ':
   1569             raw = raw + 1
   1570             effective = effective + 1
   1571         elif ch == '\t':
   1572             raw = raw + 1
   1573             effective = (effective // tabwidth + 1) * tabwidth
   1574         else:
   1575             break
   1576     return raw, effective
   1577 
   1578 import tokenize
   1579 _tokenize = tokenize
   1580 del tokenize
   1581 
   1582 class IndentSearcher(object):
   1583 
   1584     # .run() chews over the Text widget, looking for a block opener
   1585     # and the stmt following it.  Returns a pair,
   1586     #     (line containing block opener, line containing stmt)
   1587     # Either or both may be None.
   1588 
   1589     def __init__(self, text, tabwidth):
   1590         self.text = text
   1591         self.tabwidth = tabwidth
   1592         self.i = self.finished = 0
   1593         self.blkopenline = self.indentedline = None
   1594 
   1595     def readline(self):
   1596         if self.finished:
   1597             return ""
   1598         i = self.i = self.i + 1
   1599         mark = repr(i) + ".0"
   1600         if self.text.compare(mark, ">=", "end"):
   1601             return ""
   1602         return self.text.get(mark, mark + " lineend+1c")
   1603 
   1604     def tokeneater(self, type, token, start, end, line,
   1605                    INDENT=_tokenize.INDENT,
   1606                    NAME=_tokenize.NAME,
   1607                    OPENERS=('class', 'def', 'for', 'if', 'try', 'while')):
   1608         if self.finished:
   1609             pass
   1610         elif type == NAME and token in OPENERS:
   1611             self.blkopenline = line
   1612         elif type == INDENT and self.blkopenline:
   1613             self.indentedline = line
   1614             self.finished = 1
   1615 
   1616     def run(self):
   1617         save_tabsize = _tokenize.tabsize
   1618         _tokenize.tabsize = self.tabwidth
   1619         try:
   1620             try:
   1621                 _tokenize.tokenize(self.readline, self.tokeneater)
   1622             except (_tokenize.TokenError, SyntaxError):
   1623                 # since we cut off the tokenizer early, we can trigger
   1624                 # spurious errors
   1625                 pass
   1626         finally:
   1627             _tokenize.tabsize = save_tabsize
   1628         return self.blkopenline, self.indentedline
   1629 
   1630 ### end autoindent code ###
   1631 
   1632 def prepstr(s):
   1633     # Helper to extract the underscore from a string, e.g.
   1634     # prepstr("Co_py") returns (2, "Copy").
   1635     i = s.find('_')
   1636     if i >= 0:
   1637         s = s[:i] + s[i+1:]
   1638     return i, s
   1639 
   1640 
   1641 keynames = {
   1642  'bracketleft': '[',
   1643  'bracketright': ']',
   1644  'slash': '/',
   1645 }
   1646 
   1647 def get_accelerator(keydefs, eventname):
   1648     keylist = keydefs.get(eventname)
   1649     # issue10940: temporary workaround to prevent hang with OS X Cocoa Tk 8.5
   1650     # if not keylist:
   1651     if (not keylist) or (macosxSupport.runningAsOSXApp() and eventname in {
   1652                             "<<open-module>>",
   1653                             "<<goto-line>>",
   1654                             "<<change-indentwidth>>"}):
   1655         return ""
   1656     s = keylist[0]
   1657     s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s)
   1658     s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s)
   1659     s = re.sub("Key-", "", s)
   1660     s = re.sub("Cancel","Ctrl-Break",s)   # dscherer (at] cmu.edu
   1661     s = re.sub("Control-", "Ctrl-", s)
   1662     s = re.sub("-", "+", s)
   1663     s = re.sub("><", " ", s)
   1664     s = re.sub("<", "", s)
   1665     s = re.sub(">", "", s)
   1666     return s
   1667 
   1668 
   1669 def fixwordbreaks(root):
   1670     # Make sure that Tk's double-click and next/previous word
   1671     # operations use our definition of a word (i.e. an identifier)
   1672     tk = root.tk
   1673     tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
   1674     tk.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]')
   1675     tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]')
   1676 
   1677 
   1678 def test():
   1679     root = Tk()
   1680     fixwordbreaks(root)
   1681     root.withdraw()
   1682     if sys.argv[1:]:
   1683         filename = sys.argv[1]
   1684     else:
   1685         filename = None
   1686     edit = EditorWindow(root=root, filename=filename)
   1687     edit.set_close_hook(root.quit)
   1688     edit.text.bind("<<close-all-windows>>", edit.close_event)
   1689     root.mainloop()
   1690     root.destroy()
   1691 
   1692 if __name__ == '__main__':
   1693     test()
   1694