blob: 74d5b70240d3b519af80cb1e511f344d44bbf771 [file] [log] [blame]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +00001"""CodeContext - Extension to display the block context above the edit window
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +00002
Kurt B. Kaiser0a135792005-10-03 19:26:03 +00003Once code has scrolled off the top of a window, it can be difficult to
4determine which block you are in. This extension implements a pane at the top
5of each IDLE edit window which provides block structure hints. These hints are
6the lines which contain the block opening keywords, e.g. 'if', for the
7enclosing block. The number of hint lines is determined by the numlines
8variable in the CodeContext section of config-extensions.def. Lines which do
9not open blocks are not shown in the context hints pane.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000010
11"""
12import Tkinter
13from configHandler import idleConf
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000014import re
Kurt B. Kaiser74910222005-10-02 23:36:46 +000015from sys import maxint as INFINITY
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000016
Georg Brandl7b71bf32006-07-17 13:23:46 +000017BLOCKOPENERS = set(["class", "def", "elif", "else", "except", "finally", "for",
Kurt B. Kaiser2a7ff292006-08-16 03:15:26 +000018 "if", "try", "while", "with"])
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000019UPDATEINTERVAL = 100 # millisec
20FONTUPDATEINTERVAL = 1000 # millisec
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000021
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000022getspacesfirstword =\
23 lambda s, c=re.compile(r"^(\s*)(\w*)"): c.match(s).groups()
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000024
25class CodeContext:
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000026 menudefs = [('options', [('!Code Conte_xt', '<<toggle-code-context>>')])]
27
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000028 context_depth = idleConf.GetOption("extensions", "CodeContext",
29 "numlines", type="int", default=3)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000030 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):
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000035 self.editwin = editwin
36 self.text = editwin.text
37 self.textfont = self.text["font"]
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000038 self.label = None
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000039 # 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.
Kurt B. Kaiser74910222005-10-02 23:36:46 +000044 self.info = [(0, -1, "", False)]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000045 self.topvisible = 1
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +000046 visible = idleConf.GetOption("extensions", "CodeContext",
47 "visible", type="bool", default=False)
48 if visible:
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000049 self.toggle_code_context_event()
50 self.editwin.setvar('<<toggle-code-context>>', True)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000051 # 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)
54
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000055 def toggle_code_context_event(self, event=None):
56 if not self.label:
Kurt B. Kaiser389482c2005-10-03 20:08:25 +000057 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,
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000061 text="\n" * (self.context_depth - 1),
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000062 anchor="w", justify="left",
63 font=self.textfont,
64 bg=self.bgcolor, fg=self.fgcolor,
Kurt B. Kaiser389482c2005-10-03 20:08:25 +000065 border=0,
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000066 width=1, # Don't request more than we get
67 )
Kurt B. Kaiser389482c2005-10-03 20:08:25 +000068 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)
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000073 else:
74 self.label.destroy()
Kurt B. Kaiser389482c2005-10-03 20:08:25 +000075 self.pad_frame.destroy()
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000076 self.label = None
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +000077 idleConf.SetOption("extensions", "CodeContext", "visible",
78 str(self.label is not None))
79 idleConf.SaveUserCfgFiles()
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000080
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000081 def get_line_info(self, linenum):
82 """Get the line indent value, text, and any block start keyword
83
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.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000086
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000087 """
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000088 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
96
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000097 def get_context(self, new_topvisible, stopline=1, stopindent=0):
98 """Get context lines, starting at new_topvisible and working backwards.
99
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.
102
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000103 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000104 assert stopline > 0
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000105 lines = []
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000106 # 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.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000110 for linenum in xrange(new_topvisible, stopline-1, -1):
111 indent, text, opener = self.get_line_info(linenum)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000112 if indent < lastindent:
113 lastindent = indent
Kurt B. Kaisere3636e02004-04-26 22:26:04 +0000114 if opener in ("else", "elif"):
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000115 # We also show the if statement
116 lastindent += 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000117 if opener and linenum < new_topvisible and indent >= stopindent:
118 lines.append((linenum, indent, text, opener))
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000119 if lastindent <= stopindent:
120 break
121 lines.reverse()
122 return lines, lastindent
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000123
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000124 def update_code_context(self):
125 """Update context information and lines visible in the context pane.
126
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000127 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000128 new_topvisible = int(self.text.index("@0,0").split('.')[0])
129 if self.topvisible == new_topvisible: # haven't scrolled
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000130 return
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000131 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:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000136 while self.info[-1][1] >= lastindent:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000137 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000138 elif self.topvisible > new_topvisible: # scroll up
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000139 stopindent = self.info[-1][1] + 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000140 # retain only context info associated
141 # with lines above new_topvisible:
142 while self.info[-1][0] >= new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000143 stopindent = self.info[-1][1]
144 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000145 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
150
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)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000156
157 def timer_event(self):
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000158 if self.label:
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000159 self.update_code_context()
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000160 self.text.after(UPDATEINTERVAL, self.timer_event)
161
162 def font_timer_event(self):
163 newtextfont = self.text["font"]
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000164 if self.label and newtextfont != self.textfont:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000165 self.textfont = newtextfont
166 self.label["font"] = self.textfont
167 self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)