• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import importlib.abc
2import importlib.util
3import os
4import platform
5import re
6import string
7import sys
8import tokenize
9import traceback
10import webbrowser
11
12from tkinter import *
13from tkinter.font import Font
14from tkinter.ttk import Scrollbar
15from tkinter import simpledialog
16from tkinter import messagebox
17
18from idlelib.config import idleConf
19from idlelib import configdialog
20from idlelib import grep
21from idlelib import help
22from idlelib import help_about
23from idlelib import macosx
24from idlelib.multicall import MultiCallCreator
25from idlelib import pyparse
26from idlelib import query
27from idlelib import replace
28from idlelib import search
29from idlelib.tree import wheel_event
30from idlelib 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.zoomheight import ZoomHeight
64
65    filesystemencoding = sys.getfilesystemencoding()  # for file names
66    help_url = None
67
68    allow_code_context = True
69    allow_line_numbers = True
70    user_input_insert_tags = None
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, self.user_input_insert_tags)
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.insertfilterafter(filter=self.color, after=self.undo)
788
789    def _rmcolorizer(self):
790        if not self.color:
791            return
792        self.color.removecolors()
793        self.per.removefilter(self.color)
794        self.color = None
795
796    def ResetColorizer(self):
797        "Update the color theme"
798        # Called from self.filename_change_hook and from configdialog.py
799        self._rmcolorizer()
800        self._addcolorizer()
801        EditorWindow.color_config(self.text)
802
803        if self.code_context is not None:
804            self.code_context.update_highlight_colors()
805
806        if self.line_numbers is not None:
807            self.line_numbers.update_colors()
808
809    IDENTCHARS = string.ascii_letters + string.digits + "_"
810
811    def colorize_syntax_error(self, text, pos):
812        text.tag_add("ERROR", pos)
813        char = text.get(pos)
814        if char and char in self.IDENTCHARS:
815            text.tag_add("ERROR", pos + " wordstart", pos)
816        if '\n' == text.get(pos):   # error at line end
817            text.mark_set("insert", pos)
818        else:
819            text.mark_set("insert", pos + "+1c")
820        text.see(pos)
821
822    def update_cursor_blink(self):
823        "Update the cursor blink configuration."
824        cursorblink = idleConf.GetOption(
825                'main', 'EditorWindow', 'cursor-blink', type='bool')
826        if not cursorblink:
827            self.text['insertofftime'] = 0
828        else:
829            # Restore the original value
830            self.text['insertofftime'] = idleConf.blink_off_time
831
832    def ResetFont(self):
833        "Update the text widgets' font if it is changed"
834        # Called from configdialog.py
835
836        # Update the code context widget first, since its height affects
837        # the height of the text widget.  This avoids double re-rendering.
838        if self.code_context is not None:
839            self.code_context.update_font()
840        # Next, update the line numbers widget, since its width affects
841        # the width of the text widget.
842        if self.line_numbers is not None:
843            self.line_numbers.update_font()
844        # Finally, update the main text widget.
845        new_font = idleConf.GetFont(self.root, 'main', 'EditorWindow')
846        self.text['font'] = new_font
847        self.set_width()
848
849    def RemoveKeybindings(self):
850        "Remove the keybindings before they are changed."
851        # Called from configdialog.py
852        self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet()
853        for event, keylist in keydefs.items():
854            self.text.event_delete(event, *keylist)
855        for extensionName in self.get_standard_extension_names():
856            xkeydefs = idleConf.GetExtensionBindings(extensionName)
857            if xkeydefs:
858                for event, keylist in xkeydefs.items():
859                    self.text.event_delete(event, *keylist)
860
861    def ApplyKeybindings(self):
862        "Update the keybindings after they are changed"
863        # Called from configdialog.py
864        self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet()
865        self.apply_bindings()
866        for extensionName in self.get_standard_extension_names():
867            xkeydefs = idleConf.GetExtensionBindings(extensionName)
868            if xkeydefs:
869                self.apply_bindings(xkeydefs)
870        #update menu accelerators
871        menuEventDict = {}
872        for menu in self.mainmenu.menudefs:
873            menuEventDict[menu[0]] = {}
874            for item in menu[1]:
875                if item:
876                    menuEventDict[menu[0]][prepstr(item[0])[1]] = item[1]
877        for menubarItem in self.menudict:
878            menu = self.menudict[menubarItem]
879            end = menu.index(END)
880            if end is None:
881                # Skip empty menus
882                continue
883            end += 1
884            for index in range(0, end):
885                if menu.type(index) == 'command':
886                    accel = menu.entrycget(index, 'accelerator')
887                    if accel:
888                        itemName = menu.entrycget(index, 'label')
889                        event = ''
890                        if menubarItem in menuEventDict:
891                            if itemName in menuEventDict[menubarItem]:
892                                event = menuEventDict[menubarItem][itemName]
893                        if event:
894                            accel = get_accelerator(keydefs, event)
895                            menu.entryconfig(index, accelerator=accel)
896
897    def set_notabs_indentwidth(self):
898        "Update the indentwidth if changed and not using tabs in this window"
899        # Called from configdialog.py
900        if not self.usetabs:
901            self.indentwidth = idleConf.GetOption('main', 'Indent','num-spaces',
902                                                  type='int')
903
904    def reset_help_menu_entries(self):
905        "Update the additional help entries on the Help menu"
906        help_list = idleConf.GetAllExtraHelpSourcesList()
907        helpmenu = self.menudict['help']
908        # first delete the extra help entries, if any
909        helpmenu_length = helpmenu.index(END)
910        if helpmenu_length > self.base_helpmenu_length:
911            helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length)
912        # then rebuild them
913        if help_list:
914            helpmenu.add_separator()
915            for entry in help_list:
916                cmd = self.__extra_help_callback(entry[1])
917                helpmenu.add_command(label=entry[0], command=cmd)
918        # and update the menu dictionary
919        self.menudict['help'] = helpmenu
920
921    def __extra_help_callback(self, helpfile):
922        "Create a callback with the helpfile value frozen at definition time"
923        def display_extra_help(helpfile=helpfile):
924            if not helpfile.startswith(('www', 'http')):
925                helpfile = os.path.normpath(helpfile)
926            if sys.platform[:3] == 'win':
927                try:
928                    os.startfile(helpfile)
929                except OSError as why:
930                    messagebox.showerror(title='Document Start Failure',
931                        message=str(why), parent=self.text)
932            else:
933                webbrowser.open(helpfile)
934        return display_extra_help
935
936    def update_recent_files_list(self, new_file=None):
937        "Load and update the recent files list and menus"
938        # TODO: move to iomenu.
939        rf_list = []
940        file_path = self.recent_files_path
941        if file_path and os.path.exists(file_path):
942            with open(file_path, 'r',
943                      encoding='utf_8', errors='replace') as rf_list_file:
944                rf_list = rf_list_file.readlines()
945        if new_file:
946            new_file = os.path.abspath(new_file) + '\n'
947            if new_file in rf_list:
948                rf_list.remove(new_file)  # move to top
949            rf_list.insert(0, new_file)
950        # clean and save the recent files list
951        bad_paths = []
952        for path in rf_list:
953            if '\0' in path or not os.path.exists(path[0:-1]):
954                bad_paths.append(path)
955        rf_list = [path for path in rf_list if path not in bad_paths]
956        ulchars = "1234567890ABCDEFGHIJK"
957        rf_list = rf_list[0:len(ulchars)]
958        if file_path:
959            try:
960                with open(file_path, 'w',
961                          encoding='utf_8', errors='replace') as rf_file:
962                    rf_file.writelines(rf_list)
963            except OSError as err:
964                if not getattr(self.root, "recentfiles_message", False):
965                    self.root.recentfiles_message = True
966                    messagebox.showwarning(title='IDLE Warning',
967                        message="Cannot save Recent Files list to disk.\n"
968                                f"  {err}\n"
969                                "Select OK to continue.",
970                        parent=self.text)
971        # for each edit window instance, construct the recent files menu
972        for instance in self.top.instance_dict:
973            menu = instance.recent_files_menu
974            menu.delete(0, END)  # clear, and rebuild:
975            for i, file_name in enumerate(rf_list):
976                file_name = file_name.rstrip()  # zap \n
977                callback = instance.__recent_file_callback(file_name)
978                menu.add_command(label=ulchars[i] + " " + file_name,
979                                 command=callback,
980                                 underline=0)
981
982    def __recent_file_callback(self, file_name):
983        def open_recent_file(fn_closure=file_name):
984            self.io.open(editFile=fn_closure)
985        return open_recent_file
986
987    def saved_change_hook(self):
988        short = self.short_title()
989        long = self.long_title()
990        if short and long:
991            title = short + " - " + long + _py_version
992        elif short:
993            title = short
994        elif long:
995            title = long
996        else:
997            title = "untitled"
998        icon = short or long or title
999        if not self.get_saved():
1000            title = "*%s*" % title
1001            icon = "*%s" % icon
1002        self.top.wm_title(title)
1003        self.top.wm_iconname(icon)
1004
1005    def get_saved(self):
1006        return self.undo.get_saved()
1007
1008    def set_saved(self, flag):
1009        self.undo.set_saved(flag)
1010
1011    def reset_undo(self):
1012        self.undo.reset_undo()
1013
1014    def short_title(self):
1015        filename = self.io.filename
1016        return os.path.basename(filename) if filename else "untitled"
1017
1018    def long_title(self):
1019        return self.io.filename or ""
1020
1021    def center_insert_event(self, event):
1022        self.center()
1023        return "break"
1024
1025    def center(self, mark="insert"):
1026        text = self.text
1027        top, bot = self.getwindowlines()
1028        lineno = self.getlineno(mark)
1029        height = bot - top
1030        newtop = max(1, lineno - height//2)
1031        text.yview(float(newtop))
1032
1033    def getwindowlines(self):
1034        text = self.text
1035        top = self.getlineno("@0,0")
1036        bot = self.getlineno("@0,65535")
1037        if top == bot and text.winfo_height() == 1:
1038            # Geometry manager hasn't run yet
1039            height = int(text['height'])
1040            bot = top + height - 1
1041        return top, bot
1042
1043    def getlineno(self, mark="insert"):
1044        text = self.text
1045        return int(float(text.index(mark)))
1046
1047    def get_geometry(self):
1048        "Return (width, height, x, y)"
1049        geom = self.top.wm_geometry()
1050        m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom)
1051        return list(map(int, m.groups()))
1052
1053    def close_event(self, event):
1054        self.close()
1055        return "break"
1056
1057    def maybesave(self):
1058        if self.io:
1059            if not self.get_saved():
1060                if self.top.state()!='normal':
1061                    self.top.deiconify()
1062                self.top.lower()
1063                self.top.lift()
1064            return self.io.maybesave()
1065
1066    def close(self):
1067        try:
1068            reply = self.maybesave()
1069            if str(reply) != "cancel":
1070                self._close()
1071            return reply
1072        except AttributeError:  # bpo-35379: close called twice
1073            pass
1074
1075    def _close(self):
1076        if self.io.filename:
1077            self.update_recent_files_list(new_file=self.io.filename)
1078        window.unregister_callback(self.postwindowsmenu)
1079        self.unload_extensions()
1080        self.io.close()
1081        self.io = None
1082        self.undo = None
1083        if self.color:
1084            self.color.close()
1085            self.color = None
1086        self.text = None
1087        self.tkinter_vars = None
1088        self.per.close()
1089        self.per = None
1090        self.top.destroy()
1091        if self.close_hook:
1092            # unless override: unregister from flist, terminate if last window
1093            self.close_hook()
1094
1095    def load_extensions(self):
1096        self.extensions = {}
1097        self.load_standard_extensions()
1098
1099    def unload_extensions(self):
1100        for ins in list(self.extensions.values()):
1101            if hasattr(ins, "close"):
1102                ins.close()
1103        self.extensions = {}
1104
1105    def load_standard_extensions(self):
1106        for name in self.get_standard_extension_names():
1107            try:
1108                self.load_extension(name)
1109            except:
1110                print("Failed to load extension", repr(name))
1111                traceback.print_exc()
1112
1113    def get_standard_extension_names(self):
1114        return idleConf.GetExtensions(editor_only=True)
1115
1116    extfiles = {  # Map built-in config-extension section names to file names.
1117        'ZzDummy': 'zzdummy',
1118        }
1119
1120    def load_extension(self, name):
1121        fname = self.extfiles.get(name, name)
1122        try:
1123            try:
1124                mod = importlib.import_module('.' + fname, package=__package__)
1125            except (ImportError, TypeError):
1126                mod = importlib.import_module(fname)
1127        except ImportError:
1128            print("\nFailed to import extension: ", name)
1129            raise
1130        cls = getattr(mod, name)
1131        keydefs = idleConf.GetExtensionBindings(name)
1132        if hasattr(cls, "menudefs"):
1133            self.fill_menus(cls.menudefs, keydefs)
1134        ins = cls(self)
1135        self.extensions[name] = ins
1136        if keydefs:
1137            self.apply_bindings(keydefs)
1138            for vevent in keydefs:
1139                methodname = vevent.replace("-", "_")
1140                while methodname[:1] == '<':
1141                    methodname = methodname[1:]
1142                while methodname[-1:] == '>':
1143                    methodname = methodname[:-1]
1144                methodname = methodname + "_event"
1145                if hasattr(ins, methodname):
1146                    self.text.bind(vevent, getattr(ins, methodname))
1147
1148    def apply_bindings(self, keydefs=None):
1149        if keydefs is None:
1150            keydefs = self.mainmenu.default_keydefs
1151        text = self.text
1152        text.keydefs = keydefs
1153        for event, keylist in keydefs.items():
1154            if keylist:
1155                text.event_add(event, *keylist)
1156
1157    def fill_menus(self, menudefs=None, keydefs=None):
1158        """Add appropriate entries to the menus and submenus
1159
1160        Menus that are absent or None in self.menudict are ignored.
1161        """
1162        if menudefs is None:
1163            menudefs = self.mainmenu.menudefs
1164        if keydefs is None:
1165            keydefs = self.mainmenu.default_keydefs
1166        menudict = self.menudict
1167        text = self.text
1168        for mname, entrylist in menudefs:
1169            menu = menudict.get(mname)
1170            if not menu:
1171                continue
1172            for entry in entrylist:
1173                if not entry:
1174                    menu.add_separator()
1175                else:
1176                    label, eventname = entry
1177                    checkbutton = (label[:1] == '!')
1178                    if checkbutton:
1179                        label = label[1:]
1180                    underline, label = prepstr(label)
1181                    accelerator = get_accelerator(keydefs, eventname)
1182                    def command(text=text, eventname=eventname):
1183                        text.event_generate(eventname)
1184                    if checkbutton:
1185                        var = self.get_var_obj(eventname, BooleanVar)
1186                        menu.add_checkbutton(label=label, underline=underline,
1187                            command=command, accelerator=accelerator,
1188                            variable=var)
1189                    else:
1190                        menu.add_command(label=label, underline=underline,
1191                                         command=command,
1192                                         accelerator=accelerator)
1193
1194    def getvar(self, name):
1195        var = self.get_var_obj(name)
1196        if var:
1197            value = var.get()
1198            return value
1199        else:
1200            raise NameError(name)
1201
1202    def setvar(self, name, value, vartype=None):
1203        var = self.get_var_obj(name, vartype)
1204        if var:
1205            var.set(value)
1206        else:
1207            raise NameError(name)
1208
1209    def get_var_obj(self, name, vartype=None):
1210        var = self.tkinter_vars.get(name)
1211        if not var and vartype:
1212            # create a Tkinter variable object with self.text as master:
1213            self.tkinter_vars[name] = var = vartype(self.text)
1214        return var
1215
1216    # Tk implementations of "virtual text methods" -- each platform
1217    # reusing IDLE's support code needs to define these for its GUI's
1218    # flavor of widget.
1219
1220    # Is character at text_index in a Python string?  Return 0 for
1221    # "guaranteed no", true for anything else.  This info is expensive
1222    # to compute ab initio, but is probably already known by the
1223    # platform's colorizer.
1224
1225    def is_char_in_string(self, text_index):
1226        if self.color:
1227            # Return true iff colorizer hasn't (re)gotten this far
1228            # yet, or the character is tagged as being in a string
1229            return self.text.tag_prevrange("TODO", text_index) or \
1230                   "STRING" in self.text.tag_names(text_index)
1231        else:
1232            # The colorizer is missing: assume the worst
1233            return 1
1234
1235    # If a selection is defined in the text widget, return (start,
1236    # end) as Tkinter text indices, otherwise return (None, None)
1237    def get_selection_indices(self):
1238        try:
1239            first = self.text.index("sel.first")
1240            last = self.text.index("sel.last")
1241            return first, last
1242        except TclError:
1243            return None, None
1244
1245    # Return the text widget's current view of what a tab stop means
1246    # (equivalent width in spaces).
1247
1248    def get_tk_tabwidth(self):
1249        current = self.text['tabs'] or TK_TABWIDTH_DEFAULT
1250        return int(current)
1251
1252    # Set the text widget's current view of what a tab stop means.
1253
1254    def set_tk_tabwidth(self, newtabwidth):
1255        text = self.text
1256        if self.get_tk_tabwidth() != newtabwidth:
1257            # Set text widget tab width
1258            pixels = text.tk.call("font", "measure", text["font"],
1259                                  "-displayof", text.master,
1260                                  "n" * newtabwidth)
1261            text.configure(tabs=pixels)
1262
1263### begin autoindent code ###  (configuration was moved to beginning of class)
1264
1265    def set_indentation_params(self, is_py_src, guess=True):
1266        if is_py_src and guess:
1267            i = self.guess_indent()
1268            if 2 <= i <= 8:
1269                self.indentwidth = i
1270            if self.indentwidth != self.tabwidth:
1271                self.usetabs = False
1272        self.set_tk_tabwidth(self.tabwidth)
1273
1274    def smart_backspace_event(self, event):
1275        text = self.text
1276        first, last = self.get_selection_indices()
1277        if first and last:
1278            text.delete(first, last)
1279            text.mark_set("insert", first)
1280            return "break"
1281        # Delete whitespace left, until hitting a real char or closest
1282        # preceding virtual tab stop.
1283        chars = text.get("insert linestart", "insert")
1284        if chars == '':
1285            if text.compare("insert", ">", "1.0"):
1286                # easy: delete preceding newline
1287                text.delete("insert-1c")
1288            else:
1289                text.bell()     # at start of buffer
1290            return "break"
1291        if  chars[-1] not in " \t":
1292            # easy: delete preceding real char
1293            text.delete("insert-1c")
1294            return "break"
1295        # Ick.  It may require *inserting* spaces if we back up over a
1296        # tab character!  This is written to be clear, not fast.
1297        tabwidth = self.tabwidth
1298        have = len(chars.expandtabs(tabwidth))
1299        assert have > 0
1300        want = ((have - 1) // self.indentwidth) * self.indentwidth
1301        # Debug prompt is multilined....
1302        ncharsdeleted = 0
1303        while 1:
1304            chars = chars[:-1]
1305            ncharsdeleted = ncharsdeleted + 1
1306            have = len(chars.expandtabs(tabwidth))
1307            if have <= want or chars[-1] not in " \t":
1308                break
1309        text.undo_block_start()
1310        text.delete("insert-%dc" % ncharsdeleted, "insert")
1311        if have < want:
1312            text.insert("insert", ' ' * (want - have),
1313                        self.user_input_insert_tags)
1314        text.undo_block_stop()
1315        return "break"
1316
1317    def smart_indent_event(self, event):
1318        # if intraline selection:
1319        #     delete it
1320        # elif multiline selection:
1321        #     do indent-region
1322        # else:
1323        #     indent one level
1324        text = self.text
1325        first, last = self.get_selection_indices()
1326        text.undo_block_start()
1327        try:
1328            if first and last:
1329                if index2line(first) != index2line(last):
1330                    return self.fregion.indent_region_event(event)
1331                text.delete(first, last)
1332                text.mark_set("insert", first)
1333            prefix = text.get("insert linestart", "insert")
1334            raw, effective = get_line_indent(prefix, self.tabwidth)
1335            if raw == len(prefix):
1336                # only whitespace to the left
1337                self.reindent_to(effective + self.indentwidth)
1338            else:
1339                # tab to the next 'stop' within or to right of line's text:
1340                if self.usetabs:
1341                    pad = '\t'
1342                else:
1343                    effective = len(prefix.expandtabs(self.tabwidth))
1344                    n = self.indentwidth
1345                    pad = ' ' * (n - effective % n)
1346                text.insert("insert", pad, self.user_input_insert_tags)
1347            text.see("insert")
1348            return "break"
1349        finally:
1350            text.undo_block_stop()
1351
1352    def newline_and_indent_event(self, event):
1353        """Insert a newline and indentation after Enter keypress event.
1354
1355        Properly position the cursor on the new line based on information
1356        from the current line.  This takes into account if the current line
1357        is a shell prompt, is empty, has selected text, contains a block
1358        opener, contains a block closer, is a continuation line, or
1359        is inside a string.
1360        """
1361        text = self.text
1362        first, last = self.get_selection_indices()
1363        text.undo_block_start()
1364        try:  # Close undo block and expose new line in finally clause.
1365            if first and last:
1366                text.delete(first, last)
1367                text.mark_set("insert", first)
1368            line = text.get("insert linestart", "insert")
1369
1370            # Count leading whitespace for indent size.
1371            i, n = 0, len(line)
1372            while i < n and line[i] in " \t":
1373                i += 1
1374            if i == n:
1375                # The cursor is in or at leading indentation in a continuation
1376                # line; just inject an empty line at the start.
1377                text.insert("insert linestart", '\n',
1378                            self.user_input_insert_tags)
1379                return "break"
1380            indent = line[:i]
1381
1382            # Strip whitespace before insert point unless it's in the prompt.
1383            i = 0
1384            while line and line[-1] in " \t":
1385                line = line[:-1]
1386                i += 1
1387            if i:
1388                text.delete("insert - %d chars" % i, "insert")
1389
1390            # Strip whitespace after insert point.
1391            while text.get("insert") in " \t":
1392                text.delete("insert")
1393
1394            # Insert new line.
1395            text.insert("insert", '\n', self.user_input_insert_tags)
1396
1397            # Adjust indentation for continuations and block open/close.
1398            # First need to find the last statement.
1399            lno = index2line(text.index('insert'))
1400            y = pyparse.Parser(self.indentwidth, self.tabwidth)
1401            if not self.prompt_last_line:
1402                for context in self.num_context_lines:
1403                    startat = max(lno - context, 1)
1404                    startatindex = repr(startat) + ".0"
1405                    rawtext = text.get(startatindex, "insert")
1406                    y.set_code(rawtext)
1407                    bod = y.find_good_parse_start(
1408                            self._build_char_in_string_func(startatindex))
1409                    if bod is not None or startat == 1:
1410                        break
1411                y.set_lo(bod or 0)
1412            else:
1413                r = text.tag_prevrange("console", "insert")
1414                if r:
1415                    startatindex = r[1]
1416                else:
1417                    startatindex = "1.0"
1418                rawtext = text.get(startatindex, "insert")
1419                y.set_code(rawtext)
1420                y.set_lo(0)
1421
1422            c = y.get_continuation_type()
1423            if c != pyparse.C_NONE:
1424                # The current statement hasn't ended yet.
1425                if c == pyparse.C_STRING_FIRST_LINE:
1426                    # After the first line of a string do not indent at all.
1427                    pass
1428                elif c == pyparse.C_STRING_NEXT_LINES:
1429                    # Inside a string which started before this line;
1430                    # just mimic the current indent.
1431                    text.insert("insert", indent, self.user_input_insert_tags)
1432                elif c == pyparse.C_BRACKET:
1433                    # Line up with the first (if any) element of the
1434                    # last open bracket structure; else indent one
1435                    # level beyond the indent of the line with the
1436                    # last open bracket.
1437                    self.reindent_to(y.compute_bracket_indent())
1438                elif c == pyparse.C_BACKSLASH:
1439                    # If more than one line in this statement already, just
1440                    # mimic the current indent; else if initial line
1441                    # has a start on an assignment stmt, indent to
1442                    # beyond leftmost =; else to beyond first chunk of
1443                    # non-whitespace on initial line.
1444                    if y.get_num_lines_in_stmt() > 1:
1445                        text.insert("insert", indent,
1446                                    self.user_input_insert_tags)
1447                    else:
1448                        self.reindent_to(y.compute_backslash_indent())
1449                else:
1450                    assert 0, "bogus continuation type %r" % (c,)
1451                return "break"
1452
1453            # This line starts a brand new statement; indent relative to
1454            # indentation of initial line of closest preceding
1455            # interesting statement.
1456            indent = y.get_base_indent_string()
1457            text.insert("insert", indent, self.user_input_insert_tags)
1458            if y.is_block_opener():
1459                self.smart_indent_event(event)
1460            elif indent and y.is_block_closer():
1461                self.smart_backspace_event(event)
1462            return "break"
1463        finally:
1464            text.see("insert")
1465            text.undo_block_stop()
1466
1467    # Our editwin provides an is_char_in_string function that works
1468    # with a Tk text index, but PyParse only knows about offsets into
1469    # a string. This builds a function for PyParse that accepts an
1470    # offset.
1471
1472    def _build_char_in_string_func(self, startindex):
1473        def inner(offset, _startindex=startindex,
1474                  _icis=self.is_char_in_string):
1475            return _icis(_startindex + "+%dc" % offset)
1476        return inner
1477
1478    # XXX this isn't bound to anything -- see tabwidth comments
1479##     def change_tabwidth_event(self, event):
1480##         new = self._asktabwidth()
1481##         if new != self.tabwidth:
1482##             self.tabwidth = new
1483##             self.set_indentation_params(0, guess=0)
1484##         return "break"
1485
1486    # Make string that displays as n leading blanks.
1487
1488    def _make_blanks(self, n):
1489        if self.usetabs:
1490            ntabs, nspaces = divmod(n, self.tabwidth)
1491            return '\t' * ntabs + ' ' * nspaces
1492        else:
1493            return ' ' * n
1494
1495    # Delete from beginning of line to insert point, then reinsert
1496    # column logical (meaning use tabs if appropriate) spaces.
1497
1498    def reindent_to(self, column):
1499        text = self.text
1500        text.undo_block_start()
1501        if text.compare("insert linestart", "!=", "insert"):
1502            text.delete("insert linestart", "insert")
1503        if column:
1504            text.insert("insert", self._make_blanks(column),
1505                        self.user_input_insert_tags)
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