blob: 635f68c86e1f6d3a2b4edf86bfd2eb98d0276ec7 [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
7enclosing block. The number of hint lines is determined by the numlines
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
23FONTUPDATEINTERVAL = 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
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040048 bgcolor = "LightGray"
49 fgcolor = "Black"
50
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000051 def __init__(self, editwin):
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070052 """Initialize settings for context block.
53
54 editwin is the Editor window for the context block.
55 self.text is the editor window text widget.
56 self.textfont is the editor window font.
57
58 self.label displays the code context text above the editor text.
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -070059 Initially None, it is toggled via <<toggle-code-context>>.
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070060 self.topvisible is the number of the top text line displayed.
61 self.info is a list of (line number, indent level, line text,
62 block keyword) tuples for the block structure above topvisible.
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -070063 self.info[0] is initialized with a 'dummy' line which
64 starts the toplevel 'block' of the module.
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070065
66 self.t1 and self.t2 are two timer events on the editor text widget to
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -070067 monitor for changes to the context text or editor font.
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070068 """
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000069 self.editwin = editwin
70 self.text = editwin.text
71 self.textfont = self.text["font"]
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000072 self.label = None
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000073 self.topvisible = 1
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070074 self.info = [(0, -1, "", False)]
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000075 # Start two update cycles, one for context lines, one for font changes.
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040076 self.t1 = self.text.after(UPDATEINTERVAL, self.timer_event)
77 self.t2 = self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000078
wohlganger58fc71c2017-09-10 16:19:47 -050079 @classmethod
80 def reload(cls):
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070081 "Load class variables from config."
wohlganger58fc71c2017-09-10 16:19:47 -050082 cls.context_depth = idleConf.GetOption("extensions", "CodeContext",
83 "numlines", type="int", default=3)
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040084## cls.bgcolor = idleConf.GetOption("extensions", "CodeContext",
85## "bgcolor", type="str", default="LightGray")
86## cls.fgcolor = idleConf.GetOption("extensions", "CodeContext",
87## "fgcolor", type="str", default="Black")
88
89 def __del__(self):
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070090 "Cancel scheduled events."
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040091 try:
92 self.text.after_cancel(self.t1)
93 self.text.after_cancel(self.t2)
94 except:
95 pass
wohlganger58fc71c2017-09-10 16:19:47 -050096
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000097 def toggle_code_context_event(self, event=None):
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070098 """Toggle code context display.
99
100 If self.label doesn't exist, create it to match the size of the editor
101 window text (toggle on). If it does exist, destroy it (toggle off).
102 Return 'break' to complete the processing of the binding.
103 """
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000104 if not self.label:
Thomas Wouterscf297e42007-02-23 15:07:44 +0000105 # Calculate the border width and horizontal padding required to
106 # align the context with the text in the main Text widget.
Thomas Wouters89f507f2006-12-13 04:49:30 +0000107 #
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300108 # All values are passed through getint(), since some
Thomas Wouterscf297e42007-02-23 15:07:44 +0000109 # values may be pixel objects, which can't simply be added to ints.
110 widgets = self.editwin.text, self.editwin.text_frame
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700111 # Calculate the required horizontal padding and border width.
Thomas Wouters89f507f2006-12-13 04:49:30 +0000112 padx = 0
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700113 border = 0
Thomas Wouterscf297e42007-02-23 15:07:44 +0000114 for widget in widgets:
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300115 padx += widget.tk.getint(widget.pack_info()['padx'])
116 padx += widget.tk.getint(widget.cget('padx'))
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300117 border += widget.tk.getint(widget.cget('border'))
Terry Jan Reedya6bb3132017-09-17 00:56:56 -0400118 self.label = tkinter.Label(
119 self.editwin.top, text="\n" * (self.context_depth - 1),
120 anchor=W, justify=LEFT, font=self.textfont,
121 bg=self.bgcolor, fg=self.fgcolor,
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700122 width=1, # Don't request more than we get.
Terry Jan Reedya6bb3132017-09-17 00:56:56 -0400123 padx=padx, border=border, relief=SUNKEN)
Thomas Wouterscf297e42007-02-23 15:07:44 +0000124 # Pack the label widget before and above the text_frame widget,
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700125 # thus ensuring that it will appear directly above text_frame.
Thomas Wouterscf297e42007-02-23 15:07:44 +0000126 self.label.pack(side=TOP, fill=X, expand=False,
Thomas Wouters89f507f2006-12-13 04:49:30 +0000127 before=self.editwin.text_frame)
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000128 else:
129 self.label.destroy()
130 self.label = None
Serhiy Storchaka213ce122017-06-27 07:02:32 +0300131 return "break"
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000132
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000133 def get_context(self, new_topvisible, stopline=1, stopindent=0):
Miss Islington (bot)0efa1352018-05-19 13:28:03 -0700134 """Return a list of block line tuples and the 'last' indent.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000135
Miss Islington (bot)0efa1352018-05-19 13:28:03 -0700136 The tuple fields are (linenum, indent, text, opener).
137 The list represents header lines from new_topvisible back to
138 stopline with successively shorter indents > stopindent.
139 The list is returned ordered by line number.
140 Last indent returned is the smallest indent observed.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000141 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000142 assert stopline > 0
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000143 lines = []
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700144 # The indentation level we are currently in.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000145 lastindent = INFINITY
146 # For a line to be interesting, it must begin with a block opening
147 # keyword, and have less indentation than lastindent.
Guido van Rossum805365e2007-05-07 22:24:25 +0000148 for linenum in range(new_topvisible, stopline-1, -1):
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700149 codeline = self.text.get(f'{linenum}.0', f'{linenum}.end')
150 indent, text, opener = get_line_info(codeline)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000151 if indent < lastindent:
152 lastindent = indent
Kurt B. Kaisere3636e02004-04-26 22:26:04 +0000153 if opener in ("else", "elif"):
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700154 # Also show the if statement.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000155 lastindent += 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000156 if opener and linenum < new_topvisible and indent >= stopindent:
157 lines.append((linenum, indent, text, opener))
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000158 if lastindent <= stopindent:
159 break
160 lines.reverse()
161 return lines, lastindent
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000162
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000163 def update_code_context(self):
164 """Update context information and lines visible in the context pane.
165
Miss Islington (bot)0efa1352018-05-19 13:28:03 -0700166 No update is done if the text hasn't been scrolled. If the text
167 was scrolled, the lines that should be shown in the context will
168 be retrieved and the label widget will be updated with the code,
169 padded with blank lines so that the code appears on the bottom of
170 the context label.
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])
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700173 if self.topvisible == new_topvisible: # Haven't scrolled.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000174 return
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700175 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)
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700178 # 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]
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700182 else: # self.topvisible > new_topvisible: # Scroll up.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000183 stopindent = self.info[-1][1] + 1
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700184 # 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
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700194 # Empty lines in context pane.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000195 context_strings = [""] * max(0, self.context_depth - len(self.info))
Miss Islington (bot)b2ab5dc2018-05-23 20:26:54 -0700196 # Followed by the context hint lines.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000197 context_strings += [x[2] for x in self.info[-self.context_depth:]]
198 self.label["text"] = '\n'.join(context_strings)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000199
200 def timer_event(self):
Miss Islington (bot)0efa1352018-05-19 13:28:03 -0700201 "Event on editor text widget triggered every UPDATEINTERVAL ms."
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000202 if self.label:
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000203 self.update_code_context()
Terry Jan Reedya6bb3132017-09-17 00:56:56 -0400204 self.t1 = self.text.after(UPDATEINTERVAL, self.timer_event)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000205
206 def font_timer_event(self):
Miss Islington (bot)0efa1352018-05-19 13:28:03 -0700207 "Event on editor text widget triggered every FONTUPDATEINTERVAL ms."
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000208 newtextfont = self.text["font"]
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000209 if self.label and newtextfont != self.textfont:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000210 self.textfont = newtextfont
211 self.label["font"] = self.textfont
Terry Jan Reedya6bb3132017-09-17 00:56:56 -0400212 self.t2 = self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)
wohlganger58fc71c2017-09-10 16:19:47 -0500213
214
215CodeContext.reload()
Miss Islington (bot)0efa1352018-05-19 13:28:03 -0700216
217
218if __name__ == "__main__": # pragma: no cover
219 import unittest
220 unittest.main('idlelib.idle_test.test_codecontext', verbosity=2, exit=False)