blob: f25e1b33a086e8597da97456063d2cd10117e44b [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()
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +000092
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000093 def get_line_info(self, linenum):
94 """Get the line indent value, text, and any block start keyword
95
96 If the line does not start a block, the keyword value is False.
97 The indentation of empty lines (or comment lines) is INFINITY.
Kurt B. Kaiser0a135792005-10-03 19:26:03 +000098
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +000099 """
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000100 text = self.text.get("%d.0" % linenum, "%d.end" % linenum)
101 spaces, firstword = getspacesfirstword(text)
102 opener = firstword in BLOCKOPENERS and firstword
103 if len(text) == len(spaces) or text[len(spaces)] == '#':
104 indent = INFINITY
105 else:
106 indent = len(spaces)
107 return indent, text, opener
108
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000109 def get_context(self, new_topvisible, stopline=1, stopindent=0):
110 """Get context lines, starting at new_topvisible and working backwards.
111
112 Stop when stopline or stopindent is reached. Return a tuple of context
113 data and the indent level at the top of the region inspected.
114
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000115 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000116 assert stopline > 0
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000117 lines = []
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000118 # The indentation level we are currently in:
119 lastindent = INFINITY
120 # For a line to be interesting, it must begin with a block opening
121 # keyword, and have less indentation than lastindent.
Guido van Rossum805365e2007-05-07 22:24:25 +0000122 for linenum in range(new_topvisible, stopline-1, -1):
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000123 indent, text, opener = self.get_line_info(linenum)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000124 if indent < lastindent:
125 lastindent = indent
Kurt B. Kaisere3636e02004-04-26 22:26:04 +0000126 if opener in ("else", "elif"):
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000127 # We also show the if statement
128 lastindent += 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000129 if opener and linenum < new_topvisible and indent >= stopindent:
130 lines.append((linenum, indent, text, opener))
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000131 if lastindent <= stopindent:
132 break
133 lines.reverse()
134 return lines, lastindent
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000135
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000136 def update_code_context(self):
137 """Update context information and lines visible in the context pane.
138
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000139 """
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000140 new_topvisible = int(self.text.index("@0,0").split('.')[0])
141 if self.topvisible == new_topvisible: # haven't scrolled
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000142 return
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000143 if self.topvisible < new_topvisible: # scroll down
144 lines, lastindent = self.get_context(new_topvisible,
145 self.topvisible)
146 # retain only context info applicable to the region
147 # between topvisible and new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000148 while self.info[-1][1] >= lastindent:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000149 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000150 elif self.topvisible > new_topvisible: # scroll up
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000151 stopindent = self.info[-1][1] + 1
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000152 # retain only context info associated
153 # with lines above new_topvisible:
154 while self.info[-1][0] >= new_topvisible:
Kurt B. Kaiser74910222005-10-02 23:36:46 +0000155 stopindent = self.info[-1][1]
156 del self.info[-1]
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000157 lines, lastindent = self.get_context(new_topvisible,
158 self.info[-1][0]+1,
159 stopindent)
160 self.info.extend(lines)
161 self.topvisible = new_topvisible
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000162 # empty lines in context pane:
163 context_strings = [""] * max(0, self.context_depth - len(self.info))
164 # followed by the context hint lines:
165 context_strings += [x[2] for x in self.info[-self.context_depth:]]
166 self.label["text"] = '\n'.join(context_strings)
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000167
168 def timer_event(self):
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000169 if self.label:
Kurt B. Kaiser0a135792005-10-03 19:26:03 +0000170 self.update_code_context()
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000171 self.text.after(UPDATEINTERVAL, self.timer_event)
172
173 def font_timer_event(self):
174 newtextfont = self.text["font"]
Kurt B. Kaiserd00587a2004-04-24 03:08:13 +0000175 if self.label and newtextfont != self.textfont:
Kurt B. Kaiser54d1a3b2004-04-21 20:06:26 +0000176 self.textfont = newtextfont
177 self.label["font"] = self.textfont
178 self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)