Coverage for editor.py: 11%

1204 statements  

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

1import importlib.abc 

2import importlib.util 

3import os 

4import platform 

5import re 

6import string 

7import sys 

8import tokenize 

9import traceback 

10import webbrowser 

11import ast 

12 

13from tkinter import * 

14from tkinter.font import Font 

15from tkinter.ttk import Scrollbar 

16from tkinter import simpledialog 

17from tkinter import messagebox 

18 

19from idlelib.config import idleConf 

20from idlelib import configdialog 

21from idlelib import grep 

22from idlelib import help 

23from idlelib import help_about 

24from idlelib import macosx 

25from idlelib.multicall import MultiCallCreator 

26from idlelib import pyparse 

27from idlelib import query 

28from idlelib import replace 

29from idlelib import search 

30from idlelib.tree import wheel_event 

31from idlelib.util import py_extensions 

32from idlelib import window 

33from code_outline import ClassFuncNodeVisitor 

34 

35# The default tab setting for a Text widget, in average-width characters. 

36TK_TABWIDTH_DEFAULT = 8 

37_py_version = ' (%s)' % platform.python_version() 

38darwin = sys.platform == 'darwin' 

39 

40def _sphinx_version(): 

41 "Format sys.version_info to produce the Sphinx version string used to install the chm docs" 

42 major, minor, micro, level, serial = sys.version_info 

43 release = '%s%s' % (major, minor) 

44 release += '%s' % (micro,) 

45 if level == 'candidate': 

46 release += 'rc%s' % (serial,) 

47 elif level != 'final': 

48 release += '%s%s' % (level[0], serial) 

49 return release 

50 

51 

52class EditorWindow: 

53 from idlelib.percolator import Percolator 

54 from idlelib.colorizer import ColorDelegator, color_config 

55 from idlelib.undo import UndoDelegator 

56 from idlelib.iomenu import IOBinding, encoding 

57 from idlelib import mainmenu 

58 from idlelib.statusbar import MultiStatusBar 

59 from idlelib.autocomplete import AutoComplete 

60 from idlelib.autoexpand import AutoExpand 

61 from idlelib.calltip import Calltip 

62 from idlelib.codecontext import CodeContext 

63 from idlelib.sidebar import LineNumbers 

64 from idlelib.format import FormatParagraph, FormatRegion, Indents, Rstrip 

65 from idlelib.parenmatch import ParenMatch 

66 from idlelib.zoomheight import ZoomHeight 

67 

68 filesystemencoding = sys.getfilesystemencoding() # for file names 

69 help_url = None 

70 

71 allow_code_context = True 

72 allow_line_numbers = True 

73 user_input_insert_tags = None 

74 

75 def __init__(self, flist=None, filename=None, key=None, root=None): 

76 # Delay import: runscript imports pyshell imports EditorWindow. 

77 from idlelib.runscript import ScriptBinding 

78 

79 if EditorWindow.help_url is None: 

80 dochome = os.path.join(sys.base_prefix, 'Doc', 'index.html') 

81 if sys.platform.count('linux'): 

82 # look for html docs in a couple of standard places 

83 pyver = 'python-docs-' + '%s.%s.%s' % sys.version_info[:3] 

84 if os.path.isdir('/var/www/html/python/'): # "python2" rpm 

85 dochome = '/var/www/html/python/index.html' 

86 else: 

87 basepath = '/usr/share/doc/' # standard location 

88 dochome = os.path.join(basepath, pyver, 

89 'Doc', 'index.html') 

90 elif sys.platform[:3] == 'win': 

91 import winreg # Windows only, block only executed once. 

92 docfile = '' 

93 KEY = (rf"Software\Python\PythonCore\{sys.winver}" 

94 r"\Help\Main Python Documentation") 

95 try: 

96 docfile = winreg.QueryValue(winreg.HKEY_CURRENT_USER, KEY) 

97 except FileNotFoundError: 

98 try: 

99 docfile = winreg.QueryValue(winreg.HKEY_LOCAL_MACHINE, 

100 KEY) 

101 except FileNotFoundError: 

102 pass 

103 if os.path.isfile(docfile): 

104 dochome = docfile 

105 elif sys.platform == 'darwin': 

106 # documentation may be stored inside a python framework 

107 dochome = os.path.join(sys.base_prefix, 

108 'Resources/English.lproj/Documentation/index.html') 

109 dochome = os.path.normpath(dochome) 

110 if os.path.isfile(dochome): 

111 EditorWindow.help_url = dochome 

112 if sys.platform == 'darwin': 

113 # Safari requires real file:-URLs 

114 EditorWindow.help_url = 'file://' + EditorWindow.help_url 

115 else: 

116 EditorWindow.help_url = ("https://docs.python.org/%d.%d/" 

117 % sys.version_info[:2]) 

118 self.flist = flist 

119 root = root or flist.root 

120 self.root = root 

121 self.menubar = Menu(root) 

122 self.top = top = window.ListedToplevel(root, menu=self.menubar) 

123 if flist: 

124 self.tkinter_vars = flist.vars 

125 #self.top.instance_dict makes flist.inversedict available to 

126 #configdialog.py so it can access all EditorWindow instances 

127 self.top.instance_dict = flist.inversedict 

128 else: 

129 self.tkinter_vars = {} # keys: Tkinter event names 

130 # values: Tkinter variable instances 

131 self.top.instance_dict = {} 

132 self.recent_files_path = idleConf.userdir and os.path.join( 

133 idleConf.userdir, 'recent-files.lst') 

134 

135 self.prompt_last_line = '' # Override in PyShell 

136 self.text_frame = text_frame = Frame(top) 

137 self.vbar = vbar = Scrollbar(text_frame, name='vbar') 

138 width = idleConf.GetOption('main', 'EditorWindow', 'width', type='int') 

139 text_options = { 

140 'name': 'text', 

141 'padx': 5, 

142 'wrap': 'none', 

143 'highlightthickness': 0, 

144 'width': width, 

145 'tabstyle': 'wordprocessor', # new in 8.5 

146 'height': idleConf.GetOption( 

147 'main', 'EditorWindow', 'height', type='int'), 

148 } 

149 self.text = text = MultiCallCreator(Text)(text_frame, **text_options) 

150 self.top.focused_widget = self.text 

151 

152 self.createmenubar() 

153 self.apply_bindings() 

154 

155 self.top.protocol("WM_DELETE_WINDOW", self.close) 

156 self.top.bind("<<close-window>>", self.close_event) 

157 if macosx.isAquaTk(): 

158 # Command-W on editor windows doesn't work without this. 

159 text.bind('<<close-window>>', self.close_event) 

160 # Some OS X systems have only one mouse button, so use 

161 # control-click for popup context menus there. For two 

162 # buttons, AquaTk defines <2> as the right button, not <3>. 

163 text.bind("<Control-Button-1>",self.right_menu_event) 

164 text.bind("<2>", self.right_menu_event) 

165 else: 

166 # Elsewhere, use right-click for popup menus. 

167 text.bind("<3>",self.right_menu_event) 

168 

169 text.bind('<MouseWheel>', wheel_event) 

170 text.bind('<Button-4>', wheel_event) 

171 text.bind('<Button-5>', wheel_event) 

172 text.bind('<Configure>', self.handle_winconfig) 

173 text.bind("<<cut>>", self.cut) 

174 text.bind("<<copy>>", self.copy) 

175 text.bind("<<paste>>", self.paste) 

176 text.bind("<<center-insert>>", self.center_insert_event) 

177 text.bind("<<help>>", self.help_dialog) 

178 text.bind("<<python-docs>>", self.python_docs) 

179 text.bind("<<about-idle>>", self.about_dialog) 

180 text.bind("<<open-config-dialog>>", self.config_dialog) 

181 text.bind("<<open-module>>", self.open_module_event) 

182 text.bind("<<do-nothing>>", lambda event: "break") 

183 text.bind("<<select-all>>", self.select_all) 

184 text.bind("<<remove-selection>>", self.remove_selection) 

185 text.bind("<<find>>", self.find_event) 

186 text.bind("<<find-again>>", self.find_again_event) 

187 text.bind("<<find-in-files>>", self.find_in_files_event) 

188 text.bind("<<find-selection>>", self.find_selection_event) 

189 text.bind("<<replace>>", self.replace_event) 

190 text.bind("<<goto-line>>", self.goto_line_event) 

191 text.bind("<<show-code-outline>>", self.show_code_outline_event) 

192 text.bind("<<smart-backspace>>",self.smart_backspace_event) 

193 text.bind("<<newline-and-indent>>",self.newline_and_indent_event) 

194 text.bind("<<smart-indent>>",self.smart_indent_event) 

195 self.fregion = fregion = self.FormatRegion(self) 

196 # self.fregion used in smart_indent_event to access indent_region. 

197 text.bind("<<indent-region>>", fregion.indent_region_event) 

198 text.bind("<<dedent-region>>", fregion.dedent_region_event) 

199 text.bind("<<comment-region>>", fregion.comment_region_event) 

200 text.bind("<<uncomment-region>>", fregion.uncomment_region_event) 

201 text.bind("<<tabify-region>>", fregion.tabify_region_event) 

