blob: 86532f873bd7f49061e07d0e7e6ce1b1ef56ffea [file] [log] [blame]
David Scherer7aced172000-08-15 01:13:23 +00001import sys
2import os
David Scherer7aced172000-08-15 01:13:23 +00003import re
4import imp
5from Tkinter import *
6import tkSimpleDialog
7import tkMessageBox
Kurt B. Kaiserfd182cd2001-07-14 03:58:25 +00008
9import webbrowser
David Scherer7aced172000-08-15 01:13:23 +000010import idlever
11import WindowList
Steven M. Gavac5976402002-01-04 03:06:08 +000012import SearchDialog
13import GrepDialog
14import ReplaceDialog
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +000015import PyParse
Steven M. Gavadc72f482002-01-03 11:51:07 +000016from configHandler import idleConf
Steven M. Gava3b55a892001-11-21 05:56:26 +000017import aboutDialog, textView, configDialog
David Scherer7aced172000-08-15 01:13:23 +000018
19# The default tab setting for a Text widget, in average-width characters.
20TK_TABWIDTH_DEFAULT = 8
21
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +000022def _find_module(fullname, path=None):
23 """Version of imp.find_module() that handles hierarchical module names"""
24
25 file = None
26 for tgt in fullname.split('.'):
27 if file is not None:
28 file.close() # close intermediate files
29 (file, filename, descr) = imp.find_module(tgt, path)
30 if descr[2] == imp.PY_SOURCE:
31 break # find but not load the source file
32 module = imp.load_module(tgt, file, filename, descr)
Kurt B. Kaiser69e8afc2003-01-10 21:25:20 +000033 try:
34 path = module.__path__
35 except AttributeError:
36 raise ImportError, 'No source for module ' + module.__name__
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +000037 return file, filename, descr
38
David Scherer7aced172000-08-15 01:13:23 +000039class EditorWindow:
David Scherer7aced172000-08-15 01:13:23 +000040 from Percolator import Percolator
41 from ColorDelegator import ColorDelegator
42 from UndoDelegator import UndoDelegator
43 from IOBinding import IOBinding
44 import Bindings
45 from Tkinter import Toplevel
46 from MultiStatusBar import MultiStatusBar
47
David Scherer7aced172000-08-15 01:13:23 +000048 vars = {}
Kurt B. Kaiser114713d2003-01-10 05:07:24 +000049 help_url = None
David Scherer7aced172000-08-15 01:13:23 +000050
51 def __init__(self, flist=None, filename=None, key=None, root=None):
Kurt B. Kaiser114713d2003-01-10 05:07:24 +000052 if EditorWindow.help_url is None:
53 if sys.platform.count('linux'):
54 # look for html docs in a couple of standard places
55 pyver = 'python-docs-' + '%s.%s.%s' % sys.version_info[:3]
56 if os.path.isdir('/var/www/html/python/'): # "python2" rpm
57 dochome = '/var/www/html/python/index.html'
58 else:
59 basepath = '/usr/share/doc/' # standard location
60 dochome = os.path.join(basepath, pyver,
61 'Doc', 'index.html')
62 else:
63 dochome = os.path.join(sys.prefix, 'Doc', 'index.html')
64 dochome = os.path.normpath(dochome)
65 if os.path.isfile(dochome):
66 EditorWindow.help_url = dochome
67 else:
68 EditorWindow.help_url = "http://www.python.org/doc/current"
Steven M. Gavadc72f482002-01-03 11:51:07 +000069 currentTheme=idleConf.CurrentTheme()
David Scherer7aced172000-08-15 01:13:23 +000070 self.flist = flist
71 root = root or flist.root
72 self.root = root
David Scherer7aced172000-08-15 01:13:23 +000073 self.menubar = Menu(root)
74 self.top = top = self.Toplevel(root, menu=self.menubar)
Steven M. Gava0c5bc8c2002-03-27 02:25:44 +000075 if flist:
76 self.vars = flist.vars
77 #self.top.instanceDict makes flist.inversedict avalable to
78 #configDialog.py so it can access all EditorWindow instaces
79 self.top.instanceDict=flist.inversedict
Steven M. Gava1d46e402002-03-27 08:40:46 +000080 self.recentFilesPath=os.path.join(idleConf.GetUserCfgDir(),
81 'recent-files.lst')
David Scherer7aced172000-08-15 01:13:23 +000082 self.vbar = vbar = Scrollbar(top, name='vbar')
83 self.text_frame = text_frame = Frame(top)
Kurt B. Kaiser1061e722003-01-04 01:43:53 +000084 self.width = idleConf.GetOption('main','EditorWindow','width')
Kurt B. Kaisera1dee062002-10-04 21:33:57 +000085 self.text = text = Text(text_frame, name='text', padx=5, wrap='none',
Steven M. Gavadc72f482002-01-03 11:51:07 +000086 foreground=idleConf.GetHighlight(currentTheme,
87 'normal',fgBg='fg'),
88 background=idleConf.GetHighlight(currentTheme,
89 'normal',fgBg='bg'),
90 highlightcolor=idleConf.GetHighlight(currentTheme,
91 'hilite',fgBg='fg'),
92 highlightbackground=idleConf.GetHighlight(currentTheme,
93 'hilite',fgBg='bg'),
94 insertbackground=idleConf.GetHighlight(currentTheme,
95 'cursor',fgBg='fg'),
Kurt B. Kaiser1061e722003-01-04 01:43:53 +000096 width=self.width,
Steven M. Gavadc72f482002-01-03 11:51:07 +000097 height=idleConf.GetOption('main','EditorWindow','height') )
David Scherer7aced172000-08-15 01:13:23 +000098
99 self.createmenubar()
100 self.apply_bindings()
101
102 self.top.protocol("WM_DELETE_WINDOW", self.close)
103 self.top.bind("<<close-window>>", self.close_event)
Kurt B. Kaiser84f48032002-09-26 22:13:22 +0000104 text.bind("<<cut>>", self.cut)
105 text.bind("<<copy>>", self.copy)
106 text.bind("<<paste>>", self.paste)
David Scherer7aced172000-08-15 01:13:23 +0000107 text.bind("<<center-insert>>", self.center_insert_event)
108 text.bind("<<help>>", self.help_dialog)
Steven M. Gavaabdfc412001-08-11 07:46:26 +0000109 text.bind("<<view-readme>>", self.view_readme)
David Scherer7aced172000-08-15 01:13:23 +0000110 text.bind("<<python-docs>>", self.python_docs)
111 text.bind("<<about-idle>>", self.about_dialog)
Steven M. Gava3b55a892001-11-21 05:56:26 +0000112 text.bind("<<open-config-dialog>>", self.config_dialog)
David Scherer7aced172000-08-15 01:13:23 +0000113 text.bind("<<open-module>>", self.open_module)
114 text.bind("<<do-nothing>>", lambda event: "break")
115 text.bind("<<select-all>>", self.select_all)
116 text.bind("<<remove-selection>>", self.remove_selection)
Steven M. Gavac5976402002-01-04 03:06:08 +0000117 text.bind("<<find>>", self.find_event)
118 text.bind("<<find-again>>", self.find_again_event)
119 text.bind("<<find-in-files>>", self.find_in_files_event)
120 text.bind("<<find-selection>>", self.find_selection_event)
121 text.bind("<<replace>>", self.replace_event)
122 text.bind("<<goto-line>>", self.goto_line_event)
David Scherer7aced172000-08-15 01:13:23 +0000123 text.bind("<3>", self.right_menu_event)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000124 text.bind("<<smart-backspace>>",self.smart_backspace_event)
125 text.bind("<<newline-and-indent>>",self.newline_and_indent_event)
126 text.bind("<<smart-indent>>",self.smart_indent_event)
127 text.bind("<<indent-region>>",self.indent_region_event)
128 text.bind("<<dedent-region>>",self.dedent_region_event)
129 text.bind("<<comment-region>>",self.comment_region_event)
130 text.bind("<<uncomment-region>>",self.uncomment_region_event)
131 text.bind("<<tabify-region>>",self.tabify_region_event)
132 text.bind("<<untabify-region>>",self.untabify_region_event)
133 text.bind("<<toggle-tabs>>",self.toggle_tabs_event)
134 text.bind("<<change-indentwidth>>",self.change_indentwidth_event)
Kurt B. Kaiser5ec186b2003-01-17 04:04:06 +0000135 text.bind("<Left>", self.move_at_edge_if_selection(0))
136 text.bind("<Right>", self.move_at_edge_if_selection(1))
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000137
David Scherer7aced172000-08-15 01:13:23 +0000138 if flist:
139 flist.inversedict[self] = key
140 if key:
141 flist.dict[key] = self
Kurt B. Kaiserd2f48612003-06-05 02:34:04 +0000142 text.bind("<<open-new-window>>", self.new_callback)
David Scherer7aced172000-08-15 01:13:23 +0000143 text.bind("<<close-all-windows>>", self.flist.close_all_callback)
144 text.bind("<<open-class-browser>>", self.open_class_browser)
145 text.bind("<<open-path-browser>>", self.open_path_browser)
146
Steven M. Gava898a3652001-10-07 11:10:44 +0000147 self.set_status_bar()
David Scherer7aced172000-08-15 01:13:23 +0000148 vbar['command'] = text.yview
149 vbar.pack(side=RIGHT, fill=Y)
David Scherer7aced172000-08-15 01:13:23 +0000150 text['yscrollcommand'] = vbar.set
Steven M. Gavab1585412002-03-12 00:21:56 +0000151 fontWeight='normal'
152 if idleConf.GetOption('main','EditorWindow','font-bold',type='bool'):
153 fontWeight='bold'
Steven M. Gavadc72f482002-01-03 11:51:07 +0000154 text.config(font=(idleConf.GetOption('main','EditorWindow','font'),
Steven M. Gavab1585412002-03-12 00:21:56 +0000155 idleConf.GetOption('main','EditorWindow','font-size'),
156 fontWeight))
David Scherer7aced172000-08-15 01:13:23 +0000157 text_frame.pack(side=LEFT, fill=BOTH, expand=1)
158 text.pack(side=TOP, fill=BOTH, expand=1)
159 text.focus_set()
160
161 self.per = per = self.Percolator(text)
162 if self.ispythonsource(filename):
Kurt B. Kaiserdc1e7092002-07-11 04:33:41 +0000163 self.color = color = self.ColorDelegator()
164 per.insertfilter(color)
David Scherer7aced172000-08-15 01:13:23 +0000165 else:
David Scherer7aced172000-08-15 01:13:23 +0000166 self.color = None
Kurt B. Kaiserdc1e7092002-07-11 04:33:41 +0000167
168 self.undo = undo = self.UndoDelegator()
169 per.insertfilter(undo)
170 text.undo_block_start = undo.undo_block_start
171 text.undo_block_stop = undo.undo_block_stop
172 undo.set_saved_change_hook(self.saved_change_hook)
173
174 # IOBinding implements file I/O and printing functionality
David Scherer7aced172000-08-15 01:13:23 +0000175 self.io = io = self.IOBinding(self)
Kurt B. Kaiserdc1e7092002-07-11 04:33:41 +0000176 io.set_filename_change_hook(self.filename_change_hook)
177
Steven M. Gava1d46e402002-03-27 08:40:46 +0000178 #create the Recent Files submenu
179 self.menuRecentFiles=Menu(self.menubar)
180 self.menudict['file'].insert_cascade(3,label='Recent Files',
181 underline=0,menu=self.menuRecentFiles)
182 self.UpdateRecentFilesList()
David Scherer7aced172000-08-15 01:13:23 +0000183
David Scherer7aced172000-08-15 01:13:23 +0000184 if filename:
Kurt B. Kaiserd2f48612003-06-05 02:34:04 +0000185 if os.path.exists(filename) and not os.path.isdir(filename):
David Scherer7aced172000-08-15 01:13:23 +0000186 io.loadfile(filename)
187 else:
188 io.set_filename(filename)
David Scherer7aced172000-08-15 01:13:23 +0000189 self.saved_change_hook()
190
191 self.load_extensions()
192
193 menu = self.menudict.get('windows')
194 if menu:
195 end = menu.index("end")
196 if end is None:
197 end = -1
198 if end >= 0:
199 menu.add_separator()
200 end = end + 1
201 self.wmenu_end = end
202 WindowList.register_callback(self.postwindowsmenu)
203
204 # Some abstractions so IDLE extensions are cross-IDE
205 self.askyesno = tkMessageBox.askyesno
206 self.askinteger = tkSimpleDialog.askinteger
207 self.showerror = tkMessageBox.showerror
208
209 if self.extensions.has_key('AutoIndent'):
210 self.extensions['AutoIndent'].set_indentation_params(
211 self.ispythonsource(filename))
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000212
Kurt B. Kaiserd2f48612003-06-05 02:34:04 +0000213 def new_callback(self, event):
214 dirname, basename = self.io.defaultfilename()
215 self.flist.new(dirname)
216 return "break"
217
David Scherer7aced172000-08-15 01:13:23 +0000218 def set_status_bar(self):
Steven M. Gava898a3652001-10-07 11:10:44 +0000219 self.status_bar = self.MultiStatusBar(self.top)
David Scherer7aced172000-08-15 01:13:23 +0000220 self.status_bar.set_label('column', 'Col: ?', side=RIGHT)
221 self.status_bar.set_label('line', 'Ln: ?', side=RIGHT)
222 self.status_bar.pack(side=BOTTOM, fill=X)
223 self.text.bind('<KeyRelease>', self.set_line_and_column)
224 self.text.bind('<ButtonRelease>', self.set_line_and_column)
225 self.text.after_idle(self.set_line_and_column)
226
227 def set_line_and_column(self, event=None):
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000228 line, column = self.text.index(INSERT).split('.')
David Scherer7aced172000-08-15 01:13:23 +0000229 self.status_bar.set_label('column', 'Col: %s' % column)
230 self.status_bar.set_label('line', 'Ln: %s' % line)
231
232 def wakeup(self):
233 if self.top.wm_state() == "iconic":
234 self.top.wm_deiconify()
235 else:
236 self.top.tkraise()
237 self.text.focus_set()
238
239 menu_specs = [
240 ("file", "_File"),
241 ("edit", "_Edit"),
242 ("format", "F_ormat"),
243 ("run", "_Run"),
Kurt B. Kaiser1061e722003-01-04 01:43:53 +0000244 ("options", "_Options"),
David Scherer7aced172000-08-15 01:13:23 +0000245 ("windows", "_Windows"),
246 ("help", "_Help"),
247 ]
248
249 def createmenubar(self):
250 mbar = self.menubar
251 self.menudict = menudict = {}
252 for name, label in self.menu_specs:
253 underline, label = prepstr(label)
254 menudict[name] = menu = Menu(mbar, name=name)
255 mbar.add_cascade(label=label, menu=menu, underline=underline)
256 self.fill_menus()
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000257 self.base_helpmenu_length = self.menudict['help'].index(END)
258 self.reset_help_menu_entries()
David Scherer7aced172000-08-15 01:13:23 +0000259
260 def postwindowsmenu(self):
261 # Only called when Windows menu exists
262 # XXX Actually, this Just-In-Time updating interferes badly
263 # XXX with the tear-off feature. It would be better to update
264 # XXX all Windows menus whenever the list of windows changes.
265 menu = self.menudict['windows']
266 end = menu.index("end")
267 if end is None:
268 end = -1
269 if end > self.wmenu_end:
270 menu.delete(self.wmenu_end+1, end)
271 WindowList.add_windows_to_menu(menu)
272
273 rmenu = None
274
275 def right_menu_event(self, event):
276 self.text.tag_remove("sel", "1.0", "end")
277 self.text.mark_set("insert", "@%d,%d" % (event.x, event.y))
278 if not self.rmenu:
279 self.make_rmenu()
280 rmenu = self.rmenu
281 self.event = event
282 iswin = sys.platform[:3] == 'win'
283 if iswin:
284 self.text.config(cursor="arrow")
285 rmenu.tk_popup(event.x_root, event.y_root)
286 if iswin:
287 self.text.config(cursor="ibeam")
288
289 rmenu_specs = [
290 # ("Label", "<<virtual-event>>"), ...
291 ("Close", "<<close-window>>"), # Example
292 ]
293
294 def make_rmenu(self):
295 rmenu = Menu(self.text, tearoff=0)
296 for label, eventname in self.rmenu_specs:
297 def command(text=self.text, eventname=eventname):
298 text.event_generate(eventname)
299 rmenu.add_command(label=label, command=command)
300 self.rmenu = rmenu
301
302 def about_dialog(self, event=None):
Steven M. Gava7d9ed722001-07-31 07:01:47 +0000303 aboutDialog.AboutDialog(self.top,'About IDLEfork')
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000304
Steven M. Gava3b55a892001-11-21 05:56:26 +0000305 def config_dialog(self, event=None):
306 configDialog.ConfigDialog(self.top,'Settings')
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000307
Steven M. Gavaabdfc412001-08-11 07:46:26 +0000308 def view_readme(self, event=None):
309 fn=os.path.join(os.path.abspath(os.path.dirname(__file__)),'README.txt')
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000310 textView.TextViewer(self.top,'IDLEfork - README',fn)
Steven M. Gavaabdfc412001-08-11 07:46:26 +0000311
David Scherer7aced172000-08-15 01:13:23 +0000312 def help_dialog(self, event=None):
Steven M. Gavab9d07b52001-07-31 11:11:38 +0000313 fn=os.path.join(os.path.abspath(os.path.dirname(__file__)),'help.txt')
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000314 textView.TextViewer(self.top,'Help',fn)
315
Kurt B. Kaiser114713d2003-01-10 05:07:24 +0000316 def python_docs(self, event=None):
317 if sys.platform.count('win') or sys.platform.count('nt'):
Steven M. Gava931625d2002-04-22 00:38:26 +0000318 os.startfile(self.help_url)
Kurt B. Kaiser114713d2003-01-10 05:07:24 +0000319 return "break"
320 else:
321 webbrowser.open(self.help_url)
322 return "break"
Steven M. Gava0c5bc8c2002-03-27 02:25:44 +0000323
324 def display_docs(self, url):
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000325 if not (url.startswith('www') or url.startswith('http')):
326 url = os.path.normpath(url)
Kurt B. Kaiser114713d2003-01-10 05:07:24 +0000327 if sys.platform.count('win') or sys.platform.count('nt'):
328 os.startfile(url)
329 else:
330 webbrowser.open(url)
David Scherer7aced172000-08-15 01:13:23 +0000331
Kurt B. Kaiser84f48032002-09-26 22:13:22 +0000332 def cut(self,event):
333 self.text.event_generate("<<Cut>>")
334 return "break"
335
336 def copy(self,event):
337 self.text.event_generate("<<Copy>>")
338 return "break"
339
340 def paste(self,event):
341 self.text.event_generate("<<Paste>>")
342 return "break"
343
David Scherer7aced172000-08-15 01:13:23 +0000344 def select_all(self, event=None):
345 self.text.tag_add("sel", "1.0", "end-1c")
346 self.text.mark_set("insert", "1.0")
347 self.text.see("insert")
348 return "break"
349
350 def remove_selection(self, event=None):
351 self.text.tag_remove("sel", "1.0", "end")
352 self.text.see("insert")
353
Kurt B. Kaiser5ec186b2003-01-17 04:04:06 +0000354 def move_at_edge_if_selection(self, edge_index):
355 """Cursor move begins at start or end of selection
356
357 When a left/right cursor key is pressed create and return to Tkinter a
358 function which causes a cursor move from the associated edge of the
359 selection.
360
361 """
362 self_text_index = self.text.index
363 self_text_mark_set = self.text.mark_set
364 edges_table = ("sel.first+1c", "sel.last-1c")
365 def move_at_edge(event):
366 if (event.state & 5) == 0: # no shift(==1) or control(==4) pressed
367 try:
368 self_text_index("sel.first")
369 self_text_mark_set("insert", edges_table[edge_index])
370 except TclError:
371 pass
372 return move_at_edge
373
Steven M. Gavac5976402002-01-04 03:06:08 +0000374 def find_event(self, event):
375 SearchDialog.find(self.text)
376 return "break"
377
378 def find_again_event(self, event):
379 SearchDialog.find_again(self.text)
380 return "break"
381
382 def find_selection_event(self, event):
383 SearchDialog.find_selection(self.text)
384 return "break"
385
386 def find_in_files_event(self, event):
387 GrepDialog.grep(self.text, self.io, self.flist)
388 return "break"
389
390 def replace_event(self, event):
391 ReplaceDialog.replace(self.text)
392 return "break"
393
394 def goto_line_event(self, event):
395 text = self.text
396 lineno = tkSimpleDialog.askinteger("Goto",
397 "Go to line number:",parent=text)
398 if lineno is None:
399 return "break"
400 if lineno <= 0:
401 text.bell()
402 return "break"
403 text.mark_set("insert", "%d.0" % lineno)
404 text.see("insert")
405
David Scherer7aced172000-08-15 01:13:23 +0000406 def open_module(self, event=None):
407 # XXX Shouldn't this be in IOBinding or in FileList?
408 try:
409 name = self.text.get("sel.first", "sel.last")
410 except TclError:
411 name = ""
412 else:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000413 name = name.strip()
Guido van Rossum852f35b2003-06-05 11:36:55 +0000414 name = tkSimpleDialog.askstring("Module",
415 "Enter the name of a Python module\n"
416 "to search on sys.path and open:",
417 parent=self.text, initialvalue=name)
418 if name:
419 name = name.strip()
David Scherer7aced172000-08-15 01:13:23 +0000420 if not name:
Guido van Rossum852f35b2003-06-05 11:36:55 +0000421 return
David Scherer7aced172000-08-15 01:13:23 +0000422 # XXX Ought to insert current file's directory in front of path
423 try:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000424 (f, file, (suffix, mode, type)) = _find_module(name)
David Scherer7aced172000-08-15 01:13:23 +0000425 except (NameError, ImportError), msg:
426 tkMessageBox.showerror("Import error", str(msg), parent=self.text)
427 return
428 if type != imp.PY_SOURCE:
429 tkMessageBox.showerror("Unsupported type",
430 "%s is not a source module" % name, parent=self.text)
431 return
432 if f:
433 f.close()
434 if self.flist:
435 self.flist.open(file)
436 else:
437 self.io.loadfile(file)
438
439 def open_class_browser(self, event=None):
440 filename = self.io.filename
441 if not filename:
442 tkMessageBox.showerror(
443 "No filename",
444 "This buffer has no associated filename",
445 master=self.text)
446 self.text.focus_set()
447 return None
448 head, tail = os.path.split(filename)
449 base, ext = os.path.splitext(tail)
450 import ClassBrowser
451 ClassBrowser.ClassBrowser(self.flist, base, [head])
452
453 def open_path_browser(self, event=None):
454 import PathBrowser
455 PathBrowser.PathBrowser(self.flist)
456
457 def gotoline(self, lineno):
458 if lineno is not None and lineno > 0:
459 self.text.mark_set("insert", "%d.0" % lineno)
460 self.text.tag_remove("sel", "1.0", "end")
461 self.text.tag_add("sel", "insert", "insert +1l")
462 self.center()
463
464 def ispythonsource(self, filename):
465 if not filename:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000466 return True
David Scherer7aced172000-08-15 01:13:23 +0000467 base, ext = os.path.splitext(os.path.basename(filename))
468 if os.path.normcase(ext) in (".py", ".pyw"):
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000469 return True
David Scherer7aced172000-08-15 01:13:23 +0000470 try:
471 f = open(filename)
472 line = f.readline()
473 f.close()
474 except IOError:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000475 return False
Kurt B. Kaiser83a35602002-12-20 17:18:03 +0000476 return line.startswith('#!') and line.find('python') >= 0
David Scherer7aced172000-08-15 01:13:23 +0000477
478 def close_hook(self):
479 if self.flist:
480 self.flist.close_edit(self)
481
482 def set_close_hook(self, close_hook):
483 self.close_hook = close_hook
484
485 def filename_change_hook(self):
486 if self.flist:
487 self.flist.filename_changed_edit(self)
488 self.saved_change_hook()
489 if self.ispythonsource(self.io.filename):
490 self.addcolorizer()
491 else:
492 self.rmcolorizer()
493
494 def addcolorizer(self):
495 if self.color:
496 return
David Scherer7aced172000-08-15 01:13:23 +0000497 self.per.removefilter(self.undo)
498 self.color = self.ColorDelegator()
499 self.per.insertfilter(self.color)
500 self.per.insertfilter(self.undo)
501
502 def rmcolorizer(self):
503 if not self.color:
504 return
David Scherer7aced172000-08-15 01:13:23 +0000505 self.per.removefilter(self.undo)
506 self.per.removefilter(self.color)
507 self.color = None
508 self.per.insertfilter(self.undo)
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000509
Steven M. Gavab77d3432002-03-02 07:16:21 +0000510 def ResetColorizer(self):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000511 "Update the colour theme if it is changed"
512 # Called from configDialog.py
Steven M. Gavab77d3432002-03-02 07:16:21 +0000513 if self.color:
514 self.color = self.ColorDelegator()
515 self.per.insertfilter(self.color)
David Scherer7aced172000-08-15 01:13:23 +0000516
Steven M. Gavab1585412002-03-12 00:21:56 +0000517 def ResetFont(self):
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000518 "Update the text widgets' font if it is changed"
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000519 # Called from configDialog.py
Steven M. Gavab1585412002-03-12 00:21:56 +0000520 fontWeight='normal'
521 if idleConf.GetOption('main','EditorWindow','font-bold',type='bool'):
522 fontWeight='bold'
523 self.text.config(font=(idleConf.GetOption('main','EditorWindow','font'),
524 idleConf.GetOption('main','EditorWindow','font-size'),
525 fontWeight))
526
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000527 def ResetKeybindings(self):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000528 "Update the keybindings if they are changed"
529 # Called from configDialog.py
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000530 self.Bindings.default_keydefs=idleConf.GetCurrentKeySet()
531 keydefs = self.Bindings.default_keydefs
532 for event, keylist in keydefs.items():
533 self.text.event_delete(event)
534 self.apply_bindings()
535 #update menu accelerators
536 menuEventDict={}
537 for menu in self.Bindings.menudefs:
538 menuEventDict[menu[0]]={}
539 for item in menu[1]:
540 if item:
541 menuEventDict[menu[0]][prepstr(item[0])[1]]=item[1]
542 for menubarItem in self.menudict.keys():
543 menu=self.menudict[menubarItem]
544 end=menu.index(END)+1
545 for index in range(0,end):
546 if menu.type(index)=='command':
547 accel=menu.entrycget(index,'accelerator')
548 if accel:
549 itemName=menu.entrycget(index,'label')
550 event=''
551 if menuEventDict.has_key(menubarItem):
552 if menuEventDict[menubarItem].has_key(itemName):
553 event=menuEventDict[menubarItem][itemName]
554 if event:
555 #print 'accel was:',accel
556 accel=get_accelerator(keydefs, event)
557 menu.entryconfig(index,accelerator=accel)
558 #print 'accel now:',accel,'\n'
559
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000560 def reset_help_menu_entries(self):
561 "Update the additional help entries on the Help menu"
562 help_list = idleConf.GetAllExtraHelpSourcesList()
563 helpmenu = self.menudict['help']
564 # first delete the extra help entries, if any
565 helpmenu_length = helpmenu.index(END)
566 if helpmenu_length > self.base_helpmenu_length:
567 helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length)
568 # then rebuild them
569 if help_list:
570 helpmenu.add_separator()
571 for entry in help_list:
572 cmd = self.__extra_help_callback(entry[1])
573 helpmenu.add_command(label=entry[0], command=cmd)
574 # and update the menu dictionary
575 self.menudict['help'] = helpmenu
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000576
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000577 def __extra_help_callback(self, helpfile):
578 "Create a callback with the helpfile value frozen at definition time"
579 def display_extra_help(helpfile=helpfile):
580 self.display_docs(helpfile)
581 return display_extra_help
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000582
Steven M. Gava1d46e402002-03-27 08:40:46 +0000583 def UpdateRecentFilesList(self,newFile=None):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000584 "Load or update the recent files list, and menu if required"
Steven M. Gava1d46e402002-03-27 08:40:46 +0000585 rfList=[]
586 if os.path.exists(self.recentFilesPath):
587 RFfile=open(self.recentFilesPath,'r')
588 try:
589 rfList=RFfile.readlines()
590 finally:
591 RFfile.close()
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000592 if newFile:
Steven M. Gava1d46e402002-03-27 08:40:46 +0000593 newFile=os.path.abspath(newFile)+'\n'
594 if newFile in rfList:
595 rfList.remove(newFile)
596 rfList.insert(0,newFile)
597 rfList=self.__CleanRecentFiles(rfList)
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000598 #print self.flist.inversedict
599 #print self.top.instanceDict
600 #print self
Kurt B. Kaiserc9a5b5c2002-10-06 01:57:45 +0000601 ullist = "1234567890ABCDEFGHIJ"
Steven M. Gava1d46e402002-03-27 08:40:46 +0000602 if rfList:
603 for instance in self.top.instanceDict.keys():
Kurt B. Kaiserc9a5b5c2002-10-06 01:57:45 +0000604 menu = instance.menuRecentFiles
605 menu.delete(1,END)
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000606 i = 0 ; ul = 0; ullen = len(ullist)
Steven M. Gava1d46e402002-03-27 08:40:46 +0000607 for file in rfList:
608 fileName=file[0:-1]
Kurt B. Kaiserc9a5b5c2002-10-06 01:57:45 +0000609 callback = instance.__RecentFileCallback(fileName)
610 if i > ullen: # don't underline menuitems
611 ul=None
612 menu.add_command(label=ullist[i] + " " + fileName,
613 command=callback,
614 underline=ul)
615 i += 1
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000616
Steven M. Gava1d46e402002-03-27 08:40:46 +0000617 def __CleanRecentFiles(self,rfList):
618 origRfList=rfList[:]
619 count=0
620 nonFiles=[]
621 for path in rfList:
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000622 if not os.path.exists(path[0:-1]):
Steven M. Gava1d46e402002-03-27 08:40:46 +0000623 nonFiles.append(count)
624 count=count+1
625 if nonFiles:
626 nonFiles.reverse()
627 for index in nonFiles:
628 del(rfList[index])
629 if len(rfList)>19:
630 rfList=rfList[0:19]
631 #if rfList != origRfList:
632 RFfile=open(self.recentFilesPath,'w')
633 try:
634 RFfile.writelines(rfList)
635 finally:
636 RFfile.close()
637 return rfList
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000638
Steven M. Gava1d46e402002-03-27 08:40:46 +0000639 def __RecentFileCallback(self,fileName):
640 def OpenRecentFile(fileName=fileName):
641 self.io.open(editFile=fileName)
642 return OpenRecentFile
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000643
David Scherer7aced172000-08-15 01:13:23 +0000644 def saved_change_hook(self):
645 short = self.short_title()
646 long = self.long_title()
647 if short and long:
648 title = short + " - " + long
649 elif short:
650 title = short
651 elif long:
652 title = long
653 else:
654 title = "Untitled"
655 icon = short or long or title
656 if not self.get_saved():
657 title = "*%s*" % title
658 icon = "*%s" % icon
659 self.top.wm_title(title)
660 self.top.wm_iconname(icon)
661
662 def get_saved(self):
663 return self.undo.get_saved()
664
665 def set_saved(self, flag):
666 self.undo.set_saved(flag)
667
668 def reset_undo(self):
669 self.undo.reset_undo()
670
671 def short_title(self):
672 filename = self.io.filename
673 if filename:
674 filename = os.path.basename(filename)
675 return filename
676
677 def long_title(self):
678 return self.io.filename or ""
679
680 def center_insert_event(self, event):
681 self.center()
682
683 def center(self, mark="insert"):
684 text = self.text
685 top, bot = self.getwindowlines()
686 lineno = self.getlineno(mark)
687 height = bot - top
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000688 newtop = max(1, lineno - height//2)
David Scherer7aced172000-08-15 01:13:23 +0000689 text.yview(float(newtop))
690
691 def getwindowlines(self):
692 text = self.text
693 top = self.getlineno("@0,0")
694 bot = self.getlineno("@0,65535")
695 if top == bot and text.winfo_height() == 1:
696 # Geometry manager hasn't run yet
697 height = int(text['height'])
698 bot = top + height - 1
699 return top, bot
700
701 def getlineno(self, mark="insert"):
702 text = self.text
703 return int(float(text.index(mark)))
704
Kurt B. Kaiser1061e722003-01-04 01:43:53 +0000705 def get_geometry(self):
706 "Return (width, height, x, y)"
707 geom = self.top.wm_geometry()
708 m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom)
709 tuple = (map(int, m.groups()))
710 return tuple
711
David Scherer7aced172000-08-15 01:13:23 +0000712 def close_event(self, event):
713 self.close()
714
715 def maybesave(self):
716 if self.io:
Steven M. Gava67716b52002-02-26 02:31:03 +0000717 if not self.get_saved():
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000718 if self.top.state()!='normal':
Steven M. Gava67716b52002-02-26 02:31:03 +0000719 self.top.deiconify()
720 self.top.lower()
721 self.top.lift()
David Scherer7aced172000-08-15 01:13:23 +0000722 return self.io.maybesave()
723
724 def close(self):
David Scherer7aced172000-08-15 01:13:23 +0000725 reply = self.maybesave()
726 if reply != "cancel":
727 self._close()
728 return reply
729
730 def _close(self):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000731 #print self.io.filename
Steven M. Gava1d46e402002-03-27 08:40:46 +0000732 if self.io.filename:
733 self.UpdateRecentFilesList(newFile=self.io.filename)
David Scherer7aced172000-08-15 01:13:23 +0000734 WindowList.unregister_callback(self.postwindowsmenu)
735 if self.close_hook:
736 self.close_hook()
737 self.flist = None
738 colorizing = 0
739 self.unload_extensions()
740 self.io.close(); self.io = None
741 self.undo = None # XXX
742 if self.color:
743 colorizing = self.color.colorizing
744 doh = colorizing and self.top
745 self.color.close(doh) # Cancel colorization
746 self.text = None
747 self.vars = None
748 self.per.close(); self.per = None
749 if not colorizing:
750 self.top.destroy()
751
752 def load_extensions(self):
753 self.extensions = {}
754 self.load_standard_extensions()
755
756 def unload_extensions(self):
757 for ins in self.extensions.values():
758 if hasattr(ins, "close"):
759 ins.close()
760 self.extensions = {}
761
762 def load_standard_extensions(self):
763 for name in self.get_standard_extension_names():
764 try:
765 self.load_extension(name)
766 except:
767 print "Failed to load extension", `name`
768 import traceback
769 traceback.print_exc()
770
771 def get_standard_extension_names(self):
Steven M. Gavadc72f482002-01-03 11:51:07 +0000772 return idleConf.GetExtensions()
David Scherer7aced172000-08-15 01:13:23 +0000773
774 def load_extension(self, name):
775 mod = __import__(name, globals(), locals(), [])
776 cls = getattr(mod, name)
777 ins = cls(self)
778 self.extensions[name] = ins
Steven M. Gava72c3bf02002-01-19 10:41:51 +0000779 keydefs=idleConf.GetExtensionBindings(name)
David Scherer7aced172000-08-15 01:13:23 +0000780 if keydefs:
781 self.apply_bindings(keydefs)
782 for vevent in keydefs.keys():
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000783 methodname = vevent.replace("-", "_")
David Scherer7aced172000-08-15 01:13:23 +0000784 while methodname[:1] == '<':
785 methodname = methodname[1:]
786 while methodname[-1:] == '>':
787 methodname = methodname[:-1]
788 methodname = methodname + "_event"
789 if hasattr(ins, methodname):
790 self.text.bind(vevent, getattr(ins, methodname))
791 if hasattr(ins, "menudefs"):
792 self.fill_menus(ins.menudefs, keydefs)
793 return ins
794
795 def apply_bindings(self, keydefs=None):
796 if keydefs is None:
797 keydefs = self.Bindings.default_keydefs
798 text = self.text
799 text.keydefs = keydefs
800 for event, keylist in keydefs.items():
801 if keylist:
802 apply(text.event_add, (event,) + tuple(keylist))
803
804 def fill_menus(self, defs=None, keydefs=None):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000805 """Add appropriate entries to the menus and submenus
806
807 Menus that are absent or None in self.menudict are ignored.
808 """
David Scherer7aced172000-08-15 01:13:23 +0000809 if defs is None:
810 defs = self.Bindings.menudefs
811 if keydefs is None:
812 keydefs = self.Bindings.default_keydefs
813 menudict = self.menudict
814 text = self.text
815 for mname, itemlist in defs:
816 menu = menudict.get(mname)
817 if not menu:
818 continue
819 for item in itemlist:
820 if not item:
821 menu.add_separator()
822 else:
823 label, event = item
824 checkbutton = (label[:1] == '!')
825 if checkbutton:
826 label = label[1:]
827 underline, label = prepstr(label)
828 accelerator = get_accelerator(keydefs, event)
829 def command(text=text, event=event):
830 text.event_generate(event)
831 if checkbutton:
832 var = self.getrawvar(event, BooleanVar)
833 menu.add_checkbutton(label=label, underline=underline,
834 command=command, accelerator=accelerator,
835 variable=var)
836 else:
837 menu.add_command(label=label, underline=underline,
Kurt B. Kaiser84f48032002-09-26 22:13:22 +0000838 command=command,
839 accelerator=accelerator)
David Scherer7aced172000-08-15 01:13:23 +0000840
841 def getvar(self, name):
842 var = self.getrawvar(name)
843 if var:
844 return var.get()
845
846 def setvar(self, name, value, vartype=None):
847 var = self.getrawvar(name, vartype)
848 if var:
849 var.set(value)
850
851 def getrawvar(self, name, vartype=None):
852 var = self.vars.get(name)
853 if not var and vartype:
854 self.vars[name] = var = vartype(self.text)
855 return var
856
857 # Tk implementations of "virtual text methods" -- each platform
858 # reusing IDLE's support code needs to define these for its GUI's
859 # flavor of widget.
860
861 # Is character at text_index in a Python string? Return 0 for
862 # "guaranteed no", true for anything else. This info is expensive
863 # to compute ab initio, but is probably already known by the
864 # platform's colorizer.
865
866 def is_char_in_string(self, text_index):
867 if self.color:
868 # Return true iff colorizer hasn't (re)gotten this far
869 # yet, or the character is tagged as being in a string
870 return self.text.tag_prevrange("TODO", text_index) or \
871 "STRING" in self.text.tag_names(text_index)
872 else:
873 # The colorizer is missing: assume the worst
874 return 1
875
876 # If a selection is defined in the text widget, return (start,
877 # end) as Tkinter text indices, otherwise return (None, None)
878 def get_selection_indices(self):
879 try:
880 first = self.text.index("sel.first")
881 last = self.text.index("sel.last")
882 return first, last
883 except TclError:
884 return None, None
885
886 # Return the text widget's current view of what a tab stop means
887 # (equivalent width in spaces).
888
889 def get_tabwidth(self):
890 current = self.text['tabs'] or TK_TABWIDTH_DEFAULT
891 return int(current)
892
893 # Set the text widget's current view of what a tab stop means.
894
895 def set_tabwidth(self, newtabwidth):
896 text = self.text
897 if self.get_tabwidth() != newtabwidth:
898 pixels = text.tk.call("font", "measure", text["font"],
899 "-displayof", text.master,
Kurt B. Kaiserafdf71b2001-07-13 03:35:32 +0000900 "n" * newtabwidth)
David Scherer7aced172000-08-15 01:13:23 +0000901 text.configure(tabs=pixels)
902
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000903### begin autoindent code ###
904
905 # usetabs true -> literal tab characters are used by indent and
906 # dedent cmds, possibly mixed with spaces if
907 # indentwidth is not a multiple of tabwidth
908 # false -> tab characters are converted to spaces by indent
909 # and dedent cmds, and ditto TAB keystrokes
910 # indentwidth is the number of characters per logical indent level.
911 # tabwidth is the display width of a literal tab character.
912 # CAUTION: telling Tk to use anything other than its default
913 # tab setting causes it to use an entirely different tabbing algorithm,
914 # treating tab stops as fixed distances from the left margin.
915 # Nobody expects this, so for now tabwidth should never be changed.
916 usetabs = 0
917 indentwidth = 4
918 tabwidth = 8 # for IDLE use, must remain 8 until Tk is fixed
919
920 # If context_use_ps1 is true, parsing searches back for a ps1 line;
921 # else searches for a popular (if, def, ...) Python stmt.
922 context_use_ps1 = 0
923
924 # When searching backwards for a reliable place to begin parsing,
925 # first start num_context_lines[0] lines back, then
926 # num_context_lines[1] lines back if that didn't work, and so on.
927 # The last value should be huge (larger than the # of lines in a
928 # conceivable file).
929 # Making the initial values larger slows things down more often.
930 num_context_lines = 50, 500, 5000000
931
932 def config(self, **options):
933 for key, value in options.items():
934 if key == 'usetabs':
935 self.usetabs = value
936 elif key == 'indentwidth':
937 self.indentwidth = value
938 elif key == 'tabwidth':
939 self.tabwidth = value
940 elif key == 'context_use_ps1':
941 self.context_use_ps1 = value
942 else:
943 raise KeyError, "bad option name: %s" % `key`
944
945 # If ispythonsource and guess are true, guess a good value for
946 # indentwidth based on file content (if possible), and if
947 # indentwidth != tabwidth set usetabs false.
948 # In any case, adjust the Text widget's view of what a tab
949 # character means.
950
951 def set_indentation_params(self, ispythonsource, guess=1):
952 if guess and ispythonsource:
953 i = self.guess_indent()
954 if 2 <= i <= 8:
955 self.indentwidth = i
956 if self.indentwidth != self.tabwidth:
957 self.usetabs = 0
958
959 self.set_tabwidth(self.tabwidth)
960
961 def smart_backspace_event(self, event):
962 text = self.text
963 first, last = self.get_selection_indices()
964 if first and last:
965 text.delete(first, last)
966 text.mark_set("insert", first)
967 return "break"
968 # Delete whitespace left, until hitting a real char or closest
969 # preceding virtual tab stop.
970 chars = text.get("insert linestart", "insert")
971 if chars == '':
972 if text.compare("insert", ">", "1.0"):
973 # easy: delete preceding newline
974 text.delete("insert-1c")
975 else:
976 text.bell() # at start of buffer
977 return "break"
978 if chars[-1] not in " \t":
979 # easy: delete preceding real char
980 text.delete("insert-1c")
981 return "break"
982 # Ick. It may require *inserting* spaces if we back up over a
983 # tab character! This is written to be clear, not fast.
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +0000984 tabwidth = self.tabwidth
985 have = len(chars.expandtabs(tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000986 assert have > 0
987 want = ((have - 1) // self.indentwidth) * self.indentwidth
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +0000988 # Debug prompt is multilined....
989 last_line_of_prompt = sys.ps1.split('\n')[-1]
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000990 ncharsdeleted = 0
991 while 1:
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +0000992 if chars == last_line_of_prompt:
Kurt B. Kaiser1bdca5e2002-12-16 22:25:10 +0000993 break
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000994 chars = chars[:-1]
995 ncharsdeleted = ncharsdeleted + 1
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +0000996 have = len(chars.expandtabs(tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000997 if have <= want or chars[-1] not in " \t":
998 break
999 text.undo_block_start()
1000 text.delete("insert-%dc" % ncharsdeleted, "insert")
1001 if have < want:
1002 text.insert("insert", ' ' * (want - have))
1003 text.undo_block_stop()
1004 return "break"
1005
1006 def smart_indent_event(self, event):
1007 # if intraline selection:
1008 # delete it
1009 # elif multiline selection:
1010 # do indent-region & return
1011 # indent one level
1012 text = self.text
1013 first, last = self.get_selection_indices()
1014 text.undo_block_start()
1015 try:
1016 if first and last:
1017 if index2line(first) != index2line(last):
1018 return self.indent_region_event(event)
1019 text.delete(first, last)
1020 text.mark_set("insert", first)
1021 prefix = text.get("insert linestart", "insert")
1022 raw, effective = classifyws(prefix, self.tabwidth)
1023 if raw == len(prefix):
1024 # only whitespace to the left
1025 self.reindent_to(effective + self.indentwidth)
1026 else:
1027 if self.usetabs:
1028 pad = '\t'
1029 else:
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001030 effective = len(prefix.expandtabs(self.tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001031 n = self.indentwidth
1032 pad = ' ' * (n - effective % n)
1033 text.insert("insert", pad)
1034 text.see("insert")
1035 return "break"
1036 finally:
1037 text.undo_block_stop()
1038
1039 def newline_and_indent_event(self, event):
1040 text = self.text
1041 first, last = self.get_selection_indices()
1042 text.undo_block_start()
1043 try:
1044 if first and last:
1045 text.delete(first, last)
1046 text.mark_set("insert", first)
1047 line = text.get("insert linestart", "insert")
1048 i, n = 0, len(line)
1049 while i < n and line[i] in " \t":
1050 i = i+1
1051 if i == n:
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001052 # the cursor is in or at leading indentation in a continuation
1053 # line; just inject an empty line at the start
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001054 text.insert("insert linestart", '\n')
1055 return "break"
1056 indent = line[:i]
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001057 # strip whitespace before insert point unless it's in the prompt
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001058 i = 0
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001059 last_line_of_prompt = sys.ps1.split('\n')[-1]
1060 while line and line[-1] in " \t" and line != last_line_of_prompt:
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001061 line = line[:-1]
1062 i = i+1
1063 if i:
1064 text.delete("insert - %d chars" % i, "insert")
1065 # strip whitespace after insert point
1066 while text.get("insert") in " \t":
1067 text.delete("insert")
1068 # start new line
1069 text.insert("insert", '\n')
1070
1071 # adjust indentation for continuations and block
1072 # open/close first need to find the last stmt
1073 lno = index2line(text.index('insert'))
1074 y = PyParse.Parser(self.indentwidth, self.tabwidth)
1075 for context in self.num_context_lines:
1076 startat = max(lno - context, 1)
1077 startatindex = `startat` + ".0"
1078 rawtext = text.get(startatindex, "insert")
1079 y.set_str(rawtext)
1080 bod = y.find_good_parse_start(
1081 self.context_use_ps1,
1082 self._build_char_in_string_func(startatindex))
1083 if bod is not None or startat == 1:
1084 break
1085 y.set_lo(bod or 0)
1086 c = y.get_continuation_type()
1087 if c != PyParse.C_NONE:
1088 # The current stmt hasn't ended yet.
1089 if c == PyParse.C_STRING:
1090 # inside a string; just mimic the current indent
1091 text.insert("insert", indent)
1092 elif c == PyParse.C_BRACKET:
1093 # line up with the first (if any) element of the
1094 # last open bracket structure; else indent one
1095 # level beyond the indent of the line with the
1096 # last open bracket
1097 self.reindent_to(y.compute_bracket_indent())
1098 elif c == PyParse.C_BACKSLASH:
1099 # if more than one line in this stmt already, just
1100 # mimic the current indent; else if initial line
1101 # has a start on an assignment stmt, indent to
1102 # beyond leftmost =; else to beyond first chunk of
1103 # non-whitespace on initial line
1104 if y.get_num_lines_in_stmt() > 1:
1105 text.insert("insert", indent)
1106 else:
1107 self.reindent_to(y.compute_backslash_indent())
1108 else:
1109 assert 0, "bogus continuation type " + `c`
1110 return "break"
1111
1112 # This line starts a brand new stmt; indent relative to
1113 # indentation of initial line of closest preceding
1114 # interesting stmt.
1115 indent = y.get_base_indent_string()
1116 text.insert("insert", indent)
1117 if y.is_block_opener():
1118 self.smart_indent_event(event)
1119 elif indent and y.is_block_closer():
1120 self.smart_backspace_event(event)
1121 return "break"
1122 finally:
1123 text.see("insert")
1124 text.undo_block_stop()
1125
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001126 # Our editwin provides a is_char_in_string function that works
1127 # with a Tk text index, but PyParse only knows about offsets into
1128 # a string. This builds a function for PyParse that accepts an
1129 # offset.
1130
1131 def _build_char_in_string_func(self, startindex):
1132 def inner(offset, _startindex=startindex,
1133 _icis=self.is_char_in_string):
1134 return _icis(_startindex + "+%dc" % offset)
1135 return inner
1136
1137 def indent_region_event(self, event):
1138 head, tail, chars, lines = self.get_region()
1139 for pos in range(len(lines)):
1140 line = lines[pos]
1141 if line:
1142 raw, effective = classifyws(line, self.tabwidth)
1143 effective = effective + self.indentwidth
1144 lines[pos] = self._make_blanks(effective) + line[raw:]
1145 self.set_region(head, tail, chars, lines)
1146 return "break"
1147
1148 def dedent_region_event(self, event):
1149 head, tail, chars, lines = self.get_region()
1150 for pos in range(len(lines)):
1151 line = lines[pos]
1152 if line:
1153 raw, effective = classifyws(line, self.tabwidth)
1154 effective = max(effective - self.indentwidth, 0)
1155 lines[pos] = self._make_blanks(effective) + line[raw:]
1156 self.set_region(head, tail, chars, lines)
1157 return "break"
1158
1159 def comment_region_event(self, event):
1160 head, tail, chars, lines = self.get_region()
1161 for pos in range(len(lines) - 1):
1162 line = lines[pos]
1163 lines[pos] = '##' + line
1164 self.set_region(head, tail, chars, lines)
1165
1166 def uncomment_region_event(self, event):
1167 head, tail, chars, lines = self.get_region()
1168 for pos in range(len(lines)):
1169 line = lines[pos]
1170 if not line:
1171 continue
1172 if line[:2] == '##':
1173 line = line[2:]
1174 elif line[:1] == '#':
1175 line = line[1:]
1176 lines[pos] = line
1177 self.set_region(head, tail, chars, lines)
1178
1179 def tabify_region_event(self, event):
1180 head, tail, chars, lines = self.get_region()
1181 tabwidth = self._asktabwidth()
1182 for pos in range(len(lines)):
1183 line = lines[pos]
1184 if line:
1185 raw, effective = classifyws(line, tabwidth)
1186 ntabs, nspaces = divmod(effective, tabwidth)
1187 lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]
1188 self.set_region(head, tail, chars, lines)
1189
1190 def untabify_region_event(self, event):
1191 head, tail, chars, lines = self.get_region()
1192 tabwidth = self._asktabwidth()
1193 for pos in range(len(lines)):
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001194 lines[pos] = lines[pos].expandtabs(tabwidth)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001195 self.set_region(head, tail, chars, lines)
1196
1197 def toggle_tabs_event(self, event):
1198 if self.askyesno(
1199 "Toggle tabs",
1200 "Turn tabs " + ("on", "off")[self.usetabs] + "?",
1201 parent=self.text):
1202 self.usetabs = not self.usetabs
1203 return "break"
1204
1205 # XXX this isn't bound to anything -- see class tabwidth comments
1206 def change_tabwidth_event(self, event):
1207 new = self._asktabwidth()
1208 if new != self.tabwidth:
1209 self.tabwidth = new
1210 self.set_indentation_params(0, guess=0)
1211 return "break"
1212
1213 def change_indentwidth_event(self, event):
1214 new = self.askinteger(
1215 "Indent width",
1216 "New indent width (2-16)",
1217 parent=self.text,
1218 initialvalue=self.indentwidth,
1219 minvalue=2,
1220 maxvalue=16)
1221 if new and new != self.indentwidth:
1222 self.indentwidth = new
1223 return "break"
1224
1225 def get_region(self):
1226 text = self.text
1227 first, last = self.get_selection_indices()
1228 if first and last:
1229 head = text.index(first + " linestart")
1230 tail = text.index(last + "-1c lineend +1c")
1231 else:
1232 head = text.index("insert linestart")
1233 tail = text.index("insert lineend +1c")
1234 chars = text.get(head, tail)
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001235 lines = chars.split("\n")
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001236 return head, tail, chars, lines
1237
1238 def set_region(self, head, tail, chars, lines):
1239 text = self.text
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001240 newchars = "\n".join(lines)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001241 if newchars == chars:
1242 text.bell()
1243 return
1244 text.tag_remove("sel", "1.0", "end")
1245 text.mark_set("insert", head)
1246 text.undo_block_start()
1247 text.delete(head, tail)
1248 text.insert(head, newchars)
1249 text.undo_block_stop()
1250 text.tag_add("sel", head, "insert")
1251
1252 # Make string that displays as n leading blanks.
1253
1254 def _make_blanks(self, n):
1255 if self.usetabs:
1256 ntabs, nspaces = divmod(n, self.tabwidth)
1257 return '\t' * ntabs + ' ' * nspaces
1258 else:
1259 return ' ' * n
1260
1261 # Delete from beginning of line to insert point, then reinsert
1262 # column logical (meaning use tabs if appropriate) spaces.
1263
1264 def reindent_to(self, column):
1265 text = self.text
1266 text.undo_block_start()
1267 if text.compare("insert linestart", "!=", "insert"):
1268 text.delete("insert linestart", "insert")
1269 if column:
1270 text.insert("insert", self._make_blanks(column))
1271 text.undo_block_stop()
1272
1273 def _asktabwidth(self):
1274 return self.askinteger(
1275 "Tab width",
1276 "Spaces per tab? (2-16)",
1277 parent=self.text,
1278 initialvalue=self.indentwidth,
1279 minvalue=2,
1280 maxvalue=16) or self.tabwidth
1281
1282 # Guess indentwidth from text content.
1283 # Return guessed indentwidth. This should not be believed unless
1284 # it's in a reasonable range (e.g., it will be 0 if no indented
1285 # blocks are found).
1286
1287 def guess_indent(self):
1288 opener, indented = IndentSearcher(self.text, self.tabwidth).run()
1289 if opener and indented:
1290 raw, indentsmall = classifyws(opener, self.tabwidth)
1291 raw, indentlarge = classifyws(indented, self.tabwidth)
1292 else:
1293 indentsmall = indentlarge = 0
1294 return indentlarge - indentsmall
1295
1296# "line.col" -> line, as an int
1297def index2line(index):
1298 return int(float(index))
1299
1300# Look at the leading whitespace in s.
1301# Return pair (# of leading ws characters,
1302# effective # of leading blanks after expanding
1303# tabs to width tabwidth)
1304
1305def classifyws(s, tabwidth):
1306 raw = effective = 0
1307 for ch in s:
1308 if ch == ' ':
1309 raw = raw + 1
1310 effective = effective + 1
1311 elif ch == '\t':
1312 raw = raw + 1
1313 effective = (effective // tabwidth + 1) * tabwidth
1314 else:
1315 break
1316 return raw, effective
1317
1318import tokenize
1319_tokenize = tokenize
1320del tokenize
1321
1322class IndentSearcher:
1323
1324 # .run() chews over the Text widget, looking for a block opener
1325 # and the stmt following it. Returns a pair,
1326 # (line containing block opener, line containing stmt)
1327 # Either or both may be None.
1328
1329 def __init__(self, text, tabwidth):
1330 self.text = text
1331 self.tabwidth = tabwidth
1332 self.i = self.finished = 0
1333 self.blkopenline = self.indentedline = None
1334
1335 def readline(self):
1336 if self.finished:
1337 return ""
1338 i = self.i = self.i + 1
1339 mark = `i` + ".0"
1340 if self.text.compare(mark, ">=", "end"):
1341 return ""
1342 return self.text.get(mark, mark + " lineend+1c")
1343
1344 def tokeneater(self, type, token, start, end, line,
1345 INDENT=_tokenize.INDENT,
1346 NAME=_tokenize.NAME,
1347 OPENERS=('class', 'def', 'for', 'if', 'try', 'while')):
1348 if self.finished:
1349 pass
1350 elif type == NAME and token in OPENERS:
1351 self.blkopenline = line
1352 elif type == INDENT and self.blkopenline:
1353 self.indentedline = line
1354 self.finished = 1
1355
1356 def run(self):
1357 save_tabsize = _tokenize.tabsize
1358 _tokenize.tabsize = self.tabwidth
1359 try:
1360 try:
1361 _tokenize.tokenize(self.readline, self.tokeneater)
1362 except _tokenize.TokenError:
1363 # since we cut off the tokenizer early, we can trigger
1364 # spurious errors
1365 pass
1366 finally:
1367 _tokenize.tabsize = save_tabsize
1368 return self.blkopenline, self.indentedline
1369
1370### end autoindent code ###
1371
David Scherer7aced172000-08-15 01:13:23 +00001372def prepstr(s):
1373 # Helper to extract the underscore from a string, e.g.
1374 # prepstr("Co_py") returns (2, "Copy").
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +00001375 i = s.find('_')
David Scherer7aced172000-08-15 01:13:23 +00001376 if i >= 0:
1377 s = s[:i] + s[i+1:]
1378 return i, s
1379
1380
1381keynames = {
1382 'bracketleft': '[',
1383 'bracketright': ']',
1384 'slash': '/',
1385}
1386
1387def get_accelerator(keydefs, event):
1388 keylist = keydefs.get(event)
1389 if not keylist:
1390 return ""
1391 s = keylist[0]
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +00001392 s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s)
David Scherer7aced172000-08-15 01:13:23 +00001393 s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s)
1394 s = re.sub("Key-", "", s)
1395 s = re.sub("Cancel","Ctrl-Break",s) # dscherer@cmu.edu
1396 s = re.sub("Control-", "Ctrl-", s)
1397 s = re.sub("-", "+", s)
1398 s = re.sub("><", " ", s)
1399 s = re.sub("<", "", s)
1400 s = re.sub(">", "", s)
1401 return s
1402
1403
1404def fixwordbreaks(root):
1405 # Make sure that Tk's double-click and next/previous word
1406 # operations use our definition of a word (i.e. an identifier)
1407 tk = root.tk
1408 tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
1409 tk.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]')
1410 tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]')
1411
1412
1413def test():
1414 root = Tk()
1415 fixwordbreaks(root)
1416 root.withdraw()
1417 if sys.argv[1:]:
1418 filename = sys.argv[1]
1419 else:
1420 filename = None
1421 edit = EditorWindow(root=root, filename=filename)
1422 edit.set_close_hook(root.quit)
1423 root.mainloop()
1424 root.destroy()
1425
1426if __name__ == '__main__':
1427 test()