blob: 5d55f77ee07e76b03eb8026fecd7600da5287012 [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. Kaiser4d5bc602004-06-06 01:29:22 +000014from sets import Set
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000015import re
Kurt B. Kaiser74910222005-10-02 23:36:46 +000016from sys import maxint as INFINITY
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000017
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +000018BLOCKOPENERS = Set(["class", "def", "elif", "else", "except", "finally", "for",
19 "if", "try", "while"])
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000020UPDATEINTERVAL = 100 # millisec
21FONTUPDATEINTERVAL = 1000 # millisec
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000022
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000023getspacesfirstword =\
24 lambda s, c=re.compile(r"^(\s*)(\w*)"): c.match(s).groups()
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000025
26class CodeContext:
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000027 menudefs = [('options', [('!Code Conte_xt', '<<toggle-code-context>>')])]
28
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000029 context_depth = idleConf.GetOption("extensions", "CodeContext",
30 "numlines", type="int", default=3)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000031 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. Kaiser0a135792005-10-03 19:26:03 +000040 # self.info is a list of (line number, indent level, line text, block
41 # keyword) tuples providing the block structure associated with
42 # self.topvisible (the linenumber of the line displayed at the top of
43 # the edit window). self.info[0] is initialized as a 'dummy' line which
44 # starts the toplevel 'block' of the module.
Kurt B. Kaiser74910222005-10-02 23:36:46 +000045 self.info = [(0, -1, "", False)]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000046 self.topvisible = 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:
Kurt B. Kaiser389482c2005-10-03 20:08:25 +000058 self.pad_frame = Tkinter.Frame(self.editwin.top,
59 bg=self.bgcolor, border=2,
60 relief="sunken")
61 self.label = Tkinter.Label(self.pad_frame,
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000062 text="\n" * (self.context_depth - 1),
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000063 anchor="w", justify="left",
64 font=self.textfont,
65 bg=self.bgcolor, fg=self.fgcolor,
Kurt B. Kaiser389482c2005-10-03 20:08:25 +000066 border=0,
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000067 width=1, # Don't request more than we get
68 )
Kurt B. Kaiser389482c2005-10-03 20:08:25 +000069 self.label.pack(side="top", fill="x", expand=True,
70 padx=4, pady=0)
71 self.pad_frame.pack(side="top", fill="x", expand=False,
72 padx=0, pady=0,
73 after=self.editwin.status_bar)
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000074 else:
75 self.label.destroy()
Kurt B. Kaiser389482c2005-10-03 20:08:25 +000076 self.pad_frame.destroy()
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000077 self.label = None
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +000078 idleConf.SetOption("extensions", "CodeContext", "visible",
79 str(self.label is not None))
80 idleConf.SaveUserCfgFiles()
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000081
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000082 def get_line_info(self, linenum):
83 """Get the line indent value, text, and any block start keyword
84
85 If the line does not start a block, the keyword value is False.
86 The indentation of empty lines (or comment lines) is INFINITY.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000087
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000088 """
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000089 text = self.text.get("%d.0" % linenum, "%d.end" % linenum)
90 spaces, firstword = getspacesfirstword(text)
91 opener = firstword in BLOCKOPENERS and firstword
92 if len(text) == len(spaces) or text[len(spaces)] == '#':
93 indent = INFINITY
94 else:
95 indent = len(spaces)
96 return indent, text, opener
97
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000098 def get_context(self, new_topvisible, stopline=1, stopindent=0):
99 """Get context lines, starting at new_topvisible and working backwards.
100
101 Stop when stopline or stopindent is reached. Return a tuple of context
102 data and the indent level at the top of the region inspected.
103
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000104 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000105 assert stopline > 0
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000106 lines = []
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000107 # The indentation level we are currently in:
108 lastindent = INFINITY
109 # For a line to be interesting, it must begin with a block opening
110 # keyword, and have less indentation than lastindent.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000111 for linenum in xrange(new_topvisible, stopline-1, -1):
112 indent, text, opener = self.get_line_info(linenum)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000113 if indent < lastindent:
114 lastindent = indent
Kurt B. Kaisere3636e02004-04-26 22:26:04 +0000115 if opener in ("else", "elif"):
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000116 # We also show the if statement
117 lastindent += 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000118 if opener and linenum < new_topvisible and indent >= stopindent:
119 lines.append((linenum, indent, text, opener))
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000120 if lastindent <= stopindent:
121 break
122 lines.reverse()
123 return lines, lastindent
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000124
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000125 def update_code_context(self):
126 """Update context information and lines visible in the context pane.
127
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000128 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000129 new_topvisible = int(self.text.index("@0,0").split('.')[0])
130 if self.topvisible == new_topvisible: # haven't scrolled
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000131 return
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000132 if self.topvisible < new_topvisible: # scroll down
133 lines, lastindent = self.get_context(new_topvisible,
134 self.topvisible)
135 # retain only context info applicable to the region
136 # between topvisible and new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000137 while self.info[-1][1] >= lastindent:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000138 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000139 elif self.topvisible > new_topvisible: # scroll up
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000140 stopindent = self.info[-1][1] + 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000141 # retain only context info associated
142 # with lines above new_topvisible:
143 while self.info[-1][0] >= new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000144 stopindent = self.info[-1][1]
145 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000146 lines, lastindent = self.get_context(new_topvisible,
147 self.info[-1][0]+1,
148 stopindent)
149 self.info.extend(lines)
150 self.topvisible = new_topvisible
151
152 # empty lines in context pane:
153 context_strings = [""] * max(0, self.context_depth - len(self.info))
154 # followed by the context hint lines:
155 context_strings += [x[2] for x in self.info[-self.context_depth:]]
156 self.label["text"] = '\n'.join(context_strings)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000157
158 def timer_event(self):
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000159 if self.label:
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000160 self.update_code_context()
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000161 self.text.after(UPDATEINTERVAL, self.timer_event)
162
163 def font_timer_event(self):
164 newtextfont = self.text["font"]
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000165 if self.label and newtextfont != self.textfont:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000166 self.textfont = newtextfont
167 self.label["font"] = self.textfont
168 self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)