jpayne@68: import importlib.abc jpayne@68: import importlib.util jpayne@68: import os jpayne@68: import platform jpayne@68: import re jpayne@68: import string jpayne@68: import sys jpayne@68: import tokenize jpayne@68: import traceback jpayne@68: import webbrowser jpayne@68: jpayne@68: from tkinter import * jpayne@68: from tkinter.font import Font jpayne@68: from tkinter.ttk import Scrollbar jpayne@68: import tkinter.simpledialog as tkSimpleDialog jpayne@68: import tkinter.messagebox as tkMessageBox jpayne@68: jpayne@68: from idlelib.config import idleConf jpayne@68: from idlelib import configdialog jpayne@68: from idlelib import grep jpayne@68: from idlelib import help jpayne@68: from idlelib import help_about jpayne@68: from idlelib import macosx jpayne@68: from idlelib.multicall import MultiCallCreator jpayne@68: from idlelib import pyparse jpayne@68: from idlelib import query jpayne@68: from idlelib import replace jpayne@68: from idlelib import search jpayne@68: from idlelib.tree import wheel_event jpayne@68: from idlelib import window jpayne@68: jpayne@68: # The default tab setting for a Text widget, in average-width characters. jpayne@68: TK_TABWIDTH_DEFAULT = 8 jpayne@68: _py_version = ' (%s)' % platform.python_version() jpayne@68: darwin = sys.platform == 'darwin' jpayne@68: jpayne@68: def _sphinx_version(): jpayne@68: "Format sys.version_info to produce the Sphinx version string used to install the chm docs" jpayne@68: major, minor, micro, level, serial = sys.version_info jpayne@68: release = '%s%s' % (major, minor) jpayne@68: release += '%s' % (micro,) jpayne@68: if level == 'candidate': jpayne@68: release += 'rc%s' % (serial,) jpayne@68: elif level != 'final': jpayne@68: release += '%s%s' % (level[0], serial) jpayne@68: return release jpayne@68: jpayne@68: jpayne@68: class EditorWindow(object): jpayne@68: from idlelib.percolator import Percolator jpayne@68: from idlelib.colorizer import ColorDelegator, color_config jpayne@68: from idlelib.undo import UndoDelegator jpayne@68: from idlelib.iomenu import IOBinding, encoding jpayne@68: from idlelib import mainmenu jpayne@68: from idlelib.statusbar import MultiStatusBar jpayne@68: from idlelib.autocomplete import AutoComplete jpayne@68: from idlelib.autoexpand import AutoExpand jpayne@68: from idlelib.calltip import Calltip jpayne@68: from idlelib.codecontext import CodeContext jpayne@68: from idlelib.sidebar import LineNumbers jpayne@68: from idlelib.format import FormatParagraph, FormatRegion, Indents, Rstrip jpayne@68: from idlelib.parenmatch import ParenMatch jpayne@68: from idlelib.squeezer import Squeezer jpayne@68: from idlelib.zoomheight import ZoomHeight jpayne@68: jpayne@68: filesystemencoding = sys.getfilesystemencoding() # for file names jpayne@68: help_url = None jpayne@68: jpayne@68: allow_code_context = True jpayne@68: allow_line_numbers = True jpayne@68: jpayne@68: def __init__(self, flist=None, filename=None, key=None, root=None): jpayne@68: # Delay import: runscript imports pyshell imports EditorWindow. jpayne@68: from idlelib.runscript import ScriptBinding jpayne@68: jpayne@68: if EditorWindow.help_url is None: jpayne@68: dochome = os.path.join(sys.base_prefix, 'Doc', 'index.html') jpayne@68: if sys.platform.count('linux'): jpayne@68: # look for html docs in a couple of standard places jpayne@68: pyver = 'python-docs-' + '%s.%s.%s' % sys.version_info[:3] jpayne@68: if os.path.isdir('/var/www/html/python/'): # "python2" rpm jpayne@68: dochome = '/var/www/html/python/index.html' jpayne@68: else: jpayne@68: basepath = '/usr/share/doc/' # standard location jpayne@68: dochome = os.path.join(basepath, pyver, jpayne@68: 'Doc', 'index.html') jpayne@68: elif sys.platform[:3] == 'win': jpayne@68: chmfile = os.path.join(sys.base_prefix, 'Doc', jpayne@68: 'Python%s.chm' % _sphinx_version()) jpayne@68: if os.path.isfile(chmfile): jpayne@68: dochome = chmfile jpayne@68: elif sys.platform == 'darwin': jpayne@68: # documentation may be stored inside a python framework jpayne@68: dochome = os.path.join(sys.base_prefix, jpayne@68: 'Resources/English.lproj/Documentation/index.html') jpayne@68: dochome = os.path.normpath(dochome) jpayne@68: if os.path.isfile(dochome): jpayne@68: EditorWindow.help_url = dochome jpayne@68: if sys.platform == 'darwin': jpayne@68: # Safari requires real file:-URLs jpayne@68: EditorWindow.help_url = 'file://' + EditorWindow.help_url jpayne@68: else: jpayne@68: EditorWindow.help_url = ("https://docs.python.org/%d.%d/" jpayne@68: % sys.version_info[:2]) jpayne@68: self.flist = flist jpayne@68: root = root or flist.root jpayne@68: self.root = root jpayne@68: self.menubar = Menu(root) jpayne@68: self.top = top = window.ListedToplevel(root, menu=self.menubar) jpayne@68: if flist: jpayne@68: self.tkinter_vars = flist.vars jpayne@68: #self.top.instance_dict makes flist.inversedict available to jpayne@68: #configdialog.py so it can access all EditorWindow instances jpayne@68: self.top.instance_dict = flist.inversedict jpayne@68: else: jpayne@68: self.tkinter_vars = {} # keys: Tkinter event names jpayne@68: # values: Tkinter variable instances jpayne@68: self.top.instance_dict = {} jpayne@68: self.recent_files_path = idleConf.userdir and os.path.join( jpayne@68: idleConf.userdir, 'recent-files.lst') jpayne@68: jpayne@68: self.prompt_last_line = '' # Override in PyShell jpayne@68: self.text_frame = text_frame = Frame(top) jpayne@68: self.vbar = vbar = Scrollbar(text_frame, name='vbar') jpayne@68: width = idleConf.GetOption('main', 'EditorWindow', 'width', type='int') jpayne@68: text_options = { jpayne@68: 'name': 'text', jpayne@68: 'padx': 5, jpayne@68: 'wrap': 'none', jpayne@68: 'highlightthickness': 0, jpayne@68: 'width': width, jpayne@68: 'tabstyle': 'wordprocessor', # new in 8.5 jpayne@68: 'height': idleConf.GetOption( jpayne@68: 'main', 'EditorWindow', 'height', type='int'), jpayne@68: } jpayne@68: self.text = text = MultiCallCreator(Text)(text_frame, **text_options) jpayne@68: self.top.focused_widget = self.text jpayne@68: jpayne@68: self.createmenubar() jpayne@68: self.apply_bindings() jpayne@68: jpayne@68: self.top.protocol("WM_DELETE_WINDOW", self.close) jpayne@68: self.top.bind("<>", self.close_event) jpayne@68: if macosx.isAquaTk(): jpayne@68: # Command-W on editor windows doesn't work without this. jpayne@68: text.bind('<>', self.close_event) jpayne@68: # Some OS X systems have only one mouse button, so use jpayne@68: # control-click for popup context menus there. For two jpayne@68: # buttons, AquaTk defines <2> as the right button, not <3>. jpayne@68: text.bind("",self.right_menu_event) jpayne@68: text.bind("<2>", self.right_menu_event) jpayne@68: else: jpayne@68: # Elsewhere, use right-click for popup menus. jpayne@68: text.bind("<3>",self.right_menu_event) jpayne@68: jpayne@68: text.bind('', wheel_event) jpayne@68: text.bind('', wheel_event) jpayne@68: text.bind('', wheel_event) jpayne@68: text.bind('', self.handle_winconfig) jpayne@68: text.bind("<>", self.cut) jpayne@68: text.bind("<>", self.copy) jpayne@68: text.bind("<>", self.paste) jpayne@68: text.bind("<>", self.center_insert_event) jpayne@68: text.bind("<>", self.help_dialog) jpayne@68: text.bind("<>", self.python_docs) jpayne@68: text.bind("<>", self.about_dialog) jpayne@68: text.bind("<>", self.config_dialog) jpayne@68: text.bind("<>", self.open_module_event) jpayne@68: text.bind("<>", lambda event: "break") jpayne@68: text.bind("<>", self.select_all) jpayne@68: text.bind("<>", self.remove_selection) jpayne@68: text.bind("<>", self.find_event) jpayne@68: text.bind("<>", self.find_again_event) jpayne@68: text.bind("<>", self.find_in_files_event) jpayne@68: text.bind("<>", self.find_selection_event) jpayne@68: text.bind("<>", self.replace_event) jpayne@68: text.bind("<>", self.goto_line_event) jpayne@68: text.bind("<>",self.smart_backspace_event) jpayne@68: text.bind("<>",self.newline_and_indent_event) jpayne@68: text.bind("<>",self.smart_indent_event) jpayne@68: self.fregion = fregion = self.FormatRegion(self) jpayne@68: # self.fregion used in smart_indent_event to access indent_region. jpayne@68: text.bind("<>", fregion.indent_region_event) jpayne@68: text.bind("<>", fregion.dedent_region_event) jpayne@68: text.bind("<>", fregion.comment_region_event) jpayne@68: text.bind("<>", fregion.uncomment_region_event) jpayne@68: text.bind("<>", fregion.tabify_region_event) jpayne@68: text.bind("<>", fregion.untabify_region_event) jpayne@68: indents = self.Indents(self) jpayne@68: text.bind("<>", indents.toggle_tabs_event) jpayne@68: text.bind("<>", indents.change_indentwidth_event) jpayne@68: text.bind("", self.move_at_edge_if_selection(0)) jpayne@68: text.bind("", self.move_at_edge_if_selection(1)) jpayne@68: text.bind("<>", self.del_word_left) jpayne@68: text.bind("<>", self.del_word_right) jpayne@68: text.bind("<>", self.home_callback) jpayne@68: jpayne@68: if flist: jpayne@68: flist.inversedict[self] = key jpayne@68: if key: jpayne@68: flist.dict[key] = self jpayne@68: text.bind("<>", self.new_callback) jpayne@68: text.bind("<>", self.flist.close_all_callback) jpayne@68: text.bind("<>", self.open_module_browser) jpayne@68: text.bind("<>", self.open_path_browser) jpayne@68: text.bind("<>", self.open_turtle_demo) jpayne@68: jpayne@68: self.set_status_bar() jpayne@68: text_frame.pack(side=LEFT, fill=BOTH, expand=1) jpayne@68: text_frame.rowconfigure(1, weight=1) jpayne@68: text_frame.columnconfigure(1, weight=1) jpayne@68: vbar['command'] = self.handle_yview jpayne@68: vbar.grid(row=1, column=2, sticky=NSEW) jpayne@68: text['yscrollcommand'] = vbar.set jpayne@68: text['font'] = idleConf.GetFont(self.root, 'main', 'EditorWindow') jpayne@68: text.grid(row=1, column=1, sticky=NSEW) jpayne@68: text.focus_set() jpayne@68: self.set_width() jpayne@68: jpayne@68: # usetabs true -> literal tab characters are used by indent and jpayne@68: # dedent cmds, possibly mixed with spaces if jpayne@68: # indentwidth is not a multiple of tabwidth, jpayne@68: # which will cause Tabnanny to nag! jpayne@68: # false -> tab characters are converted to spaces by indent jpayne@68: # and dedent cmds, and ditto TAB keystrokes jpayne@68: # Although use-spaces=0 can be configured manually in config-main.def, jpayne@68: # configuration of tabs v. spaces is not supported in the configuration jpayne@68: # dialog. IDLE promotes the preferred Python indentation: use spaces! jpayne@68: usespaces = idleConf.GetOption('main', 'Indent', jpayne@68: 'use-spaces', type='bool') jpayne@68: self.usetabs = not usespaces jpayne@68: jpayne@68: # tabwidth is the display width of a literal tab character. jpayne@68: # CAUTION: telling Tk to use anything other than its default jpayne@68: # tab setting causes it to use an entirely different tabbing algorithm, jpayne@68: # treating tab stops as fixed distances from the left margin. jpayne@68: # Nobody expects this, so for now tabwidth should never be changed. jpayne@68: self.tabwidth = 8 # must remain 8 until Tk is fixed. jpayne@68: jpayne@68: # indentwidth is the number of screen characters per indent level. jpayne@68: # The recommended Python indentation is four spaces. jpayne@68: self.indentwidth = self.tabwidth jpayne@68: self.set_notabs_indentwidth() jpayne@68: jpayne@68: # Store the current value of the insertofftime now so we can restore jpayne@68: # it if needed. jpayne@68: if not hasattr(idleConf, 'blink_off_time'): jpayne@68: idleConf.blink_off_time = self.text['insertofftime'] jpayne@68: self.update_cursor_blink() jpayne@68: jpayne@68: # When searching backwards for a reliable place to begin parsing, jpayne@68: # first start num_context_lines[0] lines back, then jpayne@68: # num_context_lines[1] lines back if that didn't work, and so on. jpayne@68: # The last value should be huge (larger than the # of lines in a jpayne@68: # conceivable file). jpayne@68: # Making the initial values larger slows things down more often. jpayne@68: self.num_context_lines = 50, 500, 5000000 jpayne@68: self.per = per = self.Percolator(text) jpayne@68: self.undo = undo = self.UndoDelegator() jpayne@68: per.insertfilter(undo) jpayne@68: text.undo_block_start = undo.undo_block_start jpayne@68: text.undo_block_stop = undo.undo_block_stop jpayne@68: undo.set_saved_change_hook(self.saved_change_hook) jpayne@68: # IOBinding implements file I/O and printing functionality jpayne@68: self.io = io = self.IOBinding(self) jpayne@68: io.set_filename_change_hook(self.filename_change_hook) jpayne@68: self.good_load = False jpayne@68: self.set_indentation_params(False) jpayne@68: self.color = None # initialized below in self.ResetColorizer jpayne@68: self.code_context = None # optionally initialized later below jpayne@68: self.line_numbers = None # optionally initialized later below jpayne@68: if filename: jpayne@68: if os.path.exists(filename) and not os.path.isdir(filename): jpayne@68: if io.loadfile(filename): jpayne@68: self.good_load = True jpayne@68: is_py_src = self.ispythonsource(filename) jpayne@68: self.set_indentation_params(is_py_src) jpayne@68: else: jpayne@68: io.set_filename(filename) jpayne@68: self.good_load = True jpayne@68: jpayne@68: self.ResetColorizer() jpayne@68: self.saved_change_hook() jpayne@68: self.update_recent_files_list() jpayne@68: self.load_extensions() jpayne@68: menu = self.menudict.get('window') jpayne@68: if menu: jpayne@68: end = menu.index("end") jpayne@68: if end is None: jpayne@68: end = -1 jpayne@68: if end >= 0: jpayne@68: menu.add_separator() jpayne@68: end = end + 1 jpayne@68: self.wmenu_end = end jpayne@68: window.register_callback(self.postwindowsmenu) jpayne@68: jpayne@68: # Some abstractions so IDLE extensions are cross-IDE jpayne@68: self.askyesno = tkMessageBox.askyesno jpayne@68: self.askinteger = tkSimpleDialog.askinteger jpayne@68: self.showerror = tkMessageBox.showerror jpayne@68: jpayne@68: # Add pseudoevents for former extension fixed keys. jpayne@68: # (This probably needs to be done once in the process.) jpayne@68: text.event_add('<>', '') jpayne@68: text.event_add('<>', '', jpayne@68: '', '') jpayne@68: text.event_add('<>', '') jpayne@68: text.event_add('<>', '') jpayne@68: text.event_add('<>', '', jpayne@68: '', '') jpayne@68: jpayne@68: # Former extension bindings depends on frame.text being packed jpayne@68: # (called from self.ResetColorizer()). jpayne@68: autocomplete = self.AutoComplete(self) jpayne@68: text.bind("<>", autocomplete.autocomplete_event) jpayne@68: text.bind("<>", jpayne@68: autocomplete.try_open_completions_event) jpayne@68: text.bind("<>", jpayne@68: autocomplete.force_open_completions_event) jpayne@68: text.bind("<>", self.AutoExpand(self).expand_word_event) jpayne@68: text.bind("<>", jpayne@68: self.FormatParagraph(self).format_paragraph_event) jpayne@68: parenmatch = self.ParenMatch(self) jpayne@68: text.bind("<>", parenmatch.flash_paren_event) jpayne@68: text.bind("<>", parenmatch.paren_closed_event) jpayne@68: scriptbinding = ScriptBinding(self) jpayne@68: text.bind("<>", scriptbinding.check_module_event) jpayne@68: text.bind("<>", scriptbinding.run_module_event) jpayne@68: text.bind("<>", scriptbinding.run_custom_event) jpayne@68: text.bind("<>", self.Rstrip(self).do_rstrip) jpayne@68: ctip = self.Calltip(self) jpayne@68: text.bind("<>", ctip.try_open_calltip_event) jpayne@68: #refresh-calltip must come after paren-closed to work right jpayne@68: text.bind("<>", ctip.refresh_calltip_event) jpayne@68: text.bind("<>", ctip.force_open_calltip_event) jpayne@68: text.bind("<>", self.ZoomHeight(self).zoom_height_event) jpayne@68: if self.allow_code_context: jpayne@68: self.code_context = self.CodeContext(self) jpayne@68: text.bind("<>", jpayne@68: self.code_context.toggle_code_context_event) jpayne@68: else: jpayne@68: self.update_menu_state('options', '*Code Context', 'disabled') jpayne@68: if self.allow_line_numbers: jpayne@68: self.line_numbers = self.LineNumbers(self) jpayne@68: if idleConf.GetOption('main', 'EditorWindow', jpayne@68: 'line-numbers-default', type='bool'): jpayne@68: self.toggle_line_numbers_event() jpayne@68: text.bind("<>", self.toggle_line_numbers_event) jpayne@68: else: jpayne@68: self.update_menu_state('options', '*Line Numbers', 'disabled') jpayne@68: jpayne@68: def handle_winconfig(self, event=None): jpayne@68: self.set_width() jpayne@68: jpayne@68: def set_width(self): jpayne@68: text = self.text jpayne@68: inner_padding = sum(map(text.tk.getint, [text.cget('border'), jpayne@68: text.cget('padx')])) jpayne@68: pixel_width = text.winfo_width() - 2 * inner_padding jpayne@68: jpayne@68: # Divide the width of the Text widget by the font width, jpayne@68: # which is taken to be the width of '0' (zero). jpayne@68: # http://www.tcl.tk/man/tcl8.6/TkCmd/text.htm#M21 jpayne@68: zero_char_width = \ jpayne@68: Font(text, font=text.cget('font')).measure('0') jpayne@68: self.width = pixel_width // zero_char_width jpayne@68: jpayne@68: def new_callback(self, event): jpayne@68: dirname, basename = self.io.defaultfilename() jpayne@68: self.flist.new(dirname) jpayne@68: return "break" jpayne@68: jpayne@68: def home_callback(self, event): jpayne@68: if (event.state & 4) != 0 and event.keysym == "Home": jpayne@68: # state&4==Control. If , use the Tk binding. jpayne@68: return None jpayne@68: if self.text.index("iomark") and \ jpayne@68: self.text.compare("iomark", "<=", "insert lineend") and \ jpayne@68: self.text.compare("insert linestart", "<=", "iomark"): jpayne@68: # In Shell on input line, go to just after prompt jpayne@68: insertpt = int(self.text.index("iomark").split(".")[1]) jpayne@68: else: jpayne@68: line = self.text.get("insert linestart", "insert lineend") jpayne@68: for insertpt in range(len(line)): jpayne@68: if line[insertpt] not in (' ','\t'): jpayne@68: break jpayne@68: else: jpayne@68: insertpt=len(line) jpayne@68: lineat = int(self.text.index("insert").split('.')[1]) jpayne@68: if insertpt == lineat: jpayne@68: insertpt = 0 jpayne@68: dest = "insert linestart+"+str(insertpt)+"c" jpayne@68: if (event.state&1) == 0: jpayne@68: # shift was not pressed jpayne@68: self.text.tag_remove("sel", "1.0", "end") jpayne@68: else: jpayne@68: if not self.text.index("sel.first"): jpayne@68: # there was no previous selection jpayne@68: self.text.mark_set("my_anchor", "insert") jpayne@68: else: jpayne@68: if self.text.compare(self.text.index("sel.first"), "<", jpayne@68: self.text.index("insert")): jpayne@68: self.text.mark_set("my_anchor", "sel.first") # extend back jpayne@68: else: jpayne@68: self.text.mark_set("my_anchor", "sel.last") # extend forward jpayne@68: first = self.text.index(dest) jpayne@68: last = self.text.index("my_anchor") jpayne@68: if self.text.compare(first,">",last): jpayne@68: first,last = last,first jpayne@68: self.text.tag_remove("sel", "1.0", "end") jpayne@68: self.text.tag_add("sel", first, last) jpayne@68: self.text.mark_set("insert", dest) jpayne@68: self.text.see("insert") jpayne@68: return "break" jpayne@68: jpayne@68: def set_status_bar(self): jpayne@68: self.status_bar = self.MultiStatusBar(self.top) jpayne@68: sep = Frame(self.top, height=1, borderwidth=1, background='grey75') jpayne@68: if sys.platform == "darwin": jpayne@68: # Insert some padding to avoid obscuring some of the statusbar jpayne@68: # by the resize widget. jpayne@68: self.status_bar.set_label('_padding1', ' ', side=RIGHT) jpayne@68: self.status_bar.set_label('column', 'Col: ?', side=RIGHT) jpayne@68: self.status_bar.set_label('line', 'Ln: ?', side=RIGHT) jpayne@68: self.status_bar.pack(side=BOTTOM, fill=X) jpayne@68: sep.pack(side=BOTTOM, fill=X) jpayne@68: self.text.bind("<>", self.set_line_and_column) jpayne@68: self.text.event_add("<>", jpayne@68: "", "") jpayne@68: self.text.after_idle(self.set_line_and_column) jpayne@68: jpayne@68: def set_line_and_column(self, event=None): jpayne@68: line, column = self.text.index(INSERT).split('.') jpayne@68: self.status_bar.set_label('column', 'Col: %s' % column) jpayne@68: self.status_bar.set_label('line', 'Ln: %s' % line) jpayne@68: jpayne@68: menu_specs = [ jpayne@68: ("file", "_File"), jpayne@68: ("edit", "_Edit"), jpayne@68: ("format", "F_ormat"), jpayne@68: ("run", "_Run"), jpayne@68: ("options", "_Options"), jpayne@68: ("window", "_Window"), jpayne@68: ("help", "_Help"), jpayne@68: ] jpayne@68: jpayne@68: jpayne@68: def createmenubar(self): jpayne@68: mbar = self.menubar jpayne@68: self.menudict = menudict = {} jpayne@68: for name, label in self.menu_specs: jpayne@68: underline, label = prepstr(label) jpayne@68: menudict[name] = menu = Menu(mbar, name=name, tearoff=0) jpayne@68: mbar.add_cascade(label=label, menu=menu, underline=underline) jpayne@68: if macosx.isCarbonTk(): jpayne@68: # Insert the application menu jpayne@68: menudict['application'] = menu = Menu(mbar, name='apple', jpayne@68: tearoff=0) jpayne@68: mbar.add_cascade(label='IDLE', menu=menu) jpayne@68: self.fill_menus() jpayne@68: self.recent_files_menu = Menu(self.menubar, tearoff=0) jpayne@68: self.menudict['file'].insert_cascade(3, label='Recent Files', jpayne@68: underline=0, jpayne@68: menu=self.recent_files_menu) jpayne@68: self.base_helpmenu_length = self.menudict['help'].index(END) jpayne@68: self.reset_help_menu_entries() jpayne@68: jpayne@68: def postwindowsmenu(self): jpayne@68: # Only called when Window menu exists jpayne@68: menu = self.menudict['window'] jpayne@68: end = menu.index("end") jpayne@68: if end is None: jpayne@68: end = -1 jpayne@68: if end > self.wmenu_end: jpayne@68: menu.delete(self.wmenu_end+1, end) jpayne@68: window.add_windows_to_menu(menu) jpayne@68: jpayne@68: def update_menu_label(self, menu, index, label): jpayne@68: "Update label for menu item at index." jpayne@68: menuitem = self.menudict[menu] jpayne@68: menuitem.entryconfig(index, label=label) jpayne@68: jpayne@68: def update_menu_state(self, menu, index, state): jpayne@68: "Update state for menu item at index." jpayne@68: menuitem = self.menudict[menu] jpayne@68: menuitem.entryconfig(index, state=state) jpayne@68: jpayne@68: def handle_yview(self, event, *args): jpayne@68: "Handle scrollbar." jpayne@68: if event == 'moveto': jpayne@68: fraction = float(args[0]) jpayne@68: lines = (round(self.getlineno('end') * fraction) - jpayne@68: self.getlineno('@0,0')) jpayne@68: event = 'scroll' jpayne@68: args = (lines, 'units') jpayne@68: self.text.yview(event, *args) jpayne@68: return 'break' jpayne@68: jpayne@68: rmenu = None jpayne@68: jpayne@68: def right_menu_event(self, event): jpayne@68: self.text.mark_set("insert", "@%d,%d" % (event.x, event.y)) jpayne@68: if not self.rmenu: jpayne@68: self.make_rmenu() jpayne@68: rmenu = self.rmenu jpayne@68: self.event = event jpayne@68: iswin = sys.platform[:3] == 'win' jpayne@68: if iswin: jpayne@68: self.text.config(cursor="arrow") jpayne@68: jpayne@68: for item in self.rmenu_specs: jpayne@68: try: jpayne@68: label, eventname, verify_state = item jpayne@68: except ValueError: # see issue1207589 jpayne@68: continue jpayne@68: jpayne@68: if verify_state is None: jpayne@68: continue jpayne@68: state = getattr(self, verify_state)() jpayne@68: rmenu.entryconfigure(label, state=state) jpayne@68: jpayne@68: jpayne@68: rmenu.tk_popup(event.x_root, event.y_root) jpayne@68: if iswin: jpayne@68: self.text.config(cursor="ibeam") jpayne@68: return "break" jpayne@68: jpayne@68: rmenu_specs = [ jpayne@68: # ("Label", "<>", "statefuncname"), ... jpayne@68: ("Close", "<>", None), # Example jpayne@68: ] jpayne@68: jpayne@68: def make_rmenu(self): jpayne@68: rmenu = Menu(self.text, tearoff=0) jpayne@68: for item in self.rmenu_specs: jpayne@68: label, eventname = item[0], item[1] jpayne@68: if label is not None: jpayne@68: def command(text=self.text, eventname=eventname): jpayne@68: text.event_generate(eventname) jpayne@68: rmenu.add_command(label=label, command=command) jpayne@68: else: jpayne@68: rmenu.add_separator() jpayne@68: self.rmenu = rmenu jpayne@68: jpayne@68: def rmenu_check_cut(self): jpayne@68: return self.rmenu_check_copy() jpayne@68: jpayne@68: def rmenu_check_copy(self): jpayne@68: try: jpayne@68: indx = self.text.index('sel.first') jpayne@68: except TclError: jpayne@68: return 'disabled' jpayne@68: else: jpayne@68: return 'normal' if indx else 'disabled' jpayne@68: jpayne@68: def rmenu_check_paste(self): jpayne@68: try: jpayne@68: self.text.tk.call('tk::GetSelection', self.text, 'CLIPBOARD') jpayne@68: except TclError: jpayne@68: return 'disabled' jpayne@68: else: jpayne@68: return 'normal' jpayne@68: jpayne@68: def about_dialog(self, event=None): jpayne@68: "Handle Help 'About IDLE' event." jpayne@68: # Synchronize with macosx.overrideRootMenu.about_dialog. jpayne@68: help_about.AboutDialog(self.top) jpayne@68: return "break" jpayne@68: jpayne@68: def config_dialog(self, event=None): jpayne@68: "Handle Options 'Configure IDLE' event." jpayne@68: # Synchronize with macosx.overrideRootMenu.config_dialog. jpayne@68: configdialog.ConfigDialog(self.top,'Settings') jpayne@68: return "break" jpayne@68: jpayne@68: def help_dialog(self, event=None): jpayne@68: "Handle Help 'IDLE Help' event." jpayne@68: # Synchronize with macosx.overrideRootMenu.help_dialog. jpayne@68: if self.root: jpayne@68: parent = self.root jpayne@68: else: jpayne@68: parent = self.top jpayne@68: help.show_idlehelp(parent) jpayne@68: return "break" jpayne@68: jpayne@68: def python_docs(self, event=None): jpayne@68: if sys.platform[:3] == 'win': jpayne@68: try: jpayne@68: os.startfile(self.help_url) jpayne@68: except OSError as why: jpayne@68: tkMessageBox.showerror(title='Document Start Failure', jpayne@68: message=str(why), parent=self.text) jpayne@68: else: jpayne@68: webbrowser.open(self.help_url) jpayne@68: return "break" jpayne@68: jpayne@68: def cut(self,event): jpayne@68: self.text.event_generate("<>") jpayne@68: return "break" jpayne@68: jpayne@68: def copy(self,event): jpayne@68: if not self.text.tag_ranges("sel"): jpayne@68: # There is no selection, so do nothing and maybe interrupt. jpayne@68: return None jpayne@68: self.text.event_generate("<>") jpayne@68: return "break" jpayne@68: jpayne@68: def paste(self,event): jpayne@68: self.text.event_generate("<>") jpayne@68: self.text.see("insert") jpayne@68: return "break" jpayne@68: jpayne@68: def select_all(self, event=None): jpayne@68: self.text.tag_add("sel", "1.0", "end-1c") jpayne@68: self.text.mark_set("insert", "1.0") jpayne@68: self.text.see("insert") jpayne@68: return "break" jpayne@68: jpayne@68: def remove_selection(self, event=None): jpayne@68: self.text.tag_remove("sel", "1.0", "end") jpayne@68: self.text.see("insert") jpayne@68: return "break" jpayne@68: jpayne@68: def move_at_edge_if_selection(self, edge_index): jpayne@68: """Cursor move begins at start or end of selection jpayne@68: jpayne@68: When a left/right cursor key is pressed create and return to Tkinter a jpayne@68: function which causes a cursor move from the associated edge of the jpayne@68: selection. jpayne@68: jpayne@68: """ jpayne@68: self_text_index = self.text.index jpayne@68: self_text_mark_set = self.text.mark_set jpayne@68: edges_table = ("sel.first+1c", "sel.last-1c") jpayne@68: def move_at_edge(event): jpayne@68: if (event.state & 5) == 0: # no shift(==1) or control(==4) pressed jpayne@68: try: jpayne@68: self_text_index("sel.first") jpayne@68: self_text_mark_set("insert", edges_table[edge_index]) jpayne@68: except TclError: jpayne@68: pass jpayne@68: return move_at_edge jpayne@68: jpayne@68: def del_word_left(self, event): jpayne@68: self.text.event_generate('') jpayne@68: return "break" jpayne@68: jpayne@68: def del_word_right(self, event): jpayne@68: self.text.event_generate('') jpayne@68: return "break" jpayne@68: jpayne@68: def find_event(self, event): jpayne@68: search.find(self.text) jpayne@68: return "break" jpayne@68: jpayne@68: def find_again_event(self, event): jpayne@68: search.find_again(self.text) jpayne@68: return "break" jpayne@68: jpayne@68: def find_selection_event(self, event): jpayne@68: search.find_selection(self.text) jpayne@68: return "break" jpayne@68: jpayne@68: def find_in_files_event(self, event): jpayne@68: grep.grep(self.text, self.io, self.flist) jpayne@68: return "break" jpayne@68: jpayne@68: def replace_event(self, event): jpayne@68: replace.replace(self.text) jpayne@68: return "break" jpayne@68: jpayne@68: def goto_line_event(self, event): jpayne@68: text = self.text jpayne@68: lineno = tkSimpleDialog.askinteger("Goto", jpayne@68: "Go to line number:",parent=text) jpayne@68: if lineno is None: jpayne@68: return "break" jpayne@68: if lineno <= 0: jpayne@68: text.bell() jpayne@68: return "break" jpayne@68: text.mark_set("insert", "%d.0" % lineno) jpayne@68: text.see("insert") jpayne@68: return "break" jpayne@68: jpayne@68: def open_module(self): jpayne@68: """Get module name from user and open it. jpayne@68: jpayne@68: Return module path or None for calls by open_module_browser jpayne@68: when latter is not invoked in named editor window. jpayne@68: """ jpayne@68: # XXX This, open_module_browser, and open_path_browser jpayne@68: # would fit better in iomenu.IOBinding. jpayne@68: try: jpayne@68: name = self.text.get("sel.first", "sel.last").strip() jpayne@68: except TclError: jpayne@68: name = '' jpayne@68: file_path = query.ModuleName( jpayne@68: self.text, "Open Module", jpayne@68: "Enter the name of a Python module\n" jpayne@68: "to search on sys.path and open:", jpayne@68: name).result jpayne@68: if file_path is not None: jpayne@68: if self.flist: jpayne@68: self.flist.open(file_path) jpayne@68: else: jpayne@68: self.io.loadfile(file_path) jpayne@68: return file_path jpayne@68: jpayne@68: def open_module_event(self, event): jpayne@68: self.open_module() jpayne@68: return "break" jpayne@68: jpayne@68: def open_module_browser(self, event=None): jpayne@68: filename = self.io.filename jpayne@68: if not (self.__class__.__name__ == 'PyShellEditorWindow' jpayne@68: and filename): jpayne@68: filename = self.open_module() jpayne@68: if filename is None: jpayne@68: return "break" jpayne@68: from idlelib import browser jpayne@68: browser.ModuleBrowser(self.root, filename) jpayne@68: return "break" jpayne@68: jpayne@68: def open_path_browser(self, event=None): jpayne@68: from idlelib import pathbrowser jpayne@68: pathbrowser.PathBrowser(self.root) jpayne@68: return "break" jpayne@68: jpayne@68: def open_turtle_demo(self, event = None): jpayne@68: import subprocess jpayne@68: jpayne@68: cmd = [sys.executable, jpayne@68: '-c', jpayne@68: 'from turtledemo.__main__ import main; main()'] jpayne@68: subprocess.Popen(cmd, shell=False) jpayne@68: return "break" jpayne@68: jpayne@68: def gotoline(self, lineno): jpayne@68: if lineno is not None and lineno > 0: jpayne@68: self.text.mark_set("insert", "%d.0" % lineno) jpayne@68: self.text.tag_remove("sel", "1.0", "end") jpayne@68: self.text.tag_add("sel", "insert", "insert +1l") jpayne@68: self.center() jpayne@68: jpayne@68: def ispythonsource(self, filename): jpayne@68: if not filename or os.path.isdir(filename): jpayne@68: return True jpayne@68: base, ext = os.path.splitext(os.path.basename(filename)) jpayne@68: if os.path.normcase(ext) in (".py", ".pyw"): jpayne@68: return True jpayne@68: line = self.text.get('1.0', '1.0 lineend') jpayne@68: return line.startswith('#!') and 'python' in line jpayne@68: jpayne@68: def close_hook(self): jpayne@68: if self.flist: jpayne@68: self.flist.unregister_maybe_terminate(self) jpayne@68: self.flist = None jpayne@68: jpayne@68: def set_close_hook(self, close_hook): jpayne@68: self.close_hook = close_hook jpayne@68: jpayne@68: def filename_change_hook(self): jpayne@68: if self.flist: jpayne@68: self.flist.filename_changed_edit(self) jpayne@68: self.saved_change_hook() jpayne@68: self.top.update_windowlist_registry(self) jpayne@68: self.ResetColorizer() jpayne@68: jpayne@68: def _addcolorizer(self): jpayne@68: if self.color: jpayne@68: return jpayne@68: if self.ispythonsource(self.io.filename): jpayne@68: self.color = self.ColorDelegator() jpayne@68: # can add more colorizers here... jpayne@68: if self.color: jpayne@68: self.per.removefilter(self.undo) jpayne@68: self.per.insertfilter(self.color) jpayne@68: self.per.insertfilter(self.undo) jpayne@68: jpayne@68: def _rmcolorizer(self): jpayne@68: if not self.color: jpayne@68: return jpayne@68: self.color.removecolors() jpayne@68: self.per.removefilter(self.color) jpayne@68: self.color = None jpayne@68: jpayne@68: def ResetColorizer(self): jpayne@68: "Update the color theme" jpayne@68: # Called from self.filename_change_hook and from configdialog.py jpayne@68: self._rmcolorizer() jpayne@68: self._addcolorizer() jpayne@68: EditorWindow.color_config(self.text) jpayne@68: jpayne@68: if self.code_context is not None: jpayne@68: self.code_context.update_highlight_colors() jpayne@68: jpayne@68: if self.line_numbers is not None: jpayne@68: self.line_numbers.update_colors() jpayne@68: jpayne@68: IDENTCHARS = string.ascii_letters + string.digits + "_" jpayne@68: jpayne@68: def colorize_syntax_error(self, text, pos): jpayne@68: text.tag_add("ERROR", pos) jpayne@68: char = text.get(pos) jpayne@68: if char and char in self.IDENTCHARS: jpayne@68: text.tag_add("ERROR", pos + " wordstart", pos) jpayne@68: if '\n' == text.get(pos): # error at line end jpayne@68: text.mark_set("insert", pos) jpayne@68: else: jpayne@68: text.mark_set("insert", pos + "+1c") jpayne@68: text.see(pos) jpayne@68: jpayne@68: def update_cursor_blink(self): jpayne@68: "Update the cursor blink configuration." jpayne@68: cursorblink = idleConf.GetOption( jpayne@68: 'main', 'EditorWindow', 'cursor-blink', type='bool') jpayne@68: if not cursorblink: jpayne@68: self.text['insertofftime'] = 0 jpayne@68: else: jpayne@68: # Restore the original value jpayne@68: self.text['insertofftime'] = idleConf.blink_off_time jpayne@68: jpayne@68: def ResetFont(self): jpayne@68: "Update the text widgets' font if it is changed" jpayne@68: # Called from configdialog.py jpayne@68: jpayne@68: # Update the code context widget first, since its height affects jpayne@68: # the height of the text widget. This avoids double re-rendering. jpayne@68: if self.code_context is not None: jpayne@68: self.code_context.update_font() jpayne@68: # Next, update the line numbers widget, since its width affects jpayne@68: # the width of the text widget. jpayne@68: if self.line_numbers is not None: jpayne@68: self.line_numbers.update_font() jpayne@68: # Finally, update the main text widget. jpayne@68: new_font = idleConf.GetFont(self.root, 'main', 'EditorWindow') jpayne@68: self.text['font'] = new_font jpayne@68: self.set_width() jpayne@68: jpayne@68: def RemoveKeybindings(self): jpayne@68: "Remove the keybindings before they are changed." jpayne@68: # Called from configdialog.py jpayne@68: self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet() jpayne@68: for event, keylist in keydefs.items(): jpayne@68: self.text.event_delete(event, *keylist) jpayne@68: for extensionName in self.get_standard_extension_names(): jpayne@68: xkeydefs = idleConf.GetExtensionBindings(extensionName) jpayne@68: if xkeydefs: jpayne@68: for event, keylist in xkeydefs.items(): jpayne@68: self.text.event_delete(event, *keylist) jpayne@68: jpayne@68: def ApplyKeybindings(self): jpayne@68: "Update the keybindings after they are changed" jpayne@68: # Called from configdialog.py jpayne@68: self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet() jpayne@68: self.apply_bindings() jpayne@68: for extensionName in self.get_standard_extension_names(): jpayne@68: xkeydefs = idleConf.GetExtensionBindings(extensionName) jpayne@68: if xkeydefs: jpayne@68: self.apply_bindings(xkeydefs) jpayne@68: #update menu accelerators jpayne@68: menuEventDict = {} jpayne@68: for menu in self.mainmenu.menudefs: jpayne@68: menuEventDict[menu[0]] = {} jpayne@68: for item in menu[1]: jpayne@68: if item: jpayne@68: menuEventDict[menu[0]][prepstr(item[0])[1]] = item[1] jpayne@68: for menubarItem in self.menudict: jpayne@68: menu = self.menudict[menubarItem] jpayne@68: end = menu.index(END) jpayne@68: if end is None: jpayne@68: # Skip empty menus jpayne@68: continue jpayne@68: end += 1 jpayne@68: for index in range(0, end): jpayne@68: if menu.type(index) == 'command': jpayne@68: accel = menu.entrycget(index, 'accelerator') jpayne@68: if accel: jpayne@68: itemName = menu.entrycget(index, 'label') jpayne@68: event = '' jpayne@68: if menubarItem in menuEventDict: jpayne@68: if itemName in menuEventDict[menubarItem]: jpayne@68: event = menuEventDict[menubarItem][itemName] jpayne@68: if event: jpayne@68: accel = get_accelerator(keydefs, event) jpayne@68: menu.entryconfig(index, accelerator=accel) jpayne@68: jpayne@68: def set_notabs_indentwidth(self): jpayne@68: "Update the indentwidth if changed and not using tabs in this window" jpayne@68: # Called from configdialog.py jpayne@68: if not self.usetabs: jpayne@68: self.indentwidth = idleConf.GetOption('main', 'Indent','num-spaces', jpayne@68: type='int') jpayne@68: jpayne@68: def reset_help_menu_entries(self): jpayne@68: "Update the additional help entries on the Help menu" jpayne@68: help_list = idleConf.GetAllExtraHelpSourcesList() jpayne@68: helpmenu = self.menudict['help'] jpayne@68: # first delete the extra help entries, if any jpayne@68: helpmenu_length = helpmenu.index(END) jpayne@68: if helpmenu_length > self.base_helpmenu_length: jpayne@68: helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length) jpayne@68: # then rebuild them jpayne@68: if help_list: jpayne@68: helpmenu.add_separator() jpayne@68: for entry in help_list: jpayne@68: cmd = self.__extra_help_callback(entry[1]) jpayne@68: helpmenu.add_command(label=entry[0], command=cmd) jpayne@68: # and update the menu dictionary jpayne@68: self.menudict['help'] = helpmenu jpayne@68: jpayne@68: def __extra_help_callback(self, helpfile): jpayne@68: "Create a callback with the helpfile value frozen at definition time" jpayne@68: def display_extra_help(helpfile=helpfile): jpayne@68: if not helpfile.startswith(('www', 'http')): jpayne@68: helpfile = os.path.normpath(helpfile) jpayne@68: if sys.platform[:3] == 'win': jpayne@68: try: jpayne@68: os.startfile(helpfile) jpayne@68: except OSError as why: jpayne@68: tkMessageBox.showerror(title='Document Start Failure', jpayne@68: message=str(why), parent=self.text) jpayne@68: else: jpayne@68: webbrowser.open(helpfile) jpayne@68: return display_extra_help jpayne@68: jpayne@68: def update_recent_files_list(self, new_file=None): jpayne@68: "Load and update the recent files list and menus" jpayne@68: # TODO: move to iomenu. jpayne@68: rf_list = [] jpayne@68: file_path = self.recent_files_path jpayne@68: if file_path and os.path.exists(file_path): jpayne@68: with open(file_path, 'r', jpayne@68: encoding='utf_8', errors='replace') as rf_list_file: jpayne@68: rf_list = rf_list_file.readlines() jpayne@68: if new_file: jpayne@68: new_file = os.path.abspath(new_file) + '\n' jpayne@68: if new_file in rf_list: jpayne@68: rf_list.remove(new_file) # move to top jpayne@68: rf_list.insert(0, new_file) jpayne@68: # clean and save the recent files list jpayne@68: bad_paths = [] jpayne@68: for path in rf_list: jpayne@68: if '\0' in path or not os.path.exists(path[0:-1]): jpayne@68: bad_paths.append(path) jpayne@68: rf_list = [path for path in rf_list if path not in bad_paths] jpayne@68: ulchars = "1234567890ABCDEFGHIJK" jpayne@68: rf_list = rf_list[0:len(ulchars)] jpayne@68: if file_path: jpayne@68: try: jpayne@68: with open(file_path, 'w', jpayne@68: encoding='utf_8', errors='replace') as rf_file: jpayne@68: rf_file.writelines(rf_list) jpayne@68: except OSError as err: jpayne@68: if not getattr(self.root, "recentfiles_message", False): jpayne@68: self.root.recentfiles_message = True jpayne@68: tkMessageBox.showwarning(title='IDLE Warning', jpayne@68: message="Cannot save Recent Files list to disk.\n" jpayne@68: f" {err}\n" jpayne@68: "Select OK to continue.", jpayne@68: parent=self.text) jpayne@68: # for each edit window instance, construct the recent files menu jpayne@68: for instance in self.top.instance_dict: jpayne@68: menu = instance.recent_files_menu jpayne@68: menu.delete(0, END) # clear, and rebuild: jpayne@68: for i, file_name in enumerate(rf_list): jpayne@68: file_name = file_name.rstrip() # zap \n jpayne@68: callback = instance.__recent_file_callback(file_name) jpayne@68: menu.add_command(label=ulchars[i] + " " + file_name, jpayne@68: command=callback, jpayne@68: underline=0) jpayne@68: jpayne@68: def __recent_file_callback(self, file_name): jpayne@68: def open_recent_file(fn_closure=file_name): jpayne@68: self.io.open(editFile=fn_closure) jpayne@68: return open_recent_file jpayne@68: jpayne@68: def saved_change_hook(self): jpayne@68: short = self.short_title() jpayne@68: long = self.long_title() jpayne@68: if short and long: jpayne@68: title = short + " - " + long + _py_version jpayne@68: elif short: jpayne@68: title = short jpayne@68: elif long: jpayne@68: title = long jpayne@68: else: jpayne@68: title = "untitled" jpayne@68: icon = short or long or title jpayne@68: if not self.get_saved(): jpayne@68: title = "*%s*" % title jpayne@68: icon = "*%s" % icon jpayne@68: self.top.wm_title(title) jpayne@68: self.top.wm_iconname(icon) jpayne@68: jpayne@68: def get_saved(self): jpayne@68: return self.undo.get_saved() jpayne@68: jpayne@68: def set_saved(self, flag): jpayne@68: self.undo.set_saved(flag) jpayne@68: jpayne@68: def reset_undo(self): jpayne@68: self.undo.reset_undo() jpayne@68: jpayne@68: def short_title(self): jpayne@68: filename = self.io.filename jpayne@68: return os.path.basename(filename) if filename else "untitled" jpayne@68: jpayne@68: def long_title(self): jpayne@68: return self.io.filename or "" jpayne@68: jpayne@68: def center_insert_event(self, event): jpayne@68: self.center() jpayne@68: return "break" jpayne@68: jpayne@68: def center(self, mark="insert"): jpayne@68: text = self.text jpayne@68: top, bot = self.getwindowlines() jpayne@68: lineno = self.getlineno(mark) jpayne@68: height = bot - top jpayne@68: newtop = max(1, lineno - height//2) jpayne@68: text.yview(float(newtop)) jpayne@68: jpayne@68: def getwindowlines(self): jpayne@68: text = self.text jpayne@68: top = self.getlineno("@0,0") jpayne@68: bot = self.getlineno("@0,65535") jpayne@68: if top == bot and text.winfo_height() == 1: jpayne@68: # Geometry manager hasn't run yet jpayne@68: height = int(text['height']) jpayne@68: bot = top + height - 1 jpayne@68: return top, bot jpayne@68: jpayne@68: def getlineno(self, mark="insert"): jpayne@68: text = self.text jpayne@68: return int(float(text.index(mark))) jpayne@68: jpayne@68: def get_geometry(self): jpayne@68: "Return (width, height, x, y)" jpayne@68: geom = self.top.wm_geometry() jpayne@68: m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom) jpayne@68: return list(map(int, m.groups())) jpayne@68: jpayne@68: def close_event(self, event): jpayne@68: self.close() jpayne@68: return "break" jpayne@68: jpayne@68: def maybesave(self): jpayne@68: if self.io: jpayne@68: if not self.get_saved(): jpayne@68: if self.top.state()!='normal': jpayne@68: self.top.deiconify() jpayne@68: self.top.lower() jpayne@68: self.top.lift() jpayne@68: return self.io.maybesave() jpayne@68: jpayne@68: def close(self): jpayne@68: try: jpayne@68: reply = self.maybesave() jpayne@68: if str(reply) != "cancel": jpayne@68: self._close() jpayne@68: return reply jpayne@68: except AttributeError: # bpo-35379: close called twice jpayne@68: pass jpayne@68: jpayne@68: def _close(self): jpayne@68: if self.io.filename: jpayne@68: self.update_recent_files_list(new_file=self.io.filename) jpayne@68: window.unregister_callback(self.postwindowsmenu) jpayne@68: self.unload_extensions() jpayne@68: self.io.close() jpayne@68: self.io = None jpayne@68: self.undo = None jpayne@68: if self.color: jpayne@68: self.color.close() jpayne@68: self.color = None jpayne@68: self.text = None jpayne@68: self.tkinter_vars = None jpayne@68: self.per.close() jpayne@68: self.per = None jpayne@68: self.top.destroy() jpayne@68: if self.close_hook: jpayne@68: # unless override: unregister from flist, terminate if last window jpayne@68: self.close_hook() jpayne@68: jpayne@68: def load_extensions(self): jpayne@68: self.extensions = {} jpayne@68: self.load_standard_extensions() jpayne@68: jpayne@68: def unload_extensions(self): jpayne@68: for ins in list(self.extensions.values()): jpayne@68: if hasattr(ins, "close"): jpayne@68: ins.close() jpayne@68: self.extensions = {} jpayne@68: jpayne@68: def load_standard_extensions(self): jpayne@68: for name in self.get_standard_extension_names(): jpayne@68: try: jpayne@68: self.load_extension(name) jpayne@68: except: jpayne@68: print("Failed to load extension", repr(name)) jpayne@68: traceback.print_exc() jpayne@68: jpayne@68: def get_standard_extension_names(self): jpayne@68: return idleConf.GetExtensions(editor_only=True) jpayne@68: jpayne@68: extfiles = { # Map built-in config-extension section names to file names. jpayne@68: 'ZzDummy': 'zzdummy', jpayne@68: } jpayne@68: jpayne@68: def load_extension(self, name): jpayne@68: fname = self.extfiles.get(name, name) jpayne@68: try: jpayne@68: try: jpayne@68: mod = importlib.import_module('.' + fname, package=__package__) jpayne@68: except (ImportError, TypeError): jpayne@68: mod = importlib.import_module(fname) jpayne@68: except ImportError: jpayne@68: print("\nFailed to import extension: ", name) jpayne@68: raise jpayne@68: cls = getattr(mod, name) jpayne@68: keydefs = idleConf.GetExtensionBindings(name) jpayne@68: if hasattr(cls, "menudefs"): jpayne@68: self.fill_menus(cls.menudefs, keydefs) jpayne@68: ins = cls(self) jpayne@68: self.extensions[name] = ins jpayne@68: if keydefs: jpayne@68: self.apply_bindings(keydefs) jpayne@68: for vevent in keydefs: jpayne@68: methodname = vevent.replace("-", "_") jpayne@68: while methodname[:1] == '<': jpayne@68: methodname = methodname[1:] jpayne@68: while methodname[-1:] == '>': jpayne@68: methodname = methodname[:-1] jpayne@68: methodname = methodname + "_event" jpayne@68: if hasattr(ins, methodname): jpayne@68: self.text.bind(vevent, getattr(ins, methodname)) jpayne@68: jpayne@68: def apply_bindings(self, keydefs=None): jpayne@68: if keydefs is None: jpayne@68: keydefs = self.mainmenu.default_keydefs jpayne@68: text = self.text jpayne@68: text.keydefs = keydefs jpayne@68: for event, keylist in keydefs.items(): jpayne@68: if keylist: jpayne@68: text.event_add(event, *keylist) jpayne@68: jpayne@68: def fill_menus(self, menudefs=None, keydefs=None): jpayne@68: """Add appropriate entries to the menus and submenus jpayne@68: jpayne@68: Menus that are absent or None in self.menudict are ignored. jpayne@68: """ jpayne@68: if menudefs is None: jpayne@68: menudefs = self.mainmenu.menudefs jpayne@68: if keydefs is None: jpayne@68: keydefs = self.mainmenu.default_keydefs jpayne@68: menudict = self.menudict jpayne@68: text = self.text jpayne@68: for mname, entrylist in menudefs: jpayne@68: menu = menudict.get(mname) jpayne@68: if not menu: jpayne@68: continue jpayne@68: for entry in entrylist: jpayne@68: if not entry: jpayne@68: menu.add_separator() jpayne@68: else: jpayne@68: label, eventname = entry jpayne@68: checkbutton = (label[:1] == '!') jpayne@68: if checkbutton: jpayne@68: label = label[1:] jpayne@68: underline, label = prepstr(label) jpayne@68: accelerator = get_accelerator(keydefs, eventname) jpayne@68: def command(text=text, eventname=eventname): jpayne@68: text.event_generate(eventname) jpayne@68: if checkbutton: jpayne@68: var = self.get_var_obj(eventname, BooleanVar) jpayne@68: menu.add_checkbutton(label=label, underline=underline, jpayne@68: command=command, accelerator=accelerator, jpayne@68: variable=var) jpayne@68: else: jpayne@68: menu.add_command(label=label, underline=underline, jpayne@68: command=command, jpayne@68: accelerator=accelerator) jpayne@68: jpayne@68: def getvar(self, name): jpayne@68: var = self.get_var_obj(name) jpayne@68: if var: jpayne@68: value = var.get() jpayne@68: return value jpayne@68: else: jpayne@68: raise NameError(name) jpayne@68: jpayne@68: def setvar(self, name, value, vartype=None): jpayne@68: var = self.get_var_obj(name, vartype) jpayne@68: if var: jpayne@68: var.set(value) jpayne@68: else: jpayne@68: raise NameError(name) jpayne@68: jpayne@68: def get_var_obj(self, name, vartype=None): jpayne@68: var = self.tkinter_vars.get(name) jpayne@68: if not var and vartype: jpayne@68: # create a Tkinter variable object with self.text as master: jpayne@68: self.tkinter_vars[name] = var = vartype(self.text) jpayne@68: return var jpayne@68: jpayne@68: # Tk implementations of "virtual text methods" -- each platform jpayne@68: # reusing IDLE's support code needs to define these for its GUI's jpayne@68: # flavor of widget. jpayne@68: jpayne@68: # Is character at text_index in a Python string? Return 0 for jpayne@68: # "guaranteed no", true for anything else. This info is expensive jpayne@68: # to compute ab initio, but is probably already known by the jpayne@68: # platform's colorizer. jpayne@68: jpayne@68: def is_char_in_string(self, text_index): jpayne@68: if self.color: jpayne@68: # Return true iff colorizer hasn't (re)gotten this far jpayne@68: # yet, or the character is tagged as being in a string jpayne@68: return self.text.tag_prevrange("TODO", text_index) or \ jpayne@68: "STRING" in self.text.tag_names(text_index) jpayne@68: else: jpayne@68: # The colorizer is missing: assume the worst jpayne@68: return 1 jpayne@68: jpayne@68: # If a selection is defined in the text widget, return (start, jpayne@68: # end) as Tkinter text indices, otherwise return (None, None) jpayne@68: def get_selection_indices(self): jpayne@68: try: jpayne@68: first = self.text.index("sel.first") jpayne@68: last = self.text.index("sel.last") jpayne@68: return first, last jpayne@68: except TclError: jpayne@68: return None, None jpayne@68: jpayne@68: # Return the text widget's current view of what a tab stop means jpayne@68: # (equivalent width in spaces). jpayne@68: jpayne@68: def get_tk_tabwidth(self): jpayne@68: current = self.text['tabs'] or TK_TABWIDTH_DEFAULT jpayne@68: return int(current) jpayne@68: jpayne@68: # Set the text widget's current view of what a tab stop means. jpayne@68: jpayne@68: def set_tk_tabwidth(self, newtabwidth): jpayne@68: text = self.text jpayne@68: if self.get_tk_tabwidth() != newtabwidth: jpayne@68: # Set text widget tab width jpayne@68: pixels = text.tk.call("font", "measure", text["font"], jpayne@68: "-displayof", text.master, jpayne@68: "n" * newtabwidth) jpayne@68: text.configure(tabs=pixels) jpayne@68: jpayne@68: ### begin autoindent code ### (configuration was moved to beginning of class) jpayne@68: jpayne@68: def set_indentation_params(self, is_py_src, guess=True): jpayne@68: if is_py_src and guess: jpayne@68: i = self.guess_indent() jpayne@68: if 2 <= i <= 8: jpayne@68: self.indentwidth = i jpayne@68: if self.indentwidth != self.tabwidth: jpayne@68: self.usetabs = False jpayne@68: self.set_tk_tabwidth(self.tabwidth) jpayne@68: jpayne@68: def smart_backspace_event(self, event): jpayne@68: text = self.text jpayne@68: first, last = self.get_selection_indices() jpayne@68: if first and last: jpayne@68: text.delete(first, last) jpayne@68: text.mark_set("insert", first) jpayne@68: return "break" jpayne@68: # Delete whitespace left, until hitting a real char or closest jpayne@68: # preceding virtual tab stop. jpayne@68: chars = text.get("insert linestart", "insert") jpayne@68: if chars == '': jpayne@68: if text.compare("insert", ">", "1.0"): jpayne@68: # easy: delete preceding newline jpayne@68: text.delete("insert-1c") jpayne@68: else: jpayne@68: text.bell() # at start of buffer jpayne@68: return "break" jpayne@68: if chars[-1] not in " \t": jpayne@68: # easy: delete preceding real char jpayne@68: text.delete("insert-1c") jpayne@68: return "break" jpayne@68: # Ick. It may require *inserting* spaces if we back up over a jpayne@68: # tab character! This is written to be clear, not fast. jpayne@68: tabwidth = self.tabwidth jpayne@68: have = len(chars.expandtabs(tabwidth)) jpayne@68: assert have > 0 jpayne@68: want = ((have - 1) // self.indentwidth) * self.indentwidth jpayne@68: # Debug prompt is multilined.... jpayne@68: ncharsdeleted = 0 jpayne@68: while 1: jpayne@68: if chars == self.prompt_last_line: # '' unless PyShell jpayne@68: break jpayne@68: chars = chars[:-1] jpayne@68: ncharsdeleted = ncharsdeleted + 1 jpayne@68: have = len(chars.expandtabs(tabwidth)) jpayne@68: if have <= want or chars[-1] not in " \t": jpayne@68: break jpayne@68: text.undo_block_start() jpayne@68: text.delete("insert-%dc" % ncharsdeleted, "insert") jpayne@68: if have < want: jpayne@68: text.insert("insert", ' ' * (want - have)) jpayne@68: text.undo_block_stop() jpayne@68: return "break" jpayne@68: jpayne@68: def smart_indent_event(self, event): jpayne@68: # if intraline selection: jpayne@68: # delete it jpayne@68: # elif multiline selection: jpayne@68: # do indent-region jpayne@68: # else: jpayne@68: # indent one level jpayne@68: text = self.text jpayne@68: first, last = self.get_selection_indices() jpayne@68: text.undo_block_start() jpayne@68: try: jpayne@68: if first and last: jpayne@68: if index2line(first) != index2line(last): jpayne@68: return self.fregion.indent_region_event(event) jpayne@68: text.delete(first, last) jpayne@68: text.mark_set("insert", first) jpayne@68: prefix = text.get("insert linestart", "insert") jpayne@68: raw, effective = get_line_indent(prefix, self.tabwidth) jpayne@68: if raw == len(prefix): jpayne@68: # only whitespace to the left jpayne@68: self.reindent_to(effective + self.indentwidth) jpayne@68: else: jpayne@68: # tab to the next 'stop' within or to right of line's text: jpayne@68: if self.usetabs: jpayne@68: pad = '\t' jpayne@68: else: jpayne@68: effective = len(prefix.expandtabs(self.tabwidth)) jpayne@68: n = self.indentwidth jpayne@68: pad = ' ' * (n - effective % n) jpayne@68: text.insert("insert", pad) jpayne@68: text.see("insert") jpayne@68: return "break" jpayne@68: finally: jpayne@68: text.undo_block_stop() jpayne@68: jpayne@68: def newline_and_indent_event(self, event): jpayne@68: text = self.text jpayne@68: first, last = self.get_selection_indices() jpayne@68: text.undo_block_start() jpayne@68: try: jpayne@68: if first and last: jpayne@68: text.delete(first, last) jpayne@68: text.mark_set("insert", first) jpayne@68: line = text.get("insert linestart", "insert") jpayne@68: i, n = 0, len(line) jpayne@68: while i < n and line[i] in " \t": jpayne@68: i = i+1 jpayne@68: if i == n: jpayne@68: # the cursor is in or at leading indentation in a continuation jpayne@68: # line; just inject an empty line at the start jpayne@68: text.insert("insert linestart", '\n') jpayne@68: return "break" jpayne@68: indent = line[:i] jpayne@68: # strip whitespace before insert point unless it's in the prompt jpayne@68: i = 0 jpayne@68: while line and line[-1] in " \t" and line != self.prompt_last_line: jpayne@68: line = line[:-1] jpayne@68: i = i+1 jpayne@68: if i: jpayne@68: text.delete("insert - %d chars" % i, "insert") jpayne@68: # strip whitespace after insert point jpayne@68: while text.get("insert") in " \t": jpayne@68: text.delete("insert") jpayne@68: # start new line jpayne@68: text.insert("insert", '\n') jpayne@68: jpayne@68: # adjust indentation for continuations and block jpayne@68: # open/close first need to find the last stmt jpayne@68: lno = index2line(text.index('insert')) jpayne@68: y = pyparse.Parser(self.indentwidth, self.tabwidth) jpayne@68: if not self.prompt_last_line: jpayne@68: for context in self.num_context_lines: jpayne@68: startat = max(lno - context, 1) jpayne@68: startatindex = repr(startat) + ".0" jpayne@68: rawtext = text.get(startatindex, "insert") jpayne@68: y.set_code(rawtext) jpayne@68: bod = y.find_good_parse_start( jpayne@68: self._build_char_in_string_func(startatindex)) jpayne@68: if bod is not None or startat == 1: jpayne@68: break jpayne@68: y.set_lo(bod or 0) jpayne@68: else: jpayne@68: r = text.tag_prevrange("console", "insert") jpayne@68: if r: jpayne@68: startatindex = r[1] jpayne@68: else: jpayne@68: startatindex = "1.0" jpayne@68: rawtext = text.get(startatindex, "insert") jpayne@68: y.set_code(rawtext) jpayne@68: y.set_lo(0) jpayne@68: jpayne@68: c = y.get_continuation_type() jpayne@68: if c != pyparse.C_NONE: jpayne@68: # The current stmt hasn't ended yet. jpayne@68: if c == pyparse.C_STRING_FIRST_LINE: jpayne@68: # after the first line of a string; do not indent at all jpayne@68: pass jpayne@68: elif c == pyparse.C_STRING_NEXT_LINES: jpayne@68: # inside a string which started before this line; jpayne@68: # just mimic the current indent jpayne@68: text.insert("insert", indent) jpayne@68: elif c == pyparse.C_BRACKET: jpayne@68: # line up with the first (if any) element of the jpayne@68: # last open bracket structure; else indent one jpayne@68: # level beyond the indent of the line with the jpayne@68: # last open bracket jpayne@68: self.reindent_to(y.compute_bracket_indent()) jpayne@68: elif c == pyparse.C_BACKSLASH: jpayne@68: # if more than one line in this stmt already, just jpayne@68: # mimic the current indent; else if initial line jpayne@68: # has a start on an assignment stmt, indent to jpayne@68: # beyond leftmost =; else to beyond first chunk of jpayne@68: # non-whitespace on initial line jpayne@68: if y.get_num_lines_in_stmt() > 1: jpayne@68: text.insert("insert", indent) jpayne@68: else: jpayne@68: self.reindent_to(y.compute_backslash_indent()) jpayne@68: else: jpayne@68: assert 0, "bogus continuation type %r" % (c,) jpayne@68: return "break" jpayne@68: jpayne@68: # This line starts a brand new stmt; indent relative to jpayne@68: # indentation of initial line of closest preceding jpayne@68: # interesting stmt. jpayne@68: indent = y.get_base_indent_string() jpayne@68: text.insert("insert", indent) jpayne@68: if y.is_block_opener(): jpayne@68: self.smart_indent_event(event) jpayne@68: elif indent and y.is_block_closer(): jpayne@68: self.smart_backspace_event(event) jpayne@68: return "break" jpayne@68: finally: jpayne@68: text.see("insert") jpayne@68: text.undo_block_stop() jpayne@68: jpayne@68: # Our editwin provides an is_char_in_string function that works jpayne@68: # with a Tk text index, but PyParse only knows about offsets into jpayne@68: # a string. This builds a function for PyParse that accepts an jpayne@68: # offset. jpayne@68: jpayne@68: def _build_char_in_string_func(self, startindex): jpayne@68: def inner(offset, _startindex=startindex, jpayne@68: _icis=self.is_char_in_string): jpayne@68: return _icis(_startindex + "+%dc" % offset) jpayne@68: return inner jpayne@68: jpayne@68: # XXX this isn't bound to anything -- see tabwidth comments jpayne@68: ## def change_tabwidth_event(self, event): jpayne@68: ## new = self._asktabwidth() jpayne@68: ## if new != self.tabwidth: jpayne@68: ## self.tabwidth = new jpayne@68: ## self.set_indentation_params(0, guess=0) jpayne@68: ## return "break" jpayne@68: jpayne@68: # Make string that displays as n leading blanks. jpayne@68: jpayne@68: def _make_blanks(self, n): jpayne@68: if self.usetabs: jpayne@68: ntabs, nspaces = divmod(n, self.tabwidth) jpayne@68: return '\t' * ntabs + ' ' * nspaces jpayne@68: else: jpayne@68: return ' ' * n jpayne@68: jpayne@68: # Delete from beginning of line to insert point, then reinsert jpayne@68: # column logical (meaning use tabs if appropriate) spaces. jpayne@68: jpayne@68: def reindent_to(self, column): jpayne@68: text = self.text jpayne@68: text.undo_block_start() jpayne@68: if text.compare("insert linestart", "!=", "insert"): jpayne@68: text.delete("insert linestart", "insert") jpayne@68: if column: jpayne@68: text.insert("insert", self._make_blanks(column)) jpayne@68: text.undo_block_stop() jpayne@68: jpayne@68: # Guess indentwidth from text content. jpayne@68: # Return guessed indentwidth. This should not be believed unless jpayne@68: # it's in a reasonable range (e.g., it will be 0 if no indented jpayne@68: # blocks are found). jpayne@68: jpayne@68: def guess_indent(self): jpayne@68: opener, indented = IndentSearcher(self.text, self.tabwidth).run() jpayne@68: if opener and indented: jpayne@68: raw, indentsmall = get_line_indent(opener, self.tabwidth) jpayne@68: raw, indentlarge = get_line_indent(indented, self.tabwidth) jpayne@68: else: jpayne@68: indentsmall = indentlarge = 0 jpayne@68: return indentlarge - indentsmall jpayne@68: jpayne@68: def toggle_line_numbers_event(self, event=None): jpayne@68: if self.line_numbers is None: jpayne@68: return jpayne@68: jpayne@68: if self.line_numbers.is_shown: jpayne@68: self.line_numbers.hide_sidebar() jpayne@68: menu_label = "Show" jpayne@68: else: jpayne@68: self.line_numbers.show_sidebar() jpayne@68: menu_label = "Hide" jpayne@68: self.update_menu_label(menu='options', index='*Line Numbers', jpayne@68: label=f'{menu_label} Line Numbers') jpayne@68: jpayne@68: # "line.col" -> line, as an int jpayne@68: def index2line(index): jpayne@68: return int(float(index)) jpayne@68: jpayne@68: jpayne@68: _line_indent_re = re.compile(r'[ \t]*') jpayne@68: def get_line_indent(line, tabwidth): jpayne@68: """Return a line's indentation as (# chars, effective # of spaces). jpayne@68: jpayne@68: The effective # of spaces is the length after properly "expanding" jpayne@68: the tabs into spaces, as done by str.expandtabs(tabwidth). jpayne@68: """ jpayne@68: m = _line_indent_re.match(line) jpayne@68: return m.end(), len(m.group().expandtabs(tabwidth)) jpayne@68: jpayne@68: jpayne@68: class IndentSearcher(object): jpayne@68: jpayne@68: # .run() chews over the Text widget, looking for a block opener jpayne@68: # and the stmt following it. Returns a pair, jpayne@68: # (line containing block opener, line containing stmt) jpayne@68: # Either or both may be None. jpayne@68: jpayne@68: def __init__(self, text, tabwidth): jpayne@68: self.text = text jpayne@68: self.tabwidth = tabwidth jpayne@68: self.i = self.finished = 0 jpayne@68: self.blkopenline = self.indentedline = None jpayne@68: jpayne@68: def readline(self): jpayne@68: if self.finished: jpayne@68: return "" jpayne@68: i = self.i = self.i + 1 jpayne@68: mark = repr(i) + ".0" jpayne@68: if self.text.compare(mark, ">=", "end"): jpayne@68: return "" jpayne@68: return self.text.get(mark, mark + " lineend+1c") jpayne@68: jpayne@68: def tokeneater(self, type, token, start, end, line, jpayne@68: INDENT=tokenize.INDENT, jpayne@68: NAME=tokenize.NAME, jpayne@68: OPENERS=('class', 'def', 'for', 'if', 'try', 'while')): jpayne@68: if self.finished: jpayne@68: pass jpayne@68: elif type == NAME and token in OPENERS: jpayne@68: self.blkopenline = line jpayne@68: elif type == INDENT and self.blkopenline: jpayne@68: self.indentedline = line jpayne@68: self.finished = 1 jpayne@68: jpayne@68: def run(self): jpayne@68: save_tabsize = tokenize.tabsize jpayne@68: tokenize.tabsize = self.tabwidth jpayne@68: try: jpayne@68: try: jpayne@68: tokens = tokenize.generate_tokens(self.readline) jpayne@68: for token in tokens: jpayne@68: self.tokeneater(*token) jpayne@68: except (tokenize.TokenError, SyntaxError): jpayne@68: # since we cut off the tokenizer early, we can trigger jpayne@68: # spurious errors jpayne@68: pass jpayne@68: finally: jpayne@68: tokenize.tabsize = save_tabsize jpayne@68: return self.blkopenline, self.indentedline jpayne@68: jpayne@68: ### end autoindent code ### jpayne@68: jpayne@68: def prepstr(s): jpayne@68: # Helper to extract the underscore from a string, e.g. jpayne@68: # prepstr("Co_py") returns (2, "Copy"). jpayne@68: i = s.find('_') jpayne@68: if i >= 0: jpayne@68: s = s[:i] + s[i+1:] jpayne@68: return i, s jpayne@68: jpayne@68: jpayne@68: keynames = { jpayne@68: 'bracketleft': '[', jpayne@68: 'bracketright': ']', jpayne@68: 'slash': '/', jpayne@68: } jpayne@68: jpayne@68: def get_accelerator(keydefs, eventname): jpayne@68: keylist = keydefs.get(eventname) jpayne@68: # issue10940: temporary workaround to prevent hang with OS X Cocoa Tk 8.5 jpayne@68: # if not keylist: jpayne@68: if (not keylist) or (macosx.isCocoaTk() and eventname in { jpayne@68: "<>", jpayne@68: "<>", jpayne@68: "<>"}): jpayne@68: return "" jpayne@68: s = keylist[0] jpayne@68: s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s) jpayne@68: s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s) jpayne@68: s = re.sub("Key-", "", s) jpayne@68: s = re.sub("Cancel","Ctrl-Break",s) # dscherer@cmu.edu jpayne@68: s = re.sub("Control-", "Ctrl-", s) jpayne@68: s = re.sub("-", "+", s) jpayne@68: s = re.sub("><", " ", s) jpayne@68: s = re.sub("<", "", s) jpayne@68: s = re.sub(">", "", s) jpayne@68: return s jpayne@68: jpayne@68: jpayne@68: def fixwordbreaks(root): jpayne@68: # On Windows, tcl/tk breaks 'words' only on spaces, as in Command Prompt. jpayne@68: # We want Motif style everywhere. See #21474, msg218992 and followup. jpayne@68: tk = root.tk jpayne@68: tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded jpayne@68: tk.call('set', 'tcl_wordchars', r'\w') jpayne@68: tk.call('set', 'tcl_nonwordchars', r'\W') jpayne@68: jpayne@68: jpayne@68: def _editor_window(parent): # htest # jpayne@68: # error if close master window first - timer event, after script jpayne@68: root = parent jpayne@68: fixwordbreaks(root) jpayne@68: if sys.argv[1:]: jpayne@68: filename = sys.argv[1] jpayne@68: else: jpayne@68: filename = None jpayne@68: macosx.setupApp(root, None) jpayne@68: edit = EditorWindow(root=root, filename=filename) jpayne@68: text = edit.text jpayne@68: text['height'] = 10 jpayne@68: for i in range(20): jpayne@68: text.insert('insert', ' '*i + str(i) + '\n') jpayne@68: # text.bind("<>", edit.close_event) jpayne@68: # Does not stop error, neither does following jpayne@68: # edit.text.bind("<>", edit.close_event) jpayne@68: jpayne@68: if __name__ == '__main__': jpayne@68: from unittest import main jpayne@68: main('idlelib.idle_test.test_editor', verbosity=2, exit=False) jpayne@68: jpayne@68: from idlelib.idle_test.htest import run jpayne@68: run(_editor_window)