1import importlib.abc 2import importlib.util 3import os 4import platform 5import re 6import string 7import sys 8import tokenize 9import traceback 10import webbrowser 11 12from tkinter import * 13from tkinter.font import Font 14from tkinter.ttk import Scrollbar 15from tkinter import simpledialog 16from tkinter import messagebox 17 18from idlelib.config import idleConf 19from idlelib import configdialog 20from idlelib import grep 21from idlelib import help 22from idlelib import help_about 23from idlelib import macosx 24from idlelib.multicall import MultiCallCreator 25from idlelib import pyparse 26from idlelib import query 27from idlelib import replace 28from idlelib import search 29from idlelib.tree import wheel_event 30from idlelib.util import py_extensions 31from idlelib import window 32 33# The default tab setting for a Text widget, in average-width characters. 34TK_TABWIDTH_DEFAULT = 8 35_py_version = ' (%s)' % platform.python_version() 36darwin = sys.platform == 'darwin' 37 38def _sphinx_version(): 39 "Format sys.version_info to produce the Sphinx version string used to install the chm docs" 40 major, minor, micro, level, serial = sys.version_info 41 # TODO remove unneeded function since .chm no longer installed 42 release = f'{major}{minor}' 43 release += f'{micro}' 44 if level == 'candidate': 45 release += f'rc{serial}' 46 elif level != 'final': 47 release += f'{level[0]}{serial}' 48 return release 49 50 51class EditorWindow: 52 from idlelib.percolator import Percolator 53 from idlelib.colorizer import ColorDelegator, color_config 54 from idlelib.undo import UndoDelegator 55 from idlelib.iomenu import IOBinding, encoding 56 from idlelib import mainmenu 57 from idlelib.statusbar import MultiStatusBar 58 from idlelib.autocomplete import AutoComplete 59 from idlelib.autoexpand import AutoExpand 60 from idlelib.calltip import Calltip 61 from idlelib.codecontext import CodeContext 62 from idlelib.sidebar import LineNumbers 63 from idlelib.format import FormatParagraph, FormatRegion, Indents, Rstrip 64 from idlelib.parenmatch import ParenMatch 65 from idlelib.zoomheight import ZoomHeight 66 67 filesystemencoding = sys.getfilesystemencoding() # for file names 68 help_url = None 69 70 allow_code_context = True 71 allow_line_numbers = True 72 user_input_insert_tags = None 73 74 def __init__(self, flist=None, filename=None, key=None, root=None): 75 # Delay import: runscript imports pyshell imports EditorWindow. 76 from idlelib.runscript import ScriptBinding 77 78 if EditorWindow.help_url is None: 79 dochome = os.path.join(sys.base_prefix, 'Doc', 'index.html') 80 if sys.platform.count('linux'): 81 # look for html docs in a couple of standard places 82 pyver = 'python-docs-' + '%s.%s.%s' % sys.version_info[:3] 83 if os.path.isdir('/var/www/html/python/'): # "python2" rpm 84 dochome = '/var/www/html/python/index.html' 85 else: 86 basepath = '/usr/share/doc/' # standard location 87 dochome = os.path.join(basepath, pyver, 88 'Doc', 'index.html') 89 elif sys.platform[:3] == 'win': 90 import winreg # Windows only, block only executed once. 91 docfile = '' 92 KEY = (rf"Software\Python\PythonCore\{sys.winver}" 93 r"\Help\Main Python Documentation") 94 try: 95 docfile = winreg.QueryValue(winreg.HKEY_CURRENT_USER, KEY) 96 except FileNotFoundError: 97 try: 98 docfile = winreg.QueryValue(winreg.HKEY_LOCAL_MACHINE, 99 KEY) 100 except FileNotFoundError: 101 pass 102 if os.path.isfile(docfile): 103 dochome = docfile 104 elif sys.platform == 'darwin': 105 # documentation may be stored inside a python framework 106 dochome = os.path.join(sys.base_prefix, 107 'Resources/English.lproj/Documentation/index.html') 108 dochome = os.path.normpath(dochome) 109 if os.path.isfile(dochome): 110 EditorWindow.help_url = dochome 111 if sys.platform == 'darwin': 112 # Safari requires real file:-URLs 113 EditorWindow.help_url = 'file://' + EditorWindow.help_url 114 else: 115 EditorWindow.help_url = ("https://docs.python.org/%d.%d/" 116 % sys.version_info[:2]) 117 self.flist = flist 118 root = root or flist.root 119 self.root = root 120 self.menubar = Menu(root) 121 self.top = top = window.ListedToplevel(root, menu=self.menubar) 122 if flist: 123 self.tkinter_vars = flist.vars 124 #self.top.instance_dict makes flist.inversedict available to 125 #configdialog.py so it can access all EditorWindow instances 126 self.top.instance_dict = flist.inversedict 127 else: 128 self.tkinter_vars = {} # keys: Tkinter event names 129 # values: Tkinter variable instances 130 self.top.instance_dict = {} 131 self.recent_files_path = idleConf.userdir and os.path.join( 132 idleConf.userdir, 'recent-files.lst') 133 134 self.prompt_last_line = '' # Override in PyShell 135 self.text_frame = text_frame = Frame(top) 136 self.vbar = vbar = Scrollbar(text_frame, name='vbar') 137 width = idleConf.GetOption('main', 'EditorWindow', 'width', type='int') 138 text_options = { 139 'name': 'text', 140 'padx': 5, 141 'wrap': 'none', 142 'highlightthickness': 0, 143 'width': width, 144 'tabstyle': 'wordprocessor', # new in 8.5 145 'height': idleConf.GetOption( 146 'main', 'EditorWindow', 'height', type='int'), 147 } 148 self.text = text = MultiCallCreator(Text)(text_frame, **text_options) 149 self.top.focused_widget = self.text 150 151 self.createmenubar() 152 self.apply_bindings() 153 154 self.top.protocol("WM_DELETE_WINDOW", self.close) 155 self.top.bind("<<close-window>>", self.close_event) 156 if macosx.isAquaTk(): 157 # Command-W on editor windows doesn't work without this. 158 text.bind('<<close-window>>', self.close_event) 159 # Some OS X systems have only one mouse button, so use 160 # control-click for popup context menus there. For two 161 # buttons, AquaTk defines <2> as the right button, not <3>. 162 text.bind("<Control-Button-1>",self.right_menu_event) 163 text.bind("<2>", self.right_menu_event) 164 else: 165 # Elsewhere, use right-click for popup menus. 166 text.bind("<3>",self.right_menu_event) 167 168 text.bind('<MouseWheel>', wheel_event) 169 if text._windowingsystem == 'x11': 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("<<smart-backspace>>",self.smart_backspace_event) 192 text.bind("<<newline-and-indent>>",self.newline_and_indent_event) 193 text.bind("<<smart-indent>>",self.smart_indent_event) 194 self.fregion = fregion = self.FormatRegion(self) 195 # self.fregion used in smart_indent_event to access indent_region. 196 text.bind("<<indent-region>>", fregion.indent_region_event) 197 text.bind("<<dedent-region>>", fregion.dedent_region_event) 198 text.bind("<<comment-region>>", fregion.comment_region_event) 199 text.bind("<<uncomment-region>>", fregion.uncomment_region_event) 200 text.bind("<<tabify-region>>", fregion.tabify_region_event) 201 text.bind("<<untabify-region>>", fregion.untabify_region_event) 202 indents = self.Indents(self) 203 text.bind("<<toggle-tabs>>", indents.toggle_tabs_event) 204 text.bind("<<change-indentwidth>>", indents.change_indentwidth_event) 205 text.bind("<Left>", self.move_at_edge_if_selection(0)) 206 text.bind("<Right>", self.move_at_edge_if_selection(1)) 207 text.bind("<<del-word-left>>", self.del_word_left) 208 text.bind("<<del-word-right>>", self.del_word_right) 209 text.bind("<<beginning-of-line>>", self.home_callback) 210 211 if flist: 212 flist.inversedict[self] = key 213 if key: 214 flist.dict[key] = self 215 text.bind("<<open-new-window>>", self.new_callback) 216 text.bind("<<close-all-windows>>", self.flist.close_all_callback) 217 text.bind("<<open-class-browser>>", self.open_module_browser) 218 text.bind("<<open-path-browser>>", self.open_path_browser) 219 text.bind("<<open-turtle-demo>>", self.open_turtle_demo) 220 221 self.set_status_bar() 222 text_frame.pack(side=LEFT, fill=BOTH, expand=1) 223 text_frame.rowconfigure(1, weight=1) 224 text_frame.columnconfigure(1, weight=1) 225 vbar['command'] = self.handle_yview 226 vbar.grid(row=1, column=2, sticky=NSEW) 227 text['yscrollcommand'] = vbar.set 228 text['font'] = idleConf.GetFont(self.root, 'main', 'EditorWindow') 229 text.grid(row=1, column=1, sticky=NSEW) 230 text.focus_set() 231 self.set_width() 232 233 # usetabs true -> literal tab characters are used by indent and 234 # dedent cmds, possibly mixed with spaces if 235 # indentwidth is not a multiple of tabwidth, 236 # which will cause Tabnanny to nag! 237 # false -> tab characters are converted to spaces by indent 238 # and dedent cmds, and ditto TAB keystrokes 239 # Although use-spaces=0 can be configured manually in config-main.def, 240 # configuration of tabs v. spaces is not supported in the configuration 241 # dialog. IDLE promotes the preferred Python indentation: use spaces! 242 usespaces = idleConf.GetOption('main', 'Indent', 243 'use-spaces', type='bool') 244 self.usetabs = not usespaces 245 246 # tabwidth is the display width of a literal tab character. 247 # CAUTION: telling Tk to use anything other than its default 248 # tab setting causes it to use an entirely different tabbing algorithm, 249 # treating tab stops as fixed distances from the left margin. 250 # Nobody expects this, so for now tabwidth should never be changed. 251 self.tabwidth = 8 # must remain 8 until Tk is fixed. 252 253 # indentwidth is the number of screen characters per indent level. 254 # The recommended Python indentation is four spaces. 255 self.indentwidth = self.tabwidth 256 self.set_notabs_indentwidth() 257 258 # Store the current value of the insertofftime now so we can restore 259 # it if needed. 260 if not hasattr(idleConf, 'blink_off_time'): 261 idleConf.blink_off_time = self.text['insertofftime'] 262 self.update_cursor_blink() 263 264 # When searching backwards for a reliable place to begin parsing, 265 # first start num_context_lines[0] lines back, then 266 # num_context_lines[1] lines back if that didn't work, and so on. 267 # The last value should be huge (larger than the # of lines in a 268 # conceivable file). 269 # Making the initial values larger slows things down more often. 270 self.num_context_lines = 50, 500, 5000000 271 self.per = per = self.Percolator(text) 272 self.undo = undo = self.UndoDelegator() 273 per.insertfilter(undo) 274 text.undo_block_start = undo.undo_block_start 275 text.undo_block_stop = undo.undo_block_stop 276 undo.set_saved_change_hook(self.saved_change_hook) 277 # IOBinding implements file I/O and printing functionality 278 self.io = io = self.IOBinding(self) 279 io.set_filename_change_hook(self.filename_change_hook) 280 self.good_load = False 281 self.set_indentation_params(False) 282 self.color = None # initialized below in self.ResetColorizer 283 self.code_context = None # optionally initialized later below 284 self.line_numbers = None # optionally initialized later below 285 if filename: 286 if os.path.exists(filename) and not os.path.isdir(filename): 287 if io.loadfile(filename): 288 self.good_load = True 289 is_py_src = self.ispythonsource(filename) 290 self.set_indentation_params(is_py_src) 291 else: 292 io.set_filename(filename) 293 self.good_load = True 294 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 451 """ Menu definitions and functions. 452 * self.menubar - the always visible horizontal menu bar. 453 * mainmenu.menudefs - a list of tuples, one for each menubar item. 454 Each tuple pairs a lower-case name and list of dropdown items. 455 Each item is a name, virtual event pair or None for separator. 456 * mainmenu.default_keydefs - maps events to keys. 457 * text.keydefs - same. 458 * cls.menu_specs - menubar name, titlecase display form pairs 459 with Alt-hotkey indicator. A subset of menudefs items. 460 * self.menudict - map menu name to dropdown menu. 461 * self.recent_files_menu - 2nd level cascade in the file cascade. 462 * self.wmenu_end - set in __init__ (purpose unclear). 463 464 createmenubar, postwindowsmenu, update_menu_label, update_menu_state, 465 ApplyKeybings (2nd part), reset_help_menu_entries, 466 _extra_help_callback, update_recent_files_list, 467 apply_bindings, fill_menus, (other functions?) 468 """ 469 470 menu_specs = [ 471 ("file", "_File"), 472 ("edit", "_Edit"), 473 ("format", "F_ormat"), 474 ("run", "_Run"), 475 ("options", "_Options"), 476 ("window", "_Window"), 477 ("help", "_Help"), 478 ] 479 480 def createmenubar(self): 481 """Populate the menu bar widget for the editor window. 482 483 Each option on the menubar is itself a cascade-type Menu widget 484 with the menubar as the parent. The names, labels, and menu 485 shortcuts for the menubar items are stored in menu_specs. Each 486 submenu is subsequently populated in fill_menus(), except for 487 'Recent Files' which is added to the File menu here. 488 489 Instance variables: 490 menubar: Menu widget containing first level menu items. 491 menudict: Dictionary of {menuname: Menu instance} items. The keys 492 represent the valid menu items for this window and may be a 493 subset of all the menudefs available. 494 recent_files_menu: Menu widget contained within the 'file' menudict. 495 """ 496 mbar = self.menubar 497 self.menudict = menudict = {} 498 for name, label in self.menu_specs: 499 underline, label = prepstr(label) 500 postcommand = getattr(self, f'{name}_menu_postcommand', None) 501 menudict[name] = menu = Menu(mbar, name=name, tearoff=0, 502 postcommand=postcommand) 503 mbar.add_cascade(label=label, menu=menu, underline=underline) 504 if macosx.isCarbonTk(): 505 # Insert the application menu 506 menudict['application'] = menu = Menu(mbar, name='apple', 507 tearoff=0) 508 mbar.add_cascade(label='IDLE', menu=menu) 509 self.fill_menus() 510 self.recent_files_menu = Menu(self.menubar, tearoff=0) 511 self.menudict['file'].insert_cascade(3, label='Recent Files', 512 underline=0, 513 menu=self.recent_files_menu) 514 self.base_helpmenu_length = self.menudict['help'].index(END) 515 self.reset_help_menu_entries() 516 517 def postwindowsmenu(self): 518 """Callback to register window. 519 520 Only called when Window menu exists. 521 """ 522 menu = self.menudict['window'] 523 end = menu.index("end") 524 if end is None: 525 end = -1 526 if end > self.wmenu_end: 527 menu.delete(self.wmenu_end+1, end) 528 window.add_windows_to_menu(menu) 529 530 def update_menu_label(self, menu, index, label): 531 "Update label for menu item at index." 532 menuitem = self.menudict[menu] 533 menuitem.entryconfig(index, label=label) 534 535 def update_menu_state(self, menu, index, state): 536 "Update state for menu item at index." 537 menuitem = self.menudict[menu] 538 menuitem.entryconfig(index, state=state) 539 540 def handle_yview(self, event, *args): 541 "Handle scrollbar." 542 if event == 'moveto': 543 fraction = float(args[0]) 544 lines = (round(self.getlineno('end') * fraction) - 545 self.getlineno('@0,0')) 546 event = 'scroll' 547 args = (lines, 'units') 548 self.text.yview(event, *args) 549 return 'break' 550 551 rmenu = None 552 553 def right_menu_event(self, event): 554 text = self.text 555 newdex = text.index(f'@{event.x},{event.y}') 556 try: 557 in_selection = (text.compare('sel.first', '<=', newdex) and 558 text.compare(newdex, '<=', 'sel.last')) 559 except TclError: 560 in_selection = False 561 if not in_selection: 562 text.tag_remove("sel", "1.0", "end") 563 text.mark_set("insert", newdex) 564 if not self.rmenu: 565 self.make_rmenu() 566 rmenu = self.rmenu 567 self.event = event 568 iswin = sys.platform[:3] == 'win' 569 if iswin: 570 text.config(cursor="arrow") 571 572 for item in self.rmenu_specs: 573 try: 574 label, eventname, verify_state = item 575 except ValueError: # see issue1207589 576 continue 577 578 if verify_state is None: 579 continue 580 state = getattr(self, verify_state)() 581 rmenu.entryconfigure(label, state=state) 582 583 rmenu.tk_popup(event.x_root, event.y_root) 584 if iswin: 585 self.text.config(cursor="ibeam") 586 return "break" 587 588 rmenu_specs = [ 589 # ("Label", "<<virtual-event>>", "statefuncname"), ... 590 ("Close", "<<close-window>>", None), # Example 591 ] 592 593 def make_rmenu(self): 594 rmenu = Menu(self.text, tearoff=0) 595 for item in self.rmenu_specs: 596 label, eventname = item[0], item[1] 597 if label is not None: 598 def command(text=self.text, eventname=eventname): 599 text.event_generate(eventname) 600 rmenu.add_command(label=label, command=command) 601 else: 602 rmenu.add_separator() 603 self.rmenu = rmenu 604 605 def rmenu_check_cut(self): 606 return self.rmenu_check_copy() 607 608 def rmenu_check_copy(self): 609 try: 610 indx = self.text.index('sel.first') 611 except TclError: 612 return 'disabled' 613 else: 614 return 'normal' if indx else 'disabled' 615 616 def rmenu_check_paste(self): 617 try: 618 self.text.tk.call('tk::GetSelection', self.text, 'CLIPBOARD') 619 except TclError: 620 return 'disabled' 621 else: 622 return 'normal' 623 624 def about_dialog(self, event=None): 625 "Handle Help 'About IDLE' event." 626 # Synchronize with macosx.overrideRootMenu.about_dialog. 627 help_about.AboutDialog(self.top) 628 return "break" 629 630 def config_dialog(self, event=None): 631 "Handle Options 'Configure IDLE' event." 632 # Synchronize with macosx.overrideRootMenu.config_dialog. 633 configdialog.ConfigDialog(self.top,'Settings') 634 return "break" 635 636 def help_dialog(self, event=None): 637 "Handle Help 'IDLE Help' event." 638 # Synchronize with macosx.overrideRootMenu.help_dialog. 639 if self.root: 640 parent = self.root 641 else: 642 parent = self.top 643 help.show_idlehelp(parent) 644 return "break" 645 646 def python_docs(self, event=None): 647 if sys.platform[:3] == 'win': 648 try: 649 os.startfile(self.help_url) 650 except OSError as why: 651 messagebox.showerror(title='Document Start Failure', 652 message=str(why), parent=self.text) 653 else: 654 webbrowser.open(self.help_url) 655 return "break" 656 657 def cut(self,event): 658 self.text.event_generate("<<Cut>>") 659 return "break" 660 661 def copy(self,event): 662 if not self.text.tag_ranges("sel"): 663 # There is no selection, so do nothing and maybe interrupt. 664 return None 665 self.text.event_generate("<<Copy>>") 666 return "break" 667 668 def paste(self,event): 669 self.text.event_generate("<<Paste>>") 670 self.text.see("insert") 671 return "break" 672 673 def select_all(self, event=None): 674 self.text.tag_add("sel", "1.0", "end-1c") 675 self.text.mark_set("insert", "1.0") 676 self.text.see("insert") 677 return "break" 678 679 def remove_selection(self, event=None): 680 self.text.tag_remove("sel", "1.0", "end") 681 self.text.see("insert") 682 return "break" 683 684 def move_at_edge_if_selection(self, edge_index): 685 """Cursor move begins at start or end of selection 686 687 When a left/right cursor key is pressed create and return to Tkinter a 688 function which causes a cursor move from the associated edge of the 689 selection. 690 691 """ 692 self_text_index = self.text.index 693 self_text_mark_set = self.text.mark_set 694 edges_table = ("sel.first+1c", "sel.last-1c") 695 def move_at_edge(event): 696 if (event.state & 5) == 0: # no shift(==1) or control(==4) pressed 697 try: 698 self_text_index("sel.first") 699 self_text_mark_set("insert", edges_table[edge_index]) 700 except TclError: 701 pass 702 return move_at_edge 703 704 def del_word_left(self, event): 705 self.text.event_generate('<Meta-Delete>') 706 return "break" 707 708 def del_word_right(self, event): 709 self.text.event_generate('<Meta-d>') 710 return "break" 711 712 def find_event(self, event): 713 search.find(self.text) 714 return "break" 715 716 def find_again_event(self, event): 717 search.find_again(self.text) 718 return "break" 719 720 def find_selection_event(self, event): 721 search.find_selection(self.text) 722 return "break" 723 724 def find_in_files_event(self, event): 725 grep.grep(self.text, self.io, self.flist) 726 return "break" 727 728 def replace_event(self, event): 729 replace.replace(self.text) 730 return "break" 731 732 def goto_line_event(self, event): 733 text = self.text 734 lineno = query.Goto( 735 text, "Go To Line", 736 "Enter a positive integer\n" 737 "('big' = end of file):" 738 ).result 739 if lineno is not None: 740 text.tag_remove("sel", "1.0", "end") 741 text.mark_set("insert", f'{lineno}.0') 742 text.see("insert") 743 self.set_line_and_column() 744 return "break" 745 746 def open_module(self): 747 """Get module name from user and open it. 748 749 Return module path or None for calls by open_module_browser 750 when latter is not invoked in named editor window. 751 """ 752 # XXX This, open_module_browser, and open_path_browser 753 # would fit better in iomenu.IOBinding. 754 try: 755 name = self.text.get("sel.first", "sel.last").strip() 756 except TclError: 757 name = '' 758 file_path = query.ModuleName( 759 self.text, "Open Module", 760 "Enter the name of a Python module\n" 761 "to search on sys.path and open:", 762 name).result 763 if file_path is not None: 764 if self.flist: 765 self.flist.open(file_path) 766 else: 767 self.io.loadfile(file_path) 768 return file_path 769 770 def open_module_event(self, event): 771 self.open_module() 772 return "break" 773 774 def open_module_browser(self, event=None): 775 filename = self.io.filename 776 if not (self.__class__.__name__ == 'PyShellEditorWindow' 777 and filename): 778 filename = self.open_module() 779 if filename is None: 780 return "break" 781 from idlelib import browser 782 browser.ModuleBrowser(self.root, filename) 783 return "break" 784 785 def open_path_browser(self, event=None): 786 from idlelib import pathbrowser 787 pathbrowser.PathBrowser(self.root) 788 return "break" 789 790 def open_turtle_demo(self, event = None): 791 import subprocess 792 793 cmd = [sys.executable, 794 '-c', 795 'from turtledemo.__main__ import main; main()'] 796 subprocess.Popen(cmd, shell=False) 797 return "break" 798 799 def gotoline(self, lineno): 800 if lineno is not None and lineno > 0: 801 self.text.mark_set("insert", "%d.0" % lineno) 802 self.text.tag_remove("sel", "1.0", "end") 803 self.text.tag_add("sel", "insert", "insert +1l") 804 self.center() 805 806 def ispythonsource(self, filename): 807 if not filename or os.path.isdir(filename): 808 return True 809 base, ext = os.path.splitext(os.path.basename(filename)) 810 if os.path.normcase(ext) in py_extensions: 811 return True 812 line = self.text.get('1.0', '1.0 lineend') 813 return line.startswith('#!') and 'python' in line 814 815 def close_hook(self): 816 if self.flist: 817 self.flist.unregister_maybe_terminate(self) 818 self.flist = None 819 820 def set_close_hook(self, close_hook): 821 self.close_hook = close_hook 822 823 def filename_change_hook(self): 824 if self.flist: 825 self.flist.filename_changed_edit(self) 826 self.saved_change_hook() 827 self.top.update_windowlist_registry(self) 828 self.ResetColorizer() 829 830 def _addcolorizer(self): 831 if self.color: 832 return 833 if self.ispythonsource(self.io.filename): 834 self.color = self.ColorDelegator() 835 # can add more colorizers here... 836 if self.color: 837 self.per.insertfilterafter(filter=self.color, after=self.undo) 838 839 def _rmcolorizer(self): 840 if not self.color: 841 return 842 self.color.removecolors() 843 self.per.removefilter(self.color) 844 self.color = None 845 846 def ResetColorizer(self): 847 "Update the color theme" 848 # Called from self.filename_change_hook and from configdialog.py 849 self._rmcolorizer() 850 self._addcolorizer() 851 EditorWindow.color_config(self.text) 852 853 if self.code_context is not None: 854 self.code_context.update_highlight_colors() 855 856 if self.line_numbers is not None: 857 self.line_numbers.update_colors() 858 859 IDENTCHARS = string.ascii_letters + string.digits + "_" 860 861 def colorize_syntax_error(self, text, pos): 862 text.tag_add("ERROR", pos) 863 char = text.get(pos) 864 if char and char in self.IDENTCHARS: 865 text.tag_add("ERROR", pos + " wordstart", pos) 866 if '\n' == text.get(pos): # error at line end 867 text.mark_set("insert", pos) 868 else: 869 text.mark_set("insert", pos + "+1c") 870 text.see(pos) 871 872 def update_cursor_blink(self): 873 "Update the cursor blink configuration." 874 cursorblink = idleConf.GetOption( 875 'main', 'EditorWindow', 'cursor-blink', type='bool') 876 if not cursorblink: 877 self.text['insertofftime'] = 0 878 else: 879 # Restore the original value 880 self.text['insertofftime'] = idleConf.blink_off_time 881 882 def ResetFont(self): 883 "Update the text widgets' font if it is changed" 884 # Called from configdialog.py 885 886 # Update the code context widget first, since its height affects 887 # the height of the text widget. This avoids double re-rendering. 888 if self.code_context is not None: 889 self.code_context.update_font() 890 # Next, update the line numbers widget, since its width affects 891 # the width of the text widget. 892 if self.line_numbers is not None: 893 self.line_numbers.update_font() 894 # Finally, update the main text widget. 895 new_font = idleConf.GetFont(self.root, 'main', 'EditorWindow') 896 self.text['font'] = new_font 897 self.set_width() 898 899 def RemoveKeybindings(self): 900 """Remove the virtual, configurable keybindings. 901 902 Leaves the default Tk Text keybindings. 903 """ 904 # Called from configdialog.deactivate_current_config. 905 self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet() 906 for event, keylist in keydefs.items(): 907 self.text.event_delete(event, *keylist) 908 for extensionName in self.get_standard_extension_names(): 909 xkeydefs = idleConf.GetExtensionBindings(extensionName) 910 if xkeydefs: 911 for event, keylist in xkeydefs.items(): 912 self.text.event_delete(event, *keylist) 913 914 def ApplyKeybindings(self): 915 """Apply the virtual, configurable keybindings. 916 917 Also update hotkeys to current keyset. 918 """ 919 # Called from configdialog.activate_config_changes. 920 self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet() 921 self.apply_bindings() 922 for extensionName in self.get_standard_extension_names(): 923 xkeydefs = idleConf.GetExtensionBindings(extensionName) 924 if xkeydefs: 925 self.apply_bindings(xkeydefs) 926 927 # Update menu accelerators. 928 menuEventDict = {} 929 for menu in self.mainmenu.menudefs: 930 menuEventDict[menu[0]] = {} 931 for item in menu[1]: 932 if item: 933 menuEventDict[menu[0]][prepstr(item[0])[1]] = item[1] 934 for menubarItem in self.menudict: 935 menu = self.menudict[menubarItem] 936 end = menu.index(END) 937 if end is None: 938 # Skip empty menus 939 continue 940 end += 1 941 for index in range(0, end): 942 if menu.type(index) == 'command': 943 accel = menu.entrycget(index, 'accelerator') 944 if accel: 945 itemName = menu.entrycget(index, 'label') 946 event = '' 947 if menubarItem in menuEventDict: 948 if itemName in menuEventDict[menubarItem]: 949 event = menuEventDict[menubarItem][itemName] 950 if event: 951 accel = get_accelerator(keydefs, event) 952 menu.entryconfig(index, accelerator=accel) 953 954 def set_notabs_indentwidth(self): 955 "Update the indentwidth if changed and not using tabs in this window" 956 # Called from configdialog.py 957 if not self.usetabs: 958 self.indentwidth = idleConf.GetOption('main', 'Indent','num-spaces', 959 type='int') 960 961 def reset_help_menu_entries(self): 962 """Update the additional help entries on the Help menu.""" 963 help_list = idleConf.GetAllExtraHelpSourcesList() 964 helpmenu = self.menudict['help'] 965 # First delete the extra help entries, if any. 966 helpmenu_length = helpmenu.index(END) 967 if helpmenu_length > self.base_helpmenu_length: 968 helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length) 969 # Then rebuild them. 970 if help_list: 971 helpmenu.add_separator() 972 for entry in help_list: 973 cmd = self._extra_help_callback(entry[1]) 974 helpmenu.add_command(label=entry[0], command=cmd) 975 # And update the menu dictionary. 976 self.menudict['help'] = helpmenu 977 978 def _extra_help_callback(self, resource): 979 """Return a callback that loads resource (file or web page).""" 980 def display_extra_help(helpfile=resource): 981 if not helpfile.startswith(('www', 'http')): 982 helpfile = os.path.normpath(helpfile) 983 if sys.platform[:3] == 'win': 984 try: 985 os.startfile(helpfile) 986 except OSError as why: 987 messagebox.showerror(title='Document Start Failure', 988 message=str(why), parent=self.text) 989 else: 990 webbrowser.open(helpfile) 991 return display_extra_help 992 993 def update_recent_files_list(self, new_file=None): 994 "Load and update the recent files list and menus" 995 # TODO: move to iomenu. 996 rf_list = [] 997 file_path = self.recent_files_path 998 if file_path and os.path.exists(file_path): 999 with open(file_path, 1000 encoding='utf_8', errors='replace') as rf_list_file: 1001 rf_list = rf_list_file.readlines() 1002 if new_file: 1003 new_file = os.path.abspath(new_file) + '\n' 1004 if new_file in rf_list: 1005 rf_list.remove(new_file) # move to top 1006 rf_list.insert(0, new_file) 1007 # clean and save the recent files list 1008 bad_paths = [] 1009 for path in rf_list: 1010 if '\0' in path or not os.path.exists(path[0:-1]): 1011 bad_paths.append(path) 1012 rf_list = [path for path in rf_list if path not in bad_paths] 1013 ulchars = "1234567890ABCDEFGHIJK" 1014 rf_list = rf_list[0:len(ulchars)] 1015 if file_path: 1016 try: 1017 with open(file_path, 'w', 1018 encoding='utf_8', errors='replace') as rf_file: 1019 rf_file.writelines(rf_list) 1020 except OSError as err: 1021 if not getattr(self.root, "recentfiles_message", False): 1022 self.root.recentfiles_message = True 1023 messagebox.showwarning(title='IDLE Warning', 1024 message="Cannot save Recent Files list to disk.\n" 1025 f" {err}\n" 1026 "Select OK to continue.", 1027 parent=self.text) 1028 # for each edit window instance, construct the recent files menu 1029 for instance in self.top.instance_dict: 1030 menu = instance.recent_files_menu 1031 menu.delete(0, END) # clear, and rebuild: 1032 for i, file_name in enumerate(rf_list): 1033 file_name = file_name.rstrip() # zap \n 1034 callback = instance.__recent_file_callback(file_name) 1035 menu.add_command(label=ulchars[i] + " " + file_name, 1036 command=callback, 1037 underline=0) 1038 1039 def __recent_file_callback(self, file_name): 1040 def open_recent_file(fn_closure=file_name): 1041 self.io.open(editFile=fn_closure) 1042 return open_recent_file 1043 1044 def saved_change_hook(self): 1045 short = self.short_title() 1046 long = self.long_title() 1047 if short and long and not macosx.isCocoaTk(): 1048 # Don't use both values on macOS because 1049 # that doesn't match platform conventions. 1050 title = short + " - " + long + _py_version 1051 elif short: 1052 title = short 1053 elif long: 1054 title = long 1055 else: 1056 title = "untitled" 1057 icon = short or long or title 1058 if not self.get_saved(): 1059 title = "*%s*" % title 1060 icon = "*%s" % icon 1061 self.top.wm_title(title) 1062 self.top.wm_iconname(icon) 1063 1064 if macosx.isCocoaTk(): 1065 # Add a proxy icon to the window title 1066 self.top.wm_attributes("-titlepath", long) 1067 1068 # Maintain the modification status for the window 1069 self.top.wm_attributes("-modified", not self.get_saved()) 1070 1071 def get_saved(self): 1072 return self.undo.get_saved() 1073 1074 def set_saved(self, flag): 1075 self.undo.set_saved(flag) 1076 1077 def reset_undo(self): 1078 self.undo.reset_undo() 1079 1080 def short_title(self): 1081 filename = self.io.filename 1082 return os.path.basename(filename) if filename else "untitled" 1083 1084 def long_title(self): 1085 return self.io.filename or "" 1086 1087 def center_insert_event(self, event): 1088 self.center() 1089 return "break" 1090 1091 def center(self, mark="insert"): 1092 text = self.text 1093 top, bot = self.getwindowlines() 1094 lineno = self.getlineno(mark) 1095 height = bot - top 1096 newtop = max(1, lineno - height//2) 1097 text.yview(float(newtop)) 1098 1099 def getwindowlines(self): 1100 text = self.text 1101 top = self.getlineno("@0,0") 1102 bot = self.getlineno("@0,65535") 1103 if top == bot and text.winfo_height() == 1: 1104 # Geometry manager hasn't run yet 1105 height = int(text['height']) 1106 bot = top + height - 1 1107 return top, bot 1108 1109 def getlineno(self, mark="insert"): 1110 text = self.text 1111 return int(float(text.index(mark))) 1112 1113 def get_geometry(self): 1114 "Return (width, height, x, y)" 1115 geom = self.top.wm_geometry() 1116 m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom) 1117 return list(map(int, m.groups())) 1118 1119 def close_event(self, event): 1120 self.close() 1121 return "break" 1122 1123 def maybesave(self): 1124 if self.io: 1125 if not self.get_saved(): 1126 if self.top.state()!='normal': 1127 self.top.deiconify() 1128 self.top.lower() 1129 self.top.lift() 1130 return self.io.maybesave() 1131 1132 def close(self): 1133 try: 1134 reply = self.maybesave() 1135 if str(reply) != "cancel": 1136 self._close() 1137 return reply 1138 except AttributeError: # bpo-35379: close called twice 1139 pass 1140 1141 def _close(self): 1142 if self.io.filename: 1143 self.update_recent_files_list(new_file=self.io.filename) 1144 window.unregister_callback(self.postwindowsmenu) 1145 self.unload_extensions() 1146 self.io.close() 1147 self.io = None 1148 self.undo = None 1149 if self.color: 1150 self.color.close() 1151 self.color = None 1152 self.text = None 1153 self.tkinter_vars = None 1154 self.per.close() 1155 self.per = None 1156 self.top.destroy() 1157 if self.close_hook: 1158 # unless override: unregister from flist, terminate if last window 1159 self.close_hook() 1160 1161 def load_extensions(self): 1162 self.extensions = {} 1163 self.load_standard_extensions() 1164 1165 def unload_extensions(self): 1166 for ins in list(self.extensions.values()): 1167 if hasattr(ins, "close"): 1168 ins.close() 1169 self.extensions = {} 1170 1171 def load_standard_extensions(self): 1172 for name in self.get_standard_extension_names(): 1173 try: 1174 self.load_extension(name) 1175 except: 1176 print("Failed to load extension", repr(name)) 1177 traceback.print_exc() 1178 1179 def get_standard_extension_names(self): 1180 return idleConf.GetExtensions(editor_only=True) 1181 1182 extfiles = { # Map built-in config-extension section names to file names. 1183 'ZzDummy': 'zzdummy', 1184 } 1185 1186 def load_extension(self, name): 1187 fname = self.extfiles.get(name, name) 1188 try: 1189 try: 1190 mod = importlib.import_module('.' + fname, package=__package__) 1191 except (ImportError, TypeError): 1192 mod = importlib.import_module(fname) 1193 except ImportError: 1194 print("\nFailed to import extension: ", name) 1195 raise 1196 cls = getattr(mod, name) 1197 keydefs = idleConf.GetExtensionBindings(name) 1198 if hasattr(cls, "menudefs"): 1199 self.fill_menus(cls.menudefs, keydefs) 1200 ins = cls(self) 1201 self.extensions[name] = ins 1202 if keydefs: 1203 self.apply_bindings(keydefs) 1204 for vevent in keydefs: 1205 methodname = vevent.replace("-", "_") 1206 while methodname[:1] == '<': 1207 methodname = methodname[1:] 1208 while methodname[-1:] == '>': 1209 methodname = methodname[:-1] 1210 methodname = methodname + "_event" 1211 if hasattr(ins, methodname): 1212 self.text.bind(vevent, getattr(ins, methodname)) 1213 1214 def apply_bindings(self, keydefs=None): 1215 """Add events with keys to self.text.""" 1216 if keydefs is None: 1217 keydefs = self.mainmenu.default_keydefs 1218 text = self.text 1219 text.keydefs = keydefs 1220 for event, keylist in keydefs.items(): 1221 if keylist: 1222 text.event_add(event, *keylist) 1223 1224 def fill_menus(self, menudefs=None, keydefs=None): 1225 """Fill in dropdown menus used by this window. 1226 1227 Items whose name begins with '!' become checkbuttons. 1228 Other names indicate commands. None becomes a separator. 1229 """ 1230 if menudefs is None: 1231 menudefs = self.mainmenu.menudefs 1232 if keydefs is None: 1233 keydefs = self.mainmenu.default_keydefs 1234 menudict = self.menudict 1235 text = self.text 1236 for mname, entrylist in menudefs: 1237 menu = menudict.get(mname) 1238 if not menu: 1239 continue 1240 for entry in entrylist: 1241 if entry is None: 1242 menu.add_separator() 1243 else: 1244 label, eventname = entry 1245 checkbutton = (label[:1] == '!') 1246 if checkbutton: 1247 label = label[1:] 1248 underline, label = prepstr(label) 1249 accelerator = get_accelerator(keydefs, eventname) 1250 def command(text=text, eventname=eventname): 1251 text.event_generate(eventname) 1252 if checkbutton: 1253 var = self.get_var_obj(eventname, BooleanVar) 1254 menu.add_checkbutton(label=label, underline=underline, 1255 command=command, accelerator=accelerator, 1256 variable=var) 1257 else: 1258 menu.add_command(label=label, underline=underline, 1259 command=command, 1260 accelerator=accelerator) 1261 1262 def getvar(self, name): 1263 var = self.get_var_obj(name) 1264 if var: 1265 value = var.get() 1266 return value 1267 else: 1268 raise NameError(name) 1269 1270 def setvar(self, name, value, vartype=None): 1271 var = self.get_var_obj(name, vartype) 1272 if var: 1273 var.set(value) 1274 else: 1275 raise NameError(name) 1276 1277 def get_var_obj(self, eventname, vartype=None): 1278 """Return a tkinter variable instance for the event. 1279 """ 1280 var = self.tkinter_vars.get(eventname) 1281 if not var and vartype: 1282 # Create a Tkinter variable object. 1283 self.tkinter_vars[eventname] = var = vartype(self.text) 1284 return var 1285 1286 # Tk implementations of "virtual text methods" -- each platform 1287 # reusing IDLE's support code needs to define these for its GUI's 1288 # flavor of widget. 1289 1290 # Is character at text_index in a Python string? Return 0 for 1291 # "guaranteed no", true for anything else. This info is expensive 1292 # to compute ab initio, but is probably already known by the 1293 # platform's colorizer. 1294 1295 def is_char_in_string(self, text_index): 1296 if self.color: 1297 # Return true iff colorizer hasn't (re)gotten this far 1298 # yet, or the character is tagged as being in a string 1299 return self.text.tag_prevrange("TODO", text_index) or \ 1300 "STRING" in self.text.tag_names(text_index) 1301 else: 1302 # The colorizer is missing: assume the worst 1303 return 1 1304 1305 # If a selection is defined in the text widget, return (start, 1306 # end) as Tkinter text indices, otherwise return (None, None) 1307 def get_selection_indices(self): 1308 try: 1309 first = self.text.index("sel.first") 1310 last = self.text.index("sel.last") 1311 return first, last 1312 except TclError: 1313 return None, None 1314 1315 # Return the text widget's current view of what a tab stop means 1316 # (equivalent width in spaces). 1317 1318 def get_tk_tabwidth(self): 1319 current = self.text['tabs'] or TK_TABWIDTH_DEFAULT 1320 return int(current) 1321 1322 # Set the text widget's current view of what a tab stop means. 1323 1324 def set_tk_tabwidth(self, newtabwidth): 1325 text = self.text 1326 if self.get_tk_tabwidth() != newtabwidth: 1327 # Set text widget tab width 1328 pixels = text.tk.call("font", "measure", text["font"], 1329 "-displayof", text.master, 1330 "n" * newtabwidth) 1331 text.configure(tabs=pixels) 1332 1333### begin autoindent code ### (configuration was moved to beginning of class) 1334 1335 def set_indentation_params(self, is_py_src, guess=True): 1336 if is_py_src and guess: 1337 i = self.guess_indent() 1338 if 2 <= i <= 8: 1339 self.indentwidth = i 1340 if self.indentwidth != self.tabwidth: 1341 self.usetabs = False 1342 self.set_tk_tabwidth(self.tabwidth) 1343 1344 def smart_backspace_event(self, event): 1345 text = self.text 1346 first, last = self.get_selection_indices() 1347 if first and last: 1348 text.delete(first, last) 1349 text.mark_set("insert", first) 1350 return "break" 1351 # Delete whitespace left, until hitting a real char or closest 1352 # preceding virtual tab stop. 1353 chars = text.get("insert linestart", "insert") 1354 if chars == '': 1355 if text.compare("insert", ">", "1.0"): 1356 # easy: delete preceding newline 1357 text.delete("insert-1c") 1358 else: 1359 text.bell() # at start of buffer 1360 return "break" 1361 if chars[-1] not in " \t": 1362 # easy: delete preceding real char 1363 text.delete("insert-1c") 1364 return "break" 1365 # Ick. It may require *inserting* spaces if we back up over a 1366 # tab character! This is written to be clear, not fast. 1367 tabwidth = self.tabwidth 1368 have = len(chars.expandtabs(tabwidth)) 1369 assert have > 0 1370 want = ((have - 1) // self.indentwidth) * self.indentwidth 1371 # Debug prompt is multilined.... 1372 ncharsdeleted = 0 1373 while True: 1374 chars = chars[:-1] 1375 ncharsdeleted = ncharsdeleted + 1 1376 have = len(chars.expandtabs(tabwidth)) 1377 if have <= want or chars[-1] not in " \t": 1378 break 1379 text.undo_block_start() 1380 text.delete("insert-%dc" % ncharsdeleted, "insert") 1381 if have < want: 1382 text.insert("insert", ' ' * (want - have), 1383 self.user_input_insert_tags) 1384 text.undo_block_stop() 1385 return "break" 1386 1387 def smart_indent_event(self, event): 1388 # if intraline selection: 1389 # delete it 1390 # elif multiline selection: 1391 # do indent-region 1392 # else: 1393 # indent one level 1394 text = self.text 1395 first, last = self.get_selection_indices() 1396 text.undo_block_start() 1397 try: 1398 if first and last: 1399 if index2line(first) != index2line(last): 1400 return self.fregion.indent_region_event(event) 1401 text.delete(first, last) 1402 text.mark_set("insert", first) 1403 prefix = text.get("insert linestart", "insert") 1404 raw, effective = get_line_indent(prefix, self.tabwidth) 1405 if raw == len(prefix): 1406 # only whitespace to the left 1407 self.reindent_to(effective + self.indentwidth) 1408 else: 1409 # tab to the next 'stop' within or to right of line's text: 1410 if self.usetabs: 1411 pad = '\t' 1412 else: 1413 effective = len(prefix.expandtabs(self.tabwidth)) 1414 n = self.indentwidth 1415 pad = ' ' * (n - effective % n) 1416 text.insert("insert", pad, self.user_input_insert_tags) 1417 text.see("insert") 1418 return "break" 1419 finally: 1420 text.undo_block_stop() 1421 1422 def newline_and_indent_event(self, event): 1423 """Insert a newline and indentation after Enter keypress event. 1424 1425 Properly position the cursor on the new line based on information 1426 from the current line. This takes into account if the current line 1427 is a shell prompt, is empty, has selected text, contains a block 1428 opener, contains a block closer, is a continuation line, or 1429 is inside a string. 1430 """ 1431 text = self.text 1432 first, last = self.get_selection_indices() 1433 text.undo_block_start() 1434 try: # Close undo block and expose new line in finally clause. 1435 if first and last: 1436 text.delete(first, last) 1437 text.mark_set("insert", first) 1438 line = text.get("insert linestart", "insert") 1439 1440 # Count leading whitespace for indent size. 1441 i, n = 0, len(line) 1442 while i < n and line[i] in " \t": 1443 i += 1 1444 if i == n: 1445 # The cursor is in or at leading indentation in a continuation 1446 # line; just inject an empty line at the start. 1447 text.insert("insert linestart", '\n', 1448 self.user_input_insert_tags) 1449 return "break" 1450 indent = line[:i] 1451 1452 # Strip whitespace before insert point unless it's in the prompt. 1453 i = 0 1454 while line and line[-1] in " \t": 1455 line = line[:-1] 1456 i += 1 1457 if i: 1458 text.delete("insert - %d chars" % i, "insert") 1459 1460 # Strip whitespace after insert point. 1461 while text.get("insert") in " \t": 1462 text.delete("insert") 1463 1464 # Insert new line. 1465 text.insert("insert", '\n', self.user_input_insert_tags) 1466 1467 # Adjust indentation for continuations and block open/close. 1468 # First need to find the last statement. 1469 lno = index2line(text.index('insert')) 1470 y = pyparse.Parser(self.indentwidth, self.tabwidth) 1471 if not self.prompt_last_line: 1472 for context in self.num_context_lines: 1473 startat = max(lno - context, 1) 1474 startatindex = repr(startat) + ".0" 1475 rawtext = text.get(startatindex, "insert") 1476 y.set_code(rawtext) 1477 bod = y.find_good_parse_start( 1478 self._build_char_in_string_func(startatindex)) 1479 if bod is not None or startat == 1: 1480 break 1481 y.set_lo(bod or 0) 1482 else: 1483 r = text.tag_prevrange("console", "insert") 1484 if r: 1485 startatindex = r[1] 1486 else: 1487 startatindex = "1.0" 1488 rawtext = text.get(startatindex, "insert") 1489 y.set_code(rawtext) 1490 y.set_lo(0) 1491 1492 c = y.get_continuation_type() 1493 if c != pyparse.C_NONE: 1494 # The current statement hasn't ended yet. 1495 if c == pyparse.C_STRING_FIRST_LINE: 1496 # After the first line of a string do not indent at all. 1497 pass 1498 elif c == pyparse.C_STRING_NEXT_LINES: 1499 # Inside a string which started before this line; 1500 # just mimic the current indent. 1501 text.insert("insert", indent, self.user_input_insert_tags) 1502 elif c == pyparse.C_BRACKET: 1503 # Line up with the first (if any) element of the 1504 # last open bracket structure; else indent one 1505 # level beyond the indent of the line with the 1506 # last open bracket. 1507 self.reindent_to(y.compute_bracket_indent()) 1508 elif c == pyparse.C_BACKSLASH: 1509 # If more than one line in this statement already, just 1510 # mimic the current indent; else if initial line 1511 # has a start on an assignment stmt, indent to 1512 # beyond leftmost =; else to beyond first chunk of 1513 # non-whitespace on initial line. 1514 if y.get_num_lines_in_stmt() > 1: 1515 text.insert("insert", indent, 1516 self.user_input_insert_tags) 1517 else: 1518 self.reindent_to(y.compute_backslash_indent()) 1519 else: 1520 assert 0, f"bogus continuation type {c!r}" 1521 return "break" 1522 1523 # This line starts a brand new statement; indent relative to 1524 # indentation of initial line of closest preceding 1525 # interesting statement. 1526 indent = y.get_base_indent_string() 1527 text.insert("insert", indent, self.user_input_insert_tags) 1528 if y.is_block_opener(): 1529 self.smart_indent_event(event) 1530 elif indent and y.is_block_closer(): 1531 self.smart_backspace_event(event) 1532 return "break" 1533 finally: 1534 text.see("insert") 1535 text.undo_block_stop() 1536 1537 # Our editwin provides an is_char_in_string function that works 1538 # with a Tk text index, but PyParse only knows about offsets into 1539 # a string. This builds a function for PyParse that accepts an 1540 # offset. 1541 1542 def _build_char_in_string_func(self, startindex): 1543 def inner(offset, _startindex=startindex, 1544 _icis=self.is_char_in_string): 1545 return _icis(_startindex + "+%dc" % offset) 1546 return inner 1547 1548 # XXX this isn't bound to anything -- see tabwidth comments 1549## def change_tabwidth_event(self, event): 1550## new = self._asktabwidth() 1551## if new != self.tabwidth: 1552## self.tabwidth = new 1553## self.set_indentation_params(0, guess=0) 1554## return "break" 1555 1556 # Make string that displays as n leading blanks. 1557 1558 def _make_blanks(self, n): 1559 if self.usetabs: 1560 ntabs, nspaces = divmod(n, self.tabwidth) 1561 return '\t' * ntabs + ' ' * nspaces 1562 else: 1563 return ' ' * n 1564 1565 # Delete from beginning of line to insert point, then reinsert 1566 # column logical (meaning use tabs if appropriate) spaces. 1567 1568 def reindent_to(self, column): 1569 text = self.text 1570 text.undo_block_start() 1571 if text.compare("insert linestart", "!=", "insert"): 1572 text.delete("insert linestart", "insert") 1573 if column: 1574 text.insert("insert", self._make_blanks(column), 1575 self.user_input_insert_tags) 1576 text.undo_block_stop() 1577 1578 # Guess indentwidth from text content. 1579 # Return guessed indentwidth. This should not be believed unless 1580 # it's in a reasonable range (e.g., it will be 0 if no indented 1581 # blocks are found). 1582 1583 def guess_indent(self): 1584 opener, indented = IndentSearcher(self.text).run() 1585 if opener and indented: 1586 raw, indentsmall = get_line_indent(opener, self.tabwidth) 1587 raw, indentlarge = get_line_indent(indented, self.tabwidth) 1588 else: 1589 indentsmall = indentlarge = 0 1590 return indentlarge - indentsmall 1591 1592 def toggle_line_numbers_event(self, event=None): 1593 if self.line_numbers is None: 1594 return 1595 1596 if self.line_numbers.is_shown: 1597 self.line_numbers.hide_sidebar() 1598 menu_label = "Show" 1599 else: 1600 self.line_numbers.show_sidebar() 1601 menu_label = "Hide" 1602 self.update_menu_label(menu='options', index='*ine*umbers', 1603 label=f'{menu_label} Line Numbers') 1604 1605# "line.col" -> line, as an int 1606def index2line(index): 1607 return int(float(index)) 1608 1609 1610_line_indent_re = re.compile(r'[ \t]*') 1611def get_line_indent(line, tabwidth): 1612 """Return a line's indentation as (# chars, effective # of spaces). 1613 1614 The effective # of spaces is the length after properly "expanding" 1615 the tabs into spaces, as done by str.expandtabs(tabwidth). 1616 """ 1617 m = _line_indent_re.match(line) 1618 return m.end(), len(m.group().expandtabs(tabwidth)) 1619 1620 1621class IndentSearcher: 1622 "Manage initial indent guess, returned by run method." 1623 1624 def __init__(self, text): 1625 self.text = text 1626 self.i = self.finished = 0 1627 self.blkopenline = self.indentedline = None 1628 1629 def readline(self): 1630 if self.finished: 1631 return "" 1632 i = self.i = self.i + 1 1633 mark = repr(i) + ".0" 1634 if self.text.compare(mark, ">=", "end"): 1635 return "" 1636 return self.text.get(mark, mark + " lineend+1c") 1637 1638 def tokeneater(self, type, token, start, end, line, 1639 INDENT=tokenize.INDENT, 1640 NAME=tokenize.NAME, 1641 OPENERS=('class', 'def', 'for', 'if', 'match', 'try', 1642 'while', 'with')): 1643 if self.finished: 1644 pass 1645 elif type == NAME and token in OPENERS: 1646 self.blkopenline = line 1647 elif type == INDENT and self.blkopenline: 1648 self.indentedline = line 1649 self.finished = 1 1650 1651 def run(self): 1652 """Return 2 lines containing block opener and and indent. 1653 1654 Either the indent line or both may be None. 1655 """ 1656 try: 1657 tokens = tokenize.generate_tokens(self.readline) 1658 for token in tokens: 1659 self.tokeneater(*token) 1660 except (tokenize.TokenError, SyntaxError): 1661 # Stopping the tokenizer early can trigger spurious errors. 1662 pass 1663 return self.blkopenline, self.indentedline 1664 1665### end autoindent code ### 1666 1667 1668def prepstr(s): 1669 """Extract the underscore from a string. 1670 1671 For example, prepstr("Co_py") returns (2, "Copy"). 1672 1673 Args: 1674 s: String with underscore. 1675 1676 Returns: 1677 Tuple of (position of underscore, string without underscore). 1678 """ 1679 i = s.find('_') 1680 if i >= 0: 1681 s = s[:i] + s[i+1:] 1682 return i, s 1683 1684 1685keynames = { 1686 'bracketleft': '[', 1687 'bracketright': ']', 1688 'slash': '/', 1689} 1690 1691def get_accelerator(keydefs, eventname): 1692 """Return a formatted string for the keybinding of an event. 1693 1694 Convert the first keybinding for a given event to a form that 1695 can be displayed as an accelerator on the menu. 1696 1697 Args: 1698 keydefs: Dictionary of valid events to keybindings. 1699 eventname: Event to retrieve keybinding for. 1700 1701 Returns: 1702 Formatted string of the keybinding. 1703 """ 1704 keylist = keydefs.get(eventname) 1705 # issue10940: temporary workaround to prevent hang with OS X Cocoa Tk 8.5 1706 # if not keylist: 1707 if (not keylist) or (macosx.isCocoaTk() and eventname in { 1708 "<<open-module>>", 1709 "<<goto-line>>", 1710 "<<change-indentwidth>>"}): 1711 return "" 1712 s = keylist[0] 1713 # Convert strings of the form -singlelowercase to -singleuppercase. 1714 s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s) 1715 # Convert certain keynames to their symbol. 1716 s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s) 1717 # Remove Key- from string. 1718 s = re.sub("Key-", "", s) 1719 # Convert Cancel to Ctrl-Break. 1720 s = re.sub("Cancel", "Ctrl-Break", s) # dscherer@cmu.edu 1721 # Convert Control to Ctrl-. 1722 s = re.sub("Control-", "Ctrl-", s) 1723 # Change - to +. 1724 s = re.sub("-", "+", s) 1725 # Change >< to space. 1726 s = re.sub("><", " ", s) 1727 # Remove <. 1728 s = re.sub("<", "", s) 1729 # Remove >. 1730 s = re.sub(">", "", s) 1731 return s 1732 1733 1734def fixwordbreaks(root): 1735 # On Windows, tcl/tk breaks 'words' only on spaces, as in Command Prompt. 1736 # We want Motif style everywhere. See #21474, msg218992 and followup. 1737 tk = root.tk 1738 tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded 1739 tk.call('set', 'tcl_wordchars', r'\w') 1740 tk.call('set', 'tcl_nonwordchars', r'\W') 1741 1742 1743def _editor_window(parent): # htest # 1744 # error if close master window first - timer event, after script 1745 root = parent 1746 fixwordbreaks(root) 1747 if sys.argv[1:]: 1748 filename = sys.argv[1] 1749 else: 1750 filename = None 1751 macosx.setupApp(root, None) 1752 edit = EditorWindow(root=root, filename=filename) 1753 text = edit.text 1754 text['height'] = 10 1755 for i in range(20): 1756 text.insert('insert', ' '*i + str(i) + '\n') 1757 # text.bind("<<close-all-windows>>", edit.close_event) 1758 # Does not stop error, neither does following 1759 # edit.text.bind("<<close-window>>", edit.close_event) 1760 1761 1762if __name__ == '__main__': 1763 from unittest import main 1764 main('idlelib.idle_test.test_editor', verbosity=2, exit=False) 1765 1766 from idlelib.idle_test.htest import run 1767 run(_editor_window) 1768