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