blob: e9859694b66be32b1228bb59abec73ccd758be19 [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
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +00003Once code has scrolled off the top of the screen, it can be difficult
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +00004to 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
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +000015from sets import Set
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000016import re
Kurt B. Kaiser74910222005-10-02 23:36:46 +000017from sys import maxint as INFINITY
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000018
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +000019BLOCKOPENERS = Set(["class", "def", "elif", "else", "except", "finally", "for",
20 "if", "try", "while"])
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000021UPDATEINTERVAL = 100 # millisec
22FONTUPDATEINTERVAL = 1000 # millisec
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000023
24getspacesfirstword = lambda s, c=re.compile(r"^(\s*)(\w*)"): c.match(s).groups()
25
26class CodeContext:
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000027 menudefs = [('options', [('!Code Conte_xt', '<<toggle-code-context>>')])]
28
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000029 numlines = idleConf.GetOption("extensions", "CodeContext",
30 "numlines", type="int", default=3)
31 bgcolor = idleConf.GetOption("extensions", "CodeContext",
32 "bgcolor", type="str", default="LightGray")
33 fgcolor = idleConf.GetOption("extensions", "CodeContext",
34 "fgcolor", type="str", default="Black")
35 def __init__(self, editwin):
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000036 self.editwin = editwin
37 self.text = editwin.text
38 self.textfont = self.text["font"]
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000039 self.label = None
Kurt B. Kaiser74910222005-10-02 23:36:46 +000040 # self.info holds information about the context lines of line number
41 # self.lastfirstline. The information is a tuple of the line's
42 # indentation, the line's text and the keyword at the beginning of the
43 # line, as returned by get_line_info. At the beginning of the list
44 # there's a dummy line, which starts the "block" of the whole document.
45 self.info = [(0, -1, "", False)]
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000046 self.lastfirstline = 1
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +000047 visible = idleConf.GetOption("extensions", "CodeContext",
48 "visible", type="bool", default=False)
49 if visible:
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000050 self.toggle_code_context_event()
51 self.editwin.setvar('<<toggle-code-context>>', True)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000052 # Start two update cycles, one for context lines, one for font changes.
53 self.text.after(UPDATEINTERVAL, self.timer_event)
54 self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)
55
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000056 def toggle_code_context_event(self, event=None):
57 if not self.label:
58 self.label = Tkinter.Label(self.editwin.top,
59 text="\n" * (self.numlines - 1),
60 anchor="w", justify="left",
61 font=self.textfont,
62 bg=self.bgcolor, fg=self.fgcolor,
63 relief="sunken",
64 width=1, # Don't request more than we get
65 )
66 self.label.pack(side="top", fill="x", expand=0,
67 after=self.editwin.status_bar)
68 else:
69 self.label.destroy()
70 self.label = None
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +000071 idleConf.SetOption("extensions", "CodeContext", "visible",
72 str(self.label is not None))
73 idleConf.SaveUserCfgFiles()
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000074
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000075 def get_line_info(self, linenum):
76 """Get the line indent value, text, and any block start keyword
77
78 If the line does not start a block, the keyword value is False.
79 The indentation of empty lines (or comment lines) is INFINITY.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000080 """
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000081 text = self.text.get("%d.0" % linenum, "%d.end" % linenum)
82 spaces, firstword = getspacesfirstword(text)
83 opener = firstword in BLOCKOPENERS and firstword
84 if len(text) == len(spaces) or text[len(spaces)] == '#':
85 indent = INFINITY
86 else:
87 indent = len(spaces)
88 return indent, text, opener
89
Kurt B. Kaiser74910222005-10-02 23:36:46 +000090 def interesting_lines(self, firstline, stopline=1, stopindent=0):
91 """
92 Find the context lines, starting at firstline.
93 Will not return lines whose index is smaller than stopline or whose
94 indentation is smaller than stopindent.
95 stopline should always be >= 1, so the dummy block start will never
96 be returned (This function doesn't know what to do about it.)
97 Returns a list with the context lines, starting from the first (top),
98 and a number which all context lines above the inspected region should
99 have a smaller indentation than it.
100 """
101 lines = []
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000102 # The indentation level we are currently in:
103 lastindent = INFINITY
104 # For a line to be interesting, it must begin with a block opening
105 # keyword, and have less indentation than lastindent.
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000106 for line_index in xrange(firstline, stopline-1, -1):
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000107 indent, text, opener = self.get_line_info(line_index)
108 if indent < lastindent:
109 lastindent = indent
Kurt B. Kaisere3636e02004-04-26 22:26:04 +0000110 if opener in ("else", "elif"):
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000111 # We also show the if statement
112 lastindent += 1
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000113 if opener and line_index < firstline and indent >= stopindent:
114 lines.append((line_index, indent, text, opener))
115 if lastindent <= stopindent:
116 break
117 lines.reverse()
118 return lines, lastindent
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000119
120 def update_label(self):
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000121 """Update the CodeContext label, if needed.
122 """
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000123 firstline = int(self.text.index("@0,0").split('.')[0])
124 if self.lastfirstline == firstline:
125 return
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000126 if self.lastfirstline < firstline:
127 lines, lastindent = self.interesting_lines(firstline,
128 self.lastfirstline)
129 while self.info[-1][1] >= lastindent:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000130 del self.info[-1]
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000131 self.info.extend(lines)
132 else:
133 stopindent = self.info[-1][1] + 1
134 while self.info[-1][0] >= firstline:
135 stopindent = self.info[-1][1]
136 del self.info[-1]
137 lines, lastindent = self.interesting_lines(
138 firstline, self.info[-1][0]+1, stopindent)
139 self.info.extend(lines)
140 self.lastfirstline = firstline
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000141 lines = [""] * max(0, self.numlines - len(self.info)) + \
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000142 [x[2] for x in self.info[-self.numlines:]]
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000143 self.label["text"] = '\n'.join(lines)
144
145 def timer_event(self):
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000146 if self.label:
147 self.update_label()
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000148 self.text.after(UPDATEINTERVAL, self.timer_event)
149
150 def font_timer_event(self):
151 newtextfont = self.text["font"]
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000152 if self.label and newtextfont != self.textfont:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000153 self.textfont = newtextfont
154 self.label["font"] = self.textfont
155 self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)