blob: efd163ed265e28d91fb2b17b83a91105d552324e [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
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040026def getspacesfirstword(s, c=re.compile(r"^(\s*)(\w*)")):
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070027 "Extract the beginning whitespace and first word from s."
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040028 return c.match(s).groups()
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000029
wohlganger58fc71c2017-09-10 16:19:47 -050030
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000031class CodeContext:
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070032 "Display block context above the edit window."
33
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040034 bgcolor = "LightGray"
35 fgcolor = "Black"
36
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000037 def __init__(self, editwin):
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070038 """Initialize settings for context block.
39
40 editwin is the Editor window for the context block.
41 self.text is the editor window text widget.
42 self.textfont is the editor window font.
43
44 self.label displays the code context text above the editor text.
45 Initially None it is toggled via <<toggle-code-context>>.
46 self.topvisible is the number of the top text line displayed.
47 self.info is a list of (line number, indent level, line text,
48 block keyword) tuples for the block structure above topvisible.
49 s self.info[0] is initialized a 'dummy' line which
50 # starts the toplevel 'block' of the module.
51
52 self.t1 and self.t2 are two timer events on the editor text widget to
53 monitor for changes to the context text or editor font.
54 """
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000055 self.editwin = editwin
56 self.text = editwin.text
57 self.textfont = self.text["font"]
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000058 self.label = None
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000059 self.topvisible = 1
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070060 self.info = [(0, -1, "", False)]
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000061 # Start two update cycles, one for context lines, one for font changes.
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040062 self.t1 = self.text.after(UPDATEINTERVAL, self.timer_event)
63 self.t2 = self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000064
wohlganger58fc71c2017-09-10 16:19:47 -050065 @classmethod
66 def reload(cls):
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070067 "Load class variables from config."
wohlganger58fc71c2017-09-10 16:19:47 -050068 cls.context_depth = idleConf.GetOption("extensions", "CodeContext",
69 "numlines", type="int", default=3)
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040070## cls.bgcolor = idleConf.GetOption("extensions", "CodeContext",
71## "bgcolor", type="str", default="LightGray")
72## cls.fgcolor = idleConf.GetOption("extensions", "CodeContext",
73## "fgcolor", type="str", default="Black")
74
75 def __del__(self):
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070076 "Cancel scheduled events."
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040077 try:
78 self.text.after_cancel(self.t1)
79 self.text.after_cancel(self.t2)
80 except:
81 pass
wohlganger58fc71c2017-09-10 16:19:47 -050082
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000083 def toggle_code_context_event(self, event=None):
Miss Islington (bot)0efa1352018-05-19 13:28:03 -070084 """Toggle code context display.
85
86 If self.label doesn't exist, create it to match the size of the editor
87 window text (toggle on). If it does exist, destroy it (toggle off).
88 Return 'break' to complete the processing of the binding.
89 """
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000090 if not self.label:
Thomas Wouterscf297e42007-02-23 15:07:44 +000091 # Calculate the border width and horizontal padding required to
92 # align the context with the text in the main Text widget.
Thomas Wouters89f507f2006-12-13 04:49:30 +000093 #
Serhiy Storchaka645058d2015-05-06 14:00:04 +030094 # All values are passed through getint(), since some
Thomas Wouterscf297e42007-02-23 15:07:44 +000095 # values may be pixel objects, which can't simply be added to ints.
96 widgets = self.editwin.text, self.editwin.text_frame
97 # Calculate the required vertical padding
Thomas Wouters89f507f2006-12-13 04:49:30 +000098 padx = 0
Thomas Wouterscf297e42007-02-23 15:07:44 +000099 for widget in widgets:
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300100 padx += widget.tk.getint(widget.pack_info()['padx'])
101 padx += widget.tk.getint(widget.cget('padx'))
Thomas Wouterscf297e42007-02-23 15:07:44 +0000102 # Calculate the required border width
103 border = 0
104 for widget in widgets:
Serhiy Storchaka645058d2015-05-06 14:00:04 +0300105 border += widget.tk.getint(widget.cget('border'))
Terry Jan Reedya6bb3132017-09-17 00:56:56 -0400106 self.label = tkinter.Label(
107 self.editwin.top, text="\n" * (self.context_depth - 1),
108 anchor=W, justify=LEFT, font=self.textfont,
109 bg=self.bgcolor, fg=self.fgcolor,
110 width=1, #don't request more than we get
111 padx=padx, border=border, relief=SUNKEN)
Thomas Wouterscf297e42007-02-23 15:07:44 +0000112 # Pack the label widget before and above the text_frame widget,
113 # thus ensuring that it will appear directly above text_frame
114 self.label.pack(side=TOP, fill=X, expand=False,
Thomas Wouters89f507f2006-12-13 04:49:30 +0000115 before=self.editwin.text_frame)
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000116 else:
117 self.label.destroy()
118 self.label = None
Serhiy Storchaka213ce122017-06-27 07:02:32 +0300119 return "break"
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000120
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000121 def get_line_info(self, linenum):
Miss Islington (bot)0efa1352018-05-19 13:28:03 -0700122 """Return tuple of (line indent value, text, and block start keyword).
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000123
124 If the line does not start a block, the keyword value is False.
125 The indentation of empty lines (or comment lines) is INFINITY.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000126 """
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000127 text = self.text.get("%d.0" % linenum, "%d.end" % linenum)
128 spaces, firstword = getspacesfirstword(text)
129 opener = firstword in BLOCKOPENERS and firstword
130 if len(text) == len(spaces) or text[len(spaces)] == '#':
131 indent = INFINITY
132 else:
133 indent = len(spaces)
134 return indent, text, opener
135
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000136 def get_context(self, new_topvisible, stopline=1, stopindent=0):
Miss Islington (bot)0efa1352018-05-19 13:28:03 -0700137 """Return a list of block line tuples and the 'last' indent.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000138
Miss Islington (bot)0efa1352018-05-19 13:28:03 -0700139 The tuple fields are (linenum, indent, text, opener).
140 The list represents header lines from new_topvisible back to
141 stopline with successively shorter indents > stopindent.
142 The list is returned ordered by line number.
143 Last indent returned is the smallest indent observed.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000144 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000145 assert stopline > 0
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000146 lines = []
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000147 # The indentation level we are currently in:
148 lastindent = INFINITY
149 # For a line to be interesting, it must begin with a block opening
150 # keyword, and have less indentation than lastindent.
Guido van Rossum805365e2007-05-07 22:24:25 +0000151 for linenum in range(new_topvisible, stopline-1, -1):
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000152 indent, text, opener = self.get_line_info(linenum)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000153 if indent < lastindent:
154 lastindent = indent
Kurt B. Kaisere3636e02004-04-26 22:26:04 +0000155 if opener in ("else", "elif"):
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000156 # We also show the if statement
157 lastindent += 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000158 if opener and linenum < new_topvisible and indent >= stopindent:
159 lines.append((linenum, indent, text, opener))
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000160 if lastindent <= stopindent:
161 break
162 lines.reverse()
163 return lines, lastindent
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000164
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000165 def update_code_context(self):
166 """Update context information and lines visible in the context pane.
167
Miss Islington (bot)0efa1352018-05-19 13:28:03 -0700168 No update is done if the text hasn't been scrolled. If the text
169 was scrolled, the lines that should be shown in the context will
170 be retrieved and the label widget will be updated with the code,
171 padded with blank lines so that the code appears on the bottom of
172 the context label.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000173 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000174 new_topvisible = int(self.text.index("@0,0").split('.')[0])
175 if self.topvisible == new_topvisible: # haven't scrolled
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000176 return
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000177 if self.topvisible < new_topvisible: # scroll down
178 lines, lastindent = self.get_context(new_topvisible,
179 self.topvisible)
180 # retain only context info applicable to the region
181 # between topvisible and new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000182 while self.info[-1][1] >= lastindent:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000183 del self.info[-1]
Miss Islington (bot)0efa1352018-05-19 13:28:03 -0700184 else: # self.topvisible > new_topvisible: # scroll up
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000185 stopindent = self.info[-1][1] + 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000186 # retain only context info associated
187 # with lines above new_topvisible:
188 while self.info[-1][0] >= new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000189 stopindent = self.info[-1][1]
190 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000191 lines, lastindent = self.get_context(new_topvisible,
192 self.info[-1][0]+1,
193 stopindent)
194 self.info.extend(lines)
195 self.topvisible = new_topvisible
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000196 # empty lines in context pane:
197 context_strings = [""] * max(0, self.context_depth - len(self.info))
198 # followed by the context hint lines:
199 context_strings += [x[2] for x in self.info[-self.context_depth:]]
200 self.label["text"] = '\n'.join(context_strings)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000201
202 def timer_event(self):
Miss Islington (bot)0efa1352018-05-19 13:28:03 -0700203 "Event on editor text widget triggered every UPDATEINTERVAL ms."
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000204 if self.label:
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000205 self.update_code_context()
Terry Jan Reedya6bb3132017-09-17 00:56:56 -0400206 self.t1 = self.text.after(UPDATEINTERVAL, self.timer_event)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000207
208 def font_timer_event(self):
Miss Islington (bot)0efa1352018-05-19 13:28:03 -0700209 "Event on editor text widget triggered every FONTUPDATEINTERVAL ms."
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000210 newtextfont = self.text["font"]
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000211 if self.label and newtextfont != self.textfont:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000212 self.textfont = newtextfont
213 self.label["font"] = self.textfont
Terry Jan Reedya6bb3132017-09-17 00:56:56 -0400214 self.t2 = self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)
wohlganger58fc71c2017-09-10 16:19:47 -0500215
216
217CodeContext.reload()
Miss Islington (bot)0efa1352018-05-19 13:28:03 -0700218
219
220if __name__ == "__main__": # pragma: no cover
221 import unittest
222 unittest.main('idlelib.idle_test.test_codecontext', verbosity=2, exit=False)