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