blob: 5d6706674772442213d7e458af2c5c855f3a9980 [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:
58 self.label = Tkinter.Label(self.editwin.top,
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000059 text="\n" * (self.context_depth - 1),
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000060 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. Kaiser0a135792005-10-03 19:26:03 +000080
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000081 """
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000082 text = self.text.get("%d.0" % linenum, "%d.end" % linenum)
83 spaces, firstword = getspacesfirstword(text)
84 opener = firstword in BLOCKOPENERS and firstword
85 if len(text) == len(spaces) or text[len(spaces)] == '#':
86 indent = INFINITY
87 else:
88 indent = len(spaces)
89 return indent, text, opener
90
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000091 def get_context(self, new_topvisible, stopline=1, stopindent=0):
92 """Get context lines, starting at new_topvisible and working backwards.
93
94 Stop when stopline or stopindent is reached. Return a tuple of context
95 data and the indent level at the top of the region inspected.
96
Kurt B. Kaiser74910222005-10-02 23:36:46 +000097 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000098 assert stopline > 0
Kurt B. Kaiser74910222005-10-02 23:36:46 +000099 lines = []
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000100 # The indentation level we are currently in:
101 lastindent = INFINITY
102 # For a line to be interesting, it must begin with a block opening
103 # keyword, and have less indentation than lastindent.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000104 for linenum in xrange(new_topvisible, stopline-1, -1):
105 indent, text, opener = self.get_line_info(linenum)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000106 if indent < lastindent:
107 lastindent = indent
Kurt B. Kaisere3636e02004-04-26 22:26:04 +0000108 if opener in ("else", "elif"):
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000109 # We also show the if statement
110 lastindent += 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000111 if opener and linenum < new_topvisible and indent >= stopindent:
112 lines.append((linenum, indent, text, opener))
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000113 if lastindent <= stopindent:
114 break
115 lines.reverse()
116 return lines, lastindent
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000117
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000118 def update_code_context(self):
119 """Update context information and lines visible in the context pane.
120
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000121 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000122 new_topvisible = int(self.text.index("@0,0").split('.')[0])
123 if self.topvisible == new_topvisible: # haven't scrolled
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000124 return
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000125 if self.topvisible < new_topvisible: # scroll down
126 lines, lastindent = self.get_context(new_topvisible,
127 self.topvisible)
128 # retain only context info applicable to the region
129 # between topvisible and new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000130 while self.info[-1][1] >= lastindent:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000131 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000132 elif self.topvisible > new_topvisible: # scroll up
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000133 stopindent = self.info[-1][1] + 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000134 # retain only context info associated
135 # with lines above new_topvisible:
136 while self.info[-1][0] >= new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000137 stopindent = self.info[-1][1]
138 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000139 lines, lastindent = self.get_context(new_topvisible,
140 self.info[-1][0]+1,
141 stopindent)
142 self.info.extend(lines)
143 self.topvisible = new_topvisible
144
145 # empty lines in context pane:
146 context_strings = [""] * max(0, self.context_depth - len(self.info))
147 # followed by the context hint lines:
148 context_strings += [x[2] for x in self.info[-self.context_depth:]]
149 self.label["text"] = '\n'.join(context_strings)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000150
151 def timer_event(self):
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000152 if self.label:
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000153 self.update_code_context()
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000154 self.text.after(UPDATEINTERVAL, self.timer_event)
155
156 def font_timer_event(self):
157 newtextfont = self.text["font"]
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000158 if self.label and newtextfont != self.textfont:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000159 self.textfont = newtextfont
160 self.label["font"] = self.textfont
161 self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)