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