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