~ubuntu-branches/ubuntu/maverick/python3.1/maverick

« back to all changes in this revision

Viewing changes to Lib/idlelib/EditorWindow.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2009-03-23 00:01:27 UTC
  • Revision ID: james.westby@ubuntu.com-20090323000127-5fstfxju4ufrhthq
Tags: upstream-3.1~a1+20090322
ImportĀ upstreamĀ versionĀ 3.1~a1+20090322

Show diffs side-by-side

added added

removed removed

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