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