1 """CodeContext - Extension to display the block context above the edit window
3 Once code has scrolled off the top of a window, it can be difficult to
4 determine which block you are in. This extension implements a pane at the top
5 of each IDLE edit window which provides block structure hints. These hints are
6 the lines which contain the block opening keywords, e.g. 'if', for the
7 enclosing block. The number of hint lines is determined by the numlines
8 variable in the CodeContext section of config-extensions.def. Lines which do
9 not open blocks are not shown in the context hints pane.
13 from tkinter
.constants
import TOP
, LEFT
, X
, W
, SUNKEN
15 from sys
import maxsize
as INFINITY
16 from idlelib
.configHandler
import idleConf
18 BLOCKOPENERS
= set(["class", "def", "elif", "else", "except", "finally", "for",
19 "if", "try", "while", "with"])
20 UPDATEINTERVAL
= 100 # millisec
21 FONTUPDATEINTERVAL
= 1000 # millisec
24 lambda s
, c
=re
.compile(r
"^(\s*)(\w*)"): c
.match(s
).groups()
27 menudefs
= [('options', [('!Code Conte_xt', '<<toggle-code-context>>')])]
28 context_depth
= idleConf
.GetOption("extensions", "CodeContext",
29 "numlines", type="int", default
=3)
30 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
):
35 self
.editwin
= editwin
36 self
.text
= editwin
.text
37 self
.textfont
= self
.text
["font"]
39 # 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.
44 self
.info
= [(0, -1, "", False)]
46 visible
= idleConf
.GetOption("extensions", "CodeContext",
47 "visible", type="bool", default
=False)
49 self
.toggle_code_context_event()
50 self
.editwin
.setvar('<<toggle-code-context>>', True)
51 # 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
)
55 def toggle_code_context_event(self
, event
=None):
57 # Calculate the border width and horizontal padding required to
58 # align the context with the text in the main Text widget.
60 # All values are passed through int(str(<value>)), since some
61 # 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
65 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
70 for widget
in widgets
:
71 border
+= int(str( widget
.cget('border') ))
72 self
.label
= tkinter
.Label(self
.editwin
.top
,
73 text
="\n" * (self
.context_depth
- 1),
74 anchor
=W
, justify
=LEFT
,
76 bg
=self
.bgcolor
, fg
=self
.fgcolor
,
77 width
=1, #don't request more than we get
78 padx
=padx
, border
=border
,
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,
83 before
=self
.editwin
.text_frame
)
87 idleConf
.SetOption("extensions", "CodeContext", "visible",
88 str(self
.label
is not None))
89 idleConf
.SaveUserCfgFiles()
91 def get_line_info(self
, linenum
):
92 """Get the line indent value, text, and any block start keyword
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.
98 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
)] == '#':
105 return indent
, text
, opener
107 def get_context(self
, new_topvisible
, stopline
=1, stopindent
=0):
108 """Get context lines, starting at new_topvisible and working backwards.
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.
116 # 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.
120 for linenum
in range(new_topvisible
, stopline
-1, -1):
121 indent
, text
, opener
= self
.get_line_info(linenum
)
122 if indent
< lastindent
:
124 if opener
in ("else", "elif"):
125 # We also show the if statement
127 if opener
and linenum
< new_topvisible
and indent
>= stopindent
:
128 lines
.append((linenum
, indent
, text
, opener
))
129 if lastindent
<= stopindent
:
132 return lines
, lastindent
134 def update_code_context(self
):
135 """Update context information and lines visible in the context pane.
138 new_topvisible
= int(self
.text
.index("@0,0").split('.')[0])
139 if self
.topvisible
== new_topvisible
: # haven't scrolled
141 if self
.topvisible
< new_topvisible
: # scroll down
142 lines
, lastindent
= self
.get_context(new_topvisible
,
144 # retain only context info applicable to the region
145 # between topvisible and new_topvisible:
146 while self
.info
[-1][1] >= lastindent
:
148 elif self
.topvisible
> new_topvisible
: # scroll up
149 stopindent
= self
.info
[-1][1] + 1
150 # retain only context info associated
151 # with lines above new_topvisible:
152 while self
.info
[-1][0] >= new_topvisible
:
153 stopindent
= self
.info
[-1][1]
155 lines
, lastindent
= self
.get_context(new_topvisible
,
158 self
.info
.extend(lines
)
159 self
.topvisible
= new_topvisible
160 # 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
)
166 def timer_event(self
):
168 self
.update_code_context()
169 self
.text
.after(UPDATEINTERVAL
, self
.timer_event
)
171 def font_timer_event(self
):
172 newtextfont
= self
.text
["font"]
173 if self
.label
and newtextfont
!= self
.textfont
:
174 self
.textfont
= newtextfont
175 self
.label
["font"] = self
.textfont
176 self
.text
.after(FONTUPDATEINTERVAL
, self
.font_timer_event
)