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