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