6
from itertools import count
8
import tkinter.simpledialog as tkSimpleDialog
9
import tkinter.messagebox as tkMessageBox
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
24
# The default tab setting for a Text widget, in average-width characters.
25
TK_TABWIDTH_DEFAULT = 8
27
def _find_module(fullname, path=None):
28
"""Version of imp.find_module() that handles hierarchical module names"""
31
for tgt in fullname.split('.'):
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)
39
path = module.__path__
40
except AttributeError:
41
raise ImportError('No source for module ' + module.__name__)
42
return file, filename, descr
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
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'
64
basepath = '/usr/share/doc/' # standard location
65
dochome = os.path.join(basepath, pyver,
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):
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')
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
85
EditorWindow.help_url = "http://docs.python.org/%d.%d" % sys.version_info[:2]
86
currentTheme=idleConf.CurrentTheme()
88
root = root or flist.root
92
except AttributeError:
94
self.menubar = Menu(root)
95
self.top = top = WindowList.ListedToplevel(root, menu=self.menubar)
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
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(),
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',
113
height=idleConf.GetOption('main','EditorWindow','height') )
114
self.top.focused_widget = self.text
117
self.apply_bindings()
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)
161
flist.inversedict[self] = 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)
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'):
176
text.config(font=(idleConf.GetOption('main', 'EditorWindow', 'font'),
177
idleConf.GetOption('main', 'EditorWindow', 'font-size'),
179
text_frame.pack(side=LEFT, fill=BOTH, expand=1)
180
text.pack(side=TOP, fill=BOTH, expand=1)
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
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.
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()
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
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
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
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)
237
self.color = color = self.ColorDelegator()
238
per.insertfilter(color)
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')
247
end = menu.index("end")
254
WindowList.register_callback(self.postwindowsmenu)
256
# Some abstractions so IDLE extensions are cross-IDE
257
self.askyesno = tkMessageBox.askyesno
258
self.askinteger = tkSimpleDialog.askinteger
259
self.showerror = tkMessageBox.showerror
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:
267
return filename.decode(self.filesystemencoding)
268
except UnicodeDecodeError:
271
return filename.decode(self.encoding)
272
except UnicodeDecodeError:
273
# byte-to-byte conversion
274
return filename.decode('iso8859-1')
276
def new_callback(self, event):
277
dirname, basename = self.io.defaultfilename()
278
self.flist.new(dirname)
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
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])
291
line = self.text.get("insert linestart", "insert lineend")
292
for insertpt in range(len(line)):
293
if line[insertpt] not in (' ','\t'):
298
lineat = int(self.text.index("insert").split('.')[1])
300
if insertpt == lineat:
303
dest = "insert linestart+"+str(insertpt)+"c"
305
if (event.state&1) == 0:
307
self.text.tag_remove("sel", "1.0", "end")
309
if not self.text.index("sel.first"):
310
self.text.mark_set("anchor","insert")
312
first = self.text.index(dest)
313
last = self.text.index("anchor")
315
if self.text.compare(first,">",last):
316
first,last = last,first
318
self.text.tag_remove("sel", "1.0", "end")
319
self.text.tag_add("sel", first, last)
321
self.text.mark_set("insert", dest)
322
self.text.see("insert")
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)
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)
347
("format", "F_ormat"),
349
("options", "_Options"),
350
("windows", "_Windows"),
354
if macosxSupport.runningAsOSXApp():
356
menu_specs[-2] = ("windows", "_Window")
359
def createmenubar(self):
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)
371
self.recent_files_menu = Menu(self.menubar)
372
self.menudict['file'].insert_cascade(3, label='Recent Files',
374
menu=self.recent_files_menu)
375
self.base_helpmenu_length = self.menudict['help'].index(END)
376
self.reset_help_menu_entries()
378
def postwindowsmenu(self):
379
# Only called when Windows menu exists
380
menu = self.menudict['windows']
381
end = menu.index("end")
384
if end > self.wmenu_end:
385
menu.delete(self.wmenu_end+1, end)
386
WindowList.add_windows_to_menu(menu)
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))
397
iswin = sys.platform[:3] == 'win'
399
self.text.config(cursor="arrow")
400
rmenu.tk_popup(event.x_root, event.y_root)
402
self.text.config(cursor="ibeam")
405
# ("Label", "<<virtual-event>>"), ...
406
("Close", "<<close-window>>"), # Example
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)
417
def about_dialog(self, event=None):
418
aboutDialog.AboutDialog(self.top,'About IDLE')
420
def config_dialog(self, event=None):
421
configDialog.ConfigDialog(self.top,'Settings')
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)
427
def python_docs(self, event=None):
428
if sys.platform[:3] == 'win':
429
os.startfile(self.help_url)
431
webbrowser.open(self.help_url)
435
self.text.event_generate("<<Cut>>")
438
def copy(self,event):
439
if not self.text.tag_ranges("sel"):
440
# There is no selection, so do nothing and maybe interrupt.
442
self.text.event_generate("<<Copy>>")
445
def paste(self,event):
446
self.text.event_generate("<<Paste>>")
447
self.text.see("insert")
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")
456
def remove_selection(self, event=None):
457
self.text.tag_remove("sel", "1.0", "end")
458
self.text.see("insert")
460
def move_at_edge_if_selection(self, edge_index):
461
"""Cursor move begins at start or end of selection
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
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
474
self_text_index("sel.first")
475
self_text_mark_set("insert", edges_table[edge_index])
480
def del_word_left(self, event):
481
self.text.event_generate('<Meta-Delete>')
484
def del_word_right(self, event):
485
self.text.event_generate('<Meta-d>')
488
def find_event(self, event):
489
SearchDialog.find(self.text)
492
def find_again_event(self, event):
493
SearchDialog.find_again(self.text)
496
def find_selection_event(self, event):
497
SearchDialog.find_selection(self.text)
500
def find_in_files_event(self, event):
501
GrepDialog.grep(self.text, self.io, self.flist)
504
def replace_event(self, event):
505
ReplaceDialog.replace(self.text)
508
def goto_line_event(self, event):
510
lineno = tkSimpleDialog.askinteger("Goto",
511
"Go to line number:",parent=text)
517
text.mark_set("insert", "%d.0" % lineno)
520
def open_module(self, event=None):
521
# XXX Shouldn't this be in IOBinding?
523
name = self.text.get("sel.first", "sel.last")
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)
536
# XXX Ought to insert current file's directory in front of path
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)
542
if type != imp.PY_SOURCE:
543
tkMessageBox.showerror("Unsupported type",
544
"%s is not a source module" % name, parent=self.text)
549
self.flist.open(file)
551
self.io.loadfile(file)
553
def open_class_browser(self, event=None):
554
filename = self.io.filename
556
tkMessageBox.showerror(
558
"This buffer has no associated filename",
560
self.text.focus_set()
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])
567
def open_path_browser(self, event=None):
568
from idlelib import PathBrowser
569
PathBrowser.PathBrowser(self.flist)
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")
578
def ispythonsource(self, filename):
579
if not filename or os.path.isdir(filename):
581
base, ext = os.path.splitext(os.path.basename(filename))
582
if os.path.normcase(ext) in (".py", ".pyw"):
584
line = self.text.get('1.0', '1.0 lineend')
585
return line.startswith('#!') and 'python' in line
587
def close_hook(self):
589
self.flist.unregister_maybe_terminate(self)
592
def set_close_hook(self, close_hook):
593
self.close_hook = close_hook
595
def filename_change_hook(self):
597
self.flist.filename_changed_edit(self)
598
self.saved_change_hook()
599
self.top.update_windowlist_registry(self)
600
self.ResetColorizer()
602
def _addcolorizer(self):
605
if self.ispythonsource(self.io.filename):
606
self.color = self.ColorDelegator()
607
# can add more colorizers here...
609
self.per.removefilter(self.undo)
610
self.per.insertfilter(self.color)
611
self.per.insertfilter(self.undo)
613
def _rmcolorizer(self):
616
self.color.removecolors()
617
self.per.removefilter(self.color)
620
def ResetColorizer(self):
621
"Update the colour theme"
622
# Called from self.filename_change_hook and from configDialog.py
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')
630
foreground=normal_colors['foreground'],
631
background=normal_colors['background'],
632
insertbackground=cursor_color,
633
selectforeground=select_colors['foreground'],
634
selectbackground=select_colors['background'],
637
IDENTCHARS = string.ascii_letters + string.digits + "_"
639
def colorize_syntax_error(self, text, pos):
640
text.tag_add("ERROR", 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)
647
text.mark_set("insert", pos + "+1c")
651
"Update the text widgets' font if it is changed"
652
# Called from configDialog.py
654
if idleConf.GetOption('main','EditorWindow','font-bold',type='bool'):
656
self.text.config(font=(idleConf.GetOption('main','EditorWindow','font'),
657
idleConf.GetOption('main','EditorWindow','font-size'),
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)
669
for event, keylist in xkeydefs.items():
670
self.text.event_delete(event, *keylist)
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)
680
self.apply_bindings(xkeydefs)
681
#update menu accelerators
683
for menu in self.Bindings.menudefs:
684
menuEventDict[menu[0]] = {}
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')
695
itemName = menu.entrycget(index, 'label')
697
if menubarItem in menuEventDict:
698
if itemName in menuEventDict[menubarItem]:
699
event = menuEventDict[menubarItem][itemName]
701
accel = get_accelerator(keydefs, event)
702
menu.entryconfig(index, accelerator=accel)
704
def set_notabs_indentwidth(self):
705
"Update the indentwidth if changed and not using tabs in this window"
706
# Called from configDialog.py
708
self.indentwidth = idleConf.GetOption('main', 'Indent','num-spaces',
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)
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
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)
736
webbrowser.open(helpfile)
737
return display_extra_help
739
def update_recent_files_list(self, new_file=None):
740
"Load and update the recent files list and menus"
742
if os.path.exists(self.recent_files_path):
743
rf_list_file = open(self.recent_files_path,'r')
745
rf_list = rf_list_file.readlines()
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
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')
763
rf_file.writelines(rf_list)
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,
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
784
def saved_change_hook(self):
785
short = self.short_title()
786
long = self.long_title()
788
title = short + " - " + long
795
icon = short or long or title
796
if not self.get_saved():
797
title = "*%s*" % title
799
self.top.wm_title(title)
800
self.top.wm_iconname(icon)
803
return self.undo.get_saved()
805
def set_saved(self, flag):
806
self.undo.set_saved(flag)
808
def reset_undo(self):
809
self.undo.reset_undo()
811
def short_title(self):
812
filename = self.io.filename
814
filename = os.path.basename(filename)
815
# return unicode string to display non-ASCII chars correctly
816
return self._filename_to_unicode(filename)
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 "")
822
def center_insert_event(self, event):
825
def center(self, mark="insert"):
827
top, bot = self.getwindowlines()
828
lineno = self.getlineno(mark)
830
newtop = max(1, lineno - height//2)
831
text.yview(float(newtop))
833
def getwindowlines(self):
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
843
def getlineno(self, mark="insert"):
845
return int(float(text.index(mark)))
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()))
853
def close_event(self, event):
858
if not self.get_saved():
859
if self.top.state()!='normal':
863
return self.io.maybesave()
866
reply = self.maybesave()
867
if str(reply) != "cancel":
873
self.update_recent_files_list(new_file=self.io.filename)
874
WindowList.unregister_callback(self.postwindowsmenu)
875
self.unload_extensions()
880
self.color.close(False)
883
self.tkinter_vars = None
888
# unless override: unregister from flist, terminate if last window
891
def load_extensions(self):
893
self.load_standard_extensions()
895
def unload_extensions(self):
896
for ins in list(self.extensions.values()):
897
if hasattr(ins, "close"):
901
def load_standard_extensions(self):
902
for name in self.get_standard_extension_names():
904
self.load_extension(name)
906
print("Failed to load extension", repr(name))
907
traceback.print_exc()
909
def get_standard_extension_names(self):
910
return idleConf.GetExtensions(editor_only=True)
912
def load_extension(self, name):
914
mod = __import__(name, globals(), locals(), [])
916
print("\nFailed to import extension: ", name)
918
cls = getattr(mod, name)
919
keydefs = idleConf.GetExtensionBindings(name)
920
if hasattr(cls, "menudefs"):
921
self.fill_menus(cls.menudefs, keydefs)
923
self.extensions[name] = ins
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))
936
def apply_bindings(self, keydefs=None):
938
keydefs = self.Bindings.default_keydefs
940
text.keydefs = keydefs
941
for event, keylist in keydefs.items():
943
text.event_add(event, *keylist)
945
def fill_menus(self, menudefs=None, keydefs=None):
946
"""Add appropriate entries to the menus and submenus
948
Menus that are absent or None in self.menudict are ignored.
951
menudefs = self.Bindings.menudefs
953
keydefs = self.Bindings.default_keydefs
954
menudict = self.menudict
956
for mname, entrylist in menudefs:
957
menu = menudict.get(mname)
960
for entry in entrylist:
964
label, eventname = entry
965
checkbutton = (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)
973
var = self.get_var_obj(eventname, BooleanVar)
974
menu.add_checkbutton(label=label, underline=underline,
975
command=command, accelerator=accelerator,
978
menu.add_command(label=label, underline=underline,
980
accelerator=accelerator)
982
def getvar(self, name):
983
var = self.get_var_obj(name)
988
raise NameError(name)
990
def setvar(self, name, value, vartype=None):
991
var = self.get_var_obj(name, vartype)
995
raise NameError(name)
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)
1004
# Tk implementations of "virtual text methods" -- each platform
1005
# reusing IDLE's support code needs to define these for its GUI's
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.
1013
def is_char_in_string(self, text_index):
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)
1020
# The colorizer is missing: assume the worst
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):
1027
first = self.text.index("sel.first")
1028
last = self.text.index("sel.last")
1033
# Return the text widget's current view of what a tab stop means
1034
# (equivalent width in spaces).
1036
def get_tk_tabwidth(self):
1037
current = self.text['tabs'] or TK_TABWIDTH_DEFAULT
1040
# Set the text widget's current view of what a tab stop means.
1042
def set_tk_tabwidth(self, newtabwidth):
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,
1049
text.configure(tabs=pixels)
1051
### begin autoindent code ### (configuration was moved to beginning of class)
1053
def set_indentation_params(self, is_py_src, guess=True):
1054
if is_py_src and guess:
1055
i = self.guess_indent()
1057
self.indentwidth = i
1058
if self.indentwidth != self.tabwidth:
1059
self.usetabs = False
1060
self.set_tk_tabwidth(self.tabwidth)
1062
def smart_backspace_event(self, event):
1064
first, last = self.get_selection_indices()
1066
text.delete(first, last)
1067
text.mark_set("insert", first)
1069
# Delete whitespace left, until hitting a real char or closest
1070
# preceding virtual tab stop.
1071
chars = text.get("insert linestart", "insert")
1073
if text.compare("insert", ">", "1.0"):
1074
# easy: delete preceding newline
1075
text.delete("insert-1c")
1077
text.bell() # at start of buffer
1079
if chars[-1] not in " \t":
1080
# easy: delete preceding real char
1081
text.delete("insert-1c")
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))
1088
want = ((have - 1) // self.indentwidth) * self.indentwidth
1089
# Debug prompt is multilined....
1090
last_line_of_prompt = sys.ps1.split('\n')[-1]
1093
if chars == last_line_of_prompt:
1096
ncharsdeleted = ncharsdeleted + 1
1097
have = len(chars.expandtabs(tabwidth))
1098
if have <= want or chars[-1] not in " \t":
1100
text.undo_block_start()
1101
text.delete("insert-%dc" % ncharsdeleted, "insert")
1103
text.insert("insert", ' ' * (want - have))
1104
text.undo_block_stop()
1107
def smart_indent_event(self, event):
1108
# if intraline selection:
1110
# elif multiline selection:
1115
first, last = self.get_selection_indices()
1116
text.undo_block_start()
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)
1129
# tab to the next 'stop' within or to right of line's text:
1133
effective = len(prefix.expandtabs(self.tabwidth))
1134
n = self.indentwidth
1135
pad = ' ' * (n - effective % n)
1136
text.insert("insert", pad)
1140
text.undo_block_stop()
1142
def newline_and_indent_event(self, event):
1144
first, last = self.get_selection_indices()
1145
text.undo_block_start()
1148
text.delete(first, last)
1149
text.mark_set("insert", first)
1150
line = text.get("insert linestart", "insert")
1152
while i < n and line[i] in " \t":
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')
1160
# strip whitespace before insert point unless it's in the prompt
1162
last_line_of_prompt = sys.ps1.split('\n')[-1]
1163
while line and line[-1] in " \t" and line != last_line_of_prompt:
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")
1172
text.insert("insert", '\n')
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")
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:
1191
r = text.tag_prevrange("console", "insert")
1195
startatindex = "1.0"
1196
rawtext = text.get(startatindex, "insert")
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
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
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)
1225
self.reindent_to(y.compute_backslash_indent())
1227
assert 0, "bogus continuation type %r" % (c,)
1230
# This line starts a brand new stmt; indent relative to
1231
# indentation of initial line of closest preceding
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)
1242
text.undo_block_stop()
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
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)
1255
def indent_region_event(self, event):
1256
head, tail, chars, lines = self.get_region()
1257
for pos in range(len(lines)):
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)
1266
def dedent_region_event(self, event):
1267
head, tail, chars, lines = self.get_region()
1268
for pos in range(len(lines)):
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)
1277
def comment_region_event(self, event):
1278
head, tail, chars, lines = self.get_region()
1279
for pos in range(len(lines) - 1):
1281
lines[pos] = '##' + line
1282
self.set_region(head, tail, chars, lines)
1284
def uncomment_region_event(self, event):
1285
head, tail, chars, lines = self.get_region()
1286
for pos in range(len(lines)):
1290
if line[:2] == '##':
1292
elif line[:1] == '#':
1295
self.set_region(head, tail, chars, lines)
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)):
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)
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)
1315
def toggle_tabs_event(self, event):
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",
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
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)
1337
def change_indentwidth_event(self, event):
1338
new = self.askinteger(
1340
"New indent width (2-16)\n(Always use 8 when using tabs)",
1342
initialvalue=self.indentwidth,
1345
if new and new != self.indentwidth and not self.usetabs:
1346
self.indentwidth = new
1349
def get_region(self):
1351
first, last = self.get_selection_indices()
1353
head = text.index(first + " linestart")
1354
tail = text.index(last + "-1c lineend +1c")
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
1362
def set_region(self, head, tail, chars, lines):
1364
newchars = "\n".join(lines)
1365
if newchars == chars:
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")
1376
# Make string that displays as n leading blanks.
1378
def _make_blanks(self, n):
1380
ntabs, nspaces = divmod(n, self.tabwidth)
1381
return '\t' * ntabs + ' ' * nspaces
1385
# Delete from beginning of line to insert point, then reinsert
1386
# column logical (meaning use tabs if appropriate) spaces.
1388
def reindent_to(self, column):
1390
text.undo_block_start()
1391
if text.compare("insert linestart", "!=", "insert"):
1392
text.delete("insert linestart", "insert")
1394
text.insert("insert", self._make_blanks(column))
1395
text.undo_block_stop()
1397
def _asktabwidth(self):
1398
return self.askinteger(
1400
"Columns per tab? (2-16)",
1402
initialvalue=self.indentwidth,
1404
maxvalue=16) or self.tabwidth
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).
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)
1417
indentsmall = indentlarge = 0
1418
return indentlarge - indentsmall
1420
# "line.col" -> line, as an int
1421
def index2line(index):
1422
return int(float(index))
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)
1429
def classifyws(s, tabwidth):
1434
effective = effective + 1
1437
effective = (effective // tabwidth + 1) * tabwidth
1440
return raw, effective
1443
_tokenize = tokenize
1446
class IndentSearcher(object):
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.
1453
def __init__(self, text, tabwidth):
1455
self.tabwidth = tabwidth
1456
self.i = self.finished = 0
1457
self.blkopenline = self.indentedline = None
1462
i = self.i = self.i + 1
1463
mark = repr(i) + ".0"
1464
if self.text.compare(mark, ">=", "end"):
1466
return self.text.get(mark, mark + " lineend+1c")
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')):
1474
elif type == NAME and token in OPENERS:
1475
self.blkopenline = line
1476
elif type == INDENT and self.blkopenline:
1477
self.indentedline = line
1481
save_tabsize = _tokenize.tabsize
1482
_tokenize.tabsize = self.tabwidth
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
1493
_tokenize.tabsize = save_tabsize
1494
return self.blkopenline, self.indentedline
1496
### end autoindent code ###
1499
# Helper to extract the underscore from a string, e.g.
1500
# prepstr("Co_py") returns (2, "Copy").
1509
'bracketright': ']',
1513
def get_accelerator(keydefs, eventname):
1514
keylist = keydefs.get(eventname)
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)
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)
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_]')
1544
filename = sys.argv[1]
1547
edit = EditorWindow(root=root, filename=filename)
1548
edit.set_close_hook(root.quit)
1549
edit.text.bind("<<close-all-windows>>", edit.close_event)
1553
if __name__ == '__main__':