Coverage for codecontext.py: 21%
135 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 13:22 -0700
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 13:22 -0700
1"""codecontext - display the block context above the edit window
3Once code has scrolled off the top of a window, it can be difficult to
4determine which block you are in. This extension implements a pane at the top
5of each IDLE edit window which provides block structure hints. These hints are
6the lines which contain the block opening keywords, e.g. 'if', for the
7enclosing block. The number of hint lines is determined by the maxlines
8variable in the codecontext section of config-extensions.def. Lines which do
9not open blocks are not shown in the context hints pane.
11For EditorWindows, <<toggle-code-context>> is bound to CodeContext(self).
12toggle_code_context_event.
13"""
14import re
15from sys import maxsize as INFINITY
17from tkinter import Frame, Text, TclError
18from tkinter.constants import NSEW, SUNKEN
20from idlelib.config import idleConf
22BLOCKOPENERS = {'class', 'def', 'if', 'elif', 'else', 'while', 'for',
23 'try', 'except', 'finally', 'with', 'async'}
26def 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() 1bc
31def get_line_info(codeline):
32 """Return tuple of (line indent value, codeline, block start keyword).
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) 1b
38 indent = len(spaces) 1b
39 if len(codeline) == indent or codeline[indent] == '#': 1b
40 indent = INFINITY 1b
41 opener = firstword in BLOCKOPENERS and firstword 1b
42 return indent, codeline, opener 1b
45class CodeContext:
46 "Display block context above the edit window."
47 UPDATEINTERVAL = 100 # millisec
49 def __init__(self, editwin):
50 """Initialize settings for context block.
52 editwin is the Editor window for the context block.
53 self.text is the editor window text widget.
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.
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()
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)]
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)
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
93 def toggle_code_context_event(self, event=None):
94 """Toggle code context display.
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)
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"
149 def get_context(self, new_topvisible, stopline=1, stopindent=0):
150 """Return a list of block line tuples and the 'last' indent.
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
179 def update_code_context(self):
180 """Update context information and lines visible in the context pane.
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'
219 def jumptoline(self, event=None):
220 """ Show clicked context line at top of editor.
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()
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)
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
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']
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'])
263CodeContext.reload()
266if __name__ == "__main__": 266 ↛ 267line 266 didn't jump to line 267, because the condition on line 266 was never true
267 from unittest import main
268 main('idlelib.idle_test.test_codecontext', verbosity=2, exit=False)
270 # Add htest.