blob: 8b378bceba250ac581fde73ef10fdd3e4fcee6a1 [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
Miss Islington (bot)0800b6c2018-06-01 16:45:54 -07007enclosing 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
16from tkinter.constants import TOP, LEFT, X, W, SUNKEN
17
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",
Miss Islington (bot)b2a02b92018-05-17 19:04:23 -070021 "if", "try", "while", "with", "async"}
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000022UPDATEINTERVAL = 100 # millisec
Miss Islington (bot)4e033c52018-06-01 19:16:04 -070023CONFIGUPDATEINTERVAL = 1000 # millisec
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000024
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070025
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -070026def 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:
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070046 "Display block context above the edit window."
47
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000048 def __init__(self, editwin):
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070049 """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
Miss Islington (bot)b7eb1022018-06-04 09:33:23 -070055 self.context displays the code context text above the editor text.
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -070056 Initially None, it is toggled via <<toggle-code-context>>.
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070057 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.
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -070060 self.info[0] is initialized with a 'dummy' line which
61 starts the toplevel 'block' of the module.
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070062
63 self.t1 and self.t2 are two timer events on the editor text widget to
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -070064 monitor for changes to the context text or editor font.
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070065 """
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000066 self.editwin = editwin
67 self.text = editwin.text
68 self.textfont = self.text["font"]
Miss Islington (bot)4e033c52018-06-01 19:16:04 -070069 self.contextcolors = CodeContext.colors
Miss Islington (bot)b7eb1022018-06-04 09:33:23 -070070 self.context = None
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000071 self.topvisible = 1
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070072 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)
Miss Islington (bot)4e033c52018-06-01 19:16:04 -070075 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):
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070079 "Load class variables from config."
wohlganger58fc71c2017-09-10 16:19:47 -050080 cls.context_depth = idleConf.GetOption("extensions", "CodeContext",
Miss Islington (bot)0800b6c2018-06-01 16:45:54 -070081 "maxlines", type="int", default=15)
Miss Islington (bot)4e033c52018-06-01 19:16:04 -070082 cls.colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'context')
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040083
84 def __del__(self):
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070085 "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):
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070093 """Toggle code context display.
94
Miss Islington (bot)b7eb1022018-06-04 09:33:23 -070095 If self.context doesn't exist, create it to match the size of the editor
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070096 window text (toggle on). If it does exist, destroy it (toggle off).
97 Return 'break' to complete the processing of the binding.
98 """
Miss Islington (bot)b7eb1022018-06-04 09:33:23 -070099 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
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700106 # Calculate the required horizontal padding and border width.
Thomas Wouters89f507f2006-12-13 04:49:30 +0000107 padx = 0
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700108 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'))
Miss Islington (bot)b7eb1022018-06-04 09:33:23 -0700113 self.context = tkinter.Text(
114 self.editwin.top, font=self.textfont,
Miss Islington (bot)4e033c52018-06-01 19:16:04 -0700115 bg=self.contextcolors['background'],
116 fg=self.contextcolors['foreground'],
Miss Islington (bot)b7eb1022018-06-04 09:33:23 -0700117 height=1,
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700118 width=1, # Don't request more than we get.
Miss Islington (bot)b7eb1022018-06-04 09:33:23 -0700119 padx=padx, border=border, relief=SUNKEN, state='disabled')
Miss Islington (bot)a4868472018-06-07 22:50:36 -0700120 self.context.bind('<ButtonRelease-1>', self.jumptoline)
Miss Islington (bot)b7eb1022018-06-04 09:33:23 -0700121 # Pack the context widget before and above the text_frame widget,
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700122 # thus ensuring that it will appear directly above text_frame.
Miss Islington (bot)b7eb1022018-06-04 09:33:23 -0700123 self.context.pack(side=TOP, fill=X, expand=False,
Thomas Wouters89f507f2006-12-13 04:49:30 +0000124 before=self.editwin.text_frame)
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000125 else:
Miss Islington (bot)b7eb1022018-06-04 09:33:23 -0700126 self.context.destroy()
127 self.context = None
Serhiy Storchaka213ce122017-06-27 07:02:32 +0300128 return "break"
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000129
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000130 def get_context(self, new_topvisible, stopline=1, stopindent=0):
Miss Islington (bot)0efa1352018-05-19 13:28:03 -0700131 """Return a list of block line tuples and the 'last' indent.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000132
Miss Islington (bot)0efa1352018-05-19 13:28:03 -0700133 The tuple fields are (linenum, indent, text, opener).
134 The list represents header lines from new_topvisible back to
135 stopline with successively shorter indents > stopindent.
136 The list is returned ordered by line number.
137 Last indent returned is the smallest indent observed.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000138 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000139 assert stopline > 0
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000140 lines = []
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700141 # The indentation level we are currently in.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000142 lastindent = INFINITY
143 # For a line to be interesting, it must begin with a block opening
144 # keyword, and have less indentation than lastindent.
Guido van Rossum805365e2007-05-07 22:24:25 +0000145 for linenum in range(new_topvisible, stopline-1, -1):
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700146 codeline = self.text.get(f'{linenum}.0', f'{linenum}.end')
147 indent, text, opener = get_line_info(codeline)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000148 if indent < lastindent:
149 lastindent = indent
Kurt B. Kaisere3636e02004-04-26 22:26:04 +0000150 if opener in ("else", "elif"):
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700151 # Also show the if statement.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000152 lastindent += 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000153 if opener and linenum < new_topvisible and indent >= stopindent:
154 lines.append((linenum, indent, text, opener))
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000155 if lastindent <= stopindent:
156 break
157 lines.reverse()
158 return lines, lastindent
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000159
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000160 def update_code_context(self):
161 """Update context information and lines visible in the context pane.
162
Miss Islington (bot)0efa1352018-05-19 13:28:03 -0700163 No update is done if the text hasn't been scrolled. If the text
164 was scrolled, the lines that should be shown in the context will
Miss Islington (bot)b7eb1022018-06-04 09:33:23 -0700165 be retrieved and the context area will be updated with the code,
166 up to the number of maxlines.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000167 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000168 new_topvisible = int(self.text.index("@0,0").split('.')[0])
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700169 if self.topvisible == new_topvisible: # Haven't scrolled.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000170 return
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700171 if self.topvisible < new_topvisible: # Scroll down.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000172 lines, lastindent = self.get_context(new_topvisible,
173 self.topvisible)
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700174 # Retain only context info applicable to the region
175 # between topvisible and new_topvisible.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000176 while self.info[-1][1] >= lastindent:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000177 del self.info[-1]
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700178 else: # self.topvisible > new_topvisible: # Scroll up.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000179 stopindent = self.info[-1][1] + 1
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700180 # Retain only context info associated
181 # with lines above new_topvisible.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000182 while self.info[-1][0] >= new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000183 stopindent = self.info[-1][1]
184 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000185 lines, lastindent = self.get_context(new_topvisible,
186 self.info[-1][0]+1,
187 stopindent)
188 self.info.extend(lines)
189 self.topvisible = new_topvisible
Miss Islington (bot)0800b6c2018-06-01 16:45:54 -0700190 # Last context_depth context lines.
191 context_strings = [x[2] for x in self.info[-self.context_depth:]]
192 showfirst = 0 if context_strings[0] else 1
Miss Islington (bot)b7eb1022018-06-04 09:33:23 -0700193 # Update widget.
194 self.context['height'] = len(context_strings) - showfirst
195 self.context['state'] = 'normal'
196 self.context.delete('1.0', 'end')
197 self.context.insert('end', '\n'.join(context_strings[showfirst:]))
198 self.context['state'] = 'disabled'
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000199
Miss Islington (bot)a4868472018-06-07 22:50:36 -0700200 def jumptoline(self, event=None):
201 "Show clicked context line at top of editor."
202 lines = len(self.info)
203 if lines == 1: # No context lines are showing.
204 newtop = 1
205 else:
206 # Line number clicked.
207 contextline = int(float(self.context.index('insert')))
208 # Lines not displayed due to maxlines.
209 offset = max(1, lines - self.context_depth) - 1
210 newtop = self.info[offset + contextline][0]
211 self.text.yview(f'{newtop}.0')
212 self.update_code_context()
213
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000214 def timer_event(self):
Miss Islington (bot)0efa1352018-05-19 13:28:03 -0700215 "Event on editor text widget triggered every UPDATEINTERVAL ms."
Miss Islington (bot)b7eb1022018-06-04 09:33:23 -0700216 if self.context:
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000217 self.update_code_context()
Terry Jan Reedya6bb3132017-09-17 00:56:56 -0400218 self.t1 = self.text.after(UPDATEINTERVAL, self.timer_event)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000219
Miss Islington (bot)4e033c52018-06-01 19:16:04 -0700220 def config_timer_event(self):
221 "Event on editor text widget triggered every CONFIGUPDATEINTERVAL ms."
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000222 newtextfont = self.text["font"]
Miss Islington (bot)b7eb1022018-06-04 09:33:23 -0700223 if (self.context and (newtextfont != self.textfont or
Miss Islington (bot)4e033c52018-06-01 19:16:04 -0700224 CodeContext.colors != self.contextcolors)):
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000225 self.textfont = newtextfont
Miss Islington (bot)4e033c52018-06-01 19:16:04 -0700226 self.contextcolors = CodeContext.colors
Miss Islington (bot)b7eb1022018-06-04 09:33:23 -0700227 self.context["font"] = self.textfont
228 self.context['background'] = self.contextcolors['background']
229 self.context['foreground'] = self.contextcolors['foreground']
Miss Islington (bot)4e033c52018-06-01 19:16:04 -0700230 self.t2 = self.text.after(CONFIGUPDATEINTERVAL, self.config_timer_event)
wohlganger58fc71c2017-09-10 16:19:47 -0500231
232
233CodeContext.reload()
Miss Islington (bot)0efa1352018-05-19 13:28:03 -0700234
235
236if __name__ == "__main__": # pragma: no cover
237 import unittest
238 unittest.main('idlelib.idle_test.test_codecontext', verbosity=2, exit=False)