Coverage for codecontext.py: 21%

135 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-11 13:22 -0700

1"""codecontext - 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 maxlines 

8variable in the codecontext section of config-extensions.def. Lines which do 

9not open blocks are not shown in the context hints pane. 

10 

11For EditorWindows, <<toggle-code-context>> is bound to CodeContext(self). 

12toggle_code_context_event. 

13""" 

14import re 

15from sys import maxsize as INFINITY 

16 

17from tkinter import Frame, Text, TclError 

18from tkinter.constants import NSEW, SUNKEN 

19 

20from idlelib.config import idleConf 

21 

22BLOCKOPENERS = {'class', 'def', 'if', 'elif', 'else', 'while', 'for', 

23 'try', 'except', 'finally', 'with', 'async'} 

24 

25 

26def get_spaces_firstword(codeline, c=re.compile(r"^(\s*)(\w*)")): 

27 "Extract the beginning whitespace and first word from codeline." 

28 return c.match(codeline).groups() 1bc

29 

30 

31def get_line_info(codeline): 

32 """Return tuple of (line indent value, codeline, block start keyword). 

33 

34 The indentation of empty lines (or comment lines) is INFINITY. 

35 If the line does not start a block, the keyword value is False. 

36 """ 

37 spaces, firstword = get_spaces_firstword(codeline) 1b

38 indent = len(spaces) 1b

39 if len(codeline) == indent or codeline[indent] == '#': 1b

40 indent = INFINITY 1b

41 opener = firstword in BLOCKOPENERS and firstword 1b

42 return indent, codeline, opener 1b

43 

44 

45class CodeContext: 

46 "Display block context above the edit window." 

47 UPDATEINTERVAL = 100 # millisec 

48 

49 def __init__(self, editwin): 

50 """Initialize settings for context block. 

51 

52 editwin is the Editor window for the context block. 

53 self.text is the editor window text widget. 

54 

55 self.context displays the code context text above the editor text. 

56 Initially None, it is toggled via <<toggle-code-context>>. 

57 self.topvisible is the number of the top text line displayed. 

58 self.info is a list of (line number, indent level, line text, 

59 block keyword) tuples for the block structure above topvisible. 

60 self.info[0] is initialized with a 'dummy' line which 

61 starts the toplevel 'block' of the module. 

62 

63 self.t1 and self.t2 are two timer events on the editor text widget to 

64 monitor for changes to the context text or editor font. 

65 """ 

66 self.editwin = editwin 

67 self.text = editwin.text 

68 self._reset() 

69 

70 def _reset(self): 

71 self.context = None 

72 self.cell00 = None 

73 self.t1 = None 

74 self.topvisible = 1 

75 self.info = [(0, -1, "", False)] 

76 

77 @classmethod 

78 def reload(cls): 

79 "Load class variables from config." 

80 cls.context_depth = idleConf.GetOption("extensions", "CodeContext", 

81 "maxlines", type="int", 

82 default=15) 

83 

84 def __del__(self): 

85 "Cancel scheduled events." 

86 if self.t1 is not None: 

87 try: 

88 self.text.after_cancel(self.t1) 

89 except TclError: # pragma: no cover 

90 pass 

91 self.t1 = None 

92 

93 def toggle_code_context_event(self, event=None): 

94 """Toggle code context display. 

95 

96 If self.context doesn't exist, create it to match the size of the editor 

97 window text (toggle on). If it does exist, destroy it (toggle off). 

98 Return 'break' to complete the processing of the binding. 

99 """ 

100 if self.context is None: 

101 # Calculate the border width and horizontal padding required to 

102 # align the context with the text in the main Text widget. 

103 # 

104 # All values are passed through getint(), since some 

105 # values may be pixel objects, which can't simply be added to ints. 

106 widgets = self.editwin.text, self.editwin.text_frame 

107 # Calculate the required horizontal padding and border width. 

108 padx = 0 

109 border = 0 

110 for widget in widgets: 

111 info = (widget.grid_info() 

112 if widget is self.editwin.text 

113 else widget.pack_info()) 

114 padx += widget.tk.getint(info['padx']) 

115 padx += widget.tk.getint(widget.cget('padx')) 

116 border += widget.tk.getint(widget.cget('border')) 

117 context = self.context = Text( 

118 self.editwin.text_frame, 

119 height=1, 

120 width=1, # Don't request more than we get. 

121 highlightthickness=0, 

122 padx=padx, border=border, relief=SUNKEN, state='disabled') 

123 self.update_font() 

124 self.update_highlight_colors() 

125 context.bind('<ButtonRelease-1>', self.jumptoline) 

126 # Get the current context and initiate the recurring update event. 

127 self.timer_event() 

128 # Grid the context widget above the text widget. 

129 context.grid(row=0, column=1, sticky=NSEW) 

130 

131 line_number_colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 

132 'linenumber') 

133 self.cell00 = Frame(self.editwin.text_frame, 

134 bg=line_number_colors['background']) 

135 self.cell00.grid(row=0, column=0, sticky=NSEW) 

136 menu_status = 'Hide' 

137 else: 

138 self.context.destroy() 

139 self.context = None 

140 self.cell00.destroy() 

141 self.cell00 = None 

