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.zoomheight import ZoomHeight 64 65 filesystemencoding = sys.getfilesystemencoding() # for file names 66 help_url = None 67 68 allow_code_context = True 69 allow_line_numbers = True 70 user_input_insert_tags = None 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, self.user_input_insert_tags) 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.insertfilterafter(filter=self.color, after=self.undo) 788 789 def _rmcolorizer(self): 790 if not self.color: 791 return 792 self.color.removecolors() 793 self.per.removefilter(self.color) 794 self.color = None 795 796 def ResetColorizer(self): 797 "Update the color theme" 798 # Called from self.filename_change_hook and from configdialog.py 799 self._rmcolorizer() 800 self._addcolorizer() 801 EditorWindow.color_config(self.text) 802 803 if self.code_context is not None: 804 self.code_context.update_highlight_colors() 805 806 if self.line_numbers is not None: 807 self.line_numbers.update_colors() 808 809 IDENTCHARS = string.ascii_letters + string.digits + "_" 810 811 def colorize_syntax_error(self, text, pos): 812 text.tag_add("ERROR", pos) 813 char = text.get(pos) 814 if char and char in self.IDENTCHARS: 815 text.tag_add("ERROR", pos + " wordstart", pos) 816 if '\n' == text.get(pos): # error at line end 817 text.mark_set("insert", pos) 818 else: 819 text.mark_set("insert", pos + "+1c") 820 text.see(pos) 821 822 def update_cursor_blink(self): 823 "Update the cursor blink configuration." 824 cursorblink = idleConf.GetOption( 825 'main', 'EditorWindow', 'cursor-blink', type='bool') 826 if not cursorblink: 827 self.text['insertofftime'] = 0 828 else: 829 # Restore the original value 830 self.text['insertofftime'] = idleConf.blink_off_time 831 832 def ResetFont(self): 833 "Update the text widgets' font if it is changed" 834 # Called from configdialog.py 835 836 # Update the code context widget first, since its height affects 837 # the height of the text widget. This avoids double re-rendering. 838 if self.code_context is not None: 839 self.code_context.update_font() 840 # Next, update the line numbers widget, since its width affects 841 # the width of the text widget. 842 if self.line_numbers is not None: 843 self.line_numbers.update_font() 844 # Finally, update the main text widget. 845 new_font = idleConf.GetFont(self.root, 'main', 'EditorWindow') 846 self.text['font'] = new_font 847 self.set_width() 848 849 def RemoveKeybindings(self): 850 "Remove the keybindings before they are changed." 851 # Called from configdialog.py 852 self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet() 853 for event, keylist in keydefs.items(): 854 self.text.event_delete(event, *keylist) 855 for extensionName in self.get_standard_extension_names(): 856 xkeydefs = idleConf.GetExtensionBindings(extensionName) 857 if xkeydefs: 858 for event, keylist in xkeydefs.items(): 859 self.text.event_delete(event, *keylist) 860 861 def ApplyKeybindings(self): 862 "Update the keybindings after they are changed" 863 # Called from configdialog.py 864 self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet() 865 self.apply_bindings() 866 for extensionName in self.get_standard_extension_names(): 867 xkeydefs = idleConf.GetExtensionBindings(extensionName) 868 if xkeydefs: 869 self.apply_bindings(xkeydefs) 870 #update menu accelerators 871 menuEventDict = {} 872 for menu in self.mainmenu.menudefs: 873 menuEventDict[menu[0]] = {} 874 for item in menu[1]: 875 if item: 876 menuEventDict[menu[0]][prepstr(item[0])[1]] = item[1] 877 for menubarItem in self.menudict: 878 menu = self.menudict[menubarItem] 879 end = menu.index(END) 880 if end is None: 881 # Skip empty menus 882 continue 883 end += 1 884 for index in range(0, end): 885 if menu.type(index) == 'command': 886 accel = menu.entrycget(index, 'accelerator') 887 if accel: 888 itemName = menu.entrycget(index, 'label') 889 event = '' 890 if menubarItem in menuEventDict: 891 if itemName in menuEventDict[menubarItem]: 892 event = menuEventDict[menubarItem][itemName] 893 if event: 894 accel = get_accelerator(keydefs, event) 895 menu.entryconfig(index, accelerator=accel) 896 897 def set_notabs_indentwidth(self): 898 "Update the indentwidth if changed and not using tabs in this window" 899 # Called from configdialog.py 900 if not self.usetabs: 901 self.indentwidth = idleConf.GetOption('main', 'Indent','num-spaces', 902 type='int') 903 904 def reset_help_menu_entries(self): 905 "Update the additional help entries on the Help menu" 906 help_list = idleConf.GetAllExtraHelpSourcesList() 907 helpmenu = self.menudict['help'] 908 # first delete the extra help entries, if any 909 helpmenu_length = helpmenu.index(END) 910 if helpmenu_length > self.base_helpmenu_length: 911 helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length) 912 # then rebuild them 913 if help_list: 914 helpmenu.add_separator() 915 for entry in help_list: 916 cmd = self.__extra_help_callback(entry[1]) 917 helpmenu.add_command(label=entry[0], command=cmd) 918 # and update the menu dictionary 919 self.menudict['help'] = helpmenu 920 921 def __extra_help_callback(self, helpfile): 922 "Create a callback with the helpfile value frozen at definition time" 923 def display_extra_help(helpfile=helpfile): 924 if not helpfile.startswith(('www', 'http')): 925 helpfile = os.path.normpath(helpfile) 926 if sys.platform[:3] == 'win': 927 try: 928 os.startfile(helpfile) 929 except OSError as why: 930 messagebox.showerror(title='Document Start Failure', 931 message=str(why), parent=self.text) 932 else: 933 webbrowser.open(helpfile) 934 return display_extra_help 935 936 def update_recent_files_list(self, new_file=None): 937 "Load and update the recent files list and menus" 938 # TODO: move to iomenu. 939 rf_list = [] 940 file_path = self.recent_files_path 941 if file_path and os.path.exists(file_path): 942 with open(file_path, 'r', 943 encoding='utf_8', errors='replace') as rf_list_file: 944 rf_list = rf_list_file.readlines() 945 if new_file: 946 new_file = os.path.abspath(new_file) + '\n' 947 if new_file in rf_list: 948 rf_list.remove(new_file) # move to top 949 rf_list.insert(0, new_file) 950 # clean and save the recent files list 951 bad_paths = [] 952 for path in rf_list: 953 if '\0' in path or not os.path.exists(path[0:-1]): 954 bad_paths.append(path) 955 rf_list = [path for path in rf_list if path not in bad_paths] 956 ulchars = "1234567890ABCDEFGHIJK" 957 rf_list = rf_list[0:len(ulchars)] 958 if file_path: 959 try: 960 with open(file_path, 'w', 961 encoding='utf_8', errors='replace') as rf_file: 962 rf_file.writelines(rf_list) 963 except OSError as err: 964 if not getattr(self.root, "recentfiles_message", False): 965 self.root.recentfiles_message = True 966 messagebox.showwarning(title='IDLE Warning', 967 message="Cannot save Recent Files list to disk.\n" 968 f" {err}\n" 969 "Select OK to continue.", 970 parent=self.text) 971 # for each edit window instance, construct the recent files menu 972 for instance in self.top.instance_dict: 973 menu = instance.recent_files_menu 974 menu.delete(0, END) # clear, and rebuild: 975 for i, file_name in enumerate(rf_list): 976 file_name = file_name.rstrip() # zap \n 977 callback = instance.__recent_file_callback(file_name) 978 menu.add_command(label=ulchars[i] + " " + file_name, 979 command=callback, 980 underline=0) 981 982 def __recent_file_callback(self, file_name): 983 def open_recent_file(fn_closure=file_name): 984 self.io.open(editFile=fn_closure) 985 return open_recent_file 986 987 def saved_change_hook(self): 988 short = self.short_title() 989 long = self.long_title() 990 if short and long: 991 title = short + " - " + long + _py_version 992 elif short: 993 title = short 994 elif long: 995 title = long 996 else: 997 title = "untitled" 998 icon = short or long or title 999 if not self.get_saved(): 1000 title = "*%s*" % title 1001 icon = "*%s" % icon 1002 self.top.wm_title(title) 1003 self.top.wm_iconname(icon) 1004 1005 def get_saved(self): 1006 return self.undo.get_saved() 1007 1008 def set_saved(self, flag): 1009 self.undo.set_saved(flag) 1010 1011 def reset_undo(self): 1012 self.undo.reset_undo() 1013 1014 def short_title(self): 1015 filename = self.io.filename 1016 return os.path.basename(filename) if filename else "untitled" 1017 1018 def long_title(self): 1019 return self.io.filename or "" 1020 1021 def center_insert_event(self, event): 1022 self.center() 1023 return "break" 1024 1025 def center(self, mark="insert"): 1026 text = self.text 1027 top, bot = self.getwindowlines() 1028 lineno = self.getlineno(mark) 1029 height = bot - top 1030 newtop = max(1, lineno - height//2) 1031 text.yview(float(newtop)) 1032 1033 def getwindowlines(self): 1034 text = self.text 1035 top = self.getlineno("@0,0") 1036 bot = self.getlineno("@0,65535") 1037 if top == bot and text.winfo_height() == 1: 1038 # Geometry manager hasn't run yet 1039 height = int(text['height']) 1040 bot = top + height - 1 1041 return top, bot 1042 1043 def getlineno(self, mark="insert"): 1044 text = self.text 1045 return int(float(text.index(mark))) 1046 1047 def get_geometry(self): 1048 "Return (width, height, x, y)" 1049 geom = self.top.wm_geometry() 1050 m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom) 1051 return list(map(int, m.groups())) 1052 1053 def close_event(self, event): 1054 self.close() 1055 return "break" 1056 1057 def maybesave(self): 1058 if self.io: 1059 if not self.get_saved(): 1060 if self.top.state()!='normal': 1061 self.top.deiconify() 1062 self.top.lower() 1063 self.top.lift() 1064 return self.io.maybesave() 1065 1066 def close(self): 1067 try: 1068 reply = self.maybesave() 1069 if str(reply) != "cancel": 1070 self._close() 1071 return reply 1072 except AttributeError: # bpo-35379: close called twice 1073 pass 1074 1075 def _close(self): 1076 if self.io.filename: 1077 self.update_recent_files_list(new_file=self.io.filename) 1078 window.unregister_callback(self.postwindowsmenu) 1079 self.unload_extensions() 1080 self.io.close() 1081 self.io = None 1082 self.undo = None 1083 if self.color: 1084 self.color.close() 1085 self.color = None 1086 self.text = None 1087 self.tkinter_vars = None 1088 self.per.close() 1089 self.per = None 1090 self.top.destroy() 1091 if self.close_hook: 1092 # unless override: unregister from flist, terminate if last window 1093 self.close_hook() 1094 1095 def load_extensions(self): 1096 self.extensions = {} 1097 self.load_standard_extensions() 1098 1099 def unload_extensions(self): 1100 for ins in list(self.extensions.values()): 1101 if hasattr(ins, "close"): 1102 ins.close() 1103 self.extensions = {} 1104 1105 def load_standard_extensions(self): 1106 for name in self.get_standard_extension_names(): 1107 try: 1108 self.load_extension(name) 1109 except: 1110 print("Failed to load extension", repr(name)) 1111 traceback.print_exc() 1112 1113 def get_standard_extension_names(self): 1114 return idleConf.GetExtensions(editor_only=True) 1115 1116 extfiles = { # Map built-in config-extension section names to file names. 1117 'ZzDummy': 'zzdummy', 1118 } 1119 1120 def load_extension(self, name): 1121 fname = self.extfiles.get(name, name) 1122 try: 1123 try: 1124 mod = importlib.import_module('.' + fname, package=__package__) 1125 except (ImportError, TypeError): 1126 mod = importlib.import_module(fname) 1127 except ImportError: 1128 print("\nFailed to import extension: ", name) 1129 raise 1130 cls = getattr(mod, name) 1131 keydefs = idleConf.GetExtensionBindings(name) 1132 if hasattr(cls, "menudefs"): 1133 self.fill_menus(cls.menudefs, keydefs) 1134 ins = cls(self) 1135 self.extensions[name] = ins 1136 if keydefs: 1137 self.apply_bindings(keydefs) 1138 for vevent in keydefs: 1139 methodname = vevent.replace("-", "_") 1140 while methodname[:1] == '<': 1141 methodname = methodname[1:] 1142 while methodname[-1:] == '>': 1143 methodname = methodname[:-1] 1144 methodname = methodname + "_event" 1145 if hasattr(ins, methodname): 1146 self.text.bind(vevent, getattr(ins, methodname)) 1147 1148 def apply_bindings(self, keydefs=None): 1149 if keydefs is None: 1150 keydefs = self.mainmenu.default_keydefs 1151 text = self.text 1152 text.keydefs = keydefs 1153 for event, keylist in keydefs.items(): 1154 if keylist: 1155 text.event_add(event, *keylist) 1156 1157 def fill_menus(self, menudefs=None, keydefs=None): 1158 """Add appropriate entries to the menus and submenus 1159 1160 Menus that are absent or None in self.menudict are ignored. 1161 """ 1162 if menudefs is None: 1163 menudefs = self.mainmenu.menudefs 1164 if keydefs is None: 1165 keydefs = self.mainmenu.default_keydefs 1166 menudict = self.menudict 1167 text = self.text 1168 for mname, entrylist in menudefs: 1169 menu = menudict.get(mname) 1170 if not menu: 1171 continue 1172 for entry in entrylist: 1173 if not entry: 1174 menu.add_separator() 1175 else: 1176 label, eventname = entry 1177 checkbutton = (label[:1] == '!') 1178 if checkbutton: 1179 label = label[1:] 1180 underline, label = prepstr(label) 1181 accelerator = get_accelerator(keydefs, eventname) 1182 def command(text=text, eventname=eventname): 1183 text.event_generate(eventname) 1184 if checkbutton: 1185 var = self.get_var_obj(eventname, BooleanVar) 1186 menu.add_checkbutton(label=label, underline=underline, 1187 command=command, accelerator=accelerator, 1188 variable=var) 1189 else: 1190 menu.add_command(label=label, underline=underline, 1191 command=command, 1192 accelerator=accelerator) 1193 1194 def getvar(self, name): 1195 var = self.get_var_obj(name) 1196 if var: 1197 value = var.get() 1198 return value 1199 else: 1200 raise NameError(name) 1201 1202 def setvar(self, name, value, vartype=None): 1203 var = self.get_var_obj(name, vartype) 1204 if var: 1205 var.set(value) 1206 else: 1207 raise NameError(name) 1208 1209 def get_var_obj(self, name, vartype=None): 1210 var = self.tkinter_vars.get(name) 1211 if not var and vartype: 1212 # create a Tkinter variable object with self.text as master: 1213 self.tkinter_vars[name] = var = vartype(self.text) 1214 return var 1215 1216 # Tk implementations of "virtual text methods" -- each platform 1217 # reusing IDLE's support code needs to define these for its GUI's 1218 # flavor of widget. 1219 1220 # Is character at text_index in a Python string? Return 0 for 1221 # "guaranteed no", true for anything else. This info is expensive 1222 # to compute ab initio, but is probably already known by the 1223 # platform's colorizer. 1224 1225 def is_char_in_string(self, text_index): 1226 if self.color: 1227 # Return true iff colorizer hasn't (re)gotten this far 1228 # yet, or the character is tagged as being in a string 1229 return self.text.tag_prevrange("TODO", text_index) or \ 1230 "STRING" in self.text.tag_names(text_index) 1231 else: 1232 # The colorizer is missing: assume the worst 1233 return 1 1234 1235 # If a selection is defined in the text widget, return (start, 1236 # end) as Tkinter text indices, otherwise return (None, None) 1237 def get_selection_indices(self): 1238 try: 1239 first = self.text.index("sel.first") 1240 last = self.text.index("sel.last") 1241 return first, last 1242 except TclError: 1243 return None, None 1244 1245 # Return the text widget's current view of what a tab stop means 1246 # (equivalent width in spaces). 1247 1248 def get_tk_tabwidth(self): 1249 current = self.text['tabs'] or TK_TABWIDTH_DEFAULT 1250 return int(current) 1251 1252 # Set the text widget's current view of what a tab stop means. 1253 1254 def set_tk_tabwidth(self, newtabwidth): 1255 text = self.text 1256 if self.get_tk_tabwidth() != newtabwidth: 1257 # Set text widget tab width 1258 pixels = text.tk.call("font", "measure", text["font"], 1259 "-displayof", text.master, 1260 "n" * newtabwidth) 1261 text.configure(tabs=pixels) 1262 1263### begin autoindent code ### (configuration was moved to beginning of class) 1264 1265 def set_indentation_params(self, is_py_src, guess=True): 1266 if is_py_src and guess: 1267 i = self.guess_indent() 1268 if 2 <= i <= 8: 1269 self.indentwidth = i 1270 if self.indentwidth != self.tabwidth: 1271 self.usetabs = False 1272 self.set_tk_tabwidth(self.tabwidth) 1273 1274 def smart_backspace_event(self, event): 1275 text = self.text 1276 first, last = self.get_selection_indices() 1277 if first and last: 1278 text.delete(first, last) 1279 text.mark_set("insert", first) 1280 return "break" 1281 # Delete whitespace left, until hitting a real char or closest 1282 # preceding virtual tab stop. 1283 chars = text.get("insert linestart", "insert") 1284 if chars == '': 1285 if text.compare("insert", ">", "1.0"): 1286 # easy: delete preceding newline 1287 text.delete("insert-1c") 1288 else: 1289 text.bell() # at start of buffer 1290 return "break" 1291 if chars[-1] not in " \t": 1292 # easy: delete preceding real char 1293 text.delete("insert-1c") 1294 return "break" 1295 # Ick. It may require *inserting* spaces if we back up over a 1296 # tab character! This is written to be clear, not fast. 1297 tabwidth = self.tabwidth 1298 have = len(chars.expandtabs(tabwidth)) 1299 assert have > 0 1300 want = ((have - 1) // self.indentwidth) * self.indentwidth 1301 # Debug prompt is multilined.... 1302 ncharsdeleted = 0 1303 while 1: 1304 chars = chars[:-1] 1305 ncharsdeleted = ncharsdeleted + 1 1306 have = len(chars.expandtabs(tabwidth)) 1307 if have <= want or chars[-1] not in " \t": 1308 break 1309 text.undo_block_start() 1310 text.delete("insert-%dc" % ncharsdeleted, "insert") 1311 if have < want: 1312 text.insert("insert", ' ' * (want - have), 1313 self.user_input_insert_tags) 1314 text.undo_block_stop() 1315 return "break" 1316 1317 def smart_indent_event(self, event): 1318 # if intraline selection: 1319 # delete it 1320 # elif multiline selection: 1321 # do indent-region 1322 # else: 1323 # indent one level 1324 text = self.text 1325 first, last = self.get_selection_indices() 1326 text.undo_block_start() 1327 try: 1328 if first and last: 1329 if index2line(first) != index2line(last): 1330 return self.fregion.indent_region_event(event) 1331 text.delete(first, last) 1332 text.mark_set("insert", first) 1333 prefix = text.get("insert linestart", "insert") 1334 raw, effective = get_line_indent(prefix, self.tabwidth) 1335 if raw == len(prefix): 1336 # only whitespace to the left 1337 self.reindent_to(effective + self.indentwidth) 1338 else: 1339 # tab to the next 'stop' within or to right of line's text: 1340 if self.usetabs: 1341 pad = '\t' 1342 else: 1343 effective = len(prefix.expandtabs(self.tabwidth)) 1344 n = self.indentwidth 1345 pad = ' ' * (n - effective % n) 1346 text.insert("insert", pad, self.user_input_insert_tags) 1347 text.see("insert") 1348 return "break" 1349 finally: 1350 text.undo_block_stop() 1351 1352 def newline_and_indent_event(self, event): 1353 """Insert a newline and indentation after Enter keypress event. 1354 1355 Properly position the cursor on the new line based on information 1356 from the current line. This takes into account if the current line 1357 is a shell prompt, is empty, has selected text, contains a block 1358 opener, contains a block closer, is a continuation line, or 1359 is inside a string. 1360 """ 1361 text = self.text 1362 first, last = self.get_selection_indices() 1363 text.undo_block_start() 1364 try: # Close undo block and expose new line in finally clause. 1365 if first and last: 1366 text.delete(first, last) 1367 text.mark_set("insert", first) 1368 line = text.get("insert linestart", "insert") 1369 1370 # Count leading whitespace for indent size. 1371 i, n = 0, len(line) 1372 while i < n and line[i] in " \t": 1373 i += 1 1374 if i == n: 1375 # The cursor is in or at leading indentation in a continuation 1376 # line; just inject an empty line at the start. 1377 text.insert("insert linestart", '\n', 1378 self.user_input_insert_tags) 1379 return "break" 1380 indent = line[:i] 1381 1382 # Strip whitespace before insert point unless it's in the prompt. 1383 i = 0 1384 while line and line[-1] in " \t": 1385 line = line[:-1] 1386 i += 1 1387 if i: 1388 text.delete("insert - %d chars" % i, "insert") 1389 1390 # Strip whitespace after insert point. 1391 while text.get("insert") in " \t": 1392 text.delete("insert") 1393 1394 # Insert new line. 1395 text.insert("insert", '\n', self.user_input_insert_tags) 1396 1397 # Adjust indentation for continuations and block open/close. 1398 # First need to find the last statement. 1399 lno = index2line(text.index('insert')) 1400 y = pyparse.Parser(self.indentwidth, self.tabwidth) 1401 if not self.prompt_last_line: 1402 for context in self.num_context_lines: 1403 startat = max(lno - context, 1) 1404 startatindex = repr(startat) + ".0" 1405 rawtext = text.get(startatindex, "insert") 1406 y.set_code(rawtext) 1407 bod = y.find_good_parse_start( 1408 self._build_char_in_string_func(startatindex)) 1409 if bod is not None or startat == 1: 1410 break 1411 y.set_lo(bod or 0) 1412 else: 1413 r = text.tag_prevrange("console", "insert") 1414 if r: 1415 startatindex = r[1] 1416 else: 1417 startatindex = "1.0" 1418 rawtext = text.get(startatindex, "insert") 1419 y.set_code(rawtext) 1420 y.set_lo(0) 1421 1422 c = y.get_continuation_type() 1423 if c != pyparse.C_NONE: 1424 # The current statement hasn't ended yet. 1425 if c == pyparse.C_STRING_FIRST_LINE: 1426 # After the first line of a string do not indent at all. 1427 pass 1428 elif c == pyparse.C_STRING_NEXT_LINES: 1429 # Inside a string which started before this line; 1430 # just mimic the current indent. 1431 text.insert("insert", indent, self.user_input_insert_tags) 1432 elif c == pyparse.C_BRACKET: 1433 # Line up with the first (if any) element of the 1434 # last open bracket structure; else indent one 1435 # level beyond the indent of the line with the 1436 # last open bracket. 1437 self.reindent_to(y.compute_bracket_indent()) 1438 elif c == pyparse.C_BACKSLASH: 1439 # If more than one line in this statement already, just 1440 # mimic the current indent; else if initial line 1441 # has a start on an assignment stmt, indent to 1442 # beyond leftmost =; else to beyond first chunk of 1443 # non-whitespace on initial line. 1444 if y.get_num_lines_in_stmt() > 1: 1445 text.insert("insert", indent, 1446 self.user_input_insert_tags) 1447 else: 1448 self.reindent_to(y.compute_backslash_indent()) 1449 else: 1450 assert 0, "bogus continuation type %r" % (c,) 1451 return "break" 1452 1453 # This line starts a brand new statement; indent relative to 1454 # indentation of initial line of closest preceding 1455 # interesting statement. 1456 indent = y.get_base_indent_string() 1457 text.insert("insert", indent, self.user_input_insert_tags) 1458 if y.is_block_opener(): 1459 self.smart_indent_event(event) 1460 elif indent and y.is_block_closer(): 1461 self.smart_backspace_event(event) 1462 return "break" 1463 finally: 1464 text.see("insert") 1465 text.undo_block_stop() 1466 1467 # Our editwin provides an is_char_in_string function that works 1468 # with a Tk text index, but PyParse only knows about offsets into 1469 # a string. This builds a function for PyParse that accepts an 1470 # offset. 1471 1472 def _build_char_in_string_func(self, startindex): 1473 def inner(offset, _startindex=startindex, 1474 _icis=self.is_char_in_string): 1475 return _icis(_startindex + "+%dc" % offset) 1476 return inner 1477 1478 # XXX this isn't bound to anything -- see tabwidth comments 1479## def change_tabwidth_event(self, event): 1480## new = self._asktabwidth() 1481## if new != self.tabwidth: 1482## self.tabwidth = new 1483## self.set_indentation_params(0, guess=0) 1484## return "break" 1485 1486 # Make string that displays as n leading blanks. 1487 1488 def _make_blanks(self, n): 1489 if self.usetabs: 1490 ntabs, nspaces = divmod(n, self.tabwidth) 1491 return '\t' * ntabs + ' ' * nspaces 1492 else: 1493 return ' ' * n 1494 1495 # Delete from beginning of line to insert point, then reinsert 1496 # column logical (meaning use tabs if appropriate) spaces. 1497 1498 def reindent_to(self, column): 1499 text = self.text 1500 text.undo_block_start() 1501 if text.compare("insert linestart", "!=", "insert"): 1502 text.delete("insert linestart", "insert") 1503 if column: 1504 text.insert("insert", self._make_blanks(column), 1505 self.user_input_insert_tags) 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