• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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