blob: 09dc078d63f1917eb58dc86d5ad3472348cff20e [file] [log] [blame]
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -04001"""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
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -04008variable in the codecontext section of config-extensions.def. Lines which do
Kurt B. Kaiser0a135792005-10-03 19:26:03 +00009not open blocks are not shown in the context hints pane.
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000010
11"""
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000012import re
Kurt B. Kaiser3536a5c2007-12-12 17:22:06 +000013from sys import maxsize as INFINITY
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040014
15import tkinter
16from tkinter.constants import TOP, LEFT, X, W, SUNKEN
17
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040018from idlelib.config import idleConf
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000019
Terry Jan Reedy049882e2014-12-11 05:33:36 -050020BLOCKOPENERS = {"class", "def", "elif", "else", "except", "finally", "for",
21 "if", "try", "while", "with"}
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000022UPDATEINTERVAL = 100 # millisec
23FONTUPDATEINTERVAL = 1000 # millisec
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000024
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000025getspacesfirstword =\
26 lambda s, c=re.compile(r"^(\s*)(\w*)"): c.match(s).groups()
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000027
28class CodeContext:
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000029 menudefs = [('options', [('!Code Conte_xt', '<<toggle-code-context>>')])]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000030 context_depth = idleConf.GetOption("extensions", "CodeContext",
31 "numlines", type="int", default=3)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000032 bgcolor = idleConf.GetOption("extensions", "CodeContext",
33 "bgcolor", type="str", default="LightGray")
34 fgcolor = idleConf.GetOption("extensions", "CodeContext",
35 "fgcolor", type="str", default="Black")
36 def __init__(self, editwin):
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000037 self.editwin = editwin
38 self.text = editwin.text
39 self.textfont = self.text["font"]
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000040 self.label = None
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000041 # self.info is a list of (line number, indent level, line text, block
42 # keyword) tuples providing the block structure associated with
43 # self.topvisible (the linenumber of the line displayed at the top of
44 # the edit window). self.info[0] is initialized as a 'dummy' line which
45 # starts the toplevel 'block' of the module.
Kurt B. Kaiser74910222005-10-02 23:36:46 +000046 self.info = [(0, -1, "", False)]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000047 self.topvisible = 1
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +000048 visible = idleConf.GetOption("extensions", "CodeContext",
49 "visible", type="bool", default=False)
50 if visible:
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000051 self.toggle_code_context_event()
52 self.editwin.setvar('<<toggle-code-context>>', True)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000053 # Start two update cycles, one for context lines, one for font changes.
54 self.text.after(UPDATEINTERVAL, self.timer_event)
55 self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)
56
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000057 def toggle_code_context_event(self, event=None):
58 if not self.label:
Thomas Wouterscf297e42007-02-23 15:07:44 +000059 # Calculate the border width and horizontal padding required to
60 # align the context with the text in the main Text widget.
Thomas Wouters89f507f2006-12-13 04:49:30 +000061 #
Serhiy Storchaka645058d2015-05-06 14:00:04 +030062 # All values are passed through getint(), since some
Thomas Wouterscf297e42007-02-23 15:07:44 +000063 # values may be pixel objects, which can't simply be added to ints.
64 widgets = self.editwin.text, self.editwin.text_frame
65 # Calculate the required vertical padding
Thomas Wouters89f507f2006-12-13 04:49:30 +000066 padx = 0
Thomas Wouterscf297e42007-02-23 15:07:44 +000067 for widget in widgets:
Serhiy Storchaka645058d2015-05-06 14:00:04 +030068 padx += widget.tk.getint(widget.pack_info()['padx'])
69 padx += widget.tk.getint(widget.cget('padx'))
Thomas Wouterscf297e42007-02-23 15:07:44 +000070 # Calculate the required border width
71 border = 0
72 for widget in widgets:
Serhiy Storchaka645058d2015-05-06 14:00:04 +030073 border += widget.tk.getint(widget.cget('border'))
Georg Brandl14fc4272008-05-17 18:39:55 +000074 self.label = tkinter.Label(self.editwin.top,
Thomas Wouters89f507f2006-12-13 04:49:30 +000075 text="\n" * (self.context_depth - 1),
Thomas Wouterscf297e42007-02-23 15:07:44 +000076 anchor=W, justify=LEFT,
Thomas Wouters89f507f2006-12-13 04:49:30 +000077 font=self.textfont,
78 bg=self.bgcolor, fg=self.fgcolor,
79 width=1, #don't request more than we get
Thomas Wouterscf297e42007-02-23 15:07:44 +000080 padx=padx, border=border,
81 relief=SUNKEN)
82 # Pack the label widget before and above the text_frame widget,
83 # thus ensuring that it will appear directly above text_frame
84 self.label.pack(side=TOP, fill=X, expand=False,
Thomas Wouters89f507f2006-12-13 04:49:30 +000085 before=self.editwin.text_frame)
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000086 else:
87 self.label.destroy()
88 self.label = None
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +000089 idleConf.SetOption("extensions", "CodeContext", "visible",
90 str(self.label is not None))
91 idleConf.SaveUserCfgFiles()
Serhiy Storchaka213ce122017-06-27 07:02:32 +030092 return "break"
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000093
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000094 def get_line_info(self, linenum):
95 """Get the line indent value, text, and any block start keyword
96
97 If the line does not start a block, the keyword value is False.
98 The indentation of empty lines (or comment lines) is INFINITY.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000099
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000100 """
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000101 text = self.text.get("%d.0" % linenum, "%d.end" % linenum)
102 spaces, firstword = getspacesfirstword(text)
103 opener = firstword in BLOCKOPENERS and firstword
104 if len(text) == len(spaces) or text[len(spaces)] == '#':
105 indent = INFINITY
106 else:
107 indent = len(spaces)
108 return indent, text, opener
109
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000110 def get_context(self, new_topvisible, stopline=1, stopindent=0):
111 """Get context lines, starting at new_topvisible and working backwards.
112
113 Stop when stopline or stopindent is reached. Return a tuple of context
114 data and the indent level at the top of the region inspected.
115
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000116 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000117 assert stopline > 0
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000118 lines = []
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000119 # The indentation level we are currently in:
120 lastindent = INFINITY
121 # For a line to be interesting, it must begin with a block opening
122 # keyword, and have less indentation than lastindent.
Guido van Rossum805365e2007-05-07 22:24:25 +0000123 for linenum in range(new_topvisible, stopline-1, -1):
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000124 indent, text, opener = self.get_line_info(linenum)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000125 if indent < lastindent:
126 lastindent = indent
Kurt B. Kaisere3636e02004-04-26 22:26:04 +0000127 if opener in ("else", "elif"):
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000128 # We also show the if statement
129 lastindent += 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000130 if opener and linenum < new_topvisible and indent >= stopindent:
131 lines.append((linenum, indent, text, opener))
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000132 if lastindent <= stopindent:
133 break
134 lines.reverse()
135 return lines, lastindent
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000136
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000137 def update_code_context(self):
138 """Update context information and lines visible in the context pane.
139
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000140 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000141 new_topvisible = int(self.text.index("@0,0").split('.')[0])
142 if self.topvisible == new_topvisible: # haven't scrolled
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000143 return
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000144 if self.topvisible < new_topvisible: # scroll down
145 lines, lastindent = self.get_context(new_topvisible,
146 self.topvisible)
147 # retain only context info applicable to the region
148 # between topvisible and new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000149 while self.info[-1][1] >= lastindent:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000150 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000151 elif self.topvisible > new_topvisible: # scroll up
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000152 stopindent = self.info[-1][1] + 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000153 # retain only context info associated
154 # with lines above new_topvisible:
155 while self.info[-1][0] >= new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000156 stopindent = self.info[-1][1]
157 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000158 lines, lastindent = self.get_context(new_topvisible,
159 self.info[-1][0]+1,
160 stopindent)
161 self.info.extend(lines)
162 self.topvisible = new_topvisible
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000163 # empty lines in context pane:
164 context_strings = [""] * max(0, self.context_depth - len(self.info))
165 # followed by the context hint lines:
166 context_strings += [x[2] for x in self.info[-self.context_depth:]]
167 self.label["text"] = '\n'.join(context_strings)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000168
169 def timer_event(self):
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000170 if self.label:
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000171 self.update_code_context()
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000172 self.text.after(UPDATEINTERVAL, self.timer_event)
173
174 def font_timer_event(self):
175 newtextfont = self.text["font"]
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000176 if self.label and newtextfont != self.textfont:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000177 self.textfont = newtextfont
178 self.label["font"] = self.textfont
179 self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)