(root)/
Python-3.12.0/
Lib/
idlelib/
codecontext.py
       1  """codecontext - display the block context above the edit window
       2  
       3  Once code has scrolled off the top of a window, it can be difficult to
       4  determine which block you are in.  This extension implements a pane at the top
       5  of each IDLE edit window which provides block structure hints.  These hints are
       6  the lines which contain the block opening keywords, e.g. 'if', for the
       7  enclosing block.  The number of hint lines is determined by the maxlines
       8  variable in the codecontext section of config-extensions.def. Lines which do
       9  not open blocks are not shown in the context hints pane.
      10  
      11  For EditorWindows, <<toggle-code-context>> is bound to CodeContext(self).
      12  toggle_code_context_event.
      13  """
      14  import re
      15  from sys import maxsize as INFINITY
      16  
      17  from tkinter import Frame, Text, TclError
      18  from tkinter.constants import NSEW, SUNKEN
      19  
      20  from idlelib.config import idleConf
      21  
      22  BLOCKOPENERS = {'class', 'def', 'if', 'elif', 'else', 'while', 'for',
      23                   'try', 'except', 'finally', 'with', 'async'}
      24  
      25  
      26  def get_spaces_firstword(codeline, c=re.compile(r"^(\s*)(\w*)")):
      27      "Extract the beginning whitespace and first word from codeline."
      28      return c.match(codeline).groups()
      29  
      30  
      31  def get_line_info(codeline):
      32      """Return tuple of (line indent value, codeline, block start keyword).
      33  
      34      The indentation of empty lines (or comment lines) is INFINITY.
      35      If the line does not start a block, the keyword value is False.
      36      """
      37      spaces, firstword = get_spaces_firstword(codeline)
      38      indent = len(spaces)
      39      if len(codeline) == indent or codeline[indent] == '#':
      40          indent = INFINITY
      41      opener = firstword in BLOCKOPENERS and firstword
      42      return indent, codeline, opener
      43  
      44  
      45  class ESC[4;38;5;81mCodeContext:
      46      "Display block context above the edit window."
      47      UPDATEINTERVAL = 100  # millisec
      48  
      49      def __init__(self, editwin):
      50          """Initialize settings for context block.
      51  
      52          editwin is the Editor window for the context block.
      53          self.text is the editor window text widget.
      54  
      55          self.context displays the code context text above the editor text.
      56            Initially None, it is toggled via <<toggle-code-context>>.
      57          self.topvisible is the number of the top text line displayed.
      58          self.info is a list of (line number, indent level, line text,
      59            block keyword) tuples for the block structure above topvisible.
      60            self.info[0] is initialized with a 'dummy' line which
      61            starts the toplevel 'block' of the module.
      62  
      63          self.t1 and self.t2 are two timer events on the editor text widget to
      64            monitor for changes to the context text or editor font.
      65          """
      66          self.editwin = editwin
      67          self.text = editwin.text
      68          self._reset()
      69  
      70      def _reset(self):
      71          self.context = None
      72          self.cell00 = None
      73          self.t1 = None
      74          self.topvisible = 1
      75          self.info = [(0, -1, "", False)]
      76  
      77      @classmethod
      78      def reload(cls):
      79          "Load class variables from config."
      80          cls.context_depth = idleConf.GetOption("extensions", "CodeContext",
      81                                                 "maxlines", type="int",
      82                                                 default=15)
      83  
      84      def __del__(self):
      85          "Cancel scheduled events."
      86          if self.t1 is not None:
      87              try:
      88                  self.text.after_cancel(self.t1)
      89              except TclError:  # pragma: no cover
      90                  pass
      91              self.t1 = None
      92  
      93      def toggle_code_context_event(self, event=None):
      94          """Toggle code context display.
      95  
      96          If self.context doesn't exist, create it to match the size of the editor
      97          window text (toggle on).  If it does exist, destroy it (toggle off).
      98          Return 'break' to complete the processing of the binding.
      99          """
     100          if self.context is None:
     101              # Calculate the border width and horizontal padding required to
     102              # align the context with the text in the main Text widget.
     103              #
     104              # All values are passed through getint(), since some
     105              # values may be pixel objects, which can't simply be added to ints.
     106              widgets = self.editwin.text, self.editwin.text_frame
     107              # Calculate the required horizontal padding and border width.
     108              padx = 0
     109              border = 0
     110              for widget in widgets:
     111                  info = (widget.grid_info()
     112                          if widget is self.editwin.text
     113                          else widget.pack_info())
     114                  padx += widget.tk.getint(info['padx'])
     115                  padx += widget.tk.getint(widget.cget('padx'))
     116                  border += widget.tk.getint(widget.cget('border'))
     117              context = self.context = Text(
     118                  self.editwin.text_frame,
     119                  height=1,
     120                  width=1,  # Don't request more than we get.
     121                  highlightthickness=0,
     122                  padx=padx, border=border, relief=SUNKEN, state='disabled')
     123              self.update_font()
     124              self.update_highlight_colors()
     125              context.bind('<ButtonRelease-1>', self.jumptoline)
     126              # Get the current context and initiate the recurring update event.
     127              self.timer_event()
     128              # Grid the context widget above the text widget.
     129              context.grid(row=0, column=1, sticky=NSEW)
     130  
     131              line_number_colors = idleConf.GetHighlight(idleConf.CurrentTheme(),
     132                                                         'linenumber')
     133              self.cell00 = Frame(self.editwin.text_frame,
     134                                          bg=line_number_colors['background'])
     135              self.cell00.grid(row=0, column=0, sticky=NSEW)
     136              menu_status = 'Hide'
     137          else:
     138              self.context.destroy()
     139              self.context = None
     140              self.cell00.destroy()
     141              self.cell00 = None
     142              self.text.after_cancel(self.t1)
     143              self._reset()
     144              menu_status = 'Show'
     145          self.editwin.update_menu_label(menu='options', index='*ode*ontext',
     146                                         label=f'{menu_status} Code Context')
     147          return "break"
     148  
     149      def get_context(self, new_topvisible, stopline=1, stopindent=0):
     150          """Return a list of block line tuples and the 'last' indent.
     151  
     152          The tuple fields are (linenum, indent, text, opener).
     153          The list represents header lines from new_topvisible back to
     154          stopline with successively shorter indents > stopindent.
     155          The list is returned ordered by line number.
     156          Last indent returned is the smallest indent observed.
     157          """
     158          assert stopline > 0
     159          lines = []
     160          # The indentation level we are currently in.
     161          lastindent = INFINITY
     162          # For a line to be interesting, it must begin with a block opening
     163          # keyword, and have less indentation than lastindent.
     164          for linenum in range(new_topvisible, stopline-1, -1):
     165              codeline = self.text.get(f'{linenum}.0', f'{linenum}.end')
     166              indent, text, opener = get_line_info(codeline)
     167              if indent < lastindent:
     168                  lastindent = indent
     169                  if opener in ("else", "elif"):
     170                      # Also show the if statement.
     171                      lastindent += 1
     172                  if opener and linenum < new_topvisible and indent >= stopindent:
     173                      lines.append((linenum, indent, text, opener))
     174                  if lastindent <= stopindent:
     175                      break
     176          lines.reverse()
     177          return lines, lastindent
     178  
     179      def update_code_context(self):
     180          """Update context information and lines visible in the context pane.
     181  
     182          No update is done if the text hasn't been scrolled.  If the text
     183          was scrolled, the lines that should be shown in the context will
     184          be retrieved and the context area will be updated with the code,
     185          up to the number of maxlines.
     186          """
     187          new_topvisible = self.editwin.getlineno("@0,0")
     188          if self.topvisible == new_topvisible:      # Haven't scrolled.
     189              return
     190          if self.topvisible < new_topvisible:       # Scroll down.
     191              lines, lastindent = self.get_context(new_topvisible,
     192                                                   self.topvisible)
     193              # Retain only context info applicable to the region
     194              # between topvisible and new_topvisible.
     195              while self.info[-1][1] >= lastindent:
     196                  del self.info[-1]
     197          else:  # self.topvisible > new_topvisible: # Scroll up.
     198              stopindent = self.info[-1][1] + 1
     199              # Retain only context info associated
     200              # with lines above new_topvisible.
     201              while self.info[-1][0] >= new_topvisible:
     202                  stopindent = self.info[-1][1]
     203                  del self.info[-1]
     204              lines, lastindent = self.get_context(new_topvisible,
     205                                                   self.info[-1][0]+1,
     206                                                   stopindent)
     207          self.info.extend(lines)
     208          self.topvisible = new_topvisible
     209          # Last context_depth context lines.
     210          context_strings = [x[2] for x in self.info[-self.context_depth:]]
     211          showfirst = 0 if context_strings[0] else 1
     212          # Update widget.
     213          self.context['height'] = len(context_strings) - showfirst
     214          self.context['state'] = 'normal'
     215          self.context.delete('1.0', 'end')
     216          self.context.insert('end', '\n'.join(context_strings[showfirst:]))
     217          self.context['state'] = 'disabled'
     218  
     219      def jumptoline(self, event=None):
     220          """ Show clicked context line at top of editor.
     221  
     222          If a selection was made, don't jump; allow copying.
     223          If no visible context, show the top line of the file.
     224          """
     225          try:
     226              self.context.index("sel.first")
     227          except TclError:
     228              lines = len(self.info)
     229              if lines == 1:  # No context lines are showing.
     230                  newtop = 1
     231              else:
     232                  # Line number clicked.
     233                  contextline = int(float(self.context.index('insert')))
     234                  # Lines not displayed due to maxlines.
     235                  offset = max(1, lines - self.context_depth) - 1
     236                  newtop = self.info[offset + contextline][0]
     237              self.text.yview(f'{newtop}.0')
     238              self.update_code_context()
     239  
     240      def timer_event(self):
     241          "Event on editor text widget triggered every UPDATEINTERVAL ms."
     242          if self.context is not None:
     243              self.update_code_context()
     244              self.t1 = self.text.after(self.UPDATEINTERVAL, self.timer_event)
     245  
     246      def update_font(self):
     247          if self.context is not None:
     248              font = idleConf.GetFont(self.text, 'main', 'EditorWindow')
     249              self.context['font'] = font
     250  
     251      def update_highlight_colors(self):
     252          if self.context is not None:
     253              colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'context')
     254              self.context['background'] = colors['background']
     255              self.context['foreground'] = colors['foreground']
     256  
     257          if self.cell00 is not None:
     258              line_number_colors = idleConf.GetHighlight(idleConf.CurrentTheme(),
     259                                                         'linenumber')
     260              self.cell00.config(bg=line_number_colors['background'])
     261  
     262  
     263  CodeContext.reload()
     264  
     265  
     266  if __name__ == "__main__":
     267      from unittest import main
     268      main('idlelib.idle_test.test_codecontext', verbosity=2, exit=False)
     269  
     270      # Add htest.