blob: 436206f063bc47353d50efae2e52422aef83e5a6 [file] [log] [blame]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +00001"""CodeContext - Extension to 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
8variable in the CodeContext section of config-extensions.def. Lines which do
9not open blocks are not shown in the context hints pane.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000010
11"""
12import Tkinter
13from configHandler import idleConf
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000014import re
Kurt B. Kaiser74910222005-10-02 23:36:46 +000015from sys import maxint as INFINITY
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000016
Georg Brandl7b71bf32006-07-17 13:23:46 +000017BLOCKOPENERS = set(["class", "def", "elif", "else", "except", "finally", "for",
Kurt B. Kaiser2a7ff292006-08-16 03:15:26 +000018 "if", "try", "while", "with"])
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000019UPDATEINTERVAL = 100 # millisec
20FONTUPDATEINTERVAL = 1000 # millisec
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000021
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000022getspacesfirstword =\
23 lambda s, c=re.compile(r"^(\s*)(\w*)"): c.match(s).groups()
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000024
25class CodeContext:
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000026 menudefs = [('options', [('!Code Conte_xt', '<<toggle-code-context>>')])]
27
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000028 context_depth = idleConf.GetOption("extensions", "CodeContext",
29 "numlines", type="int", default=3)
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
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +000046 visible = idleConf.GetOption("extensions", "CodeContext",
47 "visible", type="bool", default=False)
48 if visible:
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000049 self.toggle_code_context_event()
50 self.editwin.setvar('<<toggle-code-context>>', True)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000051 # Start two update cycles, one for context lines, one for font changes.
52 self.text.after(UPDATEINTERVAL, self.timer_event)
53 self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)
54
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000055 def toggle_code_context_event(self, event=None):
56 if not self.label:
Martin v. Löwis4ebbefe2006-11-22 08:50:02 +000057 # The following code attempts to figure out the required border
58 # width and vertical padding required for the CodeContext widget
59 # to be perfectly aligned with the text in the main Text widget.
60 # This is done by retrieving the appropriate attributes from the
61 # editwin.text and editwin.text_frame widgets.
62 #
63 # All values are passed through int(str(<value>)), since some
64 # values may be pixel objects, which can't simply be added added
65 # to ints.
66 #
67 # This code is considered somewhat unstable since it relies on
68 # some of Tk's inner workings. However its effect is merely
69 # cosmetic; failure will only cause the CodeContext text to be
70 # somewhat misaligned with the text in the main Text widget.
71 #
72 # To avoid possible errors, all references to the inner workings
73 # of Tk are executed inside try/except blocks.
74
75 widgets_for_width_calc = self.editwin.text, self.editwin.text_frame
76
77 # calculate the required vertical padding
78 padx = 0
79 for widget in widgets_for_width_calc:
80 try:
81 # retrieve the "padx" attribte from widget's pack info
82 padx += int(str( widget.pack_info()['padx'] ))
83 except:
84 pass
85 try:
86 # retrieve the widget's "padx" attribte
87 padx += int(str( widget.cget('padx') ))
88 except:
89 pass
90
91 # calculate the required border width
92 border_width = 0
93 for widget in widgets_for_width_calc:
94 try:
95 # retrieve the widget's "border" attribte
96 border_width += int(str( widget.cget('border') ))
97 except:
98 pass
99
100 self.label = Tkinter.Label(self.editwin.top,
101 text="\n" * (self.context_depth - 1),
102 anchor="w", justify="left",
103 font=self.textfont,
104 bg=self.bgcolor, fg=self.fgcolor,
105 width=1, #don't request more than we get
106 padx=padx, #line up with text widget
107 border=border_width, #match border width
108 relief="sunken",
109 )
110
111 # CodeContext's label widget is packed before and above the
112 # text_frame widget, thus ensuring that it will appear directly
113 # above it.
114 self.label.pack(side="top", fill="x", expand=False,
115 before=self.editwin.text_frame)
116
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000117 else:
118 self.label.destroy()
119 self.label = None
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +0000120 idleConf.SetOption("extensions", "CodeContext", "visible",
121 str(self.label is not None))
122 idleConf.SaveUserCfgFiles()
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000123
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000124 def get_line_info(self, linenum):
125 """Get the line indent value, text, and any block start keyword
126
127 If the line does not start a block, the keyword value is False.
128 The indentation of empty lines (or comment lines) is INFINITY.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000129
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000130 """
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000131 text = self.text.get("%d.0" % linenum, "%d.end" % linenum)
132 spaces, firstword = getspacesfirstword(text)
133 opener = firstword in BLOCKOPENERS and firstword
134 if len(text) == len(spaces) or text[len(spaces)] == '#':
135 indent = INFINITY
136 else:
137 indent = len(spaces)
138 return indent, text, opener
139
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000140 def get_context(self, new_topvisible, stopline=1, stopindent=0):
141 """Get context lines, starting at new_topvisible and working backwards.
142
143 Stop when stopline or stopindent is reached. Return a tuple of context
144 data and the indent level at the top of the region inspected.
145
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000146 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000147 assert stopline > 0
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000148 lines = []
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000149 # The indentation level we are currently in:
150 lastindent = INFINITY
151 # For a line to be interesting, it must begin with a block opening
152 # keyword, and have less indentation than lastindent.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000153 for linenum in xrange(new_topvisible, stopline-1, -1):
154 indent, text, opener = self.get_line_info(linenum)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000155 if indent < lastindent:
156 lastindent = indent
Kurt B. Kaisere3636e02004-04-26 22:26:04 +0000157 if opener in ("else", "elif"):
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000158 # We also show the if statement
159 lastindent += 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000160 if opener and linenum < new_topvisible and indent >= stopindent:
161 lines.append((linenum, indent, text, opener))
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000162 if lastindent <= stopindent:
163 break
164 lines.reverse()
165 return lines, lastindent
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000166
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000167 def update_code_context(self):
168 """Update context information and lines visible in the context pane.
169
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000170 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000171 new_topvisible = int(self.text.index("@0,0").split('.')[0])
172 if self.topvisible == new_topvisible: # haven't scrolled
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000173 return
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000174 if self.topvisible < new_topvisible: # scroll down
175 lines, lastindent = self.get_context(new_topvisible,
176 self.topvisible)
177 # retain only context info applicable to the region
178 # between topvisible and new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000179 while self.info[-1][1] >= lastindent:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000180 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000181 elif self.topvisible > new_topvisible: # scroll up
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000182 stopindent = self.info[-1][1] + 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000183 # retain only context info associated
184 # with lines above new_topvisible:
185 while self.info[-1][0] >= new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000186 stopindent = self.info[-1][1]
187 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000188 lines, lastindent = self.get_context(new_topvisible,
189 self.info[-1][0]+1,
190 stopindent)
191 self.info.extend(lines)
192 self.topvisible = new_topvisible
193
194 # empty lines in context pane:
195 context_strings = [""] * max(0, self.context_depth - len(self.info))
196 # followed by the context hint lines:
197 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):
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000201 if self.label:
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000202 self.update_code_context()
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000203 self.text.after(UPDATEINTERVAL, self.timer_event)
204
205 def font_timer_event(self):
206 newtextfont = self.text["font"]
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000207 if self.label and newtextfont != self.textfont:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000208 self.textfont = newtextfont
209 self.label["font"] = self.textfont
210 self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)