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