blob: f2f44f5f8d4e6112c75a4845e89ba005245abd9b [file] [log] [blame]
wohlganger58fc71c2017-09-10 16:19:47 -05001"""codecontext - display the block context above the edit window
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +00002
Kurt B. Kaiser0a135792005-10-03 19:26:03 +00003Once 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
Cheryl Sabella29996a12018-06-01 19:23:00 -04007enclosing block. The number of hint lines is determined by the maxlines
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -04008variable in the codecontext section of config-extensions.def. Lines which do
Kurt B. Kaiser0a135792005-10-03 19:26:03 +00009not open blocks are not shown in the context hints pane.
Terry Jan Reedy6f79e602020-12-15 00:24:01 -050010
11For EditorWindows, <<toggle-code-context>> is bound to CodeContext(self).
12toggle_code_context_event.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000013"""
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000014import re
Kurt B. Kaiser3536a5c2007-12-12 17:22:06 +000015from sys import maxsize as INFINITY
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040016
Terry Jan Reedy6f79e602020-12-15 00:24:01 -050017from tkinter import Frame, Text, TclError
Tal Einat7123ea02019-07-23 15:22:11 +030018from tkinter.constants import NSEW, SUNKEN
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040019
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040020from idlelib.config import idleConf
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000021
Terry Jan Reedyc705fd12020-02-28 13:22:55 -050022BLOCKOPENERS = {'class', 'def', 'if', 'elif', 'else', 'while', 'for',
23 'try', 'except', 'finally', 'with', 'async'}
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000024
Cheryl Sabella654038d2018-05-19 15:34:03 -040025
Cheryl Sabella85060162018-05-23 22:18:15 -040026def 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
31def 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
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000043
wohlganger58fc71c2017-09-10 16:19:47 -050044
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000045class CodeContext:
Cheryl Sabella654038d2018-05-19 15:34:03 -040046 "Display block context above the edit window."
Tal Einat7036e1d2019-07-17 11:15:53 +030047 UPDATEINTERVAL = 100 # millisec
Cheryl Sabella654038d2018-05-19 15:34:03 -040048
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000049 def __init__(self, editwin):
Cheryl Sabella654038d2018-05-19 15:34:03 -040050 """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.
Cheryl Sabella654038d2018-05-19 15:34:03 -040054
Cheryl Sabellab609e682018-06-04 11:58:44 -040055 self.context displays the code context text above the editor text.
Cheryl Sabella85060162018-05-23 22:18:15 -040056 Initially None, it is toggled via <<toggle-code-context>>.
Cheryl Sabella654038d2018-05-19 15:34:03 -040057 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.
Cheryl Sabella85060162018-05-23 22:18:15 -040060 self.info[0] is initialized with a 'dummy' line which
61 starts the toplevel 'block' of the module.
Cheryl Sabella654038d2018-05-19 15:34:03 -040062
63 self.t1 and self.t2 are two timer events on the editor text widget to
Cheryl Sabella85060162018-05-23 22:18:15 -040064 monitor for changes to the context text or editor font.
Cheryl Sabella654038d2018-05-19 15:34:03 -040065 """
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000066 self.editwin = editwin
67 self.text = editwin.text
Tal Einate0a1f8f2019-07-18 23:03:18 +030068 self._reset()
69
70 def _reset(self):
Cheryl Sabellab609e682018-06-04 11:58:44 -040071 self.context = None
Tal Einat7123ea02019-07-23 15:22:11 +030072 self.cell00 = None
Tal Einate0a1f8f2019-07-18 23:03:18 +030073 self.t1 = None
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000074 self.topvisible = 1
Cheryl Sabella654038d2018-05-19 15:34:03 -040075 self.info = [(0, -1, "", False)]
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000076
wohlganger58fc71c2017-09-10 16:19:47 -050077 @classmethod
78 def reload(cls):
Cheryl Sabella654038d2018-05-19 15:34:03 -040079 "Load class variables from config."
wohlganger58fc71c2017-09-10 16:19:47 -050080 cls.context_depth = idleConf.GetOption("extensions", "CodeContext",
Tal Einat7036e1d2019-07-17 11:15:53 +030081 "maxlines", type="int",
82 default=15)
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040083
84 def __del__(self):
Cheryl Sabella654038d2018-05-19 15:34:03 -040085 "Cancel scheduled events."
Tal Einat7036e1d2019-07-17 11:15:53 +030086 if self.t1 is not None:
87 try:
88 self.text.after_cancel(self.t1)
Terry Jan Reedy6f79e602020-12-15 00:24:01 -050089 except TclError: # pragma: no cover
Tal Einat7036e1d2019-07-17 11:15:53 +030090 pass
91 self.t1 = None
wohlganger58fc71c2017-09-10 16:19:47 -050092
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000093 def toggle_code_context_event(self, event=None):
Cheryl Sabella654038d2018-05-19 15:34:03 -040094 """Toggle code context display.
95
Cheryl Sabellab609e682018-06-04 11:58:44 -040096 If self.context doesn't exist, create it to match the size of the editor
Cheryl Sabella654038d2018-05-19 15:34:03 -040097 window text (toggle on). If it does exist, destroy it (toggle off).
98 Return 'break' to complete the processing of the binding.
99 """
Tal Einat7036e1d2019-07-17 11:15:53 +0300100 if self.context is None:
Thomas Wouterscf297e42007-02-23 15:07:44 +0000101 # Calculate the border width and horizontal padding required to
102 # align the context with the text in the main Text widget.
Thomas Wouters89f507f2006-12-13 04:49:30 +0000103 #
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300104 # All values are passed through getint(), since some
Thomas Wouterscf297e42007-02-23 15:07:44 +0000105 # values may be pixel objects, which can't simply be added to ints.
106 widgets = self.editwin.text, self.editwin.text_frame
Cheryl Sabella85060162018-05-23 22:18:15 -0400107 # Calculate the required horizontal padding and border width.
Thomas Wouters89f507f2006-12-13 04:49:30 +0000108 padx = 0
Cheryl Sabella85060162018-05-23 22:18:15 -0400109 border = 0
Thomas Wouterscf297e42007-02-23 15:07:44 +0000110 for widget in widgets:
Tal Einat7123ea02019-07-23 15:22:11 +0300111 info = (widget.grid_info()
112 if widget is self.editwin.text
113 else widget.pack_info())
114 padx += widget.tk.getint(info['padx'])
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300115 padx += widget.tk.getint(widget.cget('padx'))
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300116 border += widget.tk.getint(widget.cget('border'))
Terry Jan Reedy6f79e602020-12-15 00:24:01 -0500117 context = self.context = Text(
Tal Einat7123ea02019-07-23 15:22:11 +0300118 self.editwin.text_frame,
Tal Einat7036e1d2019-07-17 11:15:53 +0300119 height=1,
120 width=1, # Don't request more than we get.
Tal Einat7123ea02019-07-23 15:22:11 +0300121 highlightthickness=0,
Tal Einat7036e1d2019-07-17 11:15:53 +0300122 padx=padx, border=border, relief=SUNKEN, state='disabled')
Tal Einat7123ea02019-07-23 15:22:11 +0300123 self.update_font()
Tal Einat7036e1d2019-07-17 11:15:53 +0300124 self.update_highlight_colors()
Terry Jan Reedyc705fd12020-02-28 13:22:55 -0500125 context.bind('<ButtonRelease-1>', self.jumptoline)
Tal Einate0a1f8f2019-07-18 23:03:18 +0300126 # Get the current context and initiate the recurring update event.
127 self.timer_event()
Tal Einat7123ea02019-07-23 15:22:11 +0300128 # Grid the context widget above the text widget.
Terry Jan Reedyc705fd12020-02-28 13:22:55 -0500129 context.grid(row=0, column=1, sticky=NSEW)
Tal Einat7123ea02019-07-23 15:22:11 +0300130
131 line_number_colors = idleConf.GetHighlight(idleConf.CurrentTheme(),
132 'linenumber')
Terry Jan Reedy6f79e602020-12-15 00:24:01 -0500133 self.cell00 = Frame(self.editwin.text_frame,
Tal Einat7123ea02019-07-23 15:22:11 +0300134 bg=line_number_colors['background'])
135 self.cell00.grid(row=0, column=0, sticky=NSEW)
Cheryl Sabellac1b4b0f2018-12-22 01:25:45 -0500136 menu_status = 'Hide'
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000137 else:
Cheryl Sabellab609e682018-06-04 11:58:44 -0400138 self.context.destroy()
Tal Einat7123ea02019-07-23 15:22:11 +0300139 self.context = None
140 self.cell00.destroy()
141 self.cell00 = None
Tal Einat7036e1d2019-07-17 11:15:53 +0300142 self.text.after_cancel(self.t1)
Tal Einate0a1f8f2019-07-18 23:03:18 +0300143 self._reset()
Cheryl Sabellac1b4b0f2018-12-22 01:25:45 -0500144 menu_status = 'Show'
Zackery Spytz23a567c2021-01-28 16:13:22 -0700145 self.editwin.update_menu_label(menu='options', index='*ode*ontext',
Cheryl Sabellac1b4b0f2018-12-22 01:25:45 -0500146 label=f'{menu_status} Code Context')
Serhiy Storchaka213ce122017-06-27 07:02:32 +0300147 return "break"
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000148
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000149 def get_context(self, new_topvisible, stopline=1, stopindent=0):
Cheryl Sabella654038d2018-05-19 15:34:03 -0400150 """Return a list of block line tuples and the 'last' indent.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000151
Cheryl Sabella654038d2018-05-19 15:34:03 -0400152 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.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000157 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000158 assert stopline > 0
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000159 lines = []
Cheryl Sabella85060162018-05-23 22:18:15 -0400160 # The indentation level we are currently in.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000161 lastindent = INFINITY
162 # For a line to be interesting, it must begin with a block opening
163 # keyword, and have less indentation than lastindent.
Guido van Rossum805365e2007-05-07 22:24:25 +0000164 for linenum in range(new_topvisible, stopline-1, -1):
Cheryl Sabella85060162018-05-23 22:18:15 -0400165 codeline = self.text.get(f'{linenum}.0', f'{linenum}.end')
166 indent, text, opener = get_line_info(codeline)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000167 if indent < lastindent:
168 lastindent = indent
Kurt B. Kaisere3636e02004-04-26 22:26:04 +0000169 if opener in ("else", "elif"):
Cheryl Sabella85060162018-05-23 22:18:15 -0400170 # Also show the if statement.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000171 lastindent += 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000172 if opener and linenum < new_topvisible and indent >= stopindent:
173 lines.append((linenum, indent, text, opener))
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000174 if lastindent <= stopindent:
175 break
176 lines.reverse()
177 return lines, lastindent
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000178
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000179 def update_code_context(self):
180 """Update context information and lines visible in the context pane.
181
Cheryl Sabella654038d2018-05-19 15:34:03 -0400182 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
Cheryl Sabellab609e682018-06-04 11:58:44 -0400184 be retrieved and the context area will be updated with the code,
185 up to the number of maxlines.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000186 """
Tal Einat7036e1d2019-07-17 11:15:53 +0300187 new_topvisible = self.editwin.getlineno("@0,0")
Cheryl Sabella85060162018-05-23 22:18:15 -0400188 if self.topvisible == new_topvisible: # Haven't scrolled.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000189 return
Cheryl Sabella85060162018-05-23 22:18:15 -0400190 if self.topvisible < new_topvisible: # Scroll down.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000191 lines, lastindent = self.get_context(new_topvisible,
192 self.topvisible)
Cheryl Sabella85060162018-05-23 22:18:15 -0400193 # Retain only context info applicable to the region
194 # between topvisible and new_topvisible.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000195 while self.info[-1][1] >= lastindent:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000196 del self.info[-1]
Cheryl Sabella85060162018-05-23 22:18:15 -0400197 else: # self.topvisible > new_topvisible: # Scroll up.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000198 stopindent = self.info[-1][1] + 1
Cheryl Sabella85060162018-05-23 22:18:15 -0400199 # Retain only context info associated
200 # with lines above new_topvisible.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000201 while self.info[-1][0] >= new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000202 stopindent = self.info[-1][1]
203 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000204 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
Cheryl Sabella29996a12018-06-01 19:23:00 -0400209 # 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
Cheryl Sabellab609e682018-06-04 11:58:44 -0400212 # 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'
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000218
Cheryl Sabella041272b2018-06-08 01:21:15 -0400219 def jumptoline(self, event=None):
Terry Jan Reedyc705fd12020-02-28 13:22:55 -0500220 """ 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")
Terry Jan Reedy6f79e602020-12-15 00:24:01 -0500227 except TclError:
Terry Jan Reedyc705fd12020-02-28 13:22:55 -0500228 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()
Cheryl Sabella041272b2018-06-08 01:21:15 -0400239
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000240 def timer_event(self):
Cheryl Sabella654038d2018-05-19 15:34:03 -0400241 "Event on editor text widget triggered every UPDATEINTERVAL ms."
Tal Einat7036e1d2019-07-17 11:15:53 +0300242 if self.context is not None:
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000243 self.update_code_context()
Tal Einat7036e1d2019-07-17 11:15:53 +0300244 self.t1 = self.text.after(self.UPDATEINTERVAL, self.timer_event)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000245
Tal Einat7123ea02019-07-23 15:22:11 +0300246 def update_font(self):
Tal Einat7036e1d2019-07-17 11:15:53 +0300247 if self.context is not None:
Tal Einat7123ea02019-07-23 15:22:11 +0300248 font = idleConf.GetFont(self.text, 'main', 'EditorWindow')
Tal Einat7036e1d2019-07-17 11:15:53 +0300249 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']
wohlganger58fc71c2017-09-10 16:19:47 -0500256
Tal Einat7123ea02019-07-23 15:22:11 +0300257 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
wohlganger58fc71c2017-09-10 16:19:47 -0500262
263CodeContext.reload()
Cheryl Sabella654038d2018-05-19 15:34:03 -0400264
265
Terry Jan Reedy4d921582018-06-19 19:12:52 -0400266if __name__ == "__main__":
267 from unittest import main
268 main('idlelib.idle_test.test_codecontext', verbosity=2, exit=False)
269
270 # Add htest.