Fix a bug in the ``compiler`` package that caused invalid code to be
[python/dscho.git] / Lib / idlelib / CodeContext.py
blob74d5b70240d3b519af80cb1e511f344d44bbf771
1 """CodeContext - Extension to display the block context above the edit window
3 Once code has scrolled off the top of a window, it can be difficult to
4 determine which block you are in. This extension implements a pane at the top
5 of each IDLE edit window which provides block structure hints. These hints are
6 the lines which contain the block opening keywords, e.g. 'if', for the
7 enclosing block. The number of hint lines is determined by the numlines
8 variable in the CodeContext section of config-extensions.def. Lines which do
9 not open blocks are not shown in the context hints pane.
11 """
12 import Tkinter
13 from configHandler import idleConf
14 import re
15 from sys import maxint as INFINITY
17 BLOCKOPENERS = set(["class", "def", "elif", "else", "except", "finally", "for",
18 "if", "try", "while", "with"])
19 UPDATEINTERVAL = 100 # millisec
20 FONTUPDATEINTERVAL = 1000 # millisec
22 getspacesfirstword =\
23 lambda s, c=re.compile(r"^(\s*)(\w*)"): c.match(s).groups()
25 class CodeContext:
26 menudefs = [('options', [('!Code Conte_xt', '<<toggle-code-context>>')])]
28 context_depth = idleConf.GetOption("extensions", "CodeContext",
29 "numlines", type="int", default=3)
30 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):
35 self.editwin = editwin
36 self.text = editwin.text
37 self.textfont = self.text["font"]
38 self.label = None
39 # 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.
44 self.info = [(0, -1, "", False)]
45 self.topvisible = 1
46 visible = idleConf.GetOption("extensions", "CodeContext",
47 "visible", type="bool", default=False)
48 if visible:
49 self.toggle_code_context_event()
50 self.editwin.setvar('<<toggle-code-context>>', True)
51 # 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)
55 def toggle_code_context_event(self, event=None):
56 if not self.label:
57 self.pad_frame = Tkinter.Frame(self.editwin.top,
58 bg=self.bgcolor, border=2,
59 relief="sunken")
60 self.label = Tkinter.Label(self.pad_frame,
61 text="\n" * (self.context_depth - 1),
62 anchor="w", justify="left",
63 font=self.textfont,
64 bg=self.bgcolor, fg=self.fgcolor,
65 border=0,
66 width=1, # Don't request more than we get
68 self.label.pack(side="top", fill="x", expand=True,
69 padx=4, pady=0)
70 self.pad_frame.pack(side="top", fill="x", expand=False,
71 padx=0, pady=0,
72 after=self.editwin.status_bar)
73 else:
74 self.label.destroy()
75 self.pad_frame.destroy()
76 self.label = None
77 idleConf.SetOption("extensions", "CodeContext", "visible",
78 str(self.label is not None))
79 idleConf.SaveUserCfgFiles()
81 def get_line_info(self, linenum):
82 """Get the line indent value, text, and any block start keyword
84 If the line does not start a block, the keyword value is False.
85 The indentation of empty lines (or comment lines) is INFINITY.
87 """
88 text = self.text.get("%d.0" % linenum, "%d.end" % linenum)
89 spaces, firstword = getspacesfirstword(text)
90 opener = firstword in BLOCKOPENERS and firstword
91 if len(text) == len(spaces) or text[len(spaces)] == '#':
92 indent = INFINITY
93 else:
94 indent = len(spaces)
95 return indent, text, opener
97 def get_context(self, new_topvisible, stopline=1, stopindent=0):
98 """Get context lines, starting at new_topvisible and working backwards.
100 Stop when stopline or stopindent is reached. Return a tuple of context
101 data and the indent level at the top of the region inspected.
104 assert stopline > 0
105 lines = []
106 # The indentation level we are currently in:
107 lastindent = INFINITY
108 # For a line to be interesting, it must begin with a block opening
109 # keyword, and have less indentation than lastindent.
110 for linenum in xrange(new_topvisible, stopline-1, -1):
111 indent, text, opener = self.get_line_info(linenum)
112 if indent < lastindent:
113 lastindent = indent
114 if opener in ("else", "elif"):
115 # We also show the if statement
116 lastindent += 1
117 if opener and linenum < new_topvisible and indent >= stopindent:
118 lines.append((linenum, indent, text, opener))
119 if lastindent <= stopindent:
120 break
121 lines.reverse()
122 return lines, lastindent
124 def update_code_context(self):
125 """Update context information and lines visible in the context pane.
128 new_topvisible = int(self.text.index("@0,0").split('.')[0])
129 if self.topvisible == new_topvisible: # haven't scrolled
130 return
131 if self.topvisible < new_topvisible: # scroll down
132 lines, lastindent = self.get_context(new_topvisible,
133 self.topvisible)
134 # retain only context info applicable to the region
135 # between topvisible and new_topvisible:
136 while self.info[-1][1] >= lastindent:
137 del self.info[-1]
138 elif self.topvisible > new_topvisible: # scroll up
139 stopindent = self.info[-1][1] + 1
140 # retain only context info associated
141 # with lines above new_topvisible:
142 while self.info[-1][0] >= new_topvisible:
143 stopindent = self.info[-1][1]
144 del self.info[-1]
145 lines, lastindent = self.get_context(new_topvisible,
146 self.info[-1][0]+1,
147 stopindent)
148 self.info.extend(lines)
149 self.topvisible = new_topvisible
151 # empty lines in context pane:
152 context_strings = [""] * max(0, self.context_depth - len(self.info))
153 # followed by the context hint lines:
154 context_strings += [x[2] for x in self.info[-self.context_depth:]]
155 self.label["text"] = '\n'.join(context_strings)
157 def timer_event(self):
158 if self.label:
159 self.update_code_context()
160 self.text.after(UPDATEINTERVAL, self.timer_event)
162 def font_timer_event(self):
163 newtextfont = self.text["font"]
164 if self.label and newtextfont != self.textfont:
165 self.textfont = newtextfont
166 self.label["font"] = self.textfont
167 self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)