blob: 2aed76de7fb6f870ac3f0fb2fe4255b6a91b74e1 [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. Kaiserd00587a2004-04-24 03:08:13 +000022UPDATEINTERVAL = 100 # millisec
Cheryl Sabellade651622018-06-01 21:45:54 -040023CONFIGUPDATEINTERVAL = 1000 # millisec
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."
47
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000048 def __init__(self, editwin):
Cheryl Sabella654038d2018-05-19 15:34:03 -040049 """Initialize settings for context block.
50
51 editwin is the Editor window for the context block.
52 self.text is the editor window text widget.
53 self.textfont is the editor window font.
54
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
68 self.textfont = self.text["font"]
Cheryl Sabellade651622018-06-01 21:45:54 -040069 self.contextcolors = CodeContext.colors
Cheryl Sabellab609e682018-06-04 11:58:44 -040070 self.context = 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 # Start two update cycles, one for context lines, one for font changes.
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040074 self.t1 = self.text.after(UPDATEINTERVAL, self.timer_event)
Cheryl Sabellade651622018-06-01 21:45:54 -040075 self.t2 = self.text.after(CONFIGUPDATEINTERVAL, self.config_timer_event)
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",
Cheryl Sabella29996a12018-06-01 19:23:00 -040081 "maxlines", type="int", default=15)
Cheryl Sabellade651622018-06-01 21:45:54 -040082 cls.colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'context')
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040083
84 def __del__(self):
Cheryl Sabella654038d2018-05-19 15:34:03 -040085 "Cancel scheduled events."
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040086 try:
87 self.text.after_cancel(self.t1)
88 self.text.after_cancel(self.t2)
89 except:
90 pass
wohlganger58fc71c2017-09-10 16:19:47 -050091
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000092 def toggle_code_context_event(self, event=None):
Cheryl Sabella654038d2018-05-19 15:34:03 -040093 """Toggle code context display.
94
Cheryl Sabellab609e682018-06-04 11:58:44 -040095 If self.context doesn't exist, create it to match the size of the editor
Cheryl Sabella654038d2018-05-19 15:34:03 -040096 window text (toggle on). If it does exist, destroy it (toggle off).
97 Return 'break' to complete the processing of the binding.
98 """
Cheryl Sabellab609e682018-06-04 11:58:44 -040099 if not self.context:
Thomas Wouterscf297e42007-02-23 15:07:44 +0000100 # Calculate the border width and horizontal padding required to
101 # align the context with the text in the main Text widget.
Thomas Wouters89f507f2006-12-13 04:49:30 +0000102 #
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300103 # All values are passed through getint(), since some
Thomas Wouterscf297e42007-02-23 15:07:44 +0000104 # values may be pixel objects, which can't simply be added to ints.
105 widgets = self.editwin.text, self.editwin.text_frame
Cheryl Sabella85060162018-05-23 22:18:15 -0400106 # Calculate the required horizontal padding and border width.
Thomas Wouters89f507f2006-12-13 04:49:30 +0000107 padx = 0
Cheryl Sabella85060162018-05-23 22:18:15 -0400108 border = 0
Thomas Wouterscf297e42007-02-23 15:07:44 +0000109 for widget in widgets:
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300110 padx += widget.tk.getint(widget.pack_info()['padx'])
111 padx += widget.tk.getint(widget.cget('padx'))
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300112 border += widget.tk.getint(widget.cget('border'))
Cheryl Sabellab609e682018-06-04 11:58:44 -0400113 self.context = tkinter.Text(
114 self.editwin.top, font=self.textfont,
Cheryl Sabellade651622018-06-01 21:45:54 -0400115 bg=self.contextcolors['background'],
116 fg=self.contextcolors['foreground'],
Cheryl Sabellab609e682018-06-04 11:58:44 -0400117 height=1,
Cheryl Sabella85060162018-05-23 22:18:15 -0400118 width=1, # Don't request more than we get.
Cheryl Sabellab609e682018-06-04 11:58:44 -0400119 padx=padx, border=border, relief=SUNKEN, state='disabled')
Cheryl Sabella041272b2018-06-08 01:21:15 -0400120 self.context.bind('<ButtonRelease-1>', self.jumptoline)
Cheryl Sabellab609e682018-06-04 11:58:44 -0400121 # Pack the context widget before and above the text_frame widget,
Cheryl Sabella85060162018-05-23 22:18:15 -0400122 # thus ensuring that it will appear directly above text_frame.
Cheryl Sabellab609e682018-06-04 11:58:44 -0400123 self.context.pack(side=TOP, fill=X, expand=False,
Thomas Wouters89f507f2006-12-13 04:49:30 +0000124 before=self.editwin.text_frame)
Cheryl Sabellac1b4b0f2018-12-22 01:25:45 -0500125 menu_status = 'Hide'
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000126 else:
Cheryl Sabellab609e682018-06-04 11:58:44 -0400127 self.context.destroy()
128 self.context = None
Cheryl Sabellac1b4b0f2018-12-22 01:25:45 -0500129 menu_status = 'Show'
130 self.editwin.update_menu_label(menu='options', index='* Code Context',
131 label=f'{menu_status} Code Context')
Serhiy Storchaka213ce122017-06-27 07:02:32 +0300132 return "break"
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000133
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000134 def get_context(self, new_topvisible, stopline=1, stopindent=0):
Cheryl Sabella654038d2018-05-19 15:34:03 -0400135 """Return a list of block line tuples and the 'last' indent.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000136
Cheryl Sabella654038d2018-05-19 15:34:03 -0400137 The tuple fields are (linenum, indent, text, opener).
138 The list represents header lines from new_topvisible back to
139 stopline with successively shorter indents > stopindent.
140 The list is returned ordered by line number.
141 Last indent returned is the smallest indent observed.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000142 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000143 assert stopline > 0
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000144 lines = []
Cheryl Sabella85060162018-05-23 22:18:15 -0400145 # The indentation level we are currently in.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000146 lastindent = INFINITY
147 # For a line to be interesting, it must begin with a block opening
148 # keyword, and have less indentation than lastindent.
Guido van Rossum805365e2007-05-07 22:24:25 +0000149 for linenum in range(new_topvisible, stopline-1, -1):
Cheryl Sabella85060162018-05-23 22:18:15 -0400150 codeline = self.text.get(f'{linenum}.0', f'{linenum}.end')
151 indent, text, opener = get_line_info(codeline)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000152 if indent < lastindent:
153 lastindent = indent
Kurt B. Kaisere3636e02004-04-26 22:26:04 +0000154 if opener in ("else", "elif"):
Cheryl Sabella85060162018-05-23 22:18:15 -0400155 # Also show the if statement.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000156 lastindent += 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000157 if opener and linenum < new_topvisible and indent >= stopindent:
158 lines.append((linenum, indent, text, opener))
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000159 if lastindent <= stopindent:
160 break
161 lines.reverse()
162 return lines, lastindent
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000163
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000164 def update_code_context(self):
165 """Update context information and lines visible in the context pane.
166
Cheryl Sabella654038d2018-05-19 15:34:03 -0400167 No update is done if the text hasn't been scrolled. If the text
168 was scrolled, the lines that should be shown in the context will
Cheryl Sabellab609e682018-06-04 11:58:44 -0400169 be retrieved and the context area will be updated with the code,
170 up to the number of maxlines.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000171 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000172 new_topvisible = int(self.text.index("@0,0").split('.')[0])
Cheryl Sabella85060162018-05-23 22:18:15 -0400173 if self.topvisible == new_topvisible: # Haven't scrolled.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000174 return
Cheryl Sabella85060162018-05-23 22:18:15 -0400175 if self.topvisible < new_topvisible: # Scroll down.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000176 lines, lastindent = self.get_context(new_topvisible,
177 self.topvisible)
Cheryl Sabella85060162018-05-23 22:18:15 -0400178 # Retain only context info applicable to the region
179 # between topvisible and new_topvisible.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000180 while self.info[-1][1] >= lastindent:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000181 del self.info[-1]
Cheryl Sabella85060162018-05-23 22:18:15 -0400182 else: # self.topvisible > new_topvisible: # Scroll up.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000183 stopindent = self.info[-1][1] + 1
Cheryl Sabella85060162018-05-23 22:18:15 -0400184 # Retain only context info associated
185 # with lines above new_topvisible.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000186 while self.info[-1][0] >= new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000187 stopindent = self.info[-1][1]
188 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000189 lines, lastindent = self.get_context(new_topvisible,
190 self.info[-1][0]+1,
191 stopindent)
192 self.info.extend(lines)
193 self.topvisible = new_topvisible
Cheryl Sabella29996a12018-06-01 19:23:00 -0400194 # Last context_depth context lines.
195 context_strings = [x[2] for x in self.info[-self.context_depth:]]
196 showfirst = 0 if context_strings[0] else 1
Cheryl Sabellab609e682018-06-04 11:58:44 -0400197 # Update widget.
198 self.context['height'] = len(context_strings) - showfirst
199 self.context['state'] = 'normal'
200 self.context.delete('1.0', 'end')
201 self.context.insert('end', '\n'.join(context_strings[showfirst:]))
202 self.context['state'] = 'disabled'
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000203
Cheryl Sabella041272b2018-06-08 01:21:15 -0400204 def jumptoline(self, event=None):
205 "Show clicked context line at top of editor."
206 lines = len(self.info)
207 if lines == 1: # No context lines are showing.
208 newtop = 1
209 else:
210 # Line number clicked.
211 contextline = int(float(self.context.index('insert')))
212 # Lines not displayed due to maxlines.
213 offset = max(1, lines - self.context_depth) - 1
214 newtop = self.info[offset + contextline][0]
215 self.text.yview(f'{newtop}.0')
216 self.update_code_context()
217
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000218 def timer_event(self):
Cheryl Sabella654038d2018-05-19 15:34:03 -0400219 "Event on editor text widget triggered every UPDATEINTERVAL ms."
Cheryl Sabellab609e682018-06-04 11:58:44 -0400220 if self.context:
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000221 self.update_code_context()
Terry Jan Reedya6bb3132017-09-17 00:56:56 -0400222 self.t1 = self.text.after(UPDATEINTERVAL, self.timer_event)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000223
Cheryl Sabellade651622018-06-01 21:45:54 -0400224 def config_timer_event(self):
225 "Event on editor text widget triggered every CONFIGUPDATEINTERVAL ms."
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000226 newtextfont = self.text["font"]
Cheryl Sabellab609e682018-06-04 11:58:44 -0400227 if (self.context and (newtextfont != self.textfont or
Cheryl Sabellade651622018-06-01 21:45:54 -0400228 CodeContext.colors != self.contextcolors)):
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000229 self.textfont = newtextfont
Cheryl Sabellade651622018-06-01 21:45:54 -0400230 self.contextcolors = CodeContext.colors
Cheryl Sabellab609e682018-06-04 11:58:44 -0400231 self.context["font"] = self.textfont
232 self.context['background'] = self.contextcolors['background']
233 self.context['foreground'] = self.contextcolors['foreground']
Cheryl Sabellade651622018-06-01 21:45:54 -0400234 self.t2 = self.text.after(CONFIGUPDATEINTERVAL, self.config_timer_event)
wohlganger58fc71c2017-09-10 16:19:47 -0500235
236
237CodeContext.reload()
Cheryl Sabella654038d2018-05-19 15:34:03 -0400238
239
Terry Jan Reedy4d921582018-06-19 19:12:52 -0400240if __name__ == "__main__":
241 from unittest import main
242 main('idlelib.idle_test.test_codecontext', verbosity=2, exit=False)
243
244 # Add htest.