202 text.bind("<<untabify-region>>", fregion.untabify_region_event) 

203 indents = self.Indents(self) 

204 text.bind("<<toggle-tabs>>", indents.toggle_tabs_event) 

205 text.bind("<<change-indentwidth>>", indents.change_indentwidth_event) 

206 text.bind("<Left>", self.move_at_edge_if_selection(0)) 

207 text.bind("<Right>", self.move_at_edge_if_selection(1)) 

208 text.bind("<<del-word-left>>", self.del_word_left) 

209 text.bind("<<del-word-right>>", self.del_word_right) 

210 text.bind("<<beginning-of-line>>", self.home_callback) 

211 

212 if flist: 

213 flist.inversedict[self] = key 

214 if key: 

215 flist.dict[key] = self 

216 text.bind("<<open-new-window>>", self.new_callback) 

217 text.bind("<<close-all-windows>>", self.flist.close_all_callback) 

218 text.bind("<<open-class-browser>>", self.open_module_browser) 

219 text.bind("<<open-path-browser>>", self.open_path_browser) 

220 text.bind("<<open-turtle-demo>>", self.open_turtle_demo) 

221 

222 self.set_status_bar() 

223 text_frame.pack(side=LEFT, fill=BOTH, expand=1) 

224 text_frame.rowconfigure(1, weight=1) 

225 text_frame.columnconfigure(1, weight=1) 

226 vbar['command'] = self.handle_yview 

227 vbar.grid(row=1, column=2, sticky=NSEW) 

228 text['yscrollcommand'] = vbar.set 

229 text['font'] = idleConf.GetFont(self.root, 'main', 'EditorWindow') 

230 text.grid(row=1, column=1, sticky=NSEW) 

231 text.focus_set() 

232 self.set_width() 

233 

234 # usetabs true -> literal tab characters are used by indent and 

235 # dedent cmds, possibly mixed with spaces if 

236 # indentwidth is not a multiple of tabwidth, 

237 # which will cause Tabnanny to nag! 

238 # false -> tab characters are converted to spaces by indent 

239 # and dedent cmds, and ditto TAB keystrokes 

240 # Although use-spaces=0 can be configured manually in config-main.def, 

241 # configuration of tabs v. spaces is not supported in the configuration 

242 # dialog. IDLE promotes the preferred Python indentation: use spaces! 

243 usespaces = idleConf.GetOption('main', 'Indent', 

244 'use-spaces', type='bool') 

245 self.usetabs = not usespaces 

246 

247 # tabwidth is the display width of a literal tab character. 

248 # CAUTION: telling Tk to use anything other than its default 

249 # tab setting causes it to use an entirely different tabbing algorithm, 

250 # treating tab stops as fixed distances from the left margin. 

251 # Nobody expects this, so for now tabwidth should never be changed. 

252 self.tabwidth = 8 # must remain 8 until Tk is fixed. 

253 

254 # indentwidth is the number of screen characters per indent level. 

255 # The recommended Python indentation is four spaces. 

256 self.indentwidth = self.tabwidth 

257 self.set_notabs_indentwidth() 

258 

259 # Store the current value of the insertofftime now so we can restore 

260 # it if needed. 

261 if not hasattr(idleConf, 'blink_off_time'): 

262 idleConf.blink_off_time = self.text['insertofftime'] 

263 self.update_cursor_blink() 

264 

265 # When searching backwards for a reliable place to begin parsing, 

266 # first start num_context_lines[0] lines back, then 

267 # num_context_lines[1] lines back if that didn't work, and so on. 

268 # The last value should be huge (larger than the # of lines in a 

269 # conceivable file). 

270 # Making the initial values larger slows things down more often. 

271 self.num_context_lines = 50, 500, 5000000 

272 self.per = per = self.Percolator(text) 

273 self.undo = undo = self.UndoDelegator() 

274 per.insertfilter(undo) 

275 text.undo_block_start = undo.undo_block_start 

276 text.undo_block_stop = undo.undo_block_stop 

277 undo.set_saved_change_hook(self.saved_change_hook) 

278 # IOBinding implements file I/O and printing functionality 

279 self.io = io = self.IOBinding(self) 

280 io.set_filename_change_hook(self.filename_change_hook) 

281 self.good_load = False 

282 self.set_indentation_params(False) 

283 self.color = None # initialized below in self.ResetColorizer 

284 self.code_context = None # optionally initialized later below 

285 self.line_numbers = None # optionally initialized later below 

286 if filename: 

287 if os.path.exists(filename) and not os.path.isdir(filename): 

288 if io.loadfile(filename): 

289 self.good_load = True 

290 is_py_src = self.ispythonsource(filename) 

291 self.set_indentation_params(is_py_src) 

292 else: 

293 io.set_filename(filename) 

294 self.good_load = True 

295 self.ResetColorizer() 

296 self.saved_change_hook() 

297 self.update_recent_files_list() 

298 self.load_extensions() 

299 menu = self.menudict.get('window') 

300 if menu: 

301 end = menu.index("end") 

302 if end is None: 

303 end = -1 

304 if end >= 0: 

305 menu.add_separator() 

306 end = end + 1 

307 self.wmenu_end = end 

308 window.register_callback(self.postwindowsmenu) 

309 

310 # Some abstractions so IDLE extensions are cross-IDE 

311 self.askinteger = simpledialog.askinteger 

312 self.askyesno = messagebox.askyesno 

313 self.showerror = messagebox.showerror 

314 

315 # Add pseudoevents for former extension fixed keys. 

316 # (This probably needs to be done once in the process.) 

317 text.event_add('<<autocomplete>>', '<Key-Tab>') 

318 text.event_add('<<try-open-completions>>', '<KeyRelease-period>', 

319 '<KeyRelease-slash>', '<KeyRelease-backslash>') 

320 text.event_add('<<try-open-calltip>>', '<KeyRelease-parenleft>') 

321 text.event_add('<<refresh-calltip>>', '<KeyRelease-parenright>') 

322 text.event_add('<<paren-closed>>', '<KeyRelease-parenright>', 

323 '<KeyRelease-bracketright>', '<KeyRelease-braceright>') 

324 

325 # Former extension bindings depends on frame.text being packed 

326 # (called from self.ResetColorizer()). 

327 autocomplete = self.AutoComplete(self, self.user_input_insert_tags) 

328 text.bind("<<autocomplete>>", autocomplete.autocomplete_event) 

329 text.bind("<<try-open-completions>>", 

330 autocomplete.try_open_completions_event) 

331 text.bind("<<force-open-completions>>", 

332 autocomplete.force_open_completions_event) 

333 text.bind("<<expand-word>>", self.AutoExpand(self).expand_word_event) 

334 text.bind("<<format-paragraph>>", 

335 self.FormatParagraph(self).format_paragraph_event) 

336 parenmatch = self.ParenMatch(self) 

337 text.bind("<<flash-paren>>", parenmatch.flash_paren_event) 

338 text.bind("<<paren-closed>>", parenmatch.paren_closed_event) 

339 scriptbinding = ScriptBinding(self) 

340 text.bind("<<check-module>>", scriptbinding.check_module_event) 

341 text.bind("<<run-module>>", scriptbinding.run_module_event) 

342 text.bind("<<run-custom>>", scriptbinding.run_custom_event) 

343 text.bind("<<do-rstrip>>", self.Rstrip(self).do_rstrip) 

344 self.ctip = ctip = self.Calltip(self) 

345 text.bind("<<try-open-calltip>>", ctip.try_open_calltip_event) 

346 #refresh-calltip must come after paren-closed to work right 

347 text.bind("<<refresh-calltip>>", ctip.refresh_calltip_event) 

348 text.bind("<<force-open-calltip>>", ctip.force_open_calltip_event) 

349 text.bind("<<zoom-height>>", self.ZoomHeight(self).zoom_height_event) 

350 if self.allow_code_context: 

351 self.code_context = self.CodeContext(self) 

352 text.bind("<<toggle-code-context>>", 

353 self.code_context.toggle_code_context_event) 

354 else: 

355 self.update_menu_state('options', '*ode*ontext', 'disabled') 

356 if self.allow_line_numbers: 

357 self.line_numbers = self.LineNumbers(self) 

358 if idleConf.GetOption('main', 'EditorWindow', 

359 'line-numbers-default', type='bool'): 

360 self.toggle_line_numbers_event() 

361 text.bind("<<toggle-line-numbers>>", self.toggle_line_numbers_event) 

362 else: 

363 self.update_menu_state('options', '*ine*umbers', 'disabled') 

364 

365 def handle_winconfig(self, event=None): 

366 self.set_width() 

367 

368 def set_width(self): 

369 text = self.text 

370 inner_padding = sum(map(text.tk.getint, [text.cget('border'), 

371 text.cget('padx')])) 

372 pixel_width = text.winfo_width() - 2 * inner_padding 

373 

374 # Divide the width of the Text widget by the font width, 

375 # which is taken to be the width of '0' (zero). 

376 # http://www.tcl.tk/man/tcl8.6/TkCmd/text.htm#M21 

377 zero_char_width = \ 

378 Font(text, font=text.cget('font')).measure('0') 

