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