142 self.text.after_cancel(self.t1) 

143 self._reset() 

144 menu_status = 'Show' 

145 self.editwin.update_menu_label(menu='options', index='*ode*ontext', 

146 label=f'{menu_status} Code Context') 

147 return "break" 

148 

149 def get_context(self, new_topvisible, stopline=1, stopindent=0): 

150 """Return a list of block line tuples and the 'last' indent. 

151 

152 The tuple fields are (linenum, indent, text, opener). 

153 The list represents header lines from new_topvisible back to 

154 stopline with successively shorter indents > stopindent. 

155 The list is returned ordered by line number. 

156 Last indent returned is the smallest indent observed. 

157 """ 

158 assert stopline > 0 

159 lines = [] 

160 # The indentation level we are currently in. 

161 lastindent = INFINITY 

162 # For a line to be interesting, it must begin with a block opening 

163 # keyword, and have less indentation than lastindent. 

164 for linenum in range(new_topvisible, stopline-1, -1): 

165 codeline = self.text.get(f'{linenum}.0', f'{linenum}.end') 

166 indent, text, opener = get_line_info(codeline) 

167 if indent < lastindent: 

168 lastindent = indent 

169 if opener in ("else", "elif"): 

170 # Also show the if statement. 

171 lastindent += 1 

172 if opener and linenum < new_topvisible and indent >= stopindent: 

173 lines.append((linenum, indent, text, opener)) 

174 if lastindent <= stopindent: 

175 break 

176 lines.reverse() 

177 return lines, lastindent 

178 

179 def update_code_context(self): 

180 """Update context information and lines visible in the context pane. 

181 

182 No update is done if the text hasn't been scrolled. If the text 

183 was scrolled, the lines that should be shown in the context will 

184 be retrieved and the context area will be updated with the code, 

185 up to the number of maxlines. 

186 """ 

187 new_topvisible = self.editwin.getlineno("@0,0") 

188 if self.topvisible == new_topvisible: # Haven't scrolled. 

189 return 

190 if self.topvisible < new_topvisible: # Scroll down. 

191 lines, lastindent = self.get_context(new_topvisible, 

192 self.topvisible) 

193 # Retain only context info applicable to the region 

194 # between topvisible and new_topvisible. 

195 while self.info[-1][1] >= lastindent: 

196 del self.info[-1] 

197 else: # self.topvisible > new_topvisible: # Scroll up. 

198 stopindent = self.info[-1][1] + 1 

199 # Retain only context info associated 

200 # with lines above new_topvisible. 

201 while self.info[-1][0] >= new_topvisible: 

202 stopindent = self.info[-1][1] 

203 del self.info[-1] 

204 lines, lastindent = self.get_context(new_topvisible, 

205 self.info[-1][0]+1, 

206 stopindent) 

207 self.info.extend(lines) 

208 self.topvisible = new_topvisible 

209 # Last context_depth context lines. 

210 context_strings = [x[2] for x in self.info[-self.context_depth:]] 

211 showfirst = 0 if context_strings[0] else 1 

212 # Update widget. 

213 self.context['height'] = len(context_strings) - showfirst 

214 self.context['state'] = 'normal' 

215 self.context.delete('1.0', 'end') 

216 self.context.insert('end', '\n'.join(context_strings[showfirst:])) 

217 self.context['state'] = 'disabled' 

218 

219 def jumptoline(self, event=None): 

220 """ Show clicked context line at top of editor. 

221 

222 If a selection was made, don't jump; allow copying. 

223 If no visible context, show the top line of the file. 

224 """ 

225 try: 

226 self.context.index("sel.first") 

227 except TclError: 

228 lines = len(self.info) 

229 if lines == 1: # No context lines are showing. 

230 newtop = 1 

231 else: 

232 # Line number clicked. 

233 contextline = int(float(self.context.index('insert'))) 

234 # Lines not displayed due to maxlines. 

235 offset = max(1, lines - self.context_depth) - 1 

236 newtop = self.info[offset + contextline][0] 

237 self.text.yview(f'{newtop}.0') 

238 self.update_code_context() 

239 

240 def timer_event(self): 

241 "Event on editor text widget triggered every UPDATEINTERVAL ms." 

242 if self.context is not None: 

243 self.update_code_context() 

244 self.t1 = self.text.after(self.UPDATEINTERVAL, self.timer_event) 

245 

246 def update_font(self): 

247 if self.context is not None: 

248 font = idleConf.GetFont(self.text, 'main', 'EditorWindow') 

249 self.context['font'] = font 

250 

251 def update_highlight_colors(self): 

252 if self.context is not None: 

253 colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'context') 

254 self.context['background'] = colors['background'] 

255 self.context['foreground'] = colors['foreground'] 

256 

257 if self.cell00 is not None: 

258 line_number_colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 

259 'linenumber') 

260 self.cell00.config(bg=line_number_colors['background']) 

261 

262 

263CodeContext.reload() 

264 

265 

266if __name__ == "__main__": 266 ↛ 267line 266 didn't jump to line 267, because the condition on line 266 was never true

267 from unittest import main 

268 main('idlelib.idle_test.test_codecontext', verbosity=2, exit=False) 

269 

270 # Add htest.