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