1"""CodeContext - Extension to display the block context above the edit window 2 3Once 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. 10 11""" 12import Tkinter 13from Tkconstants import TOP, LEFT, X, W, SUNKEN 14import re 15from sys import maxint as INFINITY 16from idlelib.configHandler import idleConf 17 18BLOCKOPENERS = {"class", "def", "elif", "else", "except", "finally", "for", 19 "if", "try", "while", "with"} 20UPDATEINTERVAL = 100 # millisec 21FONTUPDATEINTERVAL = 1000 # millisec 22 23getspacesfirstword =\ 24 lambda s, c=re.compile(r"^(\s*)(\w*)"): c.match(s).groups() 25 26class CodeContext: 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"] 38 self.label = None 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)] 45 self.topvisible = 1 46 visible = idleConf.GetOption("extensions", "CodeContext", 47 "visible", type="bool", default=False) 48 if visible: 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) 54 55 def toggle_code_context_event(self, event=None): 56 if not self.label: 57 # Calculate the border width and horizontal padding required to 58 # align the context with the text in the main Text widget. 59 # 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 64 padx = 0 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 69 border = 0 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, 75 font=self.textfont, 76 bg=self.bgcolor, fg=self.fgcolor, 77 width=1, #don't request more than we get 78 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, 83 before=self.editwin.text_frame) 84 else: 85 self.label.destroy() 86 self.label = None 87 idleConf.SetOption("extensions", "CodeContext", "visible", 88 str(self.label is not None)) 89 idleConf.SaveUserCfgFiles() 90 91 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. 96 97 """ 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)] == '#': 102 indent = INFINITY 103 else: 104 indent = len(spaces) 105 return indent, text, opener 106 107 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 113 """ 114 assert stopline > 0 115 lines = [] 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 xrange(new_topvisible, stopline-1, -1): 121 indent, text, opener = self.get_line_info(linenum) 122 if indent < lastindent: 123 lastindent = indent 124 if opener in ("else", "elif"): 125 # We also show the if statement 126 lastindent += 1 127 if opener and linenum < new_topvisible and indent >= stopindent: 128 lines.append((linenum, indent, text, opener)) 129 if lastindent <= stopindent: 130 break 131 lines.reverse() 132 return lines, lastindent 133 134 def update_code_context(self): 135 """Update context information and lines visible in the context pane. 136 137 """ 138 new_topvisible = int(self.text.index("@0,0").split('.')[0]) 139 if self.topvisible == new_topvisible: # haven't scrolled 140 return 141 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: 146 while self.info[-1][1] >= lastindent: 147 del self.info[-1] 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] 154 del self.info[-1] 155 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 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) 165 166 def timer_event(self): 167 if self.label: 168 self.update_code_context() 169 self.text.after(UPDATEINTERVAL, self.timer_event) 170 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) 177