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