379 self.width = pixel_width // zero_char_width 

380 

381 def new_callback(self, event): 

382 dirname, basename = self.io.defaultfilename() 

383 self.flist.new(dirname) 

384 return "break" 

385 

386 def home_callback(self, event): 

387 if (event.state & 4) != 0 and event.keysym == "Home": 

388 # state&4==Control. If <Control-Home>, use the Tk binding. 

389 return None 

390 if self.text.index("iomark") and \ 

391 self.text.compare("iomark", "<=", "insert lineend") and \ 

392 self.text.compare("insert linestart", "<=", "iomark"): 

393 # In Shell on input line, go to just after prompt 

394 insertpt = int(self.text.index("iomark").split(".")[1]) 

395 else: 

396 line = self.text.get("insert linestart", "insert lineend") 

397 for insertpt in range(len(line)): 

398 if line[insertpt] not in (' ','\t'): 

399 break 

400 else: 

401 insertpt=len(line) 

402 lineat = int(self.text.index("insert").split('.')[1]) 

403 if insertpt == lineat: 

404 insertpt = 0 

405 dest = "insert linestart+"+str(insertpt)+"c" 

406 if (event.state&1) == 0: 

407 # shift was not pressed 

408 self.text.tag_remove("sel", "1.0", "end") 

409 else: 

410 if not self.text.index("sel.first"): 

411 # there was no previous selection 

412 self.text.mark_set("my_anchor", "insert") 

413 else: 

414 if self.text.compare(self.text.index("sel.first"), "<", 

415 self.text.index("insert")): 

416 self.text.mark_set("my_anchor", "sel.first") # extend back 

417 else: 

418 self.text.mark_set("my_anchor", "sel.last") # extend forward 

419 first = self.text.index(dest) 

420 last = self.text.index("my_anchor") 

421 if self.text.compare(first,">",last): 

422 first,last = last,first 

423 self.text.tag_remove("sel", "1.0", "end") 

424 self.text.tag_add("sel", first, last) 

425 self.text.mark_set("insert", dest) 

426 self.text.see("insert") 

427 return "break" 

428 

429 def set_status_bar(self): 

430 self.status_bar = self.MultiStatusBar(self.top) 

431 sep = Frame(self.top, height=1, borderwidth=1, background='grey75') 

432 if sys.platform == "darwin": 

433 # Insert some padding to avoid obscuring some of the statusbar 

434 # by the resize widget. 

435 self.status_bar.set_label('_padding1', ' ', side=RIGHT) 

436 self.status_bar.set_label('column', 'Col: ?', side=RIGHT) 

437 self.status_bar.set_label('line', 'Ln: ?', side=RIGHT) 

438 self.status_bar.pack(side=BOTTOM, fill=X) 

439 sep.pack(side=BOTTOM, fill=X) 

440 self.text.bind("<<set-line-and-column>>", self.set_line_and_column) 

441 self.text.event_add("<<set-line-and-column>>", 

442 "<KeyRelease>", "<ButtonRelease>") 

443 self.text.after_idle(self.set_line_and_column) 

444 

445 def set_line_and_column(self, event=None): 

446 line, column = self.text.index(INSERT).split('.') 

447 self.status_bar.set_label('column', 'Col: %s' % column) 

448 self.status_bar.set_label('line', 'Ln: %s' % line) 

449 

450 menu_specs = [ 

451 ("file", "_File"), 

452 ("edit", "_Edit"), 

453 ("format", "F_ormat"), 

454 ("run", "_Run"), 

455 ("options", "_Options"), 

456 ("window", "_Window"), 

457 ("help", "_Help"), 

458 ] 

459 

460 

461 def createmenubar(self): 

462 mbar = self.menubar 

463 self.menudict = menudict = {} 

464 for name, label in self.menu_specs: 

465 underline, label = prepstr(label) 

466 postcommand = getattr(self, f'{name}_menu_postcommand', None) 

467 menudict[name] = menu = Menu(mbar, name=name, tearoff=0, 

468 postcommand=postcommand) 

469 mbar.add_cascade(label=label, menu=menu, underline=underline) 

470 if macosx.isCarbonTk(): 

471 # Insert the application menu 

472 menudict['application'] = menu = Menu(mbar, name='apple', 

473 tearoff=0) 

474 mbar.add_cascade(label='IDLE', menu=menu) 

475 self.fill_menus() 

476 self.recent_files_menu = Menu(self.menubar, tearoff=0) 

477 self.menudict['file'].insert_cascade(3, label='Recent Files', 

478 underline=0, 

479 menu=self.recent_files_menu) 

480 self.base_helpmenu_length = self.menudict['help'].index(END) 

481 self.reset_help_menu_entries() 

482 

483 def postwindowsmenu(self): 

484 # Only called when Window menu exists 

485 menu = self.menudict['window'] 

486 end = menu.index("end") 

487 if end is None: 

488 end = -1 

489 if end > self.wmenu_end: 

490 menu.delete(self.wmenu_end+1, end) 

491 window.add_windows_to_menu(menu) 

492 

493 def update_menu_label(self, menu, index, label): 

494 "Update label for menu item at index." 

495 menuitem = self.menudict[menu] 

496 menuitem.entryconfig(index, label=label) 

497 

498 def update_menu_state(self, menu, index, state): 

499 "Update state for menu item at index." 

500 menuitem = self.menudict[menu] 

501 menuitem.entryconfig(index, state=state) 

502 

503 def handle_yview(self, event, *args): 

504 "Handle scrollbar." 

505 if event == 'moveto': 

506 fraction = float(args[0]) 

507 lines = (round(self.getlineno('end') * fraction) - 

508 self.getlineno('@0,0')) 

509 event = 'scroll' 

510 args = (lines, 'units') 

511 self.text.yview(event, *args) 

512 return 'break' 

513 

514 rmenu = None 

515 

516 def right_menu_event(self, event): 

517 text = self.text 

518 newdex = text.index(f'@{event.x},{event.y}') 

519 try: 

520 in_selection = (text.compare('sel.first', '<=', newdex) and 

521 text.compare(newdex, '<=', 'sel.last')) 

522 except TclError: 

523 in_selection = False 

524 if not in_selection: 

525 text.tag_remove("sel", "1.0", "end") 

526 text.mark_set("insert", newdex) 

527 if not self.rmenu: 

528 self.make_rmenu() 

529 rmenu = self.rmenu 

530 self.event = event 

531 iswin = sys.platform[:3] == 'win' 

532 if iswin: 

533 text.config(cursor="arrow") 

534 

535 for item in self.rmenu_specs: 

536 try: 

537 label, eventname, verify_state = item 

538 except ValueError: # see issue1207589 

539 continue 

540 

541 if verify_state is None: 

542 continue 

543 state = getattr(self, verify_state)() 

544 rmenu.entryconfigure(label, state=state) 

545 

546 rmenu.tk_popup(event.x_root, event.y_root) 

547 if iswin: 

548 self.text.config(cursor="ibeam") 

549 return "break" 

550 

551 rmenu_specs = [ 

552 # ("Label", "<<virtual-event>>", "statefuncname"), ... 

553 ("Close", "<<close-window>>", None), # Example 

554 ] 

555 

556 def make_rmenu(self): 

557 rmenu = Menu(self.text, tearoff=0) 

558 for item in self.rmenu_specs: 

559 label, eventname = item[0], item[1] 

560 if label is not None: 

561 def command(text=self.text, eventname=eventname): 

562 text.event_generate(eventname) 

563 rmenu.add_command(label=label, command=command) 

564 else: 

565 rmenu.add_separator() 

566 self.rmenu = rmenu 

567 

568 def rmenu_check_cut(self): 

569 return self.rmenu_check_copy() 

570 

571 def rmenu_check_copy(self): 

572 try: 

573 indx = self.text.index('sel.first') 

574 except TclError: 

575 return 'disabled' 

576 else: 

577 return 'normal' if indx else 'disabled' 

578 

579 def rmenu_check_paste(self): 

580 try: 

581 self.text.tk.call('tk::GetSelection', self.text, 'CLIPBOARD') 

582 except TclError: 

583 return 'disabled' 

584 else: 

585 return 'normal' 

586 

587 def about_dialog(self, event=None): 

588 "Handle Help 'About IDLE' event." 

589 # Synchronize with macosx.overrideRootMenu.about_dialog. 

590 help_about.AboutDialog(self.top) 

591 return "break" 

592 

593 def config_dialog(self, event=None): 

594 "Handle Options 'Configure IDLE' event." 

595 # Synchronize with macosx.overrideRootMenu.config_dialog. 

596 configdialog.ConfigDialog(self.top,'Settings') 

597 return "break" 

598 

599 def help_dialog(self, event=None): 

600 "Handle Help 'IDLE Help' event." 

601 # Synchronize with macosx.overrideRootMenu.help_dialog. 

602 if self.root: 

603 parent = self.root 

604 else: 

605 parent = self.top 

606 help.show_idlehelp(parent) 

607 return "break" 

608 

609 def python_docs(self, event=None): 

610 if sys.platform[:3] == 'win': 

611 try: 

612 os.startfile(self.help_url) 

