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 15import tkinter.simpledialog as tkSimpleDialog 16import tkinter.messagebox as tkMessageBox 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(object): 50 from idlelib.percolator import Percolator 51 from idlelib.colorizer import ColorDelegator, color_config 52 from idlelib.undo import UndoDelegator 53 from idlelib.iomenu import IOBinding, encoding 54 from idlelib import mainmenu 55 from idlelib.statusbar import MultiStatusBar 56 from idlelib.autocomplete import AutoComplete 57 from idlelib.autoexpand import AutoExpand 58 from idlelib.calltip import Calltip 59 from idlelib.codecontext import CodeContext 60 from idlelib.sidebar import LineNumbers 61 from idlelib.format import FormatParagraph, FormatRegion, Indents, Rstrip 62 from idlelib.parenmatch import ParenMatch 63 from idlelib.squeezer import Squeezer 64 from idlelib.zoomheight import ZoomHeight 65 66 filesystemencoding = sys.getfilesystemencoding() # for file names 67 help_url = None 68 69 allow_code_context = True 70 allow_line_numbers = True 71 72 def __init__(self, flist=None, filename=None, key=None, root=None): 73 # Delay import: runscript imports pyshell imports EditorWindow. 74 from idlelib.runscript import ScriptBinding 75 76 if EditorWindow.help_url is None: 77 dochome = os.path.join(sys.base_prefix, 'Doc', 'index.html') 78 if sys.platform.count('linux'): 79 # look for html docs in a couple of standard places 80 pyver = 'python-docs-' + '%s.%s.%s' % sys.version_info[:3] 81 if os.path.isdir('/var/www/html/python/'): # "python2" rpm 82 dochome = '/var/www/html/python/index.html' 83 else: 84 basepath = '/usr/share/doc/' # standard location 85 dochome = os.path.join(basepath, pyver, 86 'Doc', 'index.html') 87 elif sys.platform[:3] == 'win': 88 chmfile = os.path.join(sys.base_prefix, 'Doc', 89 'Python%s.chm' % _sphinx_version()) 90 if os.path.isfile(chmfile): 91 dochome = chmfile 92 elif sys.platform == 'darwin': 93 # documentation may be stored inside a python framework 94 dochome = os.path.join(sys.base_prefix, 95 'Resources/English.lproj/Documentation/index.html') 96 dochome = os.path.normpath(dochome) 97 if os.path.isfile(dochome): 98 EditorWindow.help_url = dochome 99 if sys.platform == 'darwin': 100 # Safari requires real file:-URLs 101 EditorWindow.help_url = 'file://' + EditorWindow.help_url 102 else: 103 EditorWindow.help_url = ("https://docs.python.org/%d.%d/" 104 % sys.version_info[:2]) 105 self.flist = flist 106 root = root or flist.root 107 self.root = root 108 self.menubar = Menu(root) 109 self.top = top = window.ListedToplevel(root, menu=self.menubar) 110 if flist: 111 self.tkinter_vars = flist.vars 112 #self.top.instance_dict makes flist.inversedict available to 113 #configdialog.py so it can access all EditorWindow instances 114 self.top.instance_dict = flist.inversedict 115 else: 116 self.tkinter_vars = {} # keys: Tkinter event names 117 # values: Tkinter variable instances 118 self.top.instance_dict = {} 119 self.recent_files_path = idleConf.userdir and os.path.join( 120 idleConf.userdir, 'recent-files.lst') 121 122 self.prompt_last_line = '' # Override in PyShell 123 self.text_frame = text_frame = Frame(top) 124 self.vbar = vbar = Scrollbar(text_frame, name='vbar') 125 width = idleConf.GetOption('main', 'EditorWindow', 'width', type='int') 126 text_options = { 127 'name': 'text', 128 'padx': 5, 129 'wrap': 'none', 130 'highlightthickness': 0, 131 'width': width, 132 'tabstyle': 'wordprocessor', # new in 8.5 133 'height': idleConf.GetOption( 134 'main', 'EditorWindow', 'height', type='int'), 135 } 136 self.text = text = MultiCallCreator(Text)(text_frame, **text_options) 137 self.top.focused_widget = self.text 138 139 self.createmenubar() 140 self.apply_bindings() 141 142 self.top.protocol("WM_DELETE_WINDOW", self.close) 143 self.top.bind("<<close-window>>", self.close_event) 144 if macosx.isAquaTk(): 145 # Command-W on editor windows doesn't work without this. 146 text.bind('<<close-window>>', self.close_event) 147 # Some OS X systems have only one mouse button, so use 148 # control-click for popup context menus there. For two 149 # buttons, AquaTk defines <2> as the right button, not <3>. 150 text.bind("<Control-Button-1>",self.right_menu_event) 151 text.bind("<2>", self.right_menu_event) 152 else: 153 # Elsewhere, use right-click for popup menus. 154 text.bind("<3>",self.right_menu_event) 155 156 text.bind('<MouseWheel>', wheel_event) 157 text.bind('<Button-4>', wheel_event) 158 text.bind('<Button-5>', wheel_event) 159 text.bind('<Configure>', self.handle_winconfig) 160 text.bind("<<cut>>", self.cut) 161 text.bind("<<copy>>", self.copy) 162 text.bind("<<paste>>", self.paste) 163 text.bind("<<center-insert>>", self.center_insert_event) 164 text.bind("<<help>>", self.help_dialog) 165 text.bind("<<python-docs>>", self.python_docs) 166 text.bind("<<about-idle>>", self.about_dialog) 167 text.bind("<<open-config-dialog>>", self.config_dialog) 168 text.bind("<<open-module>>", self.open_module_event) 169 text.bind("<<do-nothing>>", lambda event: "break") 170 text.bind("<<select-all>>", self.select_all) 171 text.bind("<<remove-selection>>", self.remove_selection) 172 text.bind("<<find>>", self.find_event) 173 text.bind("<<find-again>>", self.find_again_event) 174 text.bind("<<find-in-files>>", self.find_in_files_event) 175 text.bind("<<find-selection>>", self.find_selection_event) 176 text.bind("<<replace>>", self.replace_event) 177 text.bind("<<goto-line>>", self.goto_line_event) 178 text.bind("<<smart-backspace>>",self.smart_backspace_event) 179 text.bind("<<newline-and-indent>>",self.newline_and_indent_event) 180 text.bind("<<smart-indent>>",self.smart_indent_event) 181 self.fregion = fregion = self.FormatRegion(self) 182 # self.fregion used in smart_indent_event to access indent_region. 183 text.bind("<<indent-region>>", fregion.indent_region_event) 184 text.bind("<<dedent-region>>", fregion.dedent_region_event) 185 text.bind("<<comment-region>>", fregion.comment_region_event) 186 text.bind("<<uncomment-region>>", fregion.uncomment_region_event) 187 text.bind("<<tabify-region>>", fregion.tabify_region_event) 188 text.bind("<<untabify-region>>", fregion.untabify_region_event) 189 indents = self.Indents(self) 190 text.bind("<<toggle-tabs>>", indents.toggle_tabs_event) 191 text.bind("<<change-indentwidth>>", indents.change_indentwidth_event) 192 text.bind("<Left>", self.move_at_edge_if_selection(0)) 193 text.bind("<Right>", self.move_at_edge_if_selection(1)) 194 text.bind("<<del-word-left>>", self.del_word_left) 195 text.bind("<<del-word-right>>", self.del_word_right) 196 text.bind("<<beginning-of-line>>", self.home_callback) 197 198 if flist: 199 flist.inversedict[self] = key 200 if key: 201 flist.dict[key] = self 202 text.bind("<<open-new-window>>", self.new_callback) 203 text.bind("<<close-all-windows>>", self.flist.close_all_callback) 204 text.bind("<<open-class-browser>>", self.open_module_browser) 205 text.bind("<<open-path-browser>>", self.open_path_browser) 206 text.bind("<<open-turtle-demo>>", self.open_turtle_demo) 207 208 self.set_status_bar() 209 text_frame.pack(side=LEFT, fill=BOTH, expand=1) 210 text_frame.rowconfigure(1, weight=1) 211 text_frame.columnconfigure(1, weight=1) 212 vbar['command'] = self.handle_yview 213 vbar.grid(row=1, column=2, sticky=NSEW) 214 text['yscrollcommand'] = vbar.set 215 text['font'] = idleConf.GetFont(self.root, 'main', 'EditorWindow') 216 text.grid(row=1, column=1, sticky=NSEW) 217 text.focus_set() 218 self.set_width() 219 220 # usetabs true -> literal tab characters are used by indent and 221 # dedent cmds, possibly mixed with spaces if 222 # indentwidth is not a multiple of tabwidth, 223 # which will cause Tabnanny to nag! 224 # false -> tab characters are converted to spaces by indent 225 # and dedent cmds, and ditto TAB keystrokes 226 # Although use-spaces=0 can be configured manually in config-main.def, 227 # configuration of tabs v. spaces is not supported in the configuration 228 # dialog. IDLE promotes the preferred Python indentation: use spaces! 229 usespaces = idleConf.GetOption('main', 'Indent', 230 'use-spaces', type='bool') 231 self.usetabs = not usespaces 232 233 # tabwidth is the display width of a literal tab character. 234 # CAUTION: telling Tk to use anything other than its default 235 # tab setting causes it to use an entirely different tabbing algorithm, 236 # treating tab stops as fixed distances from the left margin. 237 # Nobody expects this, so for now tabwidth should never be changed. 238 self.tabwidth = 8 # must remain 8 until Tk is fixed. 239 240 # indentwidth is the number of screen characters per indent level. 241 # The recommended Python indentation is four spaces. 242 self.indentwidth = self.tabwidth 243 self.set_notabs_indentwidth() 244 245 # Store the current value of the insertofftime now so we can restore 246 # it if needed. 247 if not hasattr(idleConf, 'blink_off_time'): 248 idleConf.blink_off_time = self.text['insertofftime'] 249 self.update_cursor_blink() 250 251 # When searching backwards for a reliable place to begin parsing, 252 # first start num_context_lines[0] lines back, then 253 # num_context_lines[1] lines back if that didn't work, and so on. 254 # The last value should be huge (larger than the # of lines in a 255 # conceivable file). 256 # Making the initial values larger slows things down more often. 257 self.num_context_lines = 50, 500, 5000000 258 self.per = per = self.Percolator(text) 259 self.undo = undo = self.UndoDelegator() 260 per.insertfilter(undo) 261 text.undo_block_start = undo.undo_block_start 262 text.undo_block_stop = undo.undo_block_stop 263 undo.set_saved_change_hook(self.saved_change_hook) 264 # IOBinding implements file I/O and printing functionality 265 self.io = io = self.IOBinding(self) 266 io.set_filename_change_hook(self.filename_change_hook) 267 self.good_load = False 268 self.set_indentation_params(False) 269 self.color = None # initialized below in self.ResetColorizer 270 self.code_context = None # optionally initialized later below 271 self.line_numbers = None # optionally initialized later below 272 if filename: 273 if os.path.exists(filename) and not os.path.isdir(filename): 274 if io.loadfile(filename): 275 self.good_load = True 276 is_py_src = self.ispythonsource(filename) 277 self.set_indentation_params(is_py_src) 278 else: 279 io.set_filename(filename) 280 self.good_load = True 281 282 self.ResetColorizer() 283 self.saved_change_hook() 284 self.update_recent_files_list() 285 self.load_extensions() 286 menu = self.menudict.get('window') 287 if menu: 288 end = menu.index("end") 289 if end is None: 290 end = -1 291 if end >= 0: 292 menu.add_separator() 293 end = end + 1 294 self.wmenu_end = end 295 window.register_callback(self.postwindowsmenu) 296 297 # Some abstractions so IDLE extensions are cross-IDE 298 self.askyesno = tkMessageBox.askyesno 299 self.askinteger = tkSimpleDialog.askinteger 300 self.showerror = tkMessageBox.showerror 301 302 # Add pseudoevents for former extension fixed keys. 303 # (This probably needs to be done once in the process.) 304 text.event_add('<<autocomplete>>', '<Key-Tab>') 305 text.event_add('<<try-open-completions>>', '<KeyRelease-period>', 306 '<KeyRelease-slash>', '<KeyRelease-backslash>') 307 text.event_add('<<try-open-calltip>>', '<KeyRelease-parenleft>') 308 text.event_add('<<refresh-calltip>>', '<KeyRelease-parenright>') 309 text.event_add('<<paren-closed>>', '<KeyRelease-parenright>', 310 '<KeyRelease-bracketright>', '<KeyRelease-braceright>') 311 312 # Former extension bindings depends on frame.text being packed 313 # (called from self.ResetColorizer()). 314 autocomplete = self.AutoComplete(self) 315 text.bind("<<autocomplete>>", autocomplete.autocomplete_event) 316 text.bind("<<try-open-completions>>", 317 autocomplete.try_open_completions_event) 318 text.bind("<<force-open-completions>>", 319 autocomplete.force_open_completions_event) 320 text.bind("<<expand-word>>", self.AutoExpand(self).expand_word_event) 321 text.bind("<<format-paragraph>>", 322 self.FormatParagraph(self).format_paragraph_event) 323 parenmatch = self.ParenMatch(self) 324 text.bind("<<flash-paren>>", parenmatch.flash_paren_event) 325 text.bind("<<paren-closed>>", parenmatch.paren_closed_event) 326 scriptbinding = ScriptBinding(self) 327 text.bind("<<check-module>>", scriptbinding.check_module_event) 328 text.bind("<<run-module>>", scriptbinding.run_module_event) 329 text.bind("<<run-custom>>", scriptbinding.run_custom_event) 330 text.bind("<<do-rstrip>>", self.Rstrip(self).do_rstrip) 331 self.ctip = ctip = self.Calltip(self) 332 text.bind("<<try-open-calltip>>", ctip.try_open_calltip_event) 333 #refresh-calltip must come after paren-closed to work right 334 text.bind("<<refresh-calltip>>", ctip.refresh_calltip_event) 335 text.bind("<<force-open-calltip>>", ctip.force_open_calltip_event) 336 text.bind("<<zoom-height>>", self.ZoomHeight(self).zoom_height_event) 337 if self.allow_code_context: 338 self.code_context = self.CodeContext(self) 339 text.bind("<<toggle-code-context>>", 340 self.code_context.toggle_code_context_event) 341 else: 342 self.update_menu_state('options', '*Code Context', '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', '*Line Numbers', '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 menudict[name] = menu = Menu(mbar, name=name, tearoff=0) 454 mbar.add_cascade(label=label, menu=menu, underline=underline) 455 if macosx.isCarbonTk(): 456 # Insert the application menu 457 menudict['application'] = menu = Menu(mbar, name='apple', 458 tearoff=0) 459 mbar.add_cascade(label='IDLE', menu=menu) 460 self.fill_menus() 461 self.recent_files_menu = Menu(self.menubar, tearoff=0) 462 self.menudict['file'].insert_cascade(3, label='Recent Files', 463 underline=0, 464 menu=self.recent_files_menu) 465 self.base_helpmenu_length = self.menudict['help'].index(END) 466 self.reset_help_menu_entries() 467 468 def postwindowsmenu(self): 469 # Only called when Window menu exists 470 menu = self.menudict['window'] 471 end = menu.index("end") 472 if end is None: 473 end = -1 474 if end > self.wmenu_end: 475 menu.delete(self.wmenu_end+1, end) 476 window.add_windows_to_menu(menu) 477 478 def update_menu_label(self, menu, index, label): 479 "Update label for menu item at index." 480 menuitem = self.menudict[menu] 481 menuitem.entryconfig(index, label=label) 482 483 def update_menu_state(self, menu, index, state): 484 "Update state for menu item at index." 485 menuitem = self.menudict[menu] 486 menuitem.entryconfig(index, state=state) 487 488 def handle_yview(self, event, *args): 489 "Handle scrollbar." 490 if event == 'moveto': 491 fraction = float(args[0]) 492 lines = (round(self.getlineno('end') * fraction) - 493 self.getlineno('@0,0')) 494 event = 'scroll' 495 args = (lines, 'units') 496 self.text.yview(event, *args) 497 return 'break' 498 499 rmenu = None 500 501 def right_menu_event(self, event): 502 text = self.text 503 newdex = text.index(f'@{event.x},{event.y}') 504 try: 505 in_selection = (text.compare('sel.first', '<=', newdex) and 506 text.compare(newdex, '<=', 'sel.last')) 507 except TclError: 508 in_selection = False 509 if not in_selection: 510 text.tag_remove("sel", "1.0", "end") 511 text.mark_set("insert", newdex) 512 if not self.rmenu: 513 self.make_rmenu() 514 rmenu = self.rmenu 515 self.event = event 516 iswin = sys.platform[:3] == 'win' 517 if iswin: 518 text.config(cursor="arrow") 519 520 for item in self.rmenu_specs: 521 try: 522 label, eventname, verify_state = item 523 except ValueError: # see issue1207589 524 continue 525 526 if verify_state is None: 527 continue 528 state = getattr(self, verify_state)() 529 rmenu.entryconfigure(label, state=state) 530 531 rmenu.tk_popup(event.x_root, event.y_root) 532 if iswin: 533 self.text.config(cursor="ibeam") 534 return "break" 535 536 rmenu_specs = [ 537 # ("Label", "<<virtual-event>>", "statefuncname"), ... 538 ("Close", "<<close-window>>", None), # Example 539 ] 540 541 def make_rmenu(self): 542 rmenu = Menu(self.text, tearoff=0) 543 for item in self.rmenu_specs: 544 label, eventname = item[0], item[1] 545 if label is not None: 546 def command(text=self.text, eventname=eventname): 547 text.event_generate(eventname) 548 rmenu.add_command(label=label, command=command) 549 else: 550 rmenu.add_separator() 551 self.rmenu = rmenu 552 553 def rmenu_check_cut(self): 554 return self.rmenu_check_copy() 555 556 def rmenu_check_copy(self): 557 try: 558 indx = self.text.index('sel.first') 559 except TclError: 560 return 'disabled' 561 else: 562 return 'normal' if indx else 'disabled' 563 564 def rmenu_check_paste(self): 565 try: 566 self.text.tk.call('tk::GetSelection', self.text, 'CLIPBOARD') 567 except TclError: 568 return 'disabled' 569 else: 570 return 'normal' 571 572 def about_dialog(self, event=None): 573 "Handle Help 'About IDLE' event." 574 # Synchronize with macosx.overrideRootMenu.about_dialog. 575 help_about.AboutDialog(self.top) 576 return "break" 577 578 def config_dialog(self, event=None): 579 "Handle Options 'Configure IDLE' event." 580 # Synchronize with macosx.overrideRootMenu.config_dialog. 581 configdialog.ConfigDialog(self.top,'Settings') 582 return "break" 583 584 def help_dialog(self, event=None): 585 "Handle Help 'IDLE Help' event." 586 # Synchronize with macosx.overrideRootMenu.help_dialog. 587 if self.root: 588 parent = self.root 589 else: 590 parent = self.top 591 help.show_idlehelp(parent) 592 return "break" 593 594 def python_docs(self, event=None): 595 if sys.platform[:3] == 'win': 596 try: 597 os.startfile(self.help_url) 598 except OSError as why: 599 tkMessageBox.showerror(title='Document Start Failure', 600 message=str(why), parent=self.text) 601 else: 602 webbrowser.open(self.help_url) 603 return "break" 604 605 def cut(self,event): 606 self.text.event_generate("<<Cut>>") 607 return "break" 608 609 def copy(self,event): 610 if not self.text.tag_ranges("sel"): 611 # There is no selection, so do nothing and maybe interrupt. 612 return None 613 self.text.event_generate("<<Copy>>") 614 return "break" 615 616 def paste(self,event): 617 self.text.event_generate("<<Paste>>") 618 self.text.see("insert") 619 return "break" 620 621 def select_all(self, event=None): 622 self.text.tag_add("sel", "1.0", "end-1c") 623 self.text.mark_set("insert", "1.0") 624 self.text.see("insert") 625 return "break" 626 627 def remove_selection(self, event=None): 628 self.text.tag_remove("sel", "1.0", "end") 629 self.text.see("insert") 630 return "break" 631 632 def move_at_edge_if_selection(self, edge_index): 633 """Cursor move begins at start or end of selection 634 635 When a left/right cursor key is pressed create and return to Tkinter a 636 function which causes a cursor move from the associated edge of the 637 selection. 638 639 """ 640 self_text_index = self.text.index 641 self_text_mark_set = self.text.mark_set 642 edges_table = ("sel.first+1c", "sel.last-1c") 643 def move_at_edge(event): 644 if (event.state & 5) == 0: # no shift(==1) or control(==4) pressed 645 try: 646 self_text_index("sel.first") 647 self_text_mark_set("insert", edges_table[edge_index]) 648 except TclError: 649 pass 650 return move_at_edge 651 652 def del_word_left(self, event): 653 self.text.event_generate('<Meta-Delete>') 654 return "break" 655 656 def del_word_right(self, event): 657 self.text.event_generate('<Meta-d>') 658 return "break" 659 660 def find_event(self, event): 661 search.find(self.text) 662 return "break" 663 664 def find_again_event(self, event): 665 search.find_again(self.text) 666 return "break" 667 668 def find_selection_event(self, event): 669 search.find_selection(self.text) 670 return "break" 671 672 def find_in_files_event(self, event): 673 grep.grep(self.text, self.io, self.flist) 674 return "break" 675 676 def replace_event(self, event): 677 replace.replace(self.text) 678 return "break" 679 680 def goto_line_event(self, event): 681 text = self.text 682 lineno = query.Goto( 683 text, "Go To Line", 684 "Enter a positive integer\n" 685 "('big' = end of file):" 686 ).result 687 if lineno is not None: 688 text.tag_remove("sel", "1.0", "end") 689 text.mark_set("insert", f'{lineno}.0') 690 text.see("insert") 691 self.set_line_and_column() 692 return "break" 693 694 def open_module(self): 695 """Get module name from user and open it. 696 697 Return module path or None for calls by open_module_browser 698 when latter is not invoked in named editor window. 699 """ 700 # XXX This, open_module_browser, and open_path_browser 701 # would fit better in iomenu.IOBinding. 702 try: 703 name = self.text.get("sel.first", "sel.last").strip() 704 except TclError: 705 name = '' 706 file_path = query.ModuleName( 707 self.text, "Open Module", 708 "Enter the name of a Python module\n" 709 "to search on sys.path and open:", 710 name).result 711 if file_path is not None: 712 if self.flist: 713 self.flist.open(file_path) 714 else: 715 self.io.loadfile(file_path) 716 return file_path 717 718 def open_module_event(self, event): 719 self.open_module() 720 return "break" 721 722 def open_module_browser(self, event=None): 723 filename = self.io.filename 724 if not (self.__class__.__name__ == 'PyShellEditorWindow' 725 and filename): 726 filename = self.open_module() 727 if filename is None: 728 return "break" 729 from idlelib import browser 730 browser.ModuleBrowser(self.root, filename) 731 return "break" 732 733 def open_path_browser(self, event=None): 734 from idlelib import pathbrowser 735 pathbrowser.PathBrowser(self.root) 736 return "break" 737 738 def open_turtle_demo(self, event = None): 739 import subprocess 740 741 cmd = [sys.executable, 742 '-c', 743 'from turtledemo.__main__ import main; main()'] 744 subprocess.Popen(cmd, shell=False) 745 return "break" 746 747 def gotoline(self, lineno): 748 if lineno is not None and lineno > 0: 749 self.text.mark_set("insert", "%d.0" % lineno) 750 self.text.tag_remove("sel", "1.0", "end") 751 self.text.tag_add("sel", "insert", "insert +1l") 752 self.center() 753 754 def ispythonsource(self, filename): 755 if not filename or os.path.isdir(filename): 756 return True 757 base, ext = os.path.splitext(os.path.basename(filename)) 758 if os.path.normcase(ext) in (".py", ".pyw"): 759 return True 760 line = self.text.get('1.0', '1.0 lineend') 761 return line.startswith('#!') and 'python' in line 762 763 def close_hook(self): 764 if self.flist: 765 self.flist.unregister_maybe_terminate(self) 766 self.flist = None 767 768 def set_close_hook(self, close_hook): 769 self.close_hook = close_hook 770 771 def filename_change_hook(self): 772 if self.flist: 773 self.flist.filename_changed_edit(self) 774 self.saved_change_hook() 775 self.top.update_windowlist_registry(self) 776 self.ResetColorizer() 777 778 def _addcolorizer(self): 779 if self.color: 780 return 781 if self.ispythonsource(self.io.filename): 782 self.color = self.ColorDelegator() 783 # can add more colorizers here... 784 if self.color: 785 self.per.removefilter(self.undo) 786 self.per.insertfilter(self.color) 787 self.per.insertfilter(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 tkMessageBox.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 tkMessageBox.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 if chars == self.prompt_last_line: # '' unless PyShell 1305 break 1306 chars = chars[:-1] 1307 ncharsdeleted = ncharsdeleted + 1 1308 have = len(chars.expandtabs(tabwidth)) 1309 if have <= want or chars[-1] not in " \t": 1310 break 1311 text.undo_block_start() 1312 text.delete("insert-%dc" % ncharsdeleted, "insert") 1313 if have < want: 1314 text.insert("insert", ' ' * (want - have)) 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) 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 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" and line != self.prompt_last_line: 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') 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) 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 else: 1447 self.reindent_to(y.compute_backslash_indent()) 1448 else: 1449 assert 0, "bogus continuation type %r" % (c,) 1450 return "break" 1451 1452 # This line starts a brand new statement; indent relative to 1453 # indentation of initial line of closest preceding 1454 # interesting statement. 1455 indent = y.get_base_indent_string() 1456 text.insert("insert", indent) 1457 if y.is_block_opener(): 1458 self.smart_indent_event(event) 1459 elif indent and y.is_block_closer(): 1460 self.smart_backspace_event(event) 1461 return "break" 1462 finally: 1463 text.see("insert") 1464 text.undo_block_stop() 1465 1466 # Our editwin provides an is_char_in_string function that works 1467 # with a Tk text index, but PyParse only knows about offsets into 1468 # a string. This builds a function for PyParse that accepts an 1469 # offset. 1470 1471 def _build_char_in_string_func(self, startindex): 1472 def inner(offset, _startindex=startindex, 1473 _icis=self.is_char_in_string): 1474 return _icis(_startindex + "+%dc" % offset) 1475 return inner 1476 1477 # XXX this isn't bound to anything -- see tabwidth comments 1478## def change_tabwidth_event(self, event): 1479## new = self._asktabwidth() 1480## if new != self.tabwidth: 1481## self.tabwidth = new 1482## self.set_indentation_params(0, guess=0) 1483## return "break" 1484 1485 # Make string that displays as n leading blanks. 1486 1487 def _make_blanks(self, n): 1488 if self.usetabs: 1489 ntabs, nspaces = divmod(n, self.tabwidth) 1490 return '\t' * ntabs + ' ' * nspaces 1491 else: 1492 return ' ' * n 1493 1494 # Delete from beginning of line to insert point, then reinsert 1495 # column logical (meaning use tabs if appropriate) spaces. 1496 1497 def reindent_to(self, column): 1498 text = self.text 1499 text.undo_block_start() 1500 if text.compare("insert linestart", "!=", "insert"): 1501 text.delete("insert linestart", "insert") 1502 if column: 1503 text.insert("insert", self._make_blanks(column)) 1504 text.undo_block_stop() 1505 1506 # Guess indentwidth from text content. 1507 # Return guessed indentwidth. This should not be believed unless 1508 # it's in a reasonable range (e.g., it will be 0 if no indented 1509 # blocks are found). 1510 1511 def guess_indent(self): 1512 opener, indented = IndentSearcher(self.text, self.tabwidth).run() 1513 if opener and indented: 1514 raw, indentsmall = get_line_indent(opener, self.tabwidth) 1515 raw, indentlarge = get_line_indent(indented, self.tabwidth) 1516 else: 1517 indentsmall = indentlarge = 0 1518 return indentlarge - indentsmall 1519 1520 def toggle_line_numbers_event(self, event=None): 1521 if self.line_numbers is None: 1522 return 1523 1524 if self.line_numbers.is_shown: 1525 self.line_numbers.hide_sidebar() 1526 menu_label = "Show" 1527 else: 1528 self.line_numbers.show_sidebar() 1529 menu_label = "Hide" 1530 self.update_menu_label(menu='options', index='*Line Numbers', 1531 label=f'{menu_label} Line Numbers') 1532 1533# "line.col" -> line, as an int 1534def index2line(index): 1535 return int(float(index)) 1536 1537 1538_line_indent_re = re.compile(r'[ \t]*') 1539def get_line_indent(line, tabwidth): 1540 """Return a line's indentation as (# chars, effective # of spaces). 1541 1542 The effective # of spaces is the length after properly "expanding" 1543 the tabs into spaces, as done by str.expandtabs(tabwidth). 1544 """ 1545 m = _line_indent_re.match(line) 1546 return m.end(), len(m.group().expandtabs(tabwidth)) 1547 1548 1549class IndentSearcher(object): 1550 1551 # .run() chews over the Text widget, looking for a block opener 1552 # and the stmt following it. Returns a pair, 1553 # (line containing block opener, line containing stmt) 1554 # Either or both may be None. 1555 1556 def __init__(self, text, tabwidth): 1557 self.text = text 1558 self.tabwidth = tabwidth 1559 self.i = self.finished = 0 1560 self.blkopenline = self.indentedline = None 1561 1562 def readline(self): 1563 if self.finished: 1564 return "" 1565 i = self.i = self.i + 1 1566 mark = repr(i) + ".0" 1567 if self.text.compare(mark, ">=", "end"): 1568 return "" 1569 return self.text.get(mark, mark + " lineend+1c") 1570 1571 def tokeneater(self, type, token, start, end, line, 1572 INDENT=tokenize.INDENT, 1573 NAME=tokenize.NAME, 1574 OPENERS=('class', 'def', 'for', 'if', 'try', 'while')): 1575 if self.finished: 1576 pass 1577 elif type == NAME and token in OPENERS: 1578 self.blkopenline = line 1579 elif type == INDENT and self.blkopenline: 1580 self.indentedline = line 1581 self.finished = 1 1582 1583 def run(self): 1584 save_tabsize = tokenize.tabsize 1585 tokenize.tabsize = self.tabwidth 1586 try: 1587 try: 1588 tokens = tokenize.generate_tokens(self.readline) 1589 for token in tokens: 1590 self.tokeneater(*token) 1591 except (tokenize.TokenError, SyntaxError): 1592 # since we cut off the tokenizer early, we can trigger 1593 # spurious errors 1594 pass 1595 finally: 1596 tokenize.tabsize = save_tabsize 1597 return self.blkopenline, self.indentedline 1598 1599### end autoindent code ### 1600 1601def prepstr(s): 1602 # Helper to extract the underscore from a string, e.g. 1603 # prepstr("Co_py") returns (2, "Copy"). 1604 i = s.find('_') 1605 if i >= 0: 1606 s = s[:i] + s[i+1:] 1607 return i, s 1608 1609 1610keynames = { 1611 'bracketleft': '[', 1612 'bracketright': ']', 1613 'slash': '/', 1614} 1615 1616def get_accelerator(keydefs, eventname): 1617 keylist = keydefs.get(eventname) 1618 # issue10940: temporary workaround to prevent hang with OS X Cocoa Tk 8.5 1619 # if not keylist: 1620 if (not keylist) or (macosx.isCocoaTk() and eventname in { 1621 "<<open-module>>", 1622 "<<goto-line>>", 1623 "<<change-indentwidth>>"}): 1624 return "" 1625 s = keylist[0] 1626 s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s) 1627 s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s) 1628 s = re.sub("Key-", "", s) 1629 s = re.sub("Cancel","Ctrl-Break",s) # dscherer@cmu.edu 1630 s = re.sub("Control-", "Ctrl-", s) 1631 s = re.sub("-", "+", s) 1632 s = re.sub("><", " ", s) 1633 s = re.sub("<", "", s) 1634 s = re.sub(">", "", s) 1635 return s 1636 1637 1638def fixwordbreaks(root): 1639 # On Windows, tcl/tk breaks 'words' only on spaces, as in Command Prompt. 1640 # We want Motif style everywhere. See #21474, msg218992 and followup. 1641 tk = root.tk 1642 tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded 1643 tk.call('set', 'tcl_wordchars', r'\w') 1644 tk.call('set', 'tcl_nonwordchars', r'\W') 1645 1646 1647def _editor_window(parent): # htest # 1648 # error if close master window first - timer event, after script 1649 root = parent 1650 fixwordbreaks(root) 1651 if sys.argv[1:]: 1652 filename = sys.argv[1] 1653 else: 1654 filename = None 1655 macosx.setupApp(root, None) 1656 edit = EditorWindow(root=root, filename=filename) 1657 text = edit.text 1658 text['height'] = 10 1659 for i in range(20): 1660 text.insert('insert', ' '*i + str(i) + '\n') 1661 # text.bind("<<close-all-windows>>", edit.close_event) 1662 # Does not stop error, neither does following 1663 # edit.text.bind("<<close-window>>", edit.close_event) 1664 1665if __name__ == '__main__': 1666 from unittest import main 1667 main('idlelib.idle_test.test_editor', verbosity=2, exit=False) 1668 1669 from idlelib.idle_test.htest import run 1670 run(_editor_window) 1671