blob: 605d34c781950d205bd00798added98bb763276b [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
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +00005from itertools import count
David Scherer7aced172000-08-15 01:13:23 +00006from Tkinter import *
7import tkSimpleDialog
8import tkMessageBox
Kurt B. Kaiserfd182cd2001-07-14 03:58:25 +00009
10import webbrowser
David Scherer7aced172000-08-15 01:13:23 +000011import idlever
12import WindowList
Steven M. Gavac5976402002-01-04 03:06:08 +000013import SearchDialog
14import GrepDialog
15import ReplaceDialog
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +000016import PyParse
Steven M. Gavadc72f482002-01-03 11:51:07 +000017from configHandler import idleConf
Steven M. Gava3b55a892001-11-21 05:56:26 +000018import aboutDialog, textView, configDialog
David Scherer7aced172000-08-15 01:13:23 +000019
20# The default tab setting for a Text widget, in average-width characters.
21TK_TABWIDTH_DEFAULT = 8
22
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +000023def _find_module(fullname, path=None):
24 """Version of imp.find_module() that handles hierarchical module names"""
25
26 file = None
27 for tgt in fullname.split('.'):
28 if file is not None:
29 file.close() # close intermediate files
30 (file, filename, descr) = imp.find_module(tgt, path)
31 if descr[2] == imp.PY_SOURCE:
32 break # find but not load the source file
33 module = imp.load_module(tgt, file, filename, descr)
Kurt B. Kaiser69e8afc2003-01-10 21:25:20 +000034 try:
35 path = module.__path__
36 except AttributeError:
37 raise ImportError, 'No source for module ' + module.__name__
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +000038 return file, filename, descr
39
David Scherer7aced172000-08-15 01:13:23 +000040class EditorWindow:
David Scherer7aced172000-08-15 01:13:23 +000041 from Percolator import Percolator
42 from ColorDelegator import ColorDelegator
43 from UndoDelegator import UndoDelegator
44 from IOBinding import IOBinding
45 import Bindings
46 from Tkinter import Toplevel
47 from MultiStatusBar import MultiStatusBar
48
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:
Thomas Heller84ef1532003-09-23 20:53:10 +000053 dochome = os.path.join(sys.prefix, 'Doc', 'index.html')
Kurt B. Kaiser114713d2003-01-10 05:07:24 +000054 if sys.platform.count('linux'):
55 # look for html docs in a couple of standard places
56 pyver = 'python-docs-' + '%s.%s.%s' % sys.version_info[:3]
57 if os.path.isdir('/var/www/html/python/'): # "python2" rpm
58 dochome = '/var/www/html/python/index.html'
59 else:
60 basepath = '/usr/share/doc/' # standard location
61 dochome = os.path.join(basepath, pyver,
62 'Doc', 'index.html')
Thomas Heller84ef1532003-09-23 20:53:10 +000063 elif sys.platform.count('win') or sys.platform.count('nt'):
64 chmfile = os.path.join(sys.prefix, "Python%d%d.chm" % sys.version_info[:2])
65 if os.path.isfile(chmfile):
66 dochome = chmfile
67 print "dochome =", dochome
Kurt B. Kaiser114713d2003-01-10 05:07:24 +000068 dochome = os.path.normpath(dochome)
69 if os.path.isfile(dochome):
70 EditorWindow.help_url = dochome
71 else:
72 EditorWindow.help_url = "http://www.python.org/doc/current"
Steven M. Gavadc72f482002-01-03 11:51:07 +000073 currentTheme=idleConf.CurrentTheme()
David Scherer7aced172000-08-15 01:13:23 +000074 self.flist = flist
75 root = root or flist.root
76 self.root = root
David Scherer7aced172000-08-15 01:13:23 +000077 self.menubar = Menu(root)
78 self.top = top = self.Toplevel(root, menu=self.menubar)
Steven M. Gava0c5bc8c2002-03-27 02:25:44 +000079 if flist:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +000080 self.tkinter_vars = flist.vars
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +000081 #self.top.instance_dict makes flist.inversedict avalable to
Steven M. Gava0c5bc8c2002-03-27 02:25:44 +000082 #configDialog.py so it can access all EditorWindow instaces
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +000083 self.top.instance_dict=flist.inversedict
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +000084 else:
85 self.tkinter_vars = {} # keys: Tkinter event names
86 # values: Tkinter variable instances
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +000087 self.recent_files_path=os.path.join(idleConf.GetUserCfgDir(),
Steven M. Gava1d46e402002-03-27 08:40:46 +000088 'recent-files.lst')
David Scherer7aced172000-08-15 01:13:23 +000089 self.vbar = vbar = Scrollbar(top, name='vbar')
90 self.text_frame = text_frame = Frame(top)
Kurt B. Kaiser1061e722003-01-04 01:43:53 +000091 self.width = idleConf.GetOption('main','EditorWindow','width')
Kurt B. Kaisera1dee062002-10-04 21:33:57 +000092 self.text = text = Text(text_frame, name='text', padx=5, wrap='none',
Steven M. Gavadc72f482002-01-03 11:51:07 +000093 foreground=idleConf.GetHighlight(currentTheme,
94 'normal',fgBg='fg'),
95 background=idleConf.GetHighlight(currentTheme,
96 'normal',fgBg='bg'),
97 highlightcolor=idleConf.GetHighlight(currentTheme,
98 'hilite',fgBg='fg'),
99 highlightbackground=idleConf.GetHighlight(currentTheme,
100 'hilite',fgBg='bg'),
101 insertbackground=idleConf.GetHighlight(currentTheme,
102 'cursor',fgBg='fg'),
Kurt B. Kaiser1061e722003-01-04 01:43:53 +0000103 width=self.width,
Steven M. Gavadc72f482002-01-03 11:51:07 +0000104 height=idleConf.GetOption('main','EditorWindow','height') )
David Scherer7aced172000-08-15 01:13:23 +0000105
106 self.createmenubar()
107 self.apply_bindings()
108
109 self.top.protocol("WM_DELETE_WINDOW", self.close)
110 self.top.bind("<<close-window>>", self.close_event)
Kurt B. Kaiser84f48032002-09-26 22:13:22 +0000111 text.bind("<<cut>>", self.cut)
112 text.bind("<<copy>>", self.copy)
113 text.bind("<<paste>>", self.paste)
David Scherer7aced172000-08-15 01:13:23 +0000114 text.bind("<<center-insert>>", self.center_insert_event)
115 text.bind("<<help>>", self.help_dialog)
David Scherer7aced172000-08-15 01:13:23 +0000116 text.bind("<<python-docs>>", self.python_docs)
117 text.bind("<<about-idle>>", self.about_dialog)
Steven M. Gava3b55a892001-11-21 05:56:26 +0000118 text.bind("<<open-config-dialog>>", self.config_dialog)
David Scherer7aced172000-08-15 01:13:23 +0000119 text.bind("<<open-module>>", self.open_module)
120 text.bind("<<do-nothing>>", lambda event: "break")
121 text.bind("<<select-all>>", self.select_all)
122 text.bind("<<remove-selection>>", self.remove_selection)
Steven M. Gavac5976402002-01-04 03:06:08 +0000123 text.bind("<<find>>", self.find_event)
124 text.bind("<<find-again>>", self.find_again_event)
125 text.bind("<<find-in-files>>", self.find_in_files_event)
126 text.bind("<<find-selection>>", self.find_selection_event)
127 text.bind("<<replace>>", self.replace_event)
128 text.bind("<<goto-line>>", self.goto_line_event)
David Scherer7aced172000-08-15 01:13:23 +0000129 text.bind("<3>", self.right_menu_event)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000130 text.bind("<<smart-backspace>>",self.smart_backspace_event)
131 text.bind("<<newline-and-indent>>",self.newline_and_indent_event)
132 text.bind("<<smart-indent>>",self.smart_indent_event)
133 text.bind("<<indent-region>>",self.indent_region_event)
134 text.bind("<<dedent-region>>",self.dedent_region_event)
135 text.bind("<<comment-region>>",self.comment_region_event)
136 text.bind("<<uncomment-region>>",self.uncomment_region_event)
137 text.bind("<<tabify-region>>",self.tabify_region_event)
138 text.bind("<<untabify-region>>",self.untabify_region_event)
139 text.bind("<<toggle-tabs>>",self.toggle_tabs_event)
140 text.bind("<<change-indentwidth>>",self.change_indentwidth_event)
Kurt B. Kaiser5ec186b2003-01-17 04:04:06 +0000141 text.bind("<Left>", self.move_at_edge_if_selection(0))
142 text.bind("<Right>", self.move_at_edge_if_selection(1))
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000143
David Scherer7aced172000-08-15 01:13:23 +0000144 if flist:
145 flist.inversedict[self] = key
146 if key:
147 flist.dict[key] = self
Kurt B. Kaiserd2f48612003-06-05 02:34:04 +0000148 text.bind("<<open-new-window>>", self.new_callback)
David Scherer7aced172000-08-15 01:13:23 +0000149 text.bind("<<close-all-windows>>", self.flist.close_all_callback)
150 text.bind("<<open-class-browser>>", self.open_class_browser)
151 text.bind("<<open-path-browser>>", self.open_path_browser)
152
Steven M. Gava898a3652001-10-07 11:10:44 +0000153 self.set_status_bar()
David Scherer7aced172000-08-15 01:13:23 +0000154 vbar['command'] = text.yview
155 vbar.pack(side=RIGHT, fill=Y)
David Scherer7aced172000-08-15 01:13:23 +0000156 text['yscrollcommand'] = vbar.set
Steven M. Gavab1585412002-03-12 00:21:56 +0000157 fontWeight='normal'
158 if idleConf.GetOption('main','EditorWindow','font-bold',type='bool'):
159 fontWeight='bold'
Steven M. Gavadc72f482002-01-03 11:51:07 +0000160 text.config(font=(idleConf.GetOption('main','EditorWindow','font'),
Steven M. Gavab1585412002-03-12 00:21:56 +0000161 idleConf.GetOption('main','EditorWindow','font-size'),
162 fontWeight))
David Scherer7aced172000-08-15 01:13:23 +0000163 text_frame.pack(side=LEFT, fill=BOTH, expand=1)
164 text.pack(side=TOP, fill=BOTH, expand=1)
165 text.focus_set()
166
167 self.per = per = self.Percolator(text)
168 if self.ispythonsource(filename):
Kurt B. Kaiserdc1e7092002-07-11 04:33:41 +0000169 self.color = color = self.ColorDelegator()
170 per.insertfilter(color)
David Scherer7aced172000-08-15 01:13:23 +0000171 else:
David Scherer7aced172000-08-15 01:13:23 +0000172 self.color = None
Kurt B. Kaiserdc1e7092002-07-11 04:33:41 +0000173
174 self.undo = undo = self.UndoDelegator()
175 per.insertfilter(undo)
176 text.undo_block_start = undo.undo_block_start
177 text.undo_block_stop = undo.undo_block_stop
178 undo.set_saved_change_hook(self.saved_change_hook)
179
180 # IOBinding implements file I/O and printing functionality
David Scherer7aced172000-08-15 01:13:23 +0000181 self.io = io = self.IOBinding(self)
Kurt B. Kaiserdc1e7092002-07-11 04:33:41 +0000182 io.set_filename_change_hook(self.filename_change_hook)
183
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000184 # Create the recent files submenu
185 self.recent_files_menu = Menu(self.menubar)
186 self.menudict['file'].insert_cascade(3, label='Recent Files',
187 underline=0,
188 menu=self.recent_files_menu)
189 self.update_recent_files_list()
David Scherer7aced172000-08-15 01:13:23 +0000190
David Scherer7aced172000-08-15 01:13:23 +0000191 if filename:
Kurt B. Kaiserd2f48612003-06-05 02:34:04 +0000192 if os.path.exists(filename) and not os.path.isdir(filename):
David Scherer7aced172000-08-15 01:13:23 +0000193 io.loadfile(filename)
194 else:
195 io.set_filename(filename)
David Scherer7aced172000-08-15 01:13:23 +0000196 self.saved_change_hook()
197
198 self.load_extensions()
199
200 menu = self.menudict.get('windows')
201 if menu:
202 end = menu.index("end")
203 if end is None:
204 end = -1
205 if end >= 0:
206 menu.add_separator()
207 end = end + 1
208 self.wmenu_end = end
209 WindowList.register_callback(self.postwindowsmenu)
210
211 # Some abstractions so IDLE extensions are cross-IDE
212 self.askyesno = tkMessageBox.askyesno
213 self.askinteger = tkSimpleDialog.askinteger
214 self.showerror = tkMessageBox.showerror
215
216 if self.extensions.has_key('AutoIndent'):
217 self.extensions['AutoIndent'].set_indentation_params(
218 self.ispythonsource(filename))
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000219
Kurt B. Kaiserd2f48612003-06-05 02:34:04 +0000220 def new_callback(self, event):
221 dirname, basename = self.io.defaultfilename()
222 self.flist.new(dirname)
223 return "break"
224
David Scherer7aced172000-08-15 01:13:23 +0000225 def set_status_bar(self):
Steven M. Gava898a3652001-10-07 11:10:44 +0000226 self.status_bar = self.MultiStatusBar(self.top)
David Scherer7aced172000-08-15 01:13:23 +0000227 self.status_bar.set_label('column', 'Col: ?', side=RIGHT)
228 self.status_bar.set_label('line', 'Ln: ?', side=RIGHT)
229 self.status_bar.pack(side=BOTTOM, fill=X)
230 self.text.bind('<KeyRelease>', self.set_line_and_column)
231 self.text.bind('<ButtonRelease>', self.set_line_and_column)
232 self.text.after_idle(self.set_line_and_column)
233
234 def set_line_and_column(self, event=None):
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000235 line, column = self.text.index(INSERT).split('.')
David Scherer7aced172000-08-15 01:13:23 +0000236 self.status_bar.set_label('column', 'Col: %s' % column)
237 self.status_bar.set_label('line', 'Ln: %s' % line)
238
239 def wakeup(self):
240 if self.top.wm_state() == "iconic":
241 self.top.wm_deiconify()
242 else:
243 self.top.tkraise()
244 self.text.focus_set()
245
246 menu_specs = [
247 ("file", "_File"),
248 ("edit", "_Edit"),
249 ("format", "F_ormat"),
250 ("run", "_Run"),
Kurt B. Kaiser1061e722003-01-04 01:43:53 +0000251 ("options", "_Options"),
David Scherer7aced172000-08-15 01:13:23 +0000252 ("windows", "_Windows"),
253 ("help", "_Help"),
254 ]
255
256 def createmenubar(self):
257 mbar = self.menubar
258 self.menudict = menudict = {}
259 for name, label in self.menu_specs:
260 underline, label = prepstr(label)
261 menudict[name] = menu = Menu(mbar, name=name)
262 mbar.add_cascade(label=label, menu=menu, underline=underline)
263 self.fill_menus()
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000264 self.base_helpmenu_length = self.menudict['help'].index(END)
265 self.reset_help_menu_entries()
David Scherer7aced172000-08-15 01:13:23 +0000266
267 def postwindowsmenu(self):
268 # Only called when Windows menu exists
David Scherer7aced172000-08-15 01:13:23 +0000269 menu = self.menudict['windows']
270 end = menu.index("end")
271 if end is None:
272 end = -1
273 if end > self.wmenu_end:
274 menu.delete(self.wmenu_end+1, end)
275 WindowList.add_windows_to_menu(menu)
276
277 rmenu = None
278
279 def right_menu_event(self, event):
280 self.text.tag_remove("sel", "1.0", "end")
281 self.text.mark_set("insert", "@%d,%d" % (event.x, event.y))
282 if not self.rmenu:
283 self.make_rmenu()
284 rmenu = self.rmenu
285 self.event = event
286 iswin = sys.platform[:3] == 'win'
287 if iswin:
288 self.text.config(cursor="arrow")
289 rmenu.tk_popup(event.x_root, event.y_root)
290 if iswin:
291 self.text.config(cursor="ibeam")
292
293 rmenu_specs = [
294 # ("Label", "<<virtual-event>>"), ...
295 ("Close", "<<close-window>>"), # Example
296 ]
297
298 def make_rmenu(self):
299 rmenu = Menu(self.text, tearoff=0)
300 for label, eventname in self.rmenu_specs:
301 def command(text=self.text, eventname=eventname):
302 text.event_generate(eventname)
303 rmenu.add_command(label=label, command=command)
304 self.rmenu = rmenu
305
306 def about_dialog(self, event=None):
Kurt B. Kaiserd78b2302003-06-12 04:03:49 +0000307 aboutDialog.AboutDialog(self.top,'About IDLE')
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000308
Steven M. Gava3b55a892001-11-21 05:56:26 +0000309 def config_dialog(self, event=None):
310 configDialog.ConfigDialog(self.top,'Settings')
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +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()
Kurt B. Kaiser260cb902003-06-06 21:58:38 +0000489 self.top.update_windowlist_registry(self)
David Scherer7aced172000-08-15 01:13:23 +0000490 if self.ispythonsource(self.io.filename):
491 self.addcolorizer()
492 else:
493 self.rmcolorizer()
494
495 def addcolorizer(self):
496 if self.color:
497 return
David Scherer7aced172000-08-15 01:13:23 +0000498 self.per.removefilter(self.undo)
499 self.color = self.ColorDelegator()
500 self.per.insertfilter(self.color)
501 self.per.insertfilter(self.undo)
502
503 def rmcolorizer(self):
504 if not self.color:
505 return
David Scherer7aced172000-08-15 01:13:23 +0000506 self.per.removefilter(self.undo)
507 self.per.removefilter(self.color)
508 self.color = None
509 self.per.insertfilter(self.undo)
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000510
Steven M. Gavab77d3432002-03-02 07:16:21 +0000511 def ResetColorizer(self):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000512 "Update the colour theme if it is changed"
513 # Called from configDialog.py
Steven M. Gavab77d3432002-03-02 07:16:21 +0000514 if self.color:
515 self.color = self.ColorDelegator()
516 self.per.insertfilter(self.color)
Kurt B. Kaiser73360a32004-03-08 18:15:31 +0000517 theme = idleConf.GetOption('main','Theme','name')
518 self.text.config(idleConf.GetHighlight(theme, "normal"))
David Scherer7aced172000-08-15 01:13:23 +0000519
Steven M. Gavab1585412002-03-12 00:21:56 +0000520 def ResetFont(self):
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000521 "Update the text widgets' font if it is changed"
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000522 # Called from configDialog.py
Steven M. Gavab1585412002-03-12 00:21:56 +0000523 fontWeight='normal'
524 if idleConf.GetOption('main','EditorWindow','font-bold',type='bool'):
525 fontWeight='bold'
526 self.text.config(font=(idleConf.GetOption('main','EditorWindow','font'),
527 idleConf.GetOption('main','EditorWindow','font-size'),
528 fontWeight))
529
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000530 def ResetKeybindings(self):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000531 "Update the keybindings if they are changed"
532 # Called from configDialog.py
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000533 self.Bindings.default_keydefs=idleConf.GetCurrentKeySet()
534 keydefs = self.Bindings.default_keydefs
535 for event, keylist in keydefs.items():
536 self.text.event_delete(event)
537 self.apply_bindings()
538 #update menu accelerators
539 menuEventDict={}
540 for menu in self.Bindings.menudefs:
541 menuEventDict[menu[0]]={}
542 for item in menu[1]:
543 if item:
544 menuEventDict[menu[0]][prepstr(item[0])[1]]=item[1]
545 for menubarItem in self.menudict.keys():
546 menu=self.menudict[menubarItem]
547 end=menu.index(END)+1
548 for index in range(0,end):
549 if menu.type(index)=='command':
550 accel=menu.entrycget(index,'accelerator')
551 if accel:
552 itemName=menu.entrycget(index,'label')
553 event=''
554 if menuEventDict.has_key(menubarItem):
555 if menuEventDict[menubarItem].has_key(itemName):
556 event=menuEventDict[menubarItem][itemName]
557 if event:
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000558 accel=get_accelerator(keydefs, event)
559 menu.entryconfig(index,accelerator=accel)
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000560
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000561 def reset_help_menu_entries(self):
562 "Update the additional help entries on the Help menu"
563 help_list = idleConf.GetAllExtraHelpSourcesList()
564 helpmenu = self.menudict['help']
565 # first delete the extra help entries, if any
566 helpmenu_length = helpmenu.index(END)
567 if helpmenu_length > self.base_helpmenu_length:
568 helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length)
569 # then rebuild them
570 if help_list:
571 helpmenu.add_separator()
572 for entry in help_list:
573 cmd = self.__extra_help_callback(entry[1])
574 helpmenu.add_command(label=entry[0], command=cmd)
575 # and update the menu dictionary
576 self.menudict['help'] = helpmenu
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000577
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000578 def __extra_help_callback(self, helpfile):
579 "Create a callback with the helpfile value frozen at definition time"
580 def display_extra_help(helpfile=helpfile):
581 self.display_docs(helpfile)
582 return display_extra_help
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000583
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000584 def update_recent_files_list(self, new_file=None):
585 "Load and update the recent files list and menus"
586 rf_list = []
587 if os.path.exists(self.recent_files_path):
588 rf_list_file = open(self.recent_files_path,'r')
Steven M. Gava1d46e402002-03-27 08:40:46 +0000589 try:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000590 rf_list = rf_list_file.readlines()
Steven M. Gava1d46e402002-03-27 08:40:46 +0000591 finally:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000592 rf_list_file.close()
593 if new_file:
594 new_file = os.path.abspath(new_file) + '\n'
595 if new_file in rf_list:
596 rf_list.remove(new_file) # move to top
597 rf_list.insert(0, new_file)
598 # clean and save the recent files list
599 bad_paths = []
600 for path in rf_list:
601 if '\0' in path or not os.path.exists(path[0:-1]):
602 bad_paths.append(path)
603 rf_list = [path for path in rf_list if path not in bad_paths]
604 ulchars = "1234567890ABCDEFGHIJK"
605 rf_list = rf_list[0:len(ulchars)]
606 rf_file = open(self.recent_files_path, 'w')
Steven M. Gava1d46e402002-03-27 08:40:46 +0000607 try:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000608 rf_file.writelines(rf_list)
Steven M. Gava1d46e402002-03-27 08:40:46 +0000609 finally:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000610 rf_file.close()
611 # for each edit window instance, construct the recent files menu
612 for instance in self.top.instance_dict.keys():
613 menu = instance.recent_files_menu
614 menu.delete(1, END) # clear, and rebuild:
615 for i, file in zip(count(), rf_list):
616 file_name = file[0:-1] # zap \n
617 callback = instance.__recent_file_callback(file_name)
618 menu.add_command(label=ulchars[i] + " " + file_name,
619 command=callback,
620 underline=0)
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000621
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000622 def __recent_file_callback(self, file_name):
623 def open_recent_file(fn_closure=file_name):
624 self.io.open(editFile=fn_closure)
625 return open_recent_file
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000626
David Scherer7aced172000-08-15 01:13:23 +0000627 def saved_change_hook(self):
628 short = self.short_title()
629 long = self.long_title()
630 if short and long:
631 title = short + " - " + long
632 elif short:
633 title = short
634 elif long:
635 title = long
636 else:
637 title = "Untitled"
638 icon = short or long or title
639 if not self.get_saved():
640 title = "*%s*" % title
641 icon = "*%s" % icon
642 self.top.wm_title(title)
643 self.top.wm_iconname(icon)
644
645 def get_saved(self):
646 return self.undo.get_saved()
647
648 def set_saved(self, flag):
649 self.undo.set_saved(flag)
650
651 def reset_undo(self):
652 self.undo.reset_undo()
653
654 def short_title(self):
655 filename = self.io.filename
656 if filename:
657 filename = os.path.basename(filename)
658 return filename
659
660 def long_title(self):
661 return self.io.filename or ""
662
663 def center_insert_event(self, event):
664 self.center()
665
666 def center(self, mark="insert"):
667 text = self.text
668 top, bot = self.getwindowlines()
669 lineno = self.getlineno(mark)
670 height = bot - top
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000671 newtop = max(1, lineno - height//2)
David Scherer7aced172000-08-15 01:13:23 +0000672 text.yview(float(newtop))
673
674 def getwindowlines(self):
675 text = self.text
676 top = self.getlineno("@0,0")
677 bot = self.getlineno("@0,65535")
678 if top == bot and text.winfo_height() == 1:
679 # Geometry manager hasn't run yet
680 height = int(text['height'])
681 bot = top + height - 1
682 return top, bot
683
684 def getlineno(self, mark="insert"):
685 text = self.text
686 return int(float(text.index(mark)))
687
Kurt B. Kaiser1061e722003-01-04 01:43:53 +0000688 def get_geometry(self):
689 "Return (width, height, x, y)"
690 geom = self.top.wm_geometry()
691 m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom)
692 tuple = (map(int, m.groups()))
693 return tuple
694
David Scherer7aced172000-08-15 01:13:23 +0000695 def close_event(self, event):
696 self.close()
697
698 def maybesave(self):
699 if self.io:
Steven M. Gava67716b52002-02-26 02:31:03 +0000700 if not self.get_saved():
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000701 if self.top.state()!='normal':
Steven M. Gava67716b52002-02-26 02:31:03 +0000702 self.top.deiconify()
703 self.top.lower()
704 self.top.lift()
David Scherer7aced172000-08-15 01:13:23 +0000705 return self.io.maybesave()
706
707 def close(self):
David Scherer7aced172000-08-15 01:13:23 +0000708 reply = self.maybesave()
709 if reply != "cancel":
710 self._close()
711 return reply
712
713 def _close(self):
Steven M. Gava1d46e402002-03-27 08:40:46 +0000714 if self.io.filename:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000715 self.update_recent_files_list(new_file=self.io.filename)
David Scherer7aced172000-08-15 01:13:23 +0000716 WindowList.unregister_callback(self.postwindowsmenu)
717 if self.close_hook:
718 self.close_hook()
719 self.flist = None
720 colorizing = 0
721 self.unload_extensions()
722 self.io.close(); self.io = None
723 self.undo = None # XXX
724 if self.color:
725 colorizing = self.color.colorizing
726 doh = colorizing and self.top
727 self.color.close(doh) # Cancel colorization
728 self.text = None
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000729 self.tkinter_vars = None
David Scherer7aced172000-08-15 01:13:23 +0000730 self.per.close(); self.per = None
731 if not colorizing:
732 self.top.destroy()
733
734 def load_extensions(self):
735 self.extensions = {}
736 self.load_standard_extensions()
737
738 def unload_extensions(self):
739 for ins in self.extensions.values():
740 if hasattr(ins, "close"):
741 ins.close()
742 self.extensions = {}
743
744 def load_standard_extensions(self):
745 for name in self.get_standard_extension_names():
746 try:
747 self.load_extension(name)
748 except:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000749 print "Failed to load extension", repr(name)
David Scherer7aced172000-08-15 01:13:23 +0000750 import traceback
751 traceback.print_exc()
752
753 def get_standard_extension_names(self):
Steven M. Gavadc72f482002-01-03 11:51:07 +0000754 return idleConf.GetExtensions()
David Scherer7aced172000-08-15 01:13:23 +0000755
756 def load_extension(self, name):
757 mod = __import__(name, globals(), locals(), [])
758 cls = getattr(mod, name)
759 ins = cls(self)
760 self.extensions[name] = ins
Steven M. Gava72c3bf02002-01-19 10:41:51 +0000761 keydefs=idleConf.GetExtensionBindings(name)
David Scherer7aced172000-08-15 01:13:23 +0000762 if keydefs:
763 self.apply_bindings(keydefs)
764 for vevent in keydefs.keys():
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000765 methodname = vevent.replace("-", "_")
David Scherer7aced172000-08-15 01:13:23 +0000766 while methodname[:1] == '<':
767 methodname = methodname[1:]
768 while methodname[-1:] == '>':
769 methodname = methodname[:-1]
770 methodname = methodname + "_event"
771 if hasattr(ins, methodname):
772 self.text.bind(vevent, getattr(ins, methodname))
773 if hasattr(ins, "menudefs"):
774 self.fill_menus(ins.menudefs, keydefs)
775 return ins
776
777 def apply_bindings(self, keydefs=None):
778 if keydefs is None:
779 keydefs = self.Bindings.default_keydefs
780 text = self.text
781 text.keydefs = keydefs
782 for event, keylist in keydefs.items():
783 if keylist:
Raymond Hettinger931237e2003-07-09 18:48:24 +0000784 text.event_add(event, *keylist)
David Scherer7aced172000-08-15 01:13:23 +0000785
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000786 def fill_menus(self, menudefs=None, keydefs=None):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000787 """Add appropriate entries to the menus and submenus
788
789 Menus that are absent or None in self.menudict are ignored.
790 """
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000791 if menudefs is None:
792 menudefs = self.Bindings.menudefs
David Scherer7aced172000-08-15 01:13:23 +0000793 if keydefs is None:
794 keydefs = self.Bindings.default_keydefs
795 menudict = self.menudict
796 text = self.text
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000797 for mname, entrylist in menudefs:
David Scherer7aced172000-08-15 01:13:23 +0000798 menu = menudict.get(mname)
799 if not menu:
800 continue
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000801 for entry in entrylist:
802 if not entry:
David Scherer7aced172000-08-15 01:13:23 +0000803 menu.add_separator()
804 else:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000805 label, eventname = entry
David Scherer7aced172000-08-15 01:13:23 +0000806 checkbutton = (label[:1] == '!')
807 if checkbutton:
808 label = label[1:]
809 underline, label = prepstr(label)
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000810 accelerator = get_accelerator(keydefs, eventname)
811 def command(text=text, eventname=eventname):
812 text.event_generate(eventname)
David Scherer7aced172000-08-15 01:13:23 +0000813 if checkbutton:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000814 var = self.get_var_obj(eventname, BooleanVar)
David Scherer7aced172000-08-15 01:13:23 +0000815 menu.add_checkbutton(label=label, underline=underline,
816 command=command, accelerator=accelerator,
817 variable=var)
818 else:
819 menu.add_command(label=label, underline=underline,
Kurt B. Kaiser84f48032002-09-26 22:13:22 +0000820 command=command,
821 accelerator=accelerator)
David Scherer7aced172000-08-15 01:13:23 +0000822
823 def getvar(self, name):
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000824 var = self.get_var_obj(name)
David Scherer7aced172000-08-15 01:13:23 +0000825 if var:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000826 value = var.get()
827 return value
828 else:
829 raise NameError, name
David Scherer7aced172000-08-15 01:13:23 +0000830
831 def setvar(self, name, value, vartype=None):
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000832 var = self.get_var_obj(name, vartype)
David Scherer7aced172000-08-15 01:13:23 +0000833 if var:
834 var.set(value)
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000835 else:
836 raise NameError, name
David Scherer7aced172000-08-15 01:13:23 +0000837
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000838 def get_var_obj(self, name, vartype=None):
839 var = self.tkinter_vars.get(name)
David Scherer7aced172000-08-15 01:13:23 +0000840 if not var and vartype:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000841 # create a Tkinter variable object with self.text as master:
842 self.tkinter_vars[name] = var = vartype(self.text)
David Scherer7aced172000-08-15 01:13:23 +0000843 return var
844
845 # Tk implementations of "virtual text methods" -- each platform
846 # reusing IDLE's support code needs to define these for its GUI's
847 # flavor of widget.
848
849 # Is character at text_index in a Python string? Return 0 for
850 # "guaranteed no", true for anything else. This info is expensive
851 # to compute ab initio, but is probably already known by the
852 # platform's colorizer.
853
854 def is_char_in_string(self, text_index):
855 if self.color:
856 # Return true iff colorizer hasn't (re)gotten this far
857 # yet, or the character is tagged as being in a string
858 return self.text.tag_prevrange("TODO", text_index) or \
859 "STRING" in self.text.tag_names(text_index)
860 else:
861 # The colorizer is missing: assume the worst
862 return 1
863
864 # If a selection is defined in the text widget, return (start,
865 # end) as Tkinter text indices, otherwise return (None, None)
866 def get_selection_indices(self):
867 try:
868 first = self.text.index("sel.first")
869 last = self.text.index("sel.last")
870 return first, last
871 except TclError:
872 return None, None
873
874 # Return the text widget's current view of what a tab stop means
875 # (equivalent width in spaces).
876
877 def get_tabwidth(self):
878 current = self.text['tabs'] or TK_TABWIDTH_DEFAULT
879 return int(current)
880
881 # Set the text widget's current view of what a tab stop means.
882
883 def set_tabwidth(self, newtabwidth):
884 text = self.text
885 if self.get_tabwidth() != newtabwidth:
886 pixels = text.tk.call("font", "measure", text["font"],
887 "-displayof", text.master,
Kurt B. Kaiserafdf71b2001-07-13 03:35:32 +0000888 "n" * newtabwidth)
David Scherer7aced172000-08-15 01:13:23 +0000889 text.configure(tabs=pixels)
890
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000891### begin autoindent code ###
892
893 # usetabs true -> literal tab characters are used by indent and
894 # dedent cmds, possibly mixed with spaces if
895 # indentwidth is not a multiple of tabwidth
896 # false -> tab characters are converted to spaces by indent
897 # and dedent cmds, and ditto TAB keystrokes
898 # indentwidth is the number of characters per logical indent level.
899 # tabwidth is the display width of a literal tab character.
900 # CAUTION: telling Tk to use anything other than its default
901 # tab setting causes it to use an entirely different tabbing algorithm,
902 # treating tab stops as fixed distances from the left margin.
903 # Nobody expects this, so for now tabwidth should never be changed.
904 usetabs = 0
905 indentwidth = 4
906 tabwidth = 8 # for IDLE use, must remain 8 until Tk is fixed
907
908 # If context_use_ps1 is true, parsing searches back for a ps1 line;
909 # else searches for a popular (if, def, ...) Python stmt.
910 context_use_ps1 = 0
911
912 # When searching backwards for a reliable place to begin parsing,
913 # first start num_context_lines[0] lines back, then
914 # num_context_lines[1] lines back if that didn't work, and so on.
915 # The last value should be huge (larger than the # of lines in a
916 # conceivable file).
917 # Making the initial values larger slows things down more often.
918 num_context_lines = 50, 500, 5000000
919
920 def config(self, **options):
921 for key, value in options.items():
922 if key == 'usetabs':
923 self.usetabs = value
924 elif key == 'indentwidth':
925 self.indentwidth = value
926 elif key == 'tabwidth':
927 self.tabwidth = value
928 elif key == 'context_use_ps1':
929 self.context_use_ps1 = value
930 else:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000931 raise KeyError, "bad option name: %r" % (key,)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000932
933 # If ispythonsource and guess are true, guess a good value for
934 # indentwidth based on file content (if possible), and if
935 # indentwidth != tabwidth set usetabs false.
936 # In any case, adjust the Text widget's view of what a tab
937 # character means.
938
939 def set_indentation_params(self, ispythonsource, guess=1):
940 if guess and ispythonsource:
941 i = self.guess_indent()
942 if 2 <= i <= 8:
943 self.indentwidth = i
944 if self.indentwidth != self.tabwidth:
945 self.usetabs = 0
946
947 self.set_tabwidth(self.tabwidth)
948
949 def smart_backspace_event(self, event):
950 text = self.text
951 first, last = self.get_selection_indices()
952 if first and last:
953 text.delete(first, last)
954 text.mark_set("insert", first)
955 return "break"
956 # Delete whitespace left, until hitting a real char or closest
957 # preceding virtual tab stop.
958 chars = text.get("insert linestart", "insert")
959 if chars == '':
960 if text.compare("insert", ">", "1.0"):
961 # easy: delete preceding newline
962 text.delete("insert-1c")
963 else:
964 text.bell() # at start of buffer
965 return "break"
966 if chars[-1] not in " \t":
967 # easy: delete preceding real char
968 text.delete("insert-1c")
969 return "break"
970 # Ick. It may require *inserting* spaces if we back up over a
971 # tab character! This is written to be clear, not fast.
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +0000972 tabwidth = self.tabwidth
973 have = len(chars.expandtabs(tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000974 assert have > 0
975 want = ((have - 1) // self.indentwidth) * self.indentwidth
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +0000976 # Debug prompt is multilined....
977 last_line_of_prompt = sys.ps1.split('\n')[-1]
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000978 ncharsdeleted = 0
979 while 1:
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +0000980 if chars == last_line_of_prompt:
Kurt B. Kaiser1bdca5e2002-12-16 22:25:10 +0000981 break
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000982 chars = chars[:-1]
983 ncharsdeleted = ncharsdeleted + 1
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +0000984 have = len(chars.expandtabs(tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000985 if have <= want or chars[-1] not in " \t":
986 break
987 text.undo_block_start()
988 text.delete("insert-%dc" % ncharsdeleted, "insert")
989 if have < want:
990 text.insert("insert", ' ' * (want - have))
991 text.undo_block_stop()
992 return "break"
993
994 def smart_indent_event(self, event):
995 # if intraline selection:
996 # delete it
997 # elif multiline selection:
998 # do indent-region & return
999 # indent one level
1000 text = self.text
1001 first, last = self.get_selection_indices()
1002 text.undo_block_start()
1003 try:
1004 if first and last:
1005 if index2line(first) != index2line(last):
1006 return self.indent_region_event(event)
1007 text.delete(first, last)
1008 text.mark_set("insert", first)
1009 prefix = text.get("insert linestart", "insert")
1010 raw, effective = classifyws(prefix, self.tabwidth)
1011 if raw == len(prefix):
1012 # only whitespace to the left
1013 self.reindent_to(effective + self.indentwidth)
1014 else:
1015 if self.usetabs:
1016 pad = '\t'
1017 else:
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001018 effective = len(prefix.expandtabs(self.tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001019 n = self.indentwidth
1020 pad = ' ' * (n - effective % n)
1021 text.insert("insert", pad)
1022 text.see("insert")
1023 return "break"
1024 finally:
1025 text.undo_block_stop()
1026
1027 def newline_and_indent_event(self, event):
1028 text = self.text
1029 first, last = self.get_selection_indices()
1030 text.undo_block_start()
1031 try:
1032 if first and last:
1033 text.delete(first, last)
1034 text.mark_set("insert", first)
1035 line = text.get("insert linestart", "insert")
1036 i, n = 0, len(line)
1037 while i < n and line[i] in " \t":
1038 i = i+1
1039 if i == n:
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001040 # the cursor is in or at leading indentation in a continuation
1041 # line; just inject an empty line at the start
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001042 text.insert("insert linestart", '\n')
1043 return "break"
1044 indent = line[:i]
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001045 # strip whitespace before insert point unless it's in the prompt
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001046 i = 0
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001047 last_line_of_prompt = sys.ps1.split('\n')[-1]
1048 while line and line[-1] in " \t" and line != last_line_of_prompt:
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001049 line = line[:-1]
1050 i = i+1
1051 if i:
1052 text.delete("insert - %d chars" % i, "insert")
1053 # strip whitespace after insert point
1054 while text.get("insert") in " \t":
1055 text.delete("insert")
1056 # start new line
1057 text.insert("insert", '\n')
1058
1059 # adjust indentation for continuations and block
1060 # open/close first need to find the last stmt
1061 lno = index2line(text.index('insert'))
1062 y = PyParse.Parser(self.indentwidth, self.tabwidth)
1063 for context in self.num_context_lines:
1064 startat = max(lno - context, 1)
Walter Dörwald70a6b492004-02-12 17:35:32 +00001065 startatindex = repr(startat) + ".0"
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001066 rawtext = text.get(startatindex, "insert")
1067 y.set_str(rawtext)
1068 bod = y.find_good_parse_start(
1069 self.context_use_ps1,
1070 self._build_char_in_string_func(startatindex))
1071 if bod is not None or startat == 1:
1072 break
1073 y.set_lo(bod or 0)
1074 c = y.get_continuation_type()
1075 if c != PyParse.C_NONE:
1076 # The current stmt hasn't ended yet.
1077 if c == PyParse.C_STRING:
1078 # inside a string; just mimic the current indent
1079 text.insert("insert", indent)
1080 elif c == PyParse.C_BRACKET:
1081 # line up with the first (if any) element of the
1082 # last open bracket structure; else indent one
1083 # level beyond the indent of the line with the
1084 # last open bracket
1085 self.reindent_to(y.compute_bracket_indent())
1086 elif c == PyParse.C_BACKSLASH:
1087 # if more than one line in this stmt already, just
1088 # mimic the current indent; else if initial line
1089 # has a start on an assignment stmt, indent to
1090 # beyond leftmost =; else to beyond first chunk of
1091 # non-whitespace on initial line
1092 if y.get_num_lines_in_stmt() > 1:
1093 text.insert("insert", indent)
1094 else:
1095 self.reindent_to(y.compute_backslash_indent())
1096 else:
Walter Dörwald70a6b492004-02-12 17:35:32 +00001097 assert 0, "bogus continuation type %r" % (c,)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001098 return "break"
1099
1100 # This line starts a brand new stmt; indent relative to
1101 # indentation of initial line of closest preceding
1102 # interesting stmt.
1103 indent = y.get_base_indent_string()
1104 text.insert("insert", indent)
1105 if y.is_block_opener():
1106 self.smart_indent_event(event)
1107 elif indent and y.is_block_closer():
1108 self.smart_backspace_event(event)
1109 return "break"
1110 finally:
1111 text.see("insert")
1112 text.undo_block_stop()
1113
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001114 # Our editwin provides a is_char_in_string function that works
1115 # with a Tk text index, but PyParse only knows about offsets into
1116 # a string. This builds a function for PyParse that accepts an
1117 # offset.
1118
1119 def _build_char_in_string_func(self, startindex):
1120 def inner(offset, _startindex=startindex,
1121 _icis=self.is_char_in_string):
1122 return _icis(_startindex + "+%dc" % offset)
1123 return inner
1124
1125 def indent_region_event(self, event):
1126 head, tail, chars, lines = self.get_region()
1127 for pos in range(len(lines)):
1128 line = lines[pos]
1129 if line:
1130 raw, effective = classifyws(line, self.tabwidth)
1131 effective = effective + self.indentwidth
1132 lines[pos] = self._make_blanks(effective) + line[raw:]
1133 self.set_region(head, tail, chars, lines)
1134 return "break"
1135
1136 def dedent_region_event(self, event):
1137 head, tail, chars, lines = self.get_region()
1138 for pos in range(len(lines)):
1139 line = lines[pos]
1140 if line:
1141 raw, effective = classifyws(line, self.tabwidth)
1142 effective = max(effective - self.indentwidth, 0)
1143 lines[pos] = self._make_blanks(effective) + line[raw:]
1144 self.set_region(head, tail, chars, lines)
1145 return "break"
1146
1147 def comment_region_event(self, event):
1148 head, tail, chars, lines = self.get_region()
1149 for pos in range(len(lines) - 1):
1150 line = lines[pos]
1151 lines[pos] = '##' + line
1152 self.set_region(head, tail, chars, lines)
1153
1154 def uncomment_region_event(self, event):
1155 head, tail, chars, lines = self.get_region()
1156 for pos in range(len(lines)):
1157 line = lines[pos]
1158 if not line:
1159 continue
1160 if line[:2] == '##':
1161 line = line[2:]
1162 elif line[:1] == '#':
1163 line = line[1:]
1164 lines[pos] = line
1165 self.set_region(head, tail, chars, lines)
1166
1167 def tabify_region_event(self, event):
1168 head, tail, chars, lines = self.get_region()
1169 tabwidth = self._asktabwidth()
1170 for pos in range(len(lines)):
1171 line = lines[pos]
1172 if line:
1173 raw, effective = classifyws(line, tabwidth)
1174 ntabs, nspaces = divmod(effective, tabwidth)
1175 lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]
1176 self.set_region(head, tail, chars, lines)
1177
1178 def untabify_region_event(self, event):
1179 head, tail, chars, lines = self.get_region()
1180 tabwidth = self._asktabwidth()
1181 for pos in range(len(lines)):
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001182 lines[pos] = lines[pos].expandtabs(tabwidth)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001183 self.set_region(head, tail, chars, lines)
1184
1185 def toggle_tabs_event(self, event):
1186 if self.askyesno(
1187 "Toggle tabs",
1188 "Turn tabs " + ("on", "off")[self.usetabs] + "?",
1189 parent=self.text):
1190 self.usetabs = not self.usetabs
1191 return "break"
1192
1193 # XXX this isn't bound to anything -- see class tabwidth comments
1194 def change_tabwidth_event(self, event):
1195 new = self._asktabwidth()
1196 if new != self.tabwidth:
1197 self.tabwidth = new
1198 self.set_indentation_params(0, guess=0)
1199 return "break"
1200
1201 def change_indentwidth_event(self, event):
1202 new = self.askinteger(
1203 "Indent width",
1204 "New indent width (2-16)",
1205 parent=self.text,
1206 initialvalue=self.indentwidth,
1207 minvalue=2,
1208 maxvalue=16)
1209 if new and new != self.indentwidth:
1210 self.indentwidth = new
1211 return "break"
1212
1213 def get_region(self):
1214 text = self.text
1215 first, last = self.get_selection_indices()
1216 if first and last:
1217 head = text.index(first + " linestart")
1218 tail = text.index(last + "-1c lineend +1c")
1219 else:
1220 head = text.index("insert linestart")
1221 tail = text.index("insert lineend +1c")
1222 chars = text.get(head, tail)
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001223 lines = chars.split("\n")
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001224 return head, tail, chars, lines
1225
1226 def set_region(self, head, tail, chars, lines):
1227 text = self.text
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001228 newchars = "\n".join(lines)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001229 if newchars == chars:
1230 text.bell()
1231 return
1232 text.tag_remove("sel", "1.0", "end")
1233 text.mark_set("insert", head)
1234 text.undo_block_start()
1235 text.delete(head, tail)
1236 text.insert(head, newchars)
1237 text.undo_block_stop()
1238 text.tag_add("sel", head, "insert")
1239
1240 # Make string that displays as n leading blanks.
1241
1242 def _make_blanks(self, n):
1243 if self.usetabs:
1244 ntabs, nspaces = divmod(n, self.tabwidth)
1245 return '\t' * ntabs + ' ' * nspaces
1246 else:
1247 return ' ' * n
1248
1249 # Delete from beginning of line to insert point, then reinsert
1250 # column logical (meaning use tabs if appropriate) spaces.
1251
1252 def reindent_to(self, column):
1253 text = self.text
1254 text.undo_block_start()
1255 if text.compare("insert linestart", "!=", "insert"):
1256 text.delete("insert linestart", "insert")
1257 if column:
1258 text.insert("insert", self._make_blanks(column))
1259 text.undo_block_stop()
1260
1261 def _asktabwidth(self):
1262 return self.askinteger(
1263 "Tab width",
1264 "Spaces per tab? (2-16)",
1265 parent=self.text,
1266 initialvalue=self.indentwidth,
1267 minvalue=2,
1268 maxvalue=16) or self.tabwidth
1269
1270 # Guess indentwidth from text content.
1271 # Return guessed indentwidth. This should not be believed unless
1272 # it's in a reasonable range (e.g., it will be 0 if no indented
1273 # blocks are found).
1274
1275 def guess_indent(self):
1276 opener, indented = IndentSearcher(self.text, self.tabwidth).run()
1277 if opener and indented:
1278 raw, indentsmall = classifyws(opener, self.tabwidth)
1279 raw, indentlarge = classifyws(indented, self.tabwidth)
1280 else:
1281 indentsmall = indentlarge = 0
1282 return indentlarge - indentsmall
1283
1284# "line.col" -> line, as an int
1285def index2line(index):
1286 return int(float(index))
1287
1288# Look at the leading whitespace in s.
1289# Return pair (# of leading ws characters,
1290# effective # of leading blanks after expanding
1291# tabs to width tabwidth)
1292
1293def classifyws(s, tabwidth):
1294 raw = effective = 0
1295 for ch in s:
1296 if ch == ' ':
1297 raw = raw + 1
1298 effective = effective + 1
1299 elif ch == '\t':
1300 raw = raw + 1
1301 effective = (effective // tabwidth + 1) * tabwidth
1302 else:
1303 break
1304 return raw, effective
1305
1306import tokenize
1307_tokenize = tokenize
1308del tokenize
1309
1310class IndentSearcher:
1311
1312 # .run() chews over the Text widget, looking for a block opener
1313 # and the stmt following it. Returns a pair,
1314 # (line containing block opener, line containing stmt)
1315 # Either or both may be None.
1316
1317 def __init__(self, text, tabwidth):
1318 self.text = text
1319 self.tabwidth = tabwidth
1320 self.i = self.finished = 0
1321 self.blkopenline = self.indentedline = None
1322
1323 def readline(self):
1324 if self.finished:
1325 return ""
1326 i = self.i = self.i + 1
Walter Dörwald70a6b492004-02-12 17:35:32 +00001327 mark = repr(i) + ".0"
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001328 if self.text.compare(mark, ">=", "end"):
1329 return ""
1330 return self.text.get(mark, mark + " lineend+1c")
1331
1332 def tokeneater(self, type, token, start, end, line,
1333 INDENT=_tokenize.INDENT,
1334 NAME=_tokenize.NAME,
1335 OPENERS=('class', 'def', 'for', 'if', 'try', 'while')):
1336 if self.finished:
1337 pass
1338 elif type == NAME and token in OPENERS:
1339 self.blkopenline = line
1340 elif type == INDENT and self.blkopenline:
1341 self.indentedline = line
1342 self.finished = 1
1343
1344 def run(self):
1345 save_tabsize = _tokenize.tabsize
1346 _tokenize.tabsize = self.tabwidth
1347 try:
1348 try:
1349 _tokenize.tokenize(self.readline, self.tokeneater)
1350 except _tokenize.TokenError:
1351 # since we cut off the tokenizer early, we can trigger
1352 # spurious errors
1353 pass
1354 finally:
1355 _tokenize.tabsize = save_tabsize
1356 return self.blkopenline, self.indentedline
1357
1358### end autoindent code ###
1359
David Scherer7aced172000-08-15 01:13:23 +00001360def prepstr(s):
1361 # Helper to extract the underscore from a string, e.g.
1362 # prepstr("Co_py") returns (2, "Copy").
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +00001363 i = s.find('_')
David Scherer7aced172000-08-15 01:13:23 +00001364 if i >= 0:
1365 s = s[:i] + s[i+1:]
1366 return i, s
1367
1368
1369keynames = {
1370 'bracketleft': '[',
1371 'bracketright': ']',
1372 'slash': '/',
1373}
1374
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +00001375def get_accelerator(keydefs, eventname):
1376 keylist = keydefs.get(eventname)
David Scherer7aced172000-08-15 01:13:23 +00001377 if not keylist:
1378 return ""
1379 s = keylist[0]
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +00001380 s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s)
David Scherer7aced172000-08-15 01:13:23 +00001381 s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s)
1382 s = re.sub("Key-", "", s)
1383 s = re.sub("Cancel","Ctrl-Break",s) # dscherer@cmu.edu
1384 s = re.sub("Control-", "Ctrl-", s)
1385 s = re.sub("-", "+", s)
1386 s = re.sub("><", " ", s)
1387 s = re.sub("<", "", s)
1388 s = re.sub(">", "", s)
1389 return s
1390
1391
1392def fixwordbreaks(root):
1393 # Make sure that Tk's double-click and next/previous word
1394 # operations use our definition of a word (i.e. an identifier)
1395 tk = root.tk
1396 tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
1397 tk.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]')
1398 tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]')
1399
1400
1401def test():
1402 root = Tk()
1403 fixwordbreaks(root)
1404 root.withdraw()
1405 if sys.argv[1:]:
1406 filename = sys.argv[1]
1407 else:
1408 filename = None
1409 edit = EditorWindow(root=root, filename=filename)
1410 edit.set_close_hook(root.quit)
1411 root.mainloop()
1412 root.destroy()
1413
1414if __name__ == '__main__':
1415 test()