Backport importlib to at least Python 2.5 by getting rid of use of str.format.
[python.git] / Lib / idlelib / CodeContext.py
blob420ec339ca2704f3ce9b761eb4a2a332abeaf153
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 Tkconstants import TOP, LEFT, X, W, SUNKEN
14 from configHandler import idleConf
15 import re
16 from sys import maxint as INFINITY
18 BLOCKOPENERS = set(["class", "def", "elif", "else", "except", "finally", "for",
19 "if", "try", "while", "with"])
20 UPDATEINTERVAL = 100 # millisec
21 FONTUPDATEINTERVAL = 1000 # millisec
23 getspacesfirstword =\
24 lambda s, c=re.compile(r"^(\s*)(\w*)"): c.match(s).groups()
26 class CodeContext:
27 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 # Calculate the border width and horizontal padding required to
58 # align the context with the text in the main Text widget.
60 # All values are passed through int(str(<value>)), since some
61 # values may be pixel objects, which can't simply be added to ints.
62 widgets = self.editwin.text, self.editwin.text_frame
63 # Calculate the required vertical padding
64 padx = 0
65 for widget in widgets:
66 padx += int(str( widget.pack_info()['padx'] ))
67 padx += int(str( widget.cget('padx') ))
68 # Calculate the required border width
69 border = 0
70 for widget in widgets:
71 border += int(str( widget.cget('border') ))
72 self.label = Tkinter.Label(self.editwin.top,
73 text="\n" * (self.context_depth - 1),
74 anchor=W, justify=LEFT,
75 font=self.textfont,
76 bg=self.bgcolor, fg=self.fgcolor,
77 width=1, #don't request more than we get
78 padx=padx, border=border,
79 relief=SUNKEN)
80 # Pack the label widget before and above the text_frame widget,
81 # thus ensuring that it will appear directly above text_frame
82 self.label.pack(side=TOP, fill=X, expand=False,
83 before=self.editwin.text_frame)
84 else:
85 self.label.destroy()
86 self.label = None
87 idleConf.SetOption("extensions", "CodeContext", "visible",
88 str(self.label is not None))
89 idleConf.SaveUserCfgFiles()
91 def get_line_info(self, linenum):
92 """Get the line indent value, text, and any block start keyword
94 If the line does not start a block, the keyword value is False.
95 The indentation of empty lines (or comment lines) is INFINITY.
97 """
98 text = self.text.get("%d.0" % linenum, "%d.end" % linenum)
99 spaces, firstword = getspacesfirstword(text)
100 opener = firstword in BLOCKOPENERS and firstword
101 if len(text) == len(spaces) or text[len(spaces)] == '#':
102 indent = INFINITY
103 else:
104 indent = len(spaces)
105 return indent, text, opener
107 def get_context(self, new_topvisible, stopline=1, stopindent=0):
108 """Get context lines, starting at new_topvisible and working backwards.
110 Stop when stopline or stopindent is reached. Return a tuple of context
111 data and the indent level at the top of the region inspected.
114 assert stopline > 0
115 lines = []
116 # The indentation level we are currently in:
117 lastindent = INFINITY
118 # For a line to be interesting, it must begin with a block opening
119 # keyword, and have less indentation than lastindent.
120 for linenum in xrange(new_topvisible, stopline-1, -1):
121 indent, text, opener = self.get_line_info(linenum)
122 if indent < lastindent:
123 lastindent = indent
124 if opener in ("else", "elif"):
125 # We also show the if statement
126 lastindent += 1
127 if opener and linenum < new_topvisible and indent >= stopindent:
128 lines.append((linenum, indent, text, opener))
129 if lastindent <= stopindent:
130 break
131 lines.reverse()
132 return lines, lastindent
134 def update_code_context(self):
135 """Update context information and lines visible in the context pane.
138 new_topvisible = int(self.text.index("@0,0").split('.')[0])
139 if self.topvisible == new_topvisible: # haven't scrolled
140 return
141 if self.topvisible < new_topvisible: # scroll down
142 lines, lastindent = self.get_context(new_topvisible,
143 self.topvisible)
144 # retain only context info applicable to the region
145 # between topvisible and new_topvisible:
146 while self.info[-1][1] >= lastindent:
147 del self.info[-1]
148 elif self.topvisible > new_topvisible: # scroll up
149 stopindent = self.info[-1][1] + 1
150 # retain only context info associated
151 # with lines above new_topvisible:
152 while self.info[-1][0] >= new_topvisible:
153 stopindent = self.info[-1][1]
154 del self.info[-1]
155 lines, lastindent = self.get_context(new_topvisible,
156 self.info[-1][0]+1,
157 stopindent)
158 self.info.extend(lines)
159 self.topvisible = new_topvisible
160 # empty lines in context pane:
161 context_strings = [""] * max(0, self.context_depth - len(self.info))
162 # followed by the context hint lines:
163 context_strings += [x[2] for x in self.info[-self.context_depth:]]
164 self.label["text"] = '\n'.join(context_strings)
166 def timer_event(self):
167 if self.label:
168 self.update_code_context()
169 self.text.after(UPDATEINTERVAL, self.timer_event)
171 def font_timer_event(self):
172 newtextfont = self.text["font"]
173 if self.label and newtextfont != self.textfont:
174 self.textfont = newtextfont
175 self.label["font"] = self.textfont
176 self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)