diff CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/idlelib/codecontext.py @ 68:5028fdace37b

planemo upload commit 2e9511a184a1ca667c7be0c6321a36dc4e3d116d
author jpayne
date Tue, 18 Mar 2025 16:23:26 -0400
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/idlelib/codecontext.py	Tue Mar 18 16:23:26 2025 -0400
@@ -0,0 +1,261 @@
+"""codecontext - display the block context above the edit window
+
+Once code has scrolled off the top of a window, it can be difficult to
+determine which block you are in.  This extension implements a pane at the top
+of each IDLE edit window which provides block structure hints.  These hints are
+the lines which contain the block opening keywords, e.g. 'if', for the
+enclosing block.  The number of hint lines is determined by the maxlines
+variable in the codecontext section of config-extensions.def. Lines which do
+not open blocks are not shown in the context hints pane.
+
+"""
+import re
+from sys import maxsize as INFINITY
+
+import tkinter
+from tkinter.constants import NSEW, SUNKEN
+
+from idlelib.config import idleConf
+
+BLOCKOPENERS = {"class", "def", "elif", "else", "except", "finally", "for",
+                "if", "try", "while", "with", "async"}
+
+
+def get_spaces_firstword(codeline, c=re.compile(r"^(\s*)(\w*)")):
+    "Extract the beginning whitespace and first word from codeline."
+    return c.match(codeline).groups()
+
+
+def get_line_info(codeline):
+    """Return tuple of (line indent value, codeline, block start keyword).
+
+    The indentation of empty lines (or comment lines) is INFINITY.
+    If the line does not start a block, the keyword value is False.
+    """
+    spaces, firstword = get_spaces_firstword(codeline)
+    indent = len(spaces)
+    if len(codeline) == indent or codeline[indent] == '#':
+        indent = INFINITY
+    opener = firstword in BLOCKOPENERS and firstword
+    return indent, codeline, opener
+
+
+class CodeContext:
+    "Display block context above the edit window."
+    UPDATEINTERVAL = 100  # millisec
+
+    def __init__(self, editwin):
+        """Initialize settings for context block.
+
+        editwin is the Editor window for the context block.
+        self.text is the editor window text widget.
+
+        self.context displays the code context text above the editor text.
+          Initially None, it is toggled via <<toggle-code-context>>.
+        self.topvisible is the number of the top text line displayed.
+        self.info is a list of (line number, indent level, line text,
+          block keyword) tuples for the block structure above topvisible.
+          self.info[0] is initialized with a 'dummy' line which
+          starts the toplevel 'block' of the module.
+
+        self.t1 and self.t2 are two timer events on the editor text widget to
+          monitor for changes to the context text or editor font.
+        """
+        self.editwin = editwin
+        self.text = editwin.text
+        self._reset()
+
+    def _reset(self):
+        self.context = None
+        self.cell00 = None
+        self.t1 = None
+        self.topvisible = 1
+        self.info = [(0, -1, "", False)]
+
+    @classmethod
+    def reload(cls):
+        "Load class variables from config."
+        cls.context_depth = idleConf.GetOption("extensions", "CodeContext",
+                                               "maxlines", type="int",
+                                               default=15)
+
+    def __del__(self):
+        "Cancel scheduled events."
+        if self.t1 is not None:
+            try:
+                self.text.after_cancel(self.t1)
+            except tkinter.TclError:
+                pass
+            self.t1 = None
+
+    def toggle_code_context_event(self, event=None):
+        """Toggle code context display.
+
+        If self.context doesn't exist, create it to match the size of the editor
+        window text (toggle on).  If it does exist, destroy it (toggle off).
+        Return 'break' to complete the processing of the binding.
+        """
+        if self.context is None:
+            # Calculate the border width and horizontal padding required to
+            # align the context with the text in the main Text widget.
+            #
+            # All values are passed through getint(), since some
+            # values may be pixel objects, which can't simply be added to ints.
+            widgets = self.editwin.text, self.editwin.text_frame
+            # Calculate the required horizontal padding and border width.
+            padx = 0
+            border = 0
+            for widget in widgets:
+                info = (widget.grid_info()
+                        if widget is self.editwin.text
+                        else widget.pack_info())
+                padx += widget.tk.getint(info['padx'])
+                padx += widget.tk.getint(widget.cget('padx'))
+                border += widget.tk.getint(widget.cget('border'))
+            self.context = tkinter.Text(
+                self.editwin.text_frame,
+                height=1,
+                width=1,  # Don't request more than we get.
+                highlightthickness=0,
+                padx=padx, border=border, relief=SUNKEN, state='disabled')
+            self.update_font()
+            self.update_highlight_colors()
+            self.context.bind('<ButtonRelease-1>', self.jumptoline)
+            # Get the current context and initiate the recurring update event.
+            self.timer_event()
+            # Grid the context widget above the text widget.
+            self.context.grid(row=0, column=1, sticky=NSEW)
+
+            line_number_colors = idleConf.GetHighlight(idleConf.CurrentTheme(),
+                                                       'linenumber')
+            self.cell00 = tkinter.Frame(self.editwin.text_frame,
+                                        bg=line_number_colors['background'])
+            self.cell00.grid(row=0, column=0, sticky=NSEW)
+            menu_status = 'Hide'
+        else:
+            self.context.destroy()
+            self.context = None
+            self.cell00.destroy()
+            self.cell00 = None
+            self.text.after_cancel(self.t1)
+            self._reset()
+            menu_status = 'Show'
+        self.editwin.update_menu_label(menu='options', index='* Code Context',
+                                       label=f'{menu_status} Code Context')
+        return "break"
+
+    def get_context(self, new_topvisible, stopline=1, stopindent=0):
+        """Return a list of block line tuples and the 'last' indent.
+
+        The tuple fields are (linenum, indent, text, opener).
+        The list represents header lines from new_topvisible back to
+        stopline with successively shorter indents > stopindent.
+        The list is returned ordered by line number.
+        Last indent returned is the smallest indent observed.
+        """
+        assert stopline > 0
+        lines = []
+        # The indentation level we are currently in.
+        lastindent = INFINITY
+        # For a line to be interesting, it must begin with a block opening
+        # keyword, and have less indentation than lastindent.
+        for linenum in range(new_topvisible, stopline-1, -1):
+            codeline = self.text.get(f'{linenum}.0', f'{linenum}.end')
+            indent, text, opener = get_line_info(codeline)
+            if indent < lastindent:
+                lastindent = indent
+                if opener in ("else", "elif"):
+                    # Also show the if statement.
+                    lastindent += 1
+                if opener and linenum < new_topvisible and indent >= stopindent:
+                    lines.append((linenum, indent, text, opener))
+                if lastindent <= stopindent:
+                    break
+        lines.reverse()
+        return lines, lastindent
+
+    def update_code_context(self):
+        """Update context information and lines visible in the context pane.
+
+        No update is done if the text hasn't been scrolled.  If the text
+        was scrolled, the lines that should be shown in the context will
+        be retrieved and the context area will be updated with the code,
+        up to the number of maxlines.
+        """
+        new_topvisible = self.editwin.getlineno("@0,0")
+        if self.topvisible == new_topvisible:      # Haven't scrolled.
+            return
+        if self.topvisible < new_topvisible:       # Scroll down.
+            lines, lastindent = self.get_context(new_topvisible,
+                                                 self.topvisible)
+            # Retain only context info applicable to the region
+            # between topvisible and new_topvisible.
+            while self.info[-1][1] >= lastindent:
+                del self.info[-1]
+        else:  # self.topvisible > new_topvisible: # Scroll up.
+            stopindent = self.info[-1][1] + 1
+            # Retain only context info associated
+            # with lines above new_topvisible.
+            while self.info[-1][0] >= new_topvisible:
+                stopindent = self.info[-1][1]
+                del self.info[-1]
+            lines, lastindent = self.get_context(new_topvisible,
+                                                 self.info[-1][0]+1,
+                                                 stopindent)
+        self.info.extend(lines)
+        self.topvisible = new_topvisible
+        # Last context_depth context lines.
+        context_strings = [x[2] for x in self.info[-self.context_depth:]]
+        showfirst = 0 if context_strings[0] else 1
+        # Update widget.
+        self.context['height'] = len(context_strings) - showfirst
+        self.context['state'] = 'normal'
+        self.context.delete('1.0', 'end')
+        self.context.insert('end', '\n'.join(context_strings[showfirst:]))
+        self.context['state'] = 'disabled'
+
+    def jumptoline(self, event=None):
+        "Show clicked context line at top of editor."
+        lines = len(self.info)
+        if lines == 1:  # No context lines are showing.
+            newtop = 1
+        else:
+            # Line number clicked.
+            contextline = int(float(self.context.index('insert')))
+            # Lines not displayed due to maxlines.
+            offset = max(1, lines - self.context_depth) - 1
+            newtop = self.info[offset + contextline][0]
+        self.text.yview(f'{newtop}.0')
+        self.update_code_context()
+
+    def timer_event(self):
+        "Event on editor text widget triggered every UPDATEINTERVAL ms."
+        if self.context is not None:
+            self.update_code_context()
+            self.t1 = self.text.after(self.UPDATEINTERVAL, self.timer_event)
+
+    def update_font(self):
+        if self.context is not None:
+            font = idleConf.GetFont(self.text, 'main', 'EditorWindow')
+            self.context['font'] = font
+
+    def update_highlight_colors(self):
+        if self.context is not None:
+            colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'context')
+            self.context['background'] = colors['background']
+            self.context['foreground'] = colors['foreground']
+
+        if self.cell00 is not None:
+            line_number_colors = idleConf.GetHighlight(idleConf.CurrentTheme(),
+                                                       'linenumber')
+            self.cell00.config(bg=line_number_colors['background'])
+
+
+CodeContext.reload()
+
+
+if __name__ == "__main__":
+    from unittest import main
+    main('idlelib.idle_test.test_codecontext', verbosity=2, exit=False)
+
+    # Add htest.