blob: 32dec82a12640753635e785c27d41ec2496951b6 [file] [log] [blame]
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +00001"""CodeContext - Display the block context of code at top of edit window
2
3Once code has scolled off the top of the screen, it can be difficult
4to determine which block you are in. This extension implements a pane
5at the top of each IDLE edit window which provides block structure
6hints. These hints are the lines which contain the block opening
7keywords, e.g. 'if', for the enclosing block. The number of hint lines
8is determined by the numlines variable in the CodeContext section of
9config-extensions.def. Lines which do not open blocks are not shown in
10the context hints pane.
11
12"""
13import Tkinter
14from configHandler import idleConf
15from PyShell import PyShell
16from string import whitespace
17import re
18
19BLOCKOPENERS = dict([(x, None) for x in ("class", "def", "elif", "else",
20 "except", "finally", "for", "if",
21 "try", "while")])
22INFINITY = 1 << 30
23UPDATEINTERVAL = 100 #ms
24FONTUPDATEINTERVAL = 1000 #ms
25
26getspacesfirstword = lambda s, c=re.compile(r"^(\s*)(\w*)"): c.match(s).groups()
27
28class CodeContext:
29 menudefs = []
30 numlines = idleConf.GetOption("extensions", "CodeContext",
31 "numlines", type="int", default=3)
32 bgcolor = idleConf.GetOption("extensions", "CodeContext",
33 "bgcolor", type="str", default="LightGray")
34 fgcolor = idleConf.GetOption("extensions", "CodeContext",
35 "fgcolor", type="str", default="Black")
36 def __init__(self, editwin):
37 if isinstance(editwin, PyShell):
38 return
39 self.editwin = editwin
40 self.text = editwin.text
41 self.textfont = self.text["font"]
42 self.label = Tkinter.Label(self.editwin.top,
43 text="\n" * (self.numlines - 1),
44 anchor="w", justify="left",
45 font=self.textfont,
46 bg=self.bgcolor, fg=self.fgcolor,
47 relief="sunken",
48 width=1, # Don't request more than we get
49 )
50 self.label.pack(side="top", fill="x", expand=0,
51 after=self.editwin.status_bar)
52 # Dummy line, which starts the "block" of the whole document:
53 self.info = list(self.interesting_lines(1))
54 self.lastfirstline = 1
55 # Start two update cycles, one for context lines, one for font changes.
56 self.text.after(UPDATEINTERVAL, self.timer_event)
57 self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)
58
59 def get_line_info(self, linenum):
60 """Get the line indent value, text, and any block start keyword
61
62 If the line does not start a block, the keyword value is False.
63 The indentation of empty lines (or comment lines) is INFINITY.
64 There is a dummy block start, with indentation -1 and text "".
65
66 Return the indent level, text (including leading whitespace),
67 and the block opening keyword.
68
69 """
70 if linenum == 0:
71 return -1, "", True
72 text = self.text.get("%d.0" % linenum, "%d.end" % linenum)
73 spaces, firstword = getspacesfirstword(text)
74 opener = firstword in BLOCKOPENERS and firstword
75 if len(text) == len(spaces) or text[len(spaces)] == '#':
76 indent = INFINITY
77 else:
78 indent = len(spaces)
79 return indent, text, opener
80
81 def interesting_lines(self, firstline):
82 """Generator which yields context lines, starting at firstline."""
83 # The indentation level we are currently in:
84 lastindent = INFINITY
85 # For a line to be interesting, it must begin with a block opening
86 # keyword, and have less indentation than lastindent.
87 for line_index in xrange(firstline, -1, -1):
88 indent, text, opener = self.get_line_info(line_index)
89 if indent < lastindent:
90 lastindent = indent
91 if opener == "else" or "elif":
92 # We also show the if statement
93 lastindent += 1
94 if opener and line_index < firstline:
95 yield line_index, text
96
97 def update_label(self):
98 firstline = int(self.text.index("@0,0").split('.')[0])
99 if self.lastfirstline == firstline:
100 return
101 self.lastfirstline = firstline
102 tmpstack = []
103 for line_index, text in self.interesting_lines(firstline):
104 # Remove irrelevant self.info items, and when we reach a relevant
105 # item (which must happen because of the dummy element), break.
106 while self.info[-1][0] > line_index:
107 del self.info[-1]
108 if self.info[-1][0] == line_index:
109 break
110 # Add the block starting line info to tmpstack
111 tmpstack.append((line_index, text))
112 while tmpstack:
113 self.info.append(tmpstack.pop())
114 lines = [""] * max(0, self.numlines - len(self.info)) + \
115 [x[1] for x in self.info[-self.numlines:]]
116 self.label["text"] = '\n'.join(lines)
117
118 def timer_event(self):
119 self.update_label()
120 self.text.after(UPDATEINTERVAL, self.timer_event)
121
122 def font_timer_event(self):
123 newtextfont = self.text["font"]
124 if newtextfont != self.textfont:
125 self.textfont = newtextfont
126 self.label["font"] = self.textfont
127 self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)