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