613 except OSError as why: 

614 messagebox.showerror(title='Document Start Failure', 

615 message=str(why), parent=self.text) 

616 else: 

617 webbrowser.open(self.help_url) 

618 return "break" 

619 

620 def cut(self,event): 

621 self.text.event_generate("<<Cut>>") 

622 return "break" 

623 

624 def copy(self,event): 

625 if not self.text.tag_ranges("sel"): 

626 # There is no selection, so do nothing and maybe interrupt. 

627 return None 

628 self.text.event_generate("<<Copy>>") 

629 return "break" 

630 

631 def paste(self,event): 

632 self.text.event_generate("<<Paste>>") 

633 self.text.see("insert") 

634 return "break" 

635 

636 def select_all(self, event=None): 

637 self.text.tag_add("sel", "1.0", "end-1c") 

638 self.text.mark_set("insert", "1.0") 

639 self.text.see("insert") 

640 return "break" 

641 

642 def remove_selection(self, event=None): 

643 self.text.tag_remove("sel", "1.0", "end") 

644 self.text.see("insert") 

645 return "break" 

646 

647 def move_at_edge_if_selection(self, edge_index): 

648 """Cursor move begins at start or end of selection 

649 

650 When a left/right cursor key is pressed create and return to Tkinter a 

651 function which causes a cursor move from the associated edge of the 

652 selection. 

653 

654 """ 

655 self_text_index = self.text.index 

656 self_text_mark_set = self.text.mark_set 

657 edges_table = ("sel.first+1c", "sel.last-1c") 

658 def move_at_edge(event): 

659 if (event.state & 5) == 0: # no shift(==1) or control(==4) pressed 

660 try: 

661 self_text_index("sel.first") 

662 self_text_mark_set("insert", edges_table[edge_index]) 

663 except TclError: 

664 pass 

665 return move_at_edge 

666 

667 def del_word_left(self, event): 

668 self.text.event_generate('<Meta-Delete>') 

669 return "break" 

670 

671 def del_word_right(self, event): 

672 self.text.event_generate('<Meta-d>') 

673 return "break" 

674 

675 def find_event(self, event): 

676 search.find(self.text) 

677 return "break" 

678 

679 def find_again_event(self, event): 

680 search.find_again(self.text) 

681 return "break" 

682 

683 def find_selection_event(self, event): 

684 search.find_selection(self.text) 

685 return "break" 

686 

687 def find_in_files_event(self, event): 

688 grep.grep(self.text, self.io, self.flist) 

689 return "break" 

690 

691 def replace_event(self, event): 

692 replace.replace(self.text) 

693 return "break" 

694 

695 def goto_line(self, query): 

696 """ 

697 Jump to a line in the text widget. 

698 The query must be either a line number, or a string of the format 

699 "line:char", where both `line` and `char` are valid character and line 

700 indices present in the current Text widget. 

701 """ 

702 # Get the total line count of the Text widget. This helps us 

703 # index negatively. 

704 line_count = int(self.text.index(END).split(".")[0]) - 1 

705 

706 # If, by any chance, we errored out before, don't do anything. Otherwise... 

707 if query is not None: 

708 # Assume we are only updating the line index.  

709 line = query 

710 char = "0" 

711 # Check if we are character indexing as well. 

712 if ":" in query: 

713 line = query.split(":")[0] 

714 char = query.split(":")[1] 

715 # If we got a positive integer, just jump to line. 

716 # And char, if we happened to change it. 

717 if int(line) > 0: 

718 self.text.tag_remove("sel", "1.0", "end") 

719 self.text.mark_set("insert", f'{line}.{char}') 

720 self.text.see("insert") 

721 self.set_line_and_column() 

722 else: 

723 # Otherwise, add it to the total line count to index negatively. 

724 # TOTAL_LINE_COUNT + (-some_line) = TOTAL_LINE_COUNT - some_line 

725 # so this checks out for negative indexing functionality. 

726 self.text.tag_remove("sel", "1.0", "end") 

727 self.text.mark_set("insert", f'{line_count + int(line)}.{char}') 

728 self.text.see("insert") 

729 self.set_line_and_column() 

730 return "break" 

731 

732 def goto_line_event(self, event): 

733 text = self.text 

734 # Get the full line count of the editor window. 

735 # Note this returns the location in the format "line.char", 

736 # according to index's documentation, so we need to pull the 

737 # line count out later. 

738 count_query = text.index(END) 

739 # Get the location of the cursor in the window. 

740 mark_location = text.index(INSERT) 

741 # Pull the line count. 

742 line_count = count_query.split(".")[0] 

743 # The line count returned includes EOF. Remove it. 

744 line_count = int(line_count) - 1 

745 

746 # Create dialog box clarifying the range of possible values. 

747 # We will pass through the negative indexing feature as well 

748 # as the current line, to make it easier to know where to jump. 

749 lineno = query.Goto( 

750 text, "Go To Line", 

751 f"Type a line number between 1 and {str(line_count)} or use negative indexing (-1 to -{str(line_count)})\n" 

752 f"You are at line {mark_location.split('.')[0]}", 

753 linecount=line_count, 

754 editwin=self 

755 ).result 

756 # Update the line. 

757 self.goto_line(lineno) 

758 return "break" 

759 

760 def show_code_outline_event(self, event): 

761 """Event handler function for when user clicks 'Show Code Outline' in the menu bar. 

762 

763 This simply prints the function and classes containing the highlit region in the 

764 PyShell window. 

765 """ 

766 def write_to_shell(shell, text): 

767 shell.resetoutput() 

768 shell.text.insert(INSERT, text) 

769 shell.showprompt() 

770 

771 text = self.text 

772 # Check if user accidentally clicked "Show Code Outline" in PyShell. 

773 if self.flist.pyshell == self: 

774 write_to_shell(self, "Can't parse code from a shell window!") 

775 return "break" 

776 # Get lines of selection. If no line was selected, "select" entire file. 

777 (line_start, line_end) = (text.index("sel.first"), text.index("sel.last")) 

778 if line_start == "" and line_end == "": 

779 line_start = 1 

780 line_end = int(text.index(END).split(".")[0]) - 1 

781 else: 

782 line_start = int(line_start.split(".")[0]) 

783 line_end = int(line_end.split(".")[0]) 

784 

785 # Process the file's source code using `ast.parse` 

786 tree = ast.AST 

787 try: 

788 tree = ast.parse(text.get("1.0", "end")) 

789 except SyntaxError: 

790 write_to_shell(self.flist.pyshell, "Incorrect source code in original file!") 

791 return "break" 

792 

793 # First, we'll find all the existent classes and function definitions in the program. 

794 # We'll do a BFS of the tree and, for every entry, note the information about the node  

795 # and its level of indentation (for the outline display later). This should be enough 

796 # for us to process later. 

797 visitor = ClassFuncNodeVisitor() 

798 visitor.visit(tree) 

799 node_list = visitor.get_node_list() 

800 

801 # Now we need to only keep code relevant to what we want to display. We'll do this 

802 # by filtering for any node whose line ranges overlap with the line range of our selection. 

803 # We'll also sort by the first line, since this will order the nodes depending on how they 

804 # showed up in the file, which is about how it should show up in the outline later. 

805 contains_selection = lambda node: node.line_start <= line_end and line_start <= node.line_end 

806 outline_nodes = sorted([x for x in node_list if contains_selection(x)], key=lambda x: x.line_start) 

807 

808 # We'll use a string builder and just add everything in one go. We'll begin with the name 

809 # of the file, since we'll want to print that even if there is absolutely nothing in the file 

810 # but unindented code. Afterwards, print the type of definition and then the name. 

811 code_outline = f"{os.path.basename(self.io.filename)}:\n" 

812 for node in outline_nodes: 

813 # Indent with 4 spaces depending on whatever level of indentation the node has. 

814 # Add 1, because we want to separate from the filename as well. 

815 # 

816 # Also check if tabs are used. If they are, replace the col_offset for tabs with 4 spaces, 

817 # since by default tabs only indent by one column in `ast`'s module. 

818 indent = (" " * 4) * (node.indentation + 1) if self.usetabs else " " * (node.indentation + 4) 

819 code_outline += indent + f"{node.type}: {node.name}\n" 

820 write_to_shell(self.flist.pyshell, code_outline) 

821 return "break" 

822 

823 def open_module(self): 

824 """Get module name from user and open it. 

825 

826 Return module path or None for calls by open_module_browser 

827 when latter is not invoked in named editor window. 

828 """ 

829 # XXX This, open_module_browser, and open_path_browser 

830 # would fit better in iomenu.IOBinding. 

831 try: 

832 name = self.text.get("sel.first", "sel.last").strip() 

833 except TclError: 

834 name = '' 

835 file_path = query.ModuleName( 

836 self.text, "Open Module", 

837 "Enter the name of a Python module\n" 

838 "to search on sys.path and open:", 

839 name).result 

840 if file_path is not None: 

841 if self.flist: 

842 self.flist.open(file_path) 

843 else: 

844 self.io.loadfile(file_path) 

845 return file_path 

846 

847 def open_module_event(self, event): 

848 self.open_module() 

849 return "break" 

850 

