blob: 779b5b88cf35fa6b90ca1915602d866d2f9d3a91 [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
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000025getspacesfirstword =\
26 lambda s, c=re.compile(r"^(\s*)(\w*)"): 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:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000030 bgcolor = idleConf.GetOption("extensions", "CodeContext",
31 "bgcolor", type="str", default="LightGray")
32 fgcolor = idleConf.GetOption("extensions", "CodeContext",
33 "fgcolor", type="str", default="Black")
34 def __init__(self, editwin):
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000035 self.editwin = editwin
36 self.text = editwin.text
37 self.textfont = self.text["font"]
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000038 self.label = None
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000039 # self.info is a list of (line number, indent level, line text, block
40 # keyword) tuples providing the block structure associated with
41 # self.topvisible (the linenumber of the line displayed at the top of
42 # the edit window). self.info[0] is initialized as a 'dummy' line which
43 # starts the toplevel 'block' of the module.
Kurt B. Kaiser74910222005-10-02 23:36:46 +000044 self.info = [(0, -1, "", False)]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000045 self.topvisible = 1
wohlganger58fc71c2017-09-10 16:19:47 -050046 self.reload()
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000047 # Start two update cycles, one for context lines, one for font changes.
48 self.text.after(UPDATEINTERVAL, self.timer_event)
49 self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)
50
wohlganger58fc71c2017-09-10 16:19:47 -050051 @classmethod
52 def reload(cls):
53 cls.context_depth = idleConf.GetOption("extensions", "CodeContext",
54 "numlines", type="int", default=3)
55 cls.bgcolor = idleConf.GetOption("extensions", "CodeContext",
56 "bgcolor", type="str", default="LightGray")
57 cls.fgcolor = idleConf.GetOption("extensions", "CodeContext",
58 "fgcolor", type="str", default="Black")
59
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000060 def toggle_code_context_event(self, event=None):
61 if not self.label:
Thomas Wouterscf297e42007-02-23 15:07:44 +000062 # Calculate the border width and horizontal padding required to
63 # align the context with the text in the main Text widget.
Thomas Wouters89f507f2006-12-13 04:49:30 +000064 #
Serhiy Storchaka645058d2015-05-06 14:00:04 +030065 # All values are passed through getint(), since some
Thomas Wouterscf297e42007-02-23 15:07:44 +000066 # values may be pixel objects, which can't simply be added to ints.
67 widgets = self.editwin.text, self.editwin.text_frame
68 # Calculate the required vertical padding
Thomas Wouters89f507f2006-12-13 04:49:30 +000069 padx = 0
Thomas Wouterscf297e42007-02-23 15:07:44 +000070 for widget in widgets:
Serhiy Storchaka645058d2015-05-06 14:00:04 +030071 padx += widget.tk.getint(widget.pack_info()['padx'])
72 padx += widget.tk.getint(widget.cget('padx'))
Thomas Wouterscf297e42007-02-23 15:07:44 +000073 # Calculate the required border width
74 border = 0
75 for widget in widgets:
Serhiy Storchaka645058d2015-05-06 14:00:04 +030076 border += widget.tk.getint(widget.cget('border'))
Georg Brandl14fc4272008-05-17 18:39:55 +000077 self.label = tkinter.Label(self.editwin.top,
Thomas Wouters89f507f2006-12-13 04:49:30 +000078 text="\n" * (self.context_depth - 1),
Thomas Wouterscf297e42007-02-23 15:07:44 +000079 anchor=W, justify=LEFT,
Thomas Wouters89f507f2006-12-13 04:49:30 +000080 font=self.textfont,
81 bg=self.bgcolor, fg=self.fgcolor,
82 width=1, #don't request more than we get
Thomas Wouterscf297e42007-02-23 15:07:44 +000083 padx=padx, border=border,
84 relief=SUNKEN)
85 # Pack the label widget before and above the text_frame widget,
86 # thus ensuring that it will appear directly above text_frame
87 self.label.pack(side=TOP, fill=X, expand=False,
Thomas Wouters89f507f2006-12-13 04:49:30 +000088 before=self.editwin.text_frame)
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000089 else:
90 self.label.destroy()
91 self.label = None
wohlganger58fc71c2017-09-10 16:19:47 -050092 idleConf.SetOption("main", "Theme", "contexton",
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +000093 str(self.label is not None))
94 idleConf.SaveUserCfgFiles()
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()
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000175 self.text.after(UPDATEINTERVAL, self.timer_event)
176
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
182 self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)
wohlganger58fc71c2017-09-10 16:19:47 -0500183
184
185CodeContext.reload()