Mercurial > repos > rliterman > csp2
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.