851 def open_module_browser(self, event=None): 

852 filename = self.io.filename 

853 if not (self.__class__.__name__ == 'PyShellEditorWindow' 

854 and filename): 

855 filename = self.open_module() 

856 if filename is None: 

857 return "break" 

858 from idlelib import browser 

859 browser.ModuleBrowser(self.root, filename) 

860 return "break" 

861 

862 def open_path_browser(self, event=None): 

863 from idlelib import pathbrowser 

864 pathbrowser.PathBrowser(self.root) 

865 return "break" 

866 

867 def open_turtle_demo(self, event = None): 

868 import subprocess 

869 

870 cmd = [sys.executable, 

871 '-c', 

872 'from turtledemo.__main__ import main; main()'] 

873 subprocess.Popen(cmd, shell=False) 

874 return "break" 

875 

876 def gotoline(self, lineno): 

877 if lineno is not None and lineno > 0: 

878 self.text.mark_set("insert", "%d.0" % lineno) 

879 self.text.tag_remove("sel", "1.0", "end") 

880 self.text.tag_add("sel", "insert", "insert +1l") 

881 self.center() 

882 

883 def ispythonsource(self, filename): 

884 if not filename or os.path.isdir(filename): 

885 return True 

886 base, ext = os.path.splitext(os.path.basename(filename)) 

887 if os.path.normcase(ext) in py_extensions: 

888 return True 

889 line = self.text.get('1.0', '1.0 lineend') 

890 return line.startswith('#!') and 'python' in line 

891 

892 def close_hook(self): 

893 if self.flist: 

894 self.flist.unregister_maybe_terminate(self) 

895 self.flist = None 

896 

897 def set_close_hook(self, close_hook): 

898 self.close_hook = close_hook 

899 

900 def filename_change_hook(self): 

901 if self.flist: 

902 self.flist.filename_changed_edit(self) 

903 self.saved_change_hook() 

904 self.top.update_windowlist_registry(self) 

905 self.ResetColorizer() 

906 

907 def _addcolorizer(self): 

908 if self.color: 

909 return 

910 if self.ispythonsource(self.io.filename): 

911 self.color = self.ColorDelegator() 

912 # can add more colorizers here... 

913 if self.color: 

914 self.per.insertfilterafter(filter=self.color, after=self.undo) 

915 

916 def _rmcolorizer(self): 

917 if not self.color: 

918 return 

919 self.color.removecolors() 

920 self.per.removefilter(self.color) 

921 self.color = None 

922 

923 def ResetColorizer(self): 

924 "Update the color theme" 

925 # Called from self.filename_change_hook and from configdialog.py 

926 self._rmcolorizer() 

927 self._addcolorizer() 

928 EditorWindow.color_config(self.text) 

929 

930 if self.code_context is not None: 

931 self.code_context.update_highlight_colors() 

932 

933 if self.line_numbers is not None: 

934 self.line_numbers.update_colors() 

935 

936 IDENTCHARS = string.ascii_letters + string.digits + "_" 

937 

938 def colorize_syntax_error(self, text, pos): 

939 text.tag_add("ERROR", pos) 

940 char = text.get(pos) 

941 if char and char in self.IDENTCHARS: 

942 text.tag_add("ERROR", pos + " wordstart", pos) 

943 if '\n' == text.get(pos): # error at line end 

944 text.mark_set("insert", pos) 

945 else: 

946 text.mark_set("insert", pos + "+1c") 

947 text.see(pos) 

948 

949 def update_cursor_blink(self): 

950 "Update the cursor blink configuration." 

951 cursorblink = idleConf.GetOption( 

952 'main', 'EditorWindow', 'cursor-blink', type='bool') 

953 if not cursorblink: 

954 self.text['insertofftime'] = 0 

955 else: 

956 # Restore the original value 

957 self.text['insertofftime'] = idleConf.blink_off_time 

958 

959 def ResetFont(self): 

960 "Update the text widgets' font if it is changed" 

961 # Called from configdialog.py 

962 

963 # Update the code context widget first, since its height affects 

964 # the height of the text widget. This avoids double re-rendering. 

965 if self.code_context is not None: 

966 self.code_context.update_font() 

967 # Next, update the line numbers widget, since its width affects 

968 # the width of the text widget. 

969 if self.line_numbers is not None: 

970 self.line_numbers.update_font() 

971 # Finally, update the main text widget. 

972 new_font = idleConf.GetFont(self.root, 'main', 'EditorWindow') 

973 self.text['font'] = new_font 

974 self.set_width() 

975 

976 def RemoveKeybindings(self): 

977 "Remove the keybindings before they are changed." 

978 # Called from configdialog.py 

979 self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet() 

980 for event, keylist in keydefs.items(): 

981 self.text.event_delete(event, *keylist) 

982 for extensionName in self.get_standard_extension_names(): 

983 xkeydefs = idleConf.GetExtensionBindings(extensionName) 

984 if xkeydefs: 

985 for event, keylist in xkeydefs.items(): 

986 self.text.event_delete(event, *keylist) 

987 

988 def ApplyKeybindings(self): 

989 "Update the keybindings after they are changed" 

990 # Called from configdialog.py 

991 self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet() 

992 self.apply_bindings() 

993 for extensionName in self.get_standard_extension_names(): 

994 xkeydefs = idleConf.GetExtensionBindings(extensionName) 

995 if xkeydefs: 

996 self.apply_bindings(xkeydefs) 

997 #update menu accelerators 

998 menuEventDict = {} 

999 for menu in self.mainmenu.menudefs: 

1000 menuEventDict[menu[0]] = {} 

1001 for item in menu[1]: 

1002 if item: 

1003 menuEventDict[menu[0]][prepstr(item[0])[1]] = item[1] 

1004 for menubarItem in self.menudict: 

1005 menu = self.menudict[menubarItem] 

1006 end = menu.index(END) 

1007 if end is None: 

1008 # Skip empty menus 

1009 continue 

1010 end += 1 

1011 for index in range(0, end): 

1012 if menu.type(index) == 'command': 

1013 accel = menu.entrycget(index, 'accelerator') 

1014 if accel: 

1015 itemName = menu.entrycget(index, 'label') 

1016 event = '' 

1017 if menubarItem in menuEventDict: 

1018 if itemName in menuEventDict[menubarItem]: 

1019 event = menuEventDict[menubarItem][itemName] 

1020 if event: 

1021 accel = get_accelerator(keydefs, event) 

1022 menu.entryconfig(index, accelerator=accel) 

1023 

1024 def set_notabs_indentwidth(self): 

1025 "Update the indentwidth if changed and not using tabs in this window" 

1026 # Called from configdialog.py 

1027 if not self.usetabs: 

1028 self.indentwidth = idleConf.GetOption('main', 'Indent','num-spaces', 

1029 type='int') 

1030 

1031 def reset_help_menu_entries(self): 

1032 "Update the additional help entries on the Help menu" 

1033 help_list = idleConf.GetAllExtraHelpSourcesList() 

1034 helpmenu = self.menudict['help'] 

1035 # first delete the extra help entries, if any 

1036 helpmenu_length = helpmenu.index(END) 

1037 if helpmenu_length > self.base_helpmenu_length: 

1038 helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length) 

1039 # then rebuild them 

1040 if help_list: 

1041 helpmenu.add_separator() 

1042 for entry in help_list: 

1043 cmd = self.__extra_help_callback(entry[1]) 

1044 helpmenu.add_command(label=entry[0], command=cmd) 

1045 # and update the menu dictionary 

1046 self.menudict['help'] = helpmenu 

1047 

1048 def __extra_help_callback(self, helpfile): 

1049 "Create a callback with the helpfile value frozen at definition time" 

1050 def display_extra_help(helpfile=helpfile): 

1051 if not helpfile.startswith(('www', 'http')): 

1052 helpfile = os.path.normpath(helpfile) 

1053 if sys.platform[:3] == 'win': 

1054 try: 

1055 os.startfile(helpfile) 

1056 except OSError as why: 

1057 messagebox.showerror(title='Document Start Failure', 

1058 message=str(why), parent=self.text) 

1059 else: 

1060 webbrowser.open(helpfile) 

1061 return display_extra_help 

1062 

1063 def update_recent_files_list(self, new_file=None): 

1064 "Load and update the recent files list and menus" 

1065 # TODO: move to iomenu. 

1066 rf_list = [] 

1067 file_path = self.recent_files_path 

1068 if file_path and os.path.exists(file_path): 

1069 with open(file_path, 'r', 

1070 encoding='utf_8', errors='replace') as rf_list_file: 

1071 rf_list = rf_list_file.readlines() 

1072 if new_file: 

1073 new_file = os.path.abspath(new_file) + '\n' 

1074 if new_file in rf_list: 

1075 rf_list.remove(new_file) # move to top 

1076 rf_list.insert(0, new_file) 

1077 # clean and save the recent files list 

1078 bad_paths = [] 

1079 for path in rf_list: 

1080 if '\0' in path or not os.path.exists(path[0:-1]): 

1081 bad_paths.append(path) 

1082 rf_list = [path for path in rf_list if path not in bad_paths] 

1083 ulchars = "1234567890ABCDEFGHIJK" 

