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