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