1084 rf_list = rf_list[0:len(ulchars)] 

1085 if file_path: 

1086 try: 

1087 with open(file_path, 'w', 

1088 encoding='utf_8', errors='replace') as rf_file: 

1089 rf_file.writelines(rf_list) 

1090 except OSError as err: 

1091 if not getattr(self.root, "recentfiles_message", False): 

1092 self.root.recentfiles_message = True 

1093 messagebox.showwarning(title='IDLE Warning', 

1094 message="Cannot save Recent Files list to disk.\n" 

1095 f" {err}\n" 

1096 "Select OK to continue.", 

1097 parent=self.text) 

1098 # for each edit window instance, construct the recent files menu 

1099 for instance in self.top.instance_dict: 

1100 menu = instance.recent_files_menu 

1101 menu.delete(0, END) # clear, and rebuild: 

1102 for i, file_name in enumerate(rf_list): 

1103 file_name = file_name.rstrip() # zap \n 

1104 callback = instance.__recent_file_callback(file_name) 

1105 menu.add_command(label=ulchars[i] + " " + file_name, 

1106 command=callback, 

1107 underline=0) 

1108 

1109 def __recent_file_callback(self, file_name): 

1110 def open_recent_file(fn_closure=file_name): 

1111 self.io.open(editFile=fn_closure) 

1112 return open_recent_file 

1113 

1114 def saved_change_hook(self): 

1115 short = self.short_title() 

1116 long = self.long_title() 

1117 if short and long: 

1118 title = short + " - " + long + _py_version 

1119 elif short: 

1120 title = short 

1121 elif long: 

1122 title = long 

1123 else: 

1124 title = "untitled" 

1125 icon = short or long or title 

1126 if not self.get_saved(): 

1127 title = "*%s*" % title 

1128 icon = "*%s" % icon 

1129 self.top.wm_title(title) 

1130 self.top.wm_iconname(icon) 

1131 

1132 def get_saved(self): 

1133 return self.undo.get_saved() 

1134 

1135 def set_saved(self, flag): 

1136 self.undo.set_saved(flag) 

1137 

1138 def reset_undo(self): 

1139 self.undo.reset_undo() 

1140 

1141 def short_title(self): 

1142 filename = self.io.filename 

1143 return os.path.basename(filename) if filename else "untitled" 

1144 

1145 def long_title(self): 

1146 return self.io.filename or "" 

1147 

1148 def center_insert_event(self, event): 

1149 self.center() 

1150 return "break" 

1151 

1152 def center(self, mark="insert"): 

1153 text = self.text 

1154 top, bot = self.getwindowlines() 

1155 lineno = self.getlineno(mark) 

1156 height = bot - top 

1157 newtop = max(1, lineno - height//2) 

1158 text.yview(float(newtop)) 

1159 

1160 def getwindowlines(self): 

1161 text = self.text 

1162 top = self.getlineno("@0,0") 

1163 bot = self.getlineno("@0,65535") 

1164 if top == bot and text.winfo_height() == 1: 

1165 # Geometry manager hasn't run yet 

1166 height = int(text['height']) 

1167 bot = top + height - 1 

1168 return top, bot 

1169 

1170 def getlineno(self, mark="insert"): 

1171 text = self.text 

1172 return int(float(text.index(mark))) 

1173 

1174 def get_geometry(self): 

1175 "Return (width, height, x, y)" 

1176 geom = self.top.wm_geometry() 

1177 m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom) 

1178 return list(map(int, m.groups())) 

1179 

1180 def close_event(self, event): 

1181 self.close() 

1182 return "break" 

1183 

1184 def maybesave(self): 

1185 if self.io: 

1186 if not self.get_saved(): 

1187 if self.top.state()!='normal': 

1188 self.top.deiconify() 

1189 self.top.lower() 

1190 self.top.lift() 

1191 return self.io.maybesave() 

1192 

1193 def close(self): 

1194 try: 

1195 reply = self.maybesave() 

1196 if str(reply) != "cancel": 

1197 self._close() 

1198 return reply 

1199 except AttributeError: # bpo-35379: close called twice 

1200 pass 

1201 

1202 def _close(self): 

1203 if self.io.filename: 

1204 self.update_recent_files_list(new_file=self.io.filename) 

1205 window.unregister_callback(self.postwindowsmenu) 

1206 self.unload_extensions() 

1207 self.io.close() 

1208 self.io = None 

1209 self.undo = None 

1210 if self.color: 

1211 self.color.close() 

1212 self.color = None 

1213 self.text = None 

1214 self.tkinter_vars = None 

1215 self.per.close() 

1216 self.per = None 

1217 self.top.destroy() 

1218 if self.close_hook: 

1219 # unless override: unregister from flist, terminate if last window 

1220 self.close_hook() 

1221 

1222 def load_extensions(self): 

1223 self.extensions = {} 

1224 self.load_standard_extensions() 

1225 

1226 def unload_extensions(self): 

1227 for ins in list(self.extensions.values()): 

1228 if hasattr(ins, "close"): 

1229 ins.close() 

1230 self.extensions = {} 

1231 

1232 def load_standard_extensions(self): 

1233 for name in self.get_standard_extension_names(): 

1234 try: 

1235 self.load_extension(name) 

1236 except: 

1237 print("Failed to load extension", repr(name)) 

1238 traceback.print_exc() 

1239 

1240 def get_standard_extension_names(self): 

1241 return idleConf.GetExtensions(editor_only=True) 

1242 

1243 extfiles = { # Map built-in config-extension section names to file names. 

1244 'ZzDummy': 'zzdummy', 

1245 } 

1246 

1247 def load_extension(self, name): 

1248 fname = self.extfiles.get(name, name) 

1249 try: 

1250 try: 

1251 mod = importlib.import_module('.' + fname, package=__package__) 

1252 except (ImportError, TypeError): 

1253 mod = importlib.import_module(fname) 

1254 except ImportError: 

1255 print("\nFailed to import extension: ", name) 

1256 raise 

1257 cls = getattr(mod, name) 

1258 keydefs = idleConf.GetExtensionBindings(name) 

1259 if hasattr(cls, "menudefs"): 

1260 self.fill_menus(cls.menudefs, keydefs) 

1261 ins = cls(self) 

1262 self.extensions[name] = ins 

1263 if keydefs: 

1264 self.apply_bindings(keydefs) 

1265 for vevent in keydefs: 

1266 methodname = vevent.replace("-", "_") 

1267 while methodname[:1] == '<': 

1268 methodname = methodname[1:] 

1269 while methodname[-1:] == '>': 

1270 methodname = methodname[:-1] 

1271 methodname = methodname + "_event" 

1272 if hasattr(ins, methodname): 

1273 self.text.bind(vevent, getattr(ins, methodname)) 

1274 

1275 def apply_bindings(self, keydefs=None): 

1276 if keydefs is None: 

1277 keydefs = self.mainmenu.default_keydefs 

1278 text = self.text 

1279 text.keydefs = keydefs 

1280 for event, keylist in keydefs.items(): 

1281 if keylist: 

1282 text.event_add(event, *keylist) 

1283 

1284 def fill_menus(self, menudefs=None, keydefs=None): 

1285 """Add appropriate entries to the menus and submenus 

1286 

1287 Menus that are absent or None in self.menudict are ignored. 

1288 """ 

1289 if menudefs is None: 

1290 menudefs = self.mainmenu.menudefs 

1291 if keydefs is None: 

1292 keydefs = self.mainmenu.default_keydefs 

1293 menudict = self.menudict 

1294 text = self.text 

1295 for mname, entrylist in menudefs: 

1296 menu = menudict.get(mname) 

1297 if not menu: 

1298 continue 

1299 for entry in entrylist: 

1300 if not entry: 

1301 menu.add_separator() 

1302 else: 

1303 label, eventname = entry 

1304 checkbutton = (label[:1] == '!') 

1305 if checkbutton: 

1306 label = label[1:] 

1307 underline, label = prepstr(label) 

1308 accelerator = get_accelerator(keydefs, eventname) 

1309 def command(text=text, eventname=eventname): 

1310 text.event_generate(eventname) 

1311 if checkbutton: 

1312 var = self.get_var_obj(eventname, BooleanVar) 

1313 menu.add_checkbutton(label=label, underline=underline, 

1314 command=command, accelerator=accelerator, 

1315 variable=var) 

1316 else: 

1317 menu.add_command(label=label, underline=underline, 

1318 command=command, 

1319 accelerator=accelerator) 

1320 

1321 def getvar(self, name): 

1322 var = self.get_var_obj(name) 

1323 if var: 

1324 value = var.get() 

1325 return value 

1326 else: 

1327 raise NameError(name) 

1328 

1329 def setvar(self, name, value, vartype=None): 

1330 var = self.get_var_obj(name, vartype) 

1331 if var: 

1332 var.set(value) 

1333 else: 

1334 raise NameError(name) 

1335 

1336 def get_var_obj(self, name, vartype=None): 

1337 var = self.tkinter_vars.get(name) 

1338 if not var and vartype: 

1339 # create a Tkinter variable object with self.text as master: 

