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