blob: 316e5109e6cd4f775226580899651e9477a3c21d [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",
21 "if", "try", "while", "with"}
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000022UPDATEINTERVAL = 100 # millisec
23FONTUPDATEINTERVAL = 1000 # millisec
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000024
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040025def getspacesfirstword(s, c=re.compile(r"^(\s*)(\w*)")):
26 return c.match(s).groups()
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000027
wohlganger58fc71c2017-09-10 16:19:47 -050028
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000029class CodeContext:
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040030 bgcolor = "LightGray"
31 fgcolor = "Black"
32
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000033 def __init__(self, editwin):
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000034 self.editwin = editwin
35 self.text = editwin.text
36 self.textfont = self.text["font"]
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000037 self.label = None
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000038 # self.info is a list of (line number, indent level, line text, block
39 # keyword) tuples providing the block structure associated with
40 # self.topvisible (the linenumber of the line displayed at the top of
41 # the edit window). self.info[0] is initialized as a 'dummy' line which
42 # starts the toplevel 'block' of the module.
Kurt B. Kaiser74910222005-10-02 23:36:46 +000043 self.info = [(0, -1, "", False)]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000044 self.topvisible = 1
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000045 # Start two update cycles, one for context lines, one for font changes.
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040046 self.t1 = self.text.after(UPDATEINTERVAL, self.timer_event)
47 self.t2 = self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000048
wohlganger58fc71c2017-09-10 16:19:47 -050049 @classmethod
50 def reload(cls):
51 cls.context_depth = idleConf.GetOption("extensions", "CodeContext",
52 "numlines", type="int", default=3)
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040053## cls.bgcolor = idleConf.GetOption("extensions", "CodeContext",
54## "bgcolor", type="str", default="LightGray")
55## cls.fgcolor = idleConf.GetOption("extensions", "CodeContext",
56## "fgcolor", type="str", default="Black")
57
58 def __del__(self):
59 try:
60 self.text.after_cancel(self.t1)
61 self.text.after_cancel(self.t2)
62 except:
63 pass
wohlganger58fc71c2017-09-10 16:19:47 -050064
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000065 def toggle_code_context_event(self, event=None):
66 if not self.label:
Thomas Wouterscf297e42007-02-23 15:07:44 +000067 # Calculate the border width and horizontal padding required to
68 # align the context with the text in the main Text widget.
Thomas Wouters89f507f2006-12-13 04:49:30 +000069 #
Serhiy Storchaka645058d2015-05-06 14:00:04 +030070 # All values are passed through getint(), since some
Thomas Wouterscf297e42007-02-23 15:07:44 +000071 # values may be pixel objects, which can't simply be added to ints.
72 widgets = self.editwin.text, self.editwin.text_frame
73 # Calculate the required vertical padding
Thomas Wouters89f507f2006-12-13 04:49:30 +000074 padx = 0
Thomas Wouterscf297e42007-02-23 15:07:44 +000075 for widget in widgets:
Serhiy Storchaka645058d2015-05-06 14:00:04 +030076 padx += widget.tk.getint(widget.pack_info()['padx'])
77 padx += widget.tk.getint(widget.cget('padx'))
Thomas Wouterscf297e42007-02-23 15:07:44 +000078 # Calculate the required border width
79 border = 0
80 for widget in widgets:
Serhiy Storchaka645058d2015-05-06 14:00:04 +030081 border += widget.tk.getint(widget.cget('border'))
Terry Jan Reedya6bb3132017-09-17 00:56:56 -040082 self.label = tkinter.Label(
83 self.editwin.top, text="\n" * (self.context_depth - 1),
84 anchor=W, justify=LEFT, font=self.textfont,
85 bg=self.bgcolor, fg=self.fgcolor,
86 width=1, #don't request more than we get
87 padx=padx, border=border, relief=SUNKEN)
Thomas Wouterscf297e42007-02-23 15:07:44 +000088 # Pack the label widget before and above the text_frame widget,
89 # thus ensuring that it will appear directly above text_frame
90 self.label.pack(side=TOP, fill=X, expand=False,
Thomas Wouters89f507f2006-12-13 04:49:30 +000091 before=self.editwin.text_frame)
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000092 else:
93 self.label.destroy()
94 self.label = None
Serhiy Storchaka213ce122017-06-27 07:02:32 +030095 return "break"
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000096
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000097 def get_line_info(self, linenum):
98 """Get the line indent value, text, and any block start keyword
99
100 If the line does not start a block, the keyword value is False.
101 The indentation of empty lines (or comment lines) is INFINITY.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000102
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000103 """
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000104 text = self.text.get("%d.0" % linenum, "%d.end" % linenum)
105 spaces, firstword = getspacesfirstword(text)
106 opener = firstword in BLOCKOPENERS and firstword
107 if len(text) == len(spaces) or text[len(spaces)] == '#':
108 indent = INFINITY
109 else:
110 indent = len(spaces)
111 return indent, text, opener
112
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000113 def get_context(self, new_topvisible, stopline=1, stopindent=0):
114 """Get context lines, starting at new_topvisible and working backwards.
115
116 Stop when stopline or stopindent is reached. Return a tuple of context
117 data and the indent level at the top of the region inspected.
118
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000119 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000120 assert stopline > 0
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000121 lines = []
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000122 # The indentation level we are currently in:
123 lastindent = INFINITY
124 # For a line to be interesting, it must begin with a block opening
125 # keyword, and have less indentation than lastindent.
Guido van Rossum805365e2007-05-07 22:24:25 +0000126 for linenum in range(new_topvisible, stopline-1, -1):
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000127 indent, text, opener = self.get_line_info(linenum)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000128 if indent < lastindent:
129 lastindent = indent
Kurt B. Kaisere3636e02004-04-26 22:26:04 +0000130 if opener in ("else", "elif"):
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000131 # We also show the if statement
132 lastindent += 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000133 if opener and linenum < new_topvisible and indent >= stopindent:
134 lines.append((linenum, indent, text, opener))
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000135 if lastindent <= stopindent:
136 break
137 lines.reverse()
138 return lines, lastindent
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000139
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000140 def update_code_context(self):
141 """Update context information and lines visible in the context pane.
142
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000143 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000144 new_topvisible = int(self.text.index("@0,0").split('.')[0])
145 if self.topvisible == new_topvisible: # haven't scrolled
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000146 return
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000147 if self.topvisible < new_topvisible: # scroll down
148 lines, lastindent = self.get_context(new_topvisible,
149 self.topvisible)
150 # retain only context info applicable to the region
151 # between topvisible and new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000152 while self.info[-1][1] >= lastindent:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000153 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000154 elif self.topvisible > new_topvisible: # scroll up
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000155 stopindent = self.info[-1][1] + 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000156 # retain only context info associated
157 # with lines above new_topvisible:
158 while self.info[-1][0] >= new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000159 stopindent = self.info[-1][1]
160 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000161 lines, lastindent = self.get_context(new_topvisible,
162 self.info[-1][0]+1,
163 stopindent)
164 self.info.extend(lines)
165 self.topvisible = new_topvisible
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000166 # empty lines in context pane:
167 context_strings = [""] * max(0, self.context_depth - len(self.info))
168 # followed by the context hint lines:
169 context_strings += [x[2] for x in self.info[-self.context_depth:]]
170 self.label["text"] = '\n'.join(context_strings)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000171
172 def timer_event(self):
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000173 if self.label:
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000174 self.update_code_context()
Terry Jan Reedya6bb3132017-09-17 00:56:56 -0400175 self.t1 = self.text.after(UPDATEINTERVAL, self.timer_event)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000176
177 def font_timer_event(self):
178 newtextfont = self.text["font"]
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000179 if self.label and newtextfont != self.textfont:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000180 self.textfont = newtextfont
181 self.label["font"] = self.textfont
Terry Jan Reedya6bb3132017-09-17 00:56:56 -0400182 self.t2 = self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)
wohlganger58fc71c2017-09-10 16:19:47 -0500183
184
185CodeContext.reload()