blob: 4ce98136fe417592ba0e1aa05146ae12b6742007 [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
11"""
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000012import re
Kurt B. Kaiser3536a5c2007-12-12 17:22:06 +000013from sys import maxsize as INFINITY
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040014
15import tkinter
Miss Islington (bot)1da6a312019-07-23 06:04:13 -070016from tkinter.constants import NSEW, SUNKEN
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040017
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040018from idlelib.config import idleConf
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000019
Terry Jan Reedy049882e2014-12-11 05:33:36 -050020BLOCKOPENERS = {"class", "def", "elif", "else", "except", "finally", "for",
Terry Jan Reedyd89ca942018-05-17 20:38:41 -040021 "if", "try", "while", "with", "async"}
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000022
Cheryl Sabella654038d2018-05-19 15:34:03 -040023
Cheryl Sabella85060162018-05-23 22:18:15 -040024def 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
29def 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
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000041
wohlganger58fc71c2017-09-10 16:19:47 -050042
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000043class CodeContext:
Cheryl Sabella654038d2018-05-19 15:34:03 -040044 "Display block context above the edit window."
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -070045 UPDATEINTERVAL = 100 # millisec
Cheryl Sabella654038d2018-05-19 15:34:03 -040046
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000047 def __init__(self, editwin):
Cheryl Sabella654038d2018-05-19 15:34:03 -040048 """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.
Cheryl Sabella654038d2018-05-19 15:34:03 -040052
Cheryl Sabellab609e682018-06-04 11:58:44 -040053 self.context displays the code context text above the editor text.
Cheryl Sabella85060162018-05-23 22:18:15 -040054 Initially None, it is toggled via <<toggle-code-context>>.
Cheryl Sabella654038d2018-05-19 15:34:03 -040055 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.
Cheryl Sabella85060162018-05-23 22:18:15 -040058 self.info[0] is initialized with a 'dummy' line which
59 starts the toplevel 'block' of the module.
Cheryl Sabella654038d2018-05-19 15:34:03 -040060
61 self.t1 and self.t2 are two timer events on the editor text widget to
Cheryl Sabella85060162018-05-23 22:18:15 -040062 monitor for changes to the context text or editor font.
Cheryl Sabella654038d2018-05-19 15:34:03 -040063 """
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000064 self.editwin = editwin
65 self.text = editwin.text
Miss Islington (bot)86eb5da2019-07-18 13:47:26 -070066 self._reset()
67
68 def _reset(self):
Cheryl Sabellab609e682018-06-04 11:58:44 -040069 self.context = None
Miss Islington (bot)1da6a312019-07-23 06:04:13 -070070 self.cell00 = None
Miss Islington (bot)86eb5da2019-07-18 13:47:26 -070071 self.t1 = None
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000072 self.topvisible = 1
Cheryl Sabella654038d2018-05-19 15:34:03 -040073 self.info = [(0, -1, "", False)]
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000074
wohlganger58fc71c2017-09-10 16:19:47 -050075 @classmethod
76 def reload(cls):
Cheryl Sabella654038d2018-05-19 15:34:03 -040077 "Load class variables from config."
wohlganger58fc71c2017-09-10 16:19:47 -050078 cls.context_depth = idleConf.GetOption("extensions", "CodeContext",
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -070079 "maxlines", type="int",
80 default=15)
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040081
82 def __del__(self):
Cheryl Sabella654038d2018-05-19 15:34:03 -040083 "Cancel scheduled events."
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -070084 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
wohlganger58fc71c2017-09-10 16:19:47 -050090
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000091 def toggle_code_context_event(self, event=None):
Cheryl Sabella654038d2018-05-19 15:34:03 -040092 """Toggle code context display.
93
Cheryl Sabellab609e682018-06-04 11:58:44 -040094 If self.context doesn't exist, create it to match the size of the editor
Cheryl Sabella654038d2018-05-19 15:34:03 -040095 window text (toggle on). If it does exist, destroy it (toggle off).
96 Return 'break' to complete the processing of the binding.
97 """
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -070098 if self.context is None:
Thomas Wouterscf297e42007-02-23 15:07:44 +000099 # Calculate the border width and horizontal padding required to
100 # align the context with the text in the main Text widget.
Thomas Wouters89f507f2006-12-13 04:49:30 +0000101 #
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300102 # All values are passed through getint(), since some
Thomas Wouterscf297e42007-02-23 15:07:44 +0000103 # values may be pixel objects, which can't simply be added to ints.
104 widgets = self.editwin.text, self.editwin.text_frame
Cheryl Sabella85060162018-05-23 22:18:15 -0400105 # Calculate the required horizontal padding and border width.
Thomas Wouters89f507f2006-12-13 04:49:30 +0000106 padx = 0
Cheryl Sabella85060162018-05-23 22:18:15 -0400107 border = 0
Thomas Wouterscf297e42007-02-23 15:07:44 +0000108 for widget in widgets:
Miss Islington (bot)1da6a312019-07-23 06:04:13 -0700109 info = (widget.grid_info()
110 if widget is self.editwin.text
111 else widget.pack_info())
112 padx += widget.tk.getint(info['padx'])
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300113 padx += widget.tk.getint(widget.cget('padx'))
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300114 border += widget.tk.getint(widget.cget('border'))
Cheryl Sabellab609e682018-06-04 11:58:44 -0400115 self.context = tkinter.Text(
Miss Islington (bot)1da6a312019-07-23 06:04:13 -0700116 self.editwin.text_frame,
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -0700117 height=1,
118 width=1, # Don't request more than we get.
Miss Islington (bot)1da6a312019-07-23 06:04:13 -0700119 highlightthickness=0,
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -0700120 padx=padx, border=border, relief=SUNKEN, state='disabled')
Miss Islington (bot)1da6a312019-07-23 06:04:13 -0700121 self.update_font()
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -0700122 self.update_highlight_colors()
Cheryl Sabella041272b2018-06-08 01:21:15 -0400123 self.context.bind('<ButtonRelease-1>', self.jumptoline)
Miss Islington (bot)86eb5da2019-07-18 13:47:26 -0700124 # Get the current context and initiate the recurring update event.
125 self.timer_event()
Miss Islington (bot)1da6a312019-07-23 06:04:13 -0700126 # 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)
Cheryl Sabellac1b4b0f2018-12-22 01:25:45 -0500134 menu_status = 'Hide'
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000135 else:
Cheryl Sabellab609e682018-06-04 11:58:44 -0400136 self.context.destroy()
Miss Islington (bot)1da6a312019-07-23 06:04:13 -0700137 self.context = None
138 self.cell00.destroy()
139 self.cell00 = None
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -0700140 self.text.after_cancel(self.t1)
Miss Islington (bot)86eb5da2019-07-18 13:47:26 -0700141 self._reset()
Cheryl Sabellac1b4b0f2018-12-22 01:25:45 -0500142 menu_status = 'Show'
143 self.editwin.update_menu_label(menu='options', index='* Code Context',
144 label=f'{menu_status} Code Context')
Serhiy Storchaka213ce122017-06-27 07:02:32 +0300145 return "break"
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000146
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000147 def get_context(self, new_topvisible, stopline=1, stopindent=0):
Cheryl Sabella654038d2018-05-19 15:34:03 -0400148 """Return a list of block line tuples and the 'last' indent.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000149
Cheryl Sabella654038d2018-05-19 15:34:03 -0400150 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.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000155 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000156 assert stopline > 0
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000157 lines = []
Cheryl Sabella85060162018-05-23 22:18:15 -0400158 # The indentation level we are currently in.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000159 lastindent = INFINITY
160 # For a line to be interesting, it must begin with a block opening
161 # keyword, and have less indentation than lastindent.
Guido van Rossum805365e2007-05-07 22:24:25 +0000162 for linenum in range(new_topvisible, stopline-1, -1):
Cheryl Sabella85060162018-05-23 22:18:15 -0400163 codeline = self.text.get(f'{linenum}.0', f'{linenum}.end')
164 indent, text, opener = get_line_info(codeline)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000165 if indent < lastindent:
166 lastindent = indent
Kurt B. Kaisere3636e02004-04-26 22:26:04 +0000167 if opener in ("else", "elif"):
Cheryl Sabella85060162018-05-23 22:18:15 -0400168 # Also show the if statement.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000169 lastindent += 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000170 if opener and linenum < new_topvisible and indent >= stopindent:
171 lines.append((linenum, indent, text, opener))
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000172 if lastindent <= stopindent:
173 break
174 lines.reverse()
175 return lines, lastindent
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000176
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000177 def update_code_context(self):
178 """Update context information and lines visible in the context pane.
179
Cheryl Sabella654038d2018-05-19 15:34:03 -0400180 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
Cheryl Sabellab609e682018-06-04 11:58:44 -0400182 be retrieved and the context area will be updated with the code,
183 up to the number of maxlines.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000184 """
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -0700185 new_topvisible = self.editwin.getlineno("@0,0")
Cheryl Sabella85060162018-05-23 22:18:15 -0400186 if self.topvisible == new_topvisible: # Haven't scrolled.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000187 return
Cheryl Sabella85060162018-05-23 22:18:15 -0400188 if self.topvisible < new_topvisible: # Scroll down.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000189 lines, lastindent = self.get_context(new_topvisible,
190 self.topvisible)
Cheryl Sabella85060162018-05-23 22:18:15 -0400191 # Retain only context info applicable to the region
192 # between topvisible and new_topvisible.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000193 while self.info[-1][1] >= lastindent:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000194 del self.info[-1]
Cheryl Sabella85060162018-05-23 22:18:15 -0400195 else: # self.topvisible > new_topvisible: # Scroll up.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000196 stopindent = self.info[-1][1] + 1
Cheryl Sabella85060162018-05-23 22:18:15 -0400197 # Retain only context info associated
198 # with lines above new_topvisible.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000199 while self.info[-1][0] >= new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000200 stopindent = self.info[-1][1]
201 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000202 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
Cheryl Sabella29996a12018-06-01 19:23:00 -0400207 # 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
Cheryl Sabellab609e682018-06-04 11:58:44 -0400210 # 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'
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000216
Cheryl Sabella041272b2018-06-08 01:21:15 -0400217 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
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000231 def timer_event(self):
Cheryl Sabella654038d2018-05-19 15:34:03 -0400232 "Event on editor text widget triggered every UPDATEINTERVAL ms."
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -0700233 if self.context is not None:
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000234 self.update_code_context()
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -0700235 self.t1 = self.text.after(self.UPDATEINTERVAL, self.timer_event)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000236
Miss Islington (bot)1da6a312019-07-23 06:04:13 -0700237 def update_font(self):
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -0700238 if self.context is not None:
Miss Islington (bot)1da6a312019-07-23 06:04:13 -0700239 font = idleConf.GetFont(self.text, 'main', 'EditorWindow')
Miss Islington (bot)6cf7c452019-07-17 01:44:55 -0700240 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']
wohlganger58fc71c2017-09-10 16:19:47 -0500247
Miss Islington (bot)1da6a312019-07-23 06:04:13 -0700248 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
wohlganger58fc71c2017-09-10 16:19:47 -0500253
254CodeContext.reload()
Cheryl Sabella654038d2018-05-19 15:34:03 -0400255
256
Terry Jan Reedy4d921582018-06-19 19:12:52 -0400257if __name__ == "__main__":
258 from unittest import main
259 main('idlelib.idle_test.test_codecontext', verbosity=2, exit=False)
260
261 # Add htest.