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