blob: 84491d5a9d1ec23da86153619d017fd5bcd3486c [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"""
Georg Brandl14fc4272008-05-17 18:39:55 +000012import tkinter
13from tkinter.constants import TOP, LEFT, X, W, SUNKEN
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000014import re
Kurt B. Kaiser3536a5c2007-12-12 17:22:06 +000015from sys import maxsize as INFINITY
Kurt B. Kaiser2d7f6a02007-08-22 23:01:33 +000016from idlelib.configHandler import idleConf
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000017
Thomas Wouters0e3f5912006-08-11 14:57:12 +000018BLOCKOPENERS = set(["class", "def", "elif", "else", "except", "finally", "for",
Thomas Wouters00ee7ba2006-08-21 19:07:27 +000019 "if", "try", "while", "with"])
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>>')])]
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:
Thomas Wouterscf297e42007-02-23 15:07:44 +000057 # Calculate the border width and horizontal padding required to
58 # align the context with the text in the main Text widget.
Thomas Wouters89f507f2006-12-13 04:49:30 +000059 #
60 # All values are passed through int(str(<value>)), since some
Thomas Wouterscf297e42007-02-23 15:07:44 +000061 # values may be pixel objects, which can't simply be added to ints.
62 widgets = self.editwin.text, self.editwin.text_frame
63 # Calculate the required vertical padding
Thomas Wouters89f507f2006-12-13 04:49:30 +000064 padx = 0
Thomas Wouterscf297e42007-02-23 15:07:44 +000065 for widget in widgets:
66 padx += int(str( widget.pack_info()['padx'] ))
67 padx += int(str( widget.cget('padx') ))
68 # Calculate the required border width
69 border = 0
70 for widget in widgets:
71 border += int(str( widget.cget('border') ))
Georg Brandl14fc4272008-05-17 18:39:55 +000072 self.label = tkinter.Label(self.editwin.top,
Thomas Wouters89f507f2006-12-13 04:49:30 +000073 text="\n" * (self.context_depth - 1),
Thomas Wouterscf297e42007-02-23 15:07:44 +000074 anchor=W, justify=LEFT,
Thomas Wouters89f507f2006-12-13 04:49:30 +000075 font=self.textfont,
76 bg=self.bgcolor, fg=self.fgcolor,
77 width=1, #don't request more than we get
Thomas Wouterscf297e42007-02-23 15:07:44 +000078 padx=padx, border=border,
79 relief=SUNKEN)
80 # Pack the label widget before and above the text_frame widget,
81 # thus ensuring that it will appear directly above text_frame
82 self.label.pack(side=TOP, fill=X, expand=False,
Thomas Wouters89f507f2006-12-13 04:49:30 +000083 before=self.editwin.text_frame)
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000084 else:
85 self.label.destroy()
86 self.label = None
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +000087 idleConf.SetOption("extensions", "CodeContext", "visible",
88 str(self.label is not None))
89 idleConf.SaveUserCfgFiles()
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000090
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000091 def get_line_info(self, linenum):
92 """Get the line indent value, text, and any block start keyword
93
94 If the line does not start a block, the keyword value is False.
95 The indentation of empty lines (or comment lines) is INFINITY.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000096
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000097 """
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000098 text = self.text.get("%d.0" % linenum, "%d.end" % linenum)
99 spaces, firstword = getspacesfirstword(text)
100 opener = firstword in BLOCKOPENERS and firstword
101 if len(text) == len(spaces) or text[len(spaces)] == '#':
102 indent = INFINITY
103 else:
104 indent = len(spaces)
105 return indent, text, opener
106
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000107 def get_context(self, new_topvisible, stopline=1, stopindent=0):
108 """Get context lines, starting at new_topvisible and working backwards.
109
110 Stop when stopline or stopindent is reached. Return a tuple of context
111 data and the indent level at the top of the region inspected.
112
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000113 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000114 assert stopline > 0
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000115 lines = []
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000116 # The indentation level we are currently in:
117 lastindent = INFINITY
118 # For a line to be interesting, it must begin with a block opening
119 # keyword, and have less indentation than lastindent.
Guido van Rossum805365e2007-05-07 22:24:25 +0000120 for linenum in range(new_topvisible, stopline-1, -1):
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000121 indent, text, opener = self.get_line_info(linenum)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000122 if indent < lastindent:
123 lastindent = indent
Kurt B. Kaisere3636e02004-04-26 22:26:04 +0000124 if opener in ("else", "elif"):
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000125 # We also show the if statement
126 lastindent += 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000127 if opener and linenum < new_topvisible and indent >= stopindent:
128 lines.append((linenum, indent, text, opener))
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000129 if lastindent <= stopindent:
130 break
131 lines.reverse()
132 return lines, lastindent
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000133
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000134 def update_code_context(self):
135 """Update context information and lines visible in the context pane.
136
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000137 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000138 new_topvisible = int(self.text.index("@0,0").split('.')[0])
139 if self.topvisible == new_topvisible: # haven't scrolled
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000140 return
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000141 if self.topvisible < new_topvisible: # scroll down
142 lines, lastindent = self.get_context(new_topvisible,
143 self.topvisible)
144 # retain only context info applicable to the region
145 # between topvisible and new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000146 while self.info[-1][1] >= lastindent:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000147 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000148 elif self.topvisible > new_topvisible: # scroll up
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000149 stopindent = self.info[-1][1] + 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000150 # retain only context info associated
151 # with lines above new_topvisible:
152 while self.info[-1][0] >= new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000153 stopindent = self.info[-1][1]
154 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000155 lines, lastindent = self.get_context(new_topvisible,
156 self.info[-1][0]+1,
157 stopindent)
158 self.info.extend(lines)
159 self.topvisible = new_topvisible
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000160 # empty lines in context pane:
161 context_strings = [""] * max(0, self.context_depth - len(self.info))
162 # followed by the context hint lines:
163 context_strings += [x[2] for x in self.info[-self.context_depth:]]
164 self.label["text"] = '\n'.join(context_strings)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000165
166 def timer_event(self):
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000167 if self.label:
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000168 self.update_code_context()
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000169 self.text.after(UPDATEINTERVAL, self.timer_event)
170
171 def font_timer_event(self):
172 newtextfont = self.text["font"]
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000173 if self.label and newtextfont != self.textfont:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000174 self.textfont = newtextfont
175 self.label["font"] = self.textfont
176 self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)