blob: 9bd0fa1753fc6b06947eb5304a0312be6985a458 [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
Srinivas Thatiparthy (శ్రీనివాస్ తాటిపర్తి)43a74ab2018-11-10 12:15:31 +053016from tkinter.constants import TOP, X, 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."
Tal Einat7036e1d2019-07-17 11:15:53 +030045 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
Cheryl Sabellab609e682018-06-04 11:58:44 -040066 self.context = None
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000067 self.topvisible = 1
Cheryl Sabella654038d2018-05-19 15:34:03 -040068 self.info = [(0, -1, "", False)]
Tal Einat7036e1d2019-07-17 11:15:53 +030069 self.t1 = None
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000070
wohlganger58fc71c2017-09-10 16:19:47 -050071 @classmethod
72 def reload(cls):
Cheryl Sabella654038d2018-05-19 15:34:03 -040073 "Load class variables from config."
wohlganger58fc71c2017-09-10 16:19:47 -050074 cls.context_depth = idleConf.GetOption("extensions", "CodeContext",
Tal Einat7036e1d2019-07-17 11:15:53 +030075 "maxlines", type="int",
76 default=15)
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040077
78 def __del__(self):
Cheryl Sabella654038d2018-05-19 15:34:03 -040079 "Cancel scheduled events."
Tal Einat7036e1d2019-07-17 11:15:53 +030080 if self.t1 is not None:
81 try:
82 self.text.after_cancel(self.t1)
83 except tkinter.TclError:
84 pass
85 self.t1 = None
wohlganger58fc71c2017-09-10 16:19:47 -050086
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000087 def toggle_code_context_event(self, event=None):
Cheryl Sabella654038d2018-05-19 15:34:03 -040088 """Toggle code context display.
89
Cheryl Sabellab609e682018-06-04 11:58:44 -040090 If self.context doesn't exist, create it to match the size of the editor
Cheryl Sabella654038d2018-05-19 15:34:03 -040091 window text (toggle on). If it does exist, destroy it (toggle off).
92 Return 'break' to complete the processing of the binding.
93 """
Tal Einat7036e1d2019-07-17 11:15:53 +030094 if self.context is None:
Thomas Wouterscf297e42007-02-23 15:07:44 +000095 # Calculate the border width and horizontal padding required to
96 # align the context with the text in the main Text widget.
Thomas Wouters89f507f2006-12-13 04:49:30 +000097 #
Serhiy Storchaka645058d2015-05-06 14:00:04 +030098 # All values are passed through getint(), since some
Thomas Wouterscf297e42007-02-23 15:07:44 +000099 # values may be pixel objects, which can't simply be added to ints.
100 widgets = self.editwin.text, self.editwin.text_frame
Cheryl Sabella85060162018-05-23 22:18:15 -0400101 # Calculate the required horizontal padding and border width.
Thomas Wouters89f507f2006-12-13 04:49:30 +0000102 padx = 0
Cheryl Sabella85060162018-05-23 22:18:15 -0400103 border = 0
Thomas Wouterscf297e42007-02-23 15:07:44 +0000104 for widget in widgets:
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300105 padx += widget.tk.getint(widget.pack_info()['padx'])
106 padx += widget.tk.getint(widget.cget('padx'))
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300107 border += widget.tk.getint(widget.cget('border'))
Cheryl Sabellab609e682018-06-04 11:58:44 -0400108 self.context = tkinter.Text(
Tal Einat7036e1d2019-07-17 11:15:53 +0300109 self.editwin.top, font=self.text['font'],
110 height=1,
111 width=1, # Don't request more than we get.
112 padx=padx, border=border, relief=SUNKEN, state='disabled')
113 self.update_highlight_colors()
Cheryl Sabella041272b2018-06-08 01:21:15 -0400114 self.context.bind('<ButtonRelease-1>', self.jumptoline)
Cheryl Sabellab609e682018-06-04 11:58:44 -0400115 # Pack the context widget before and above the text_frame widget,
Cheryl Sabella85060162018-05-23 22:18:15 -0400116 # thus ensuring that it will appear directly above text_frame.
Cheryl Sabellab609e682018-06-04 11:58:44 -0400117 self.context.pack(side=TOP, fill=X, expand=False,
Tal Einat7036e1d2019-07-17 11:15:53 +0300118 before=self.editwin.text_frame)
Cheryl Sabellac1b4b0f2018-12-22 01:25:45 -0500119 menu_status = 'Hide'
Tal Einat7036e1d2019-07-17 11:15:53 +0300120 self.t1 = self.text.after(self.UPDATEINTERVAL, self.timer_event)
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000121 else:
Cheryl Sabellab609e682018-06-04 11:58:44 -0400122 self.context.destroy()
123 self.context = None
Tal Einat7036e1d2019-07-17 11:15:53 +0300124 self.text.after_cancel(self.t1)
125 self.t1 = None
Cheryl Sabellac1b4b0f2018-12-22 01:25:45 -0500126 menu_status = 'Show'
127 self.editwin.update_menu_label(menu='options', index='* Code Context',
128 label=f'{menu_status} Code Context')
Serhiy Storchaka213ce122017-06-27 07:02:32 +0300129 return "break"
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000130
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000131 def get_context(self, new_topvisible, stopline=1, stopindent=0):
Cheryl Sabella654038d2018-05-19 15:34:03 -0400132 """Return a list of block line tuples and the 'last' indent.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000133
Cheryl Sabella654038d2018-05-19 15:34:03 -0400134 The tuple fields are (linenum, indent, text, opener).
135 The list represents header lines from new_topvisible back to
136 stopline with successively shorter indents > stopindent.
137 The list is returned ordered by line number.
138 Last indent returned is the smallest indent observed.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000139 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000140 assert stopline > 0
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000141 lines = []
Cheryl Sabella85060162018-05-23 22:18:15 -0400142 # The indentation level we are currently in.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000143 lastindent = INFINITY
144 # For a line to be interesting, it must begin with a block opening
145 # keyword, and have less indentation than lastindent.
Guido van Rossum805365e2007-05-07 22:24:25 +0000146 for linenum in range(new_topvisible, stopline-1, -1):
Cheryl Sabella85060162018-05-23 22:18:15 -0400147 codeline = self.text.get(f'{linenum}.0', f'{linenum}.end')
148 indent, text, opener = get_line_info(codeline)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000149 if indent < lastindent:
150 lastindent = indent
Kurt B. Kaisere3636e02004-04-26 22:26:04 +0000151 if opener in ("else", "elif"):
Cheryl Sabella85060162018-05-23 22:18:15 -0400152 # Also show the if statement.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000153 lastindent += 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000154 if opener and linenum < new_topvisible and indent >= stopindent:
155 lines.append((linenum, indent, text, opener))
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000156 if lastindent <= stopindent:
157 break
158 lines.reverse()
159 return lines, lastindent
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000160
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000161 def update_code_context(self):
162 """Update context information and lines visible in the context pane.
163
Cheryl Sabella654038d2018-05-19 15:34:03 -0400164 No update is done if the text hasn't been scrolled. If the text
165 was scrolled, the lines that should be shown in the context will
Cheryl Sabellab609e682018-06-04 11:58:44 -0400166 be retrieved and the context area will be updated with the code,
167 up to the number of maxlines.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000168 """
Tal Einat7036e1d2019-07-17 11:15:53 +0300169 new_topvisible = self.editwin.getlineno("@0,0")
Cheryl Sabella85060162018-05-23 22:18:15 -0400170 if self.topvisible == new_topvisible: # Haven't scrolled.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000171 return
Cheryl Sabella85060162018-05-23 22:18:15 -0400172 if self.topvisible < new_topvisible: # Scroll down.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000173 lines, lastindent = self.get_context(new_topvisible,
174 self.topvisible)
Cheryl Sabella85060162018-05-23 22:18:15 -0400175 # Retain only context info applicable to the region
176 # between topvisible and new_topvisible.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000177 while self.info[-1][1] >= lastindent:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000178 del self.info[-1]
Cheryl Sabella85060162018-05-23 22:18:15 -0400179 else: # self.topvisible > new_topvisible: # Scroll up.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000180 stopindent = self.info[-1][1] + 1
Cheryl Sabella85060162018-05-23 22:18:15 -0400181 # Retain only context info associated
182 # with lines above new_topvisible.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000183 while self.info[-1][0] >= new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000184 stopindent = self.info[-1][1]
185 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000186 lines, lastindent = self.get_context(new_topvisible,
187 self.info[-1][0]+1,
188 stopindent)
189 self.info.extend(lines)
190 self.topvisible = new_topvisible
Cheryl Sabella29996a12018-06-01 19:23:00 -0400191 # Last context_depth context lines.
192 context_strings = [x[2] for x in self.info[-self.context_depth:]]
193 showfirst = 0 if context_strings[0] else 1
Cheryl Sabellab609e682018-06-04 11:58:44 -0400194 # Update widget.
195 self.context['height'] = len(context_strings) - showfirst
196 self.context['state'] = 'normal'
197 self.context.delete('1.0', 'end')
198 self.context.insert('end', '\n'.join(context_strings[showfirst:]))
199 self.context['state'] = 'disabled'
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000200
Cheryl Sabella041272b2018-06-08 01:21:15 -0400201 def jumptoline(self, event=None):
202 "Show clicked context line at top of editor."
203 lines = len(self.info)
204 if lines == 1: # No context lines are showing.
205 newtop = 1
206 else:
207 # Line number clicked.
208 contextline = int(float(self.context.index('insert')))
209 # Lines not displayed due to maxlines.
210 offset = max(1, lines - self.context_depth) - 1
211 newtop = self.info[offset + contextline][0]
212 self.text.yview(f'{newtop}.0')
213 self.update_code_context()
214
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000215 def timer_event(self):
Cheryl Sabella654038d2018-05-19 15:34:03 -0400216 "Event on editor text widget triggered every UPDATEINTERVAL ms."
Tal Einat7036e1d2019-07-17 11:15:53 +0300217 if self.context is not None:
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000218 self.update_code_context()
Tal Einat7036e1d2019-07-17 11:15:53 +0300219 self.t1 = self.text.after(self.UPDATEINTERVAL, self.timer_event)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000220
Tal Einat7036e1d2019-07-17 11:15:53 +0300221 def update_font(self, font):
222 if self.context is not None:
223 self.context['font'] = font
224
225 def update_highlight_colors(self):
226 if self.context is not None:
227 colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'context')
228 self.context['background'] = colors['background']
229 self.context['foreground'] = colors['foreground']
wohlganger58fc71c2017-09-10 16:19:47 -0500230
231
232CodeContext.reload()
Cheryl Sabella654038d2018-05-19 15:34:03 -0400233
234
Terry Jan Reedy4d921582018-06-19 19:12:52 -0400235if __name__ == "__main__":
236 from unittest import main
237 main('idlelib.idle_test.test_codecontext', verbosity=2, exit=False)
238
239 # Add htest.