blob: 989b30e5994650c745eccb302c0e24ef636a4522 [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.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000010"""
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000011import re
Kurt B. Kaiser3536a5c2007-12-12 17:22:06 +000012from sys import maxsize as INFINITY
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040013
14import tkinter
Miss Islington (bot)1da6a312019-07-23 06:04:13 -070015from tkinter.constants import NSEW, SUNKEN
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040016
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040017from idlelib.config import idleConf
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000018
Miss Islington (bot)846ca492020-02-28 10:43:25 -080019BLOCKOPENERS = {'class', 'def', 'if', 'elif', 'else', 'while', 'for',
20 'try', 'except', 'finally', 'with', 'async'}
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000021
Cheryl Sabella654038d2018-05-19 15:34:03 -040022
Cheryl Sabella85060162018-05-23 22:18:15 -040023def get_spaces_firstword(codeline, c=re.compile(r"^(\s*)(\w*)")):
24 "Extract the beginning whitespace and first word from codeline."
25 return c.match(codeline).groups()
26
27
28def get_line_info(codeline):
29 """Return tuple of (line indent value, codeline, block start keyword).
30
31 The indentation of empty lines (or comment lines) is INFINITY.
32 If the line does not start a block, the keyword value is False.
33 """
34 spaces, firstword = get_spaces_firstword(codeline)
35 indent = len(spaces)
36 if len(codeline) == indent or codeline[indent] == '#':
37 indent = INFINITY
38 opener = firstword in BLOCKOPENERS and firstword
39 return indent, codeline, opener
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000040
wohlganger58fc71c2017-09-10 16:19:47 -050041
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000042class CodeContext:
Cheryl Sabella654038d2018-05-19 15:34:03 -040043 "Display block context above the edit window."
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -070044 UPDATEINTERVAL = 100 # millisec
Cheryl Sabella654038d2018-05-19 15:34:03 -040045
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000046 def __init__(self, editwin):
Cheryl Sabella654038d2018-05-19 15:34:03 -040047 """Initialize settings for context block.
48
49 editwin is the Editor window for the context block.
50 self.text is the editor window text widget.
Cheryl Sabella654038d2018-05-19 15:34:03 -040051
Cheryl Sabellab609e682018-06-04 11:58:44 -040052 self.context displays the code context text above the editor text.
Cheryl Sabella85060162018-05-23 22:18:15 -040053 Initially None, it is toggled via <<toggle-code-context>>.
Cheryl Sabella654038d2018-05-19 15:34:03 -040054 self.topvisible is the number of the top text line displayed.
55 self.info is a list of (line number, indent level, line text,
56 block keyword) tuples for the block structure above topvisible.
Cheryl Sabella85060162018-05-23 22:18:15 -040057 self.info[0] is initialized with a 'dummy' line which
58 starts the toplevel 'block' of the module.
Cheryl Sabella654038d2018-05-19 15:34:03 -040059
60 self.t1 and self.t2 are two timer events on the editor text widget to
Cheryl Sabella85060162018-05-23 22:18:15 -040061 monitor for changes to the context text or editor font.
Cheryl Sabella654038d2018-05-19 15:34:03 -040062 """
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000063 self.editwin = editwin
64 self.text = editwin.text
Miss Islington (bot)86eb5da2019-07-18 13:47:26 -070065 self._reset()
66
67 def _reset(self):
Cheryl Sabellab609e682018-06-04 11:58:44 -040068 self.context = None
Miss Islington (bot)1da6a312019-07-23 06:04:13 -070069 self.cell00 = None
Miss Islington (bot)86eb5da2019-07-18 13:47:26 -070070 self.t1 = None
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000071 self.topvisible = 1
Cheryl Sabella654038d2018-05-19 15:34:03 -040072 self.info = [(0, -1, "", False)]
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000073
wohlganger58fc71c2017-09-10 16:19:47 -050074 @classmethod
75 def reload(cls):
Cheryl Sabella654038d2018-05-19 15:34:03 -040076 "Load class variables from config."
wohlganger58fc71c2017-09-10 16:19:47 -050077 cls.context_depth = idleConf.GetOption("extensions", "CodeContext",
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -070078 "maxlines", type="int",
79 default=15)
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040080
81 def __del__(self):
Cheryl Sabella654038d2018-05-19 15:34:03 -040082 "Cancel scheduled events."
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -070083 if self.t1 is not None:
84 try:
85 self.text.after_cancel(self.t1)
Miss Islington (bot)846ca492020-02-28 10:43:25 -080086 except tkinter.TclError: # pragma: no cover
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -070087 pass
88 self.t1 = None
wohlganger58fc71c2017-09-10 16:19:47 -050089
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000090 def toggle_code_context_event(self, event=None):
Cheryl Sabella654038d2018-05-19 15:34:03 -040091 """Toggle code context display.
92
Cheryl Sabellab609e682018-06-04 11:58:44 -040093 If self.context doesn't exist, create it to match the size of the editor
Cheryl Sabella654038d2018-05-19 15:34:03 -040094 window text (toggle on). If it does exist, destroy it (toggle off).
95 Return 'break' to complete the processing of the binding.
96 """
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -070097 if self.context is None:
Thomas Wouterscf297e42007-02-23 15:07:44 +000098 # Calculate the border width and horizontal padding required to
99 # align the context with the text in the main Text widget.
Thomas Wouters89f507f2006-12-13 04:49:30 +0000100 #
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300101 # All values are passed through getint(), since some
Thomas Wouterscf297e42007-02-23 15:07:44 +0000102 # values may be pixel objects, which can't simply be added to ints.
103 widgets = self.editwin.text, self.editwin.text_frame
Cheryl Sabella85060162018-05-23 22:18:15 -0400104 # Calculate the required horizontal padding and border width.
Thomas Wouters89f507f2006-12-13 04:49:30 +0000105 padx = 0
Cheryl Sabella85060162018-05-23 22:18:15 -0400106 border = 0
Thomas Wouterscf297e42007-02-23 15:07:44 +0000107 for widget in widgets:
Miss Islington (bot)1da6a312019-07-23 06:04:13 -0700108 info = (widget.grid_info()
109 if widget is self.editwin.text
110 else widget.pack_info())
111 padx += widget.tk.getint(info['padx'])
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300112 padx += widget.tk.getint(widget.cget('padx'))
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300113 border += widget.tk.getint(widget.cget('border'))
Miss Islington (bot)846ca492020-02-28 10:43:25 -0800114 context = self.context = tkinter.Text(
Miss Islington (bot)1da6a312019-07-23 06:04:13 -0700115 self.editwin.text_frame,
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -0700116 height=1,
117 width=1, # Don't request more than we get.
Miss Islington (bot)1da6a312019-07-23 06:04:13 -0700118 highlightthickness=0,
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -0700119 padx=padx, border=border, relief=SUNKEN, state='disabled')
Miss Islington (bot)1da6a312019-07-23 06:04:13 -0700120 self.update_font()
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -0700121 self.update_highlight_colors()
Miss Islington (bot)846ca492020-02-28 10:43:25 -0800122 context.bind('<ButtonRelease-1>', self.jumptoline)
Miss Islington (bot)86eb5da2019-07-18 13:47:26 -0700123 # Get the current context and initiate the recurring update event.
124 self.timer_event()
Miss Islington (bot)1da6a312019-07-23 06:04:13 -0700125 # Grid the context widget above the text widget.
Miss Islington (bot)846ca492020-02-28 10:43:25 -0800126 context.grid(row=0, column=1, sticky=NSEW)
Miss Islington (bot)1da6a312019-07-23 06:04:13 -0700127
128 line_number_colors = idleConf.GetHighlight(idleConf.CurrentTheme(),
129 'linenumber')
130 self.cell00 = tkinter.Frame(self.editwin.text_frame,
131 bg=line_number_colors['background'])
132 self.cell00.grid(row=0, column=0, sticky=NSEW)
Cheryl Sabellac1b4b0f2018-12-22 01:25:45 -0500133 menu_status = 'Hide'
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000134 else:
Cheryl Sabellab609e682018-06-04 11:58:44 -0400135 self.context.destroy()
Miss Islington (bot)1da6a312019-07-23 06:04:13 -0700136 self.context = None
137 self.cell00.destroy()
138 self.cell00 = None
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -0700139 self.text.after_cancel(self.t1)
Miss Islington (bot)86eb5da2019-07-18 13:47:26 -0700140 self._reset()
Cheryl Sabellac1b4b0f2018-12-22 01:25:45 -0500141 menu_status = 'Show'
142 self.editwin.update_menu_label(menu='options', index='* Code Context',
143 label=f'{menu_status} Code Context')
Serhiy Storchaka213ce122017-06-27 07:02:32 +0300144 return "break"
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000145
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000146 def get_context(self, new_topvisible, stopline=1, stopindent=0):
Cheryl Sabella654038d2018-05-19 15:34:03 -0400147 """Return a list of block line tuples and the 'last' indent.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000148
Cheryl Sabella654038d2018-05-19 15:34:03 -0400149 The tuple fields are (linenum, indent, text, opener).
150 The list represents header lines from new_topvisible back to
151 stopline with successively shorter indents > stopindent.
152 The list is returned ordered by line number.
153 Last indent returned is the smallest indent observed.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000154 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000155 assert stopline > 0
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000156 lines = []
Cheryl Sabella85060162018-05-23 22:18:15 -0400157 # The indentation level we are currently in.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000158 lastindent = INFINITY
159 # For a line to be interesting, it must begin with a block opening
160 # keyword, and have less indentation than lastindent.
Guido van Rossum805365e2007-05-07 22:24:25 +0000161 for linenum in range(new_topvisible, stopline-1, -1):
Cheryl Sabella85060162018-05-23 22:18:15 -0400162 codeline = self.text.get(f'{linenum}.0', f'{linenum}.end')
163 indent, text, opener = get_line_info(codeline)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000164 if indent < lastindent:
165 lastindent = indent
Kurt B. Kaisere3636e02004-04-26 22:26:04 +0000166 if opener in ("else", "elif"):
Cheryl Sabella85060162018-05-23 22:18:15 -0400167 # Also show the if statement.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000168 lastindent += 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000169 if opener and linenum < new_topvisible and indent >= stopindent:
170 lines.append((linenum, indent, text, opener))
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000171 if lastindent <= stopindent:
172 break
173 lines.reverse()
174 return lines, lastindent
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000175
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000176 def update_code_context(self):
177 """Update context information and lines visible in the context pane.
178
Cheryl Sabella654038d2018-05-19 15:34:03 -0400179 No update is done if the text hasn't been scrolled. If the text
180 was scrolled, the lines that should be shown in the context will
Cheryl Sabellab609e682018-06-04 11:58:44 -0400181 be retrieved and the context area will be updated with the code,
182 up to the number of maxlines.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000183 """
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -0700184 new_topvisible = self.editwin.getlineno("@0,0")
Cheryl Sabella85060162018-05-23 22:18:15 -0400185 if self.topvisible == new_topvisible: # Haven't scrolled.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000186 return
Cheryl Sabella85060162018-05-23 22:18:15 -0400187 if self.topvisible < new_topvisible: # Scroll down.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000188 lines, lastindent = self.get_context(new_topvisible,
189 self.topvisible)
Cheryl Sabella85060162018-05-23 22:18:15 -0400190 # Retain only context info applicable to the region
191 # between topvisible and new_topvisible.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000192 while self.info[-1][1] >= lastindent:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000193 del self.info[-1]
Cheryl Sabella85060162018-05-23 22:18:15 -0400194 else: # self.topvisible > new_topvisible: # Scroll up.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000195 stopindent = self.info[-1][1] + 1
Cheryl Sabella85060162018-05-23 22:18:15 -0400196 # Retain only context info associated
197 # with lines above new_topvisible.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000198 while self.info[-1][0] >= new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000199 stopindent = self.info[-1][1]
200 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000201 lines, lastindent = self.get_context(new_topvisible,
202 self.info[-1][0]+1,
203 stopindent)
204 self.info.extend(lines)
205 self.topvisible = new_topvisible
Cheryl Sabella29996a12018-06-01 19:23:00 -0400206 # Last context_depth context lines.
207 context_strings = [x[2] for x in self.info[-self.context_depth:]]
208 showfirst = 0 if context_strings[0] else 1
Cheryl Sabellab609e682018-06-04 11:58:44 -0400209 # Update widget.
210 self.context['height'] = len(context_strings) - showfirst
211 self.context['state'] = 'normal'
212 self.context.delete('1.0', 'end')
213 self.context.insert('end', '\n'.join(context_strings[showfirst:]))
214 self.context['state'] = 'disabled'
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000215
Cheryl Sabella041272b2018-06-08 01:21:15 -0400216 def jumptoline(self, event=None):
Miss Islington (bot)846ca492020-02-28 10:43:25 -0800217 """ Show clicked context line at top of editor.
218
219 If a selection was made, don't jump; allow copying.
220 If no visible context, show the top line of the file.
221 """
222 try:
223 self.context.index("sel.first")
224 except tkinter.TclError:
225 lines = len(self.info)
226 if lines == 1: # No context lines are showing.
227 newtop = 1
228 else:
229 # Line number clicked.
230 contextline = int(float(self.context.index('insert')))
231 # Lines not displayed due to maxlines.
232 offset = max(1, lines - self.context_depth) - 1
233 newtop = self.info[offset + contextline][0]
234 self.text.yview(f'{newtop}.0')
235 self.update_code_context()
Cheryl Sabella041272b2018-06-08 01:21:15 -0400236
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000237 def timer_event(self):
Cheryl Sabella654038d2018-05-19 15:34:03 -0400238 "Event on editor text widget triggered every UPDATEINTERVAL ms."
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -0700239 if self.context is not None:
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000240 self.update_code_context()
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -0700241 self.t1 = self.text.after(self.UPDATEINTERVAL, self.timer_event)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000242
Miss Islington (bot)1da6a312019-07-23 06:04:13 -0700243 def update_font(self):
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -0700244 if self.context is not None:
Miss Islington (bot)1da6a312019-07-23 06:04:13 -0700245 font = idleConf.GetFont(self.text, 'main', 'EditorWindow')
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -0700246 self.context['font'] = font
247
248 def update_highlight_colors(self):
249 if self.context is not None:
250 colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'context')
251 self.context['background'] = colors['background']
252 self.context['foreground'] = colors['foreground']
wohlganger58fc71c2017-09-10 16:19:47 -0500253
Miss Islington (bot)1da6a312019-07-23 06:04:13 -0700254 if self.cell00 is not None:
255 line_number_colors = idleConf.GetHighlight(idleConf.CurrentTheme(),
256 'linenumber')
257 self.cell00.config(bg=line_number_colors['background'])
258
wohlganger58fc71c2017-09-10 16:19:47 -0500259
260CodeContext.reload()
Cheryl Sabella654038d2018-05-19 15:34:03 -0400261
262
Terry Jan Reedy4d921582018-06-19 19:12:52 -0400263if __name__ == "__main__":
264 from unittest import main
265 main('idlelib.idle_test.test_codecontext', verbosity=2, exit=False)
266
267 # Add htest.