1340 self.tkinter_vars[name] = var = vartype(self.text) 

1341 return var 

1342 

1343 # Tk implementations of "virtual text methods" -- each platform 

1344 # reusing IDLE's support code needs to define these for its GUI's 

1345 # flavor of widget. 

1346 

1347 # Is character at text_index in a Python string? Return 0 for 

1348 # "guaranteed no", true for anything else. This info is expensive 

1349 # to compute ab initio, but is probably already known by the 

1350 # platform's colorizer. 

1351 

1352 def is_char_in_string(self, text_index): 

1353 if self.color: 

1354 # Return true iff colorizer hasn't (re)gotten this far 

1355 # yet, or the character is tagged as being in a string 

1356 return self.text.tag_prevrange("TODO", text_index) or \ 

1357 "STRING" in self.text.tag_names(text_index) 

1358 else: 

1359 # The colorizer is missing: assume the worst 

1360 return 1 

1361 

1362 # If a selection is defined in the text widget, return (start, 

1363 # end) as Tkinter text indices, otherwise return (None, None) 

1364 def get_selection_indices(self): 

1365 try: 

1366 first = self.text.index("sel.first") 

1367 last = self.text.index("sel.last") 

1368 return first, last 

1369 except TclError: 

1370 return None, None 

1371 

1372 # Return the text widget's current view of what a tab stop means 

1373 # (equivalent width in spaces). 

1374 

1375 def get_tk_tabwidth(self): 

1376 current = self.text['tabs'] or TK_TABWIDTH_DEFAULT 

1377 return int(current) 

1378 

1379 # Set the text widget's current view of what a tab stop means. 

1380 

1381 def set_tk_tabwidth(self, newtabwidth): 

1382 text = self.text 

1383 if self.get_tk_tabwidth() != newtabwidth: 

1384 # Set text widget tab width 

1385 pixels = text.tk.call("font", "measure", text["font"], 

1386 "-displayof", text.master, 

1387 "n" * newtabwidth) 

1388 text.configure(tabs=pixels) 

1389 

1390### begin autoindent code ### (configuration was moved to beginning of class) 

1391 

1392 def set_indentation_params(self, is_py_src, guess=True): 

1393 if is_py_src and guess: 

1394 i = self.guess_indent() 

1395 if 2 <= i <= 8: 

1396 self.indentwidth = i 

1397 if self.indentwidth != self.tabwidth: 

1398 self.usetabs = False 

1399 self.set_tk_tabwidth(self.tabwidth) 

1400 

1401 def smart_backspace_event(self, event): 

1402 text = self.text 

1403 first, last = self.get_selection_indices() 

1404 if first and last: 

1405 text.delete(first, last) 

1406 text.mark_set("insert", first) 

1407 return "break" 

1408 # Delete whitespace left, until hitting a real char or closest 

1409 # preceding virtual tab stop. 

1410 chars = text.get("insert linestart", "insert") 

1411 if chars == '': 

1412 if text.compare("insert", ">", "1.0"): 

1413 # easy: delete preceding newline 

1414 text.delete("insert-1c") 

1415 else: 

1416 text.bell() # at start of buffer 

1417 return "break" 

1418 if chars[-1] not in " \t": 

1419 # easy: delete preceding real char 

1420 text.delete("insert-1c") 

1421 return "break" 

1422 # Ick. It may require *inserting* spaces if we back up over a 

1423 # tab character! This is written to be clear, not fast. 

1424 tabwidth = self.tabwidth 

1425 have = len(chars.expandtabs(tabwidth)) 

1426 assert have > 0 

