• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 CodeContext:
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.
271