blob: c791f98846c6c21c320b9d4aeb24820034249942 [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
17
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +000018BLOCKOPENERS = Set(["class", "def", "elif", "else", "except", "finally", "for",
19 "if", "try", "while"])
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000020INFINITY = 1 << 30
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. Kaiser54d1a3b2004-04-21 20:06:26 +000040 # Dummy line, which starts the "block" of the whole document:
41 self.info = list(self.interesting_lines(1))
42 self.lastfirstline = 1
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +000043 visible = idleConf.GetOption("extensions", "CodeContext",
44 "visible", type="bool", default=False)
45 if visible:
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000046 self.toggle_code_context_event()
47 self.editwin.setvar('<<toggle-code-context>>', True)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000048 # Start two update cycles, one for context lines, one for font changes.
49 self.text.after(UPDATEINTERVAL, self.timer_event)
50 self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)
51
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000052 def toggle_code_context_event(self, event=None):
53 if not self.label:
54 self.label = Tkinter.Label(self.editwin.top,
55 text="\n" * (self.numlines - 1),
56 anchor="w", justify="left",
57 font=self.textfont,
58 bg=self.bgcolor, fg=self.fgcolor,
59 relief="sunken",
60 width=1, # Don't request more than we get
61 )
62 self.label.pack(side="top", fill="x", expand=0,
63 after=self.editwin.status_bar)
64 else:
65 self.label.destroy()
66 self.label = None
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +000067 idleConf.SetOption("extensions", "CodeContext", "visible",
68 str(self.label is not None))
69 idleConf.SaveUserCfgFiles()
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000070
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000071 def get_line_info(self, linenum):
72 """Get the line indent value, text, and any block start keyword
73
74 If the line does not start a block, the keyword value is False.
75 The indentation of empty lines (or comment lines) is INFINITY.
76 There is a dummy block start, with indentation -1 and text "".
77
78 Return the indent level, text (including leading whitespace),
79 and the block opening keyword.
80
81 """
82 if linenum == 0:
83 return -1, "", True
84 text = self.text.get("%d.0" % linenum, "%d.end" % linenum)
85 spaces, firstword = getspacesfirstword(text)
86 opener = firstword in BLOCKOPENERS and firstword
87 if len(text) == len(spaces) or text[len(spaces)] == '#':
88 indent = INFINITY
89 else:
90 indent = len(spaces)
91 return indent, text, opener
92
93 def interesting_lines(self, firstline):
94 """Generator which yields context lines, starting at firstline."""
95 # The indentation level we are currently in:
96 lastindent = INFINITY
97 # For a line to be interesting, it must begin with a block opening
98 # keyword, and have less indentation than lastindent.
99 for line_index in xrange(firstline, -1, -1):
100 indent, text, opener = self.get_line_info(line_index)
101 if indent < lastindent:
102 lastindent = indent
Kurt B. Kaisere3636e02004-04-26 22:26:04 +0000103 if opener in ("else", "elif"):
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000104 # We also show the if statement
105 lastindent += 1
106 if opener and line_index < firstline:
107 yield line_index, text
108
109 def update_label(self):
110 firstline = int(self.text.index("@0,0").split('.')[0])
111 if self.lastfirstline == firstline:
112 return
113 self.lastfirstline = firstline
114 tmpstack = []
115 for line_index, text in self.interesting_lines(firstline):
116 # Remove irrelevant self.info items, and when we reach a relevant
117 # item (which must happen because of the dummy element), break.
118 while self.info[-1][0] > line_index:
119 del self.info[-1]
120 if self.info[-1][0] == line_index:
121 break
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000122 tmpstack.append((line_index, text))
123 while tmpstack:
124 self.info.append(tmpstack.pop())
125 lines = [""] * max(0, self.numlines - len(self.info)) + \
126 [x[1] for x in self.info[-self.numlines:]]
127 self.label["text"] = '\n'.join(lines)
128
129 def timer_event(self):
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000130 if self.label:
131 self.update_label()
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000132 self.text.after(UPDATEINTERVAL, self.timer_event)
133
134 def font_timer_event(self):
135 newtextfont = self.text["font"]
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000136 if self.label and newtextfont != self.textfont:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000137 self.textfont = newtextfont
138 self.label["font"] = self.textfont
139 self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)