1427 want = ((have - 1) // self.indentwidth) * self.indentwidth 

1428 # Debug prompt is multilined.... 

1429 ncharsdeleted = 0 

1430 while True: 

1431 chars = chars[:-1] 

1432 ncharsdeleted = ncharsdeleted + 1 

1433 have = len(chars.expandtabs(tabwidth)) 

1434 if have <= want or chars[-1] not in " \t": 

1435 break 

1436 text.undo_block_start() 

1437 text.delete("insert-%dc" % ncharsdeleted, "insert") 

1438 if have < want: 

1439 text.insert("insert", ' ' * (want - have), 

1440 self.user_input_insert_tags) 

1441 text.undo_block_stop() 

1442 return "break" 

1443 

1444 def smart_indent_event(self, event): 

1445 # if intraline selection: 

1446 # delete it 

1447 # elif multiline selection: 

1448 # do indent-region 

1449 # else: 

1450 # indent one level 

1451 text = self.text 

1452 first, last = self.get_selection_indices() 

1453 text.undo_block_start() 

1454 try: 

1455 if first and last: 

1456 if index2line(first) != index2line(last): 

1457 return self.fregion.indent_region_event(event) 

1458 text.delete(first, last) 

1459 text.mark_set("insert", first) 

1460 prefix = text.get("insert linestart", "insert") 

1461 raw, effective = get_line_indent(prefix, self.tabwidth) 

1462 if raw == len(prefix): 

1463 # only whitespace to the left 

1464 self.reindent_to(effective + self.indentwidth) 

1465 else: 

1466 # tab to the next 'stop' within or to right of line's text: 

1467 if self.usetabs: 

1468 pad = '\t' 

1469 else: 

1470 effective = len(prefix.expandtabs(self.tabwidth)) 

1471 n = self.indentwidth 

1472 pad = ' ' * (n - effective % n) 

1473 text.insert("insert", pad, self.user_input_insert_tags) 

1474 text.see("insert") 

1475 return "break" 

1476 finally: 

1477 text.undo_block_stop() 

1478 

1479 def newline_and_indent_event(self, event): 

1480 """Insert a newline and indentation after Enter keypress event. 

1481 

1482 Properly position the cursor on the new line based on information 

1483 from the current line. This takes into account if the current line 

1484 is a shell prompt, is empty, has selected text, contains a block 

1485 opener, contains a block closer, is a continuation line, or 

1486 is inside a string. 

1487 """ 

1488 text = self.text 

1489 first, last = self.get_selection_indices() 

1490 text.undo_block_start() 

1491 try: # Close undo block and expose new line in finally clause. 

1492 if first and last: 

1493 text.delete(first, last) 

1494 text.mark_set("insert", first) 

1495 line = text.get("insert linestart", "insert") 

1496 

1497 # Count leading whitespace for indent size. 

1498 i, n = 0, len(line) 

1499 while i < n and line[i] in " \t": 

1500 i += 1 

1501 if i == n: 

1502 # The cursor is in or at leading indentation in a continuation 

1503 # line; just inject an empty line at the start. 

1504 text.insert("insert linestart", '\n', 

1505 self.user_input_insert_tags) 

1506 return "break" 

1507 indent = line[:i] 

1508 

1509 # Strip whitespace before insert point unless it's in the prompt. 

1510 i = 0 

1511 while line and line[-1] in " \t": 

1512 line = line[:-1] 

1513 i += 1 

1514 if i: 

1515 text.delete("insert - %d chars" % i, "insert") 

1516 

1517 # Strip whitespace after insert point. 

1518 while text.get("insert") in " \t": 

1519 text.delete("insert") 

1520 

1521 # Insert new line. 

1522 text.insert("insert", '\n', self.user_input_insert_tags) 

1523 

1524 # Adjust indentation for continuations and block open/close. 

1525 # First need to find the last statement. 

1526 lno = index2line(text.index('insert')) 

1527 y = pyparse.Parser(self.indentwidth, self.tabwidth) 

1528 if not self.prompt_last_line: 

1529 for context in self.num_context_lines: 

1530 startat = max(lno - context, 1) 

1531 startatindex = repr(startat) + ".0" 

1532 rawtext = text.get(startatindex, "insert") 

1533 y.set_code(rawtext) 

1534 bod = y.find_good_parse_start( 

1535 self._build_char_in_string_func(startatindex)) 

1536 if bod is not None or startat == 1: 

1537 break 

1538 y.set_lo(bod or 0) 

1539 else: 

1540 r = text.tag_prevrange("console", "insert") 

1541 if r: 

1542 startatindex = r[1] 

1543 else: 

1544 startatindex = "1.0" 

1545 rawtext = text.get(startatindex, "insert") 

1546 y.set_code(rawtext) 

1547 y.set_lo(0) 

1548 

1549 c = y.get_continuation_type() 

1550 if c != pyparse.C_NONE: 

1551 # The current statement hasn't ended yet. 

1552 if c == pyparse.C_STRING_FIRST_LINE: 

1553 # After the first line of a string do not indent at all. 

1554 pass 

1555 elif c == pyparse.C_STRING_NEXT_LINES: 

1556 # Inside a string which started before this line; 

1557 # just mimic the current indent. 

1558 text.insert("insert", indent, self.user_input_insert_tags) 

1559 elif c == pyparse.C_BRACKET: 

1560 # Line up with the first (if any) element of the 

1561 # last open bracket structure; else indent one 

1562 # level beyond the indent of the line with the 

1563 # last open bracket. 

1564 self.reindent_to(y.compute_bracket_indent()) 

1565 elif c == pyparse.C_BACKSLASH: 

1566 # If more than one line in this statement already, just 

1567 # mimic the current indent; else if initial line 

1568 # has a start on an assignment stmt, indent to 

1569 # beyond leftmost =; else to beyond first chunk of 

1570 # non-whitespace on initial line. 

1571 if y.get_num_lines_in_stmt() > 1: 

1572 text.insert("insert", indent, 

1573 self.user_input_insert_tags) 

1574 else: 

1575 self.reindent_to(y.compute_backslash_indent()) 

1576 else: 

1577 assert 0, "bogus continuation type %r" % (c,) 

1578 return "break" 

1579 

1580 # This line starts a brand new statement; indent relative to 

1581 # indentation of initial line of closest preceding 

1582 # interesting statement. 

1583 indent = y.get_base_indent_string() 

1584 text.insert("insert", indent, self.user_input_insert_tags) 

1585 if y.is_block_opener(): 

1586 self.smart_indent_event(event) 

1587 elif indent and y.is_block_closer(): 

1588 self.smart_backspace_event(event) 

1589 return "break" 

1590 finally: 

1591 text.see("insert") 

1592 text.undo_block_stop() 

1593 

1594 # Our editwin provides an is_char_in_string function that works 

1595 # with a Tk text index, but PyParse only knows about offsets into 

1596 # a string. This builds a function for PyParse that accepts an 

1597 # offset. 

1598 

1599 def _build_char_in_string_func(self, startindex): 

1600 def inner(offset, _startindex=startindex, 

1601 _icis=self.is_char_in_string): 

1602 return _icis(_startindex + "+%dc" % offset) 

1603 return inner 

1604 

1605 # XXX this isn't bound to anything -- see tabwidth comments 

1606## def change_tabwidth_event(self, event): 

1607## new = self._asktabwidth() 

1608## if new != self.tabwidth: 

1609## self.tabwidth = new 

1610## self.set_indentation_params(0, guess=0) 

1611## return "break" 

1612 

1613 # Make string that displays as n leading blanks. 

1614 

1615 def _make_blanks(self, n): 

1616 if self.usetabs: 

1617 ntabs, nspaces = divmod(n, self.tabwidth) 

1618 return '\t' * ntabs + ' ' * nspaces 

1619 else: 

1620 return ' ' * n 

1621 

1622 # Delete from beginning of line to insert point, then reinsert 

1623 # column logical (meaning use tabs if appropriate) spaces. 

1624 

1625 def reindent_to(self, column): 

1626 text = self.text 

1627 text.undo_block_start() 

1628 if text.compare("insert linestart", "!=", "insert"): 

1629 text.delete("insert linestart", "insert") 

1630 if column: 

1631 text.insert("insert", self._make_blanks(column), 

1632 self.user_input_insert_tags) 

1633 text.undo_block_stop() 

1634 

1635 # Guess indentwidth from text content. 

1636 # Return guessed indentwidth. This should not be believed unless 

1637 # it's in a reasonable range (e.g., it will be 0 if no indented 

1638 # blocks are found). 

1639 

1640 def guess_indent(self): 

1641 opener, indented = IndentSearcher(self.text, self.tabwidth).run() 

1642 if opener and indented: 

1643 raw, indentsmall = get_line_indent(opener, self.tabwidth) 

1644 raw, indentlarge = get_line_indent(indented, self.tabwidth) 

1645 else: 

1646 indentsmall = indentlarge = 0 

1647 return indentlarge - indentsmall 

1648 

1649 def toggle_line_numbers_event(self, event=None): 

1650 if self.line_numbers is None: 

1651 return 

1652 

1653 if self.line_numbers.is_shown: 

1654 self.line_numbers.hide_sidebar() 

1655 menu_label = "Show" 

1656 else: 

1657 self.line_numbers.show_sidebar() 

1658 menu_label = "Hide" 

1659 self.update_menu_label(menu='options', index='*ine*umbers', 

1660 label=f'{menu_label} Line Numbers') 

1661 

1662# "line.col" -> line, as an int 

1663def index2line(index): 

1664 return int(float(index)) 

1665 

1666 

1667_line_indent_re = re.compile(r'[ \t]*') 

1668def get_line_indent(line, tabwidth): 

1669 """Return a line's indentation as (# chars, effective # of spaces). 

1670 

1671 The effective # of spaces is the length after properly "expanding" 

1672 the tabs into spaces, as done by str.expandtabs(tabwidth). 

1673 """ 

1674 m = _line_indent_re.match(line) 1bcd

1675 return m.end(), len(m.group().expandtabs(tabwidth)) 1bcd

1676 

1677 

1678class IndentSearcher: 

1679 

1680 # .run() chews over the Text widget, looking for a block opener 

1681 # and the stmt following it. Returns a pair, 

1682 # (line containing block opener, line containing stmt) 

1683 # Either or both may be None. 

1684 

1685 def __init__(self, text, tabwidth): 

1686 self.text = text 

1687 self.tabwidth = tabwidth 

1688 self.i = self.finished = 0 

1689 self.blkopenline = self.indentedline = None 

1690 

1691 def readline(self): 

1692 if self.finished: 

1693 return "" 

1694 i = self.i = self.i + 1 

1695 mark = repr(i) + ".0" 

1696 if self.text.compare(mark, ">=", "end"): 

1697 return "" 

1698 return self.text.get(mark, mark + " lineend+1c") 

1699 

1700 def tokeneater(self, type, token, start, end, line, 

1701 INDENT=tokenize.INDENT, 

1702 NAME=tokenize.NAME, 

1703 OPENERS=('class', 'def', 'for', 'if', 'try', 'while')): 

1704 if self.finished: 

1705 pass 

1706 elif type == NAME and token in OPENERS: 

1707 self.blkopenline = line 

1708 elif type == INDENT and self.blkopenline: 

1709 self.indentedline = line 

1710 self.finished = 1 

1711 

1712 def run(self): 

1713 save_tabsize = tokenize.tabsize 

1714 tokenize.tabsize = self.tabwidth 

1715 try: 

1716 try: 

1717 tokens = tokenize.generate_tokens(self.readline) 

1718 for token in tokens: 

1719 self.tokeneater(*token) 

1720 except (tokenize.TokenError, SyntaxError): 

1721 # since we cut off the tokenizer early, we can trigger 

1722 # spurious errors 

1723 pass 

1724 finally: 

1725 tokenize.tabsize = save_tabsize 

1726 return self.blkopenline, self.indentedline 

1727 

1728### end autoindent code ### 

1729 

1730def prepstr(s): 

1731 # Helper to extract the underscore from a string, e.g. 

1732 # prepstr("Co_py") returns (2, "Copy"). 

1733 i = s.find('_') 

1734 if i >= 0: 

1735 s = s[:i] + s[i+1:] 

1736 return i, s 

1737 

1738 

1739keynames = { 

1740 'bracketleft': '[', 

1741 'bracketright': ']', 

1742 'slash': '/', 

1743} 

1744 

1745def get_accelerator(keydefs, eventname): 

1746 keylist = keydefs.get(eventname) 

1747 # issue10940: temporary workaround to prevent hang with OS X Cocoa Tk 8.5 

1748 # if not keylist: 

1749 if (not keylist) or (macosx.isCocoaTk() and eventname in { 

1750 "<<open-module>>", 

1751 "<<goto-line>>", 

1752 "<<change-indentwidth>>"}): 

1753 return "" 

1754 s = keylist[0] 

1755 s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s) 

1756 s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s) 

1757 s = re.sub("Key-", "", s) 

1758 s = re.sub("Cancel","Ctrl-Break",s) # dscherer@cmu.edu 

1759 s = re.sub("Control-", "Ctrl-", s) 

1760 s = re.sub("-", "+", s) 

1761 s = re.sub("><", " ", s) 

1762 s = re.sub("<", "", s) 

1763 s = re.sub(">", "", s) 

1764 return s 

1765 

1766 

1767def fixwordbreaks(root): 

1768 # On Windows, tcl/tk breaks 'words' only on spaces, as in Command Prompt. 

1769 # We want Motif style everywhere. See #21474, msg218992 and followup. 

1770 tk = root.tk 

1771 tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded 

1772 tk.call('set', 'tcl_wordchars', r'\w') 

1773 tk.call('set', 'tcl_nonwordchars', r'\W') 

1774 

1775 

1776def _editor_window(parent): # htest # 

1777 # error if close master window first - timer event, after script 

1778 root = parent 

1779 fixwordbreaks(root) 

1780 if sys.argv[1:]: 

1781 filename = sys.argv[1] 

1782 else: 

1783 filename = None 

1784 macosx.setupApp(root, None) 

1785 edit = EditorWindow(root=root, filename=filename) 

1786 text = edit.text 

1787 text['height'] = 10 

1788 for i in range(20): 

1789 text.insert('insert', ' '*i + str(i) + '\n') 

1790 # text.bind("<<close-all-windows>>", edit.close_event) 

1791 # Does not stop error, neither does following 

1792 # edit.text.bind("<<close-window>>", edit.close_event) 

1793 

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

1795 from unittest import main 

1796 main('idlelib.idle_test.test_editor', verbosity=2, exit=False) 

1797 

1798 from idlelib.idle_test.htest import run 

1799 run(_editor_window)