• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import importlib
2import importlib.abc
3import importlib.util
4import os
5import platform
6import re
7import string
8import sys
9import tokenize
10import traceback
11import webbrowser
12
13from tkinter import *
14from tkinter.ttk import Scrollbar
15import tkinter.simpledialog as tkSimpleDialog
16import tkinter.messagebox as tkMessageBox
17
18from idlelib.config import idleConf
19from idlelib import configdialog
20from idlelib import grep
21from idlelib import help
22from idlelib import help_about
23from idlelib import macosx
24from idlelib.multicall import MultiCallCreator
25from idlelib import pyparse
26from idlelib import query
27from idlelib import replace
28from idlelib import search
29from idlelib import textview
30from idlelib import windows
31
32# The default tab setting for a Text widget, in average-width characters.
33TK_TABWIDTH_DEFAULT = 8
34_py_version = ' (%s)' % platform.python_version()
35
36
37def _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
49class 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
1501def 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
1509def 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
1523class 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
1575def 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
1584keynames = {
1585 'bracketleft': '[',
1586 'bracketright': ']',
1587 'slash': '/',
1588}
1589
1590def 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@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
1612def 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
1621def _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
1635if __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