blob: e4274792c2f80884e03edd5e757f6c7da89305a9 [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')
Kurt B. Kaiser8aa23922004-07-15 04:54:57 +000063 elif sys.platform[:3] == 'win':
Kurt B. Kaiser090e6362004-07-21 03:33:58 +000064 chmfile = os.path.join(sys.prefix, 'Doc',
65 'Python%d%d.chm' % sys.version_info[:2])
Thomas Heller84ef1532003-09-23 20:53:10 +000066 if os.path.isfile(chmfile):
67 dochome = chmfile
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):
Kurt B. Kaiser8aa23922004-07-15 04:54:57 +0000317 if sys.platform[:3] == 'win':
Steven M. Gava931625d2002-04-22 00:38:26 +0000318 os.startfile(self.help_url)
Kurt B. Kaiser114713d2003-01-10 05:07:24 +0000319 else:
320 webbrowser.open(self.help_url)
Kurt B. Kaiser8aa23922004-07-15 04:54:57 +0000321 return "break"
David Scherer7aced172000-08-15 01:13:23 +0000322
Kurt B. Kaiser84f48032002-09-26 22:13:22 +0000323 def cut(self,event):
324 self.text.event_generate("<<Cut>>")
325 return "break"
326
327 def copy(self,event):
328 self.text.event_generate("<<Copy>>")
329 return "break"
330
331 def paste(self,event):
332 self.text.event_generate("<<Paste>>")
333 return "break"
334
David Scherer7aced172000-08-15 01:13:23 +0000335 def select_all(self, event=None):
336 self.text.tag_add("sel", "1.0", "end-1c")
337 self.text.mark_set("insert", "1.0")
338 self.text.see("insert")
339 return "break"
340
341 def remove_selection(self, event=None):
342 self.text.tag_remove("sel", "1.0", "end")
343 self.text.see("insert")
344
Kurt B. Kaiser5ec186b2003-01-17 04:04:06 +0000345 def move_at_edge_if_selection(self, edge_index):
346 """Cursor move begins at start or end of selection
347
348 When a left/right cursor key is pressed create and return to Tkinter a
349 function which causes a cursor move from the associated edge of the
350 selection.
351
352 """
353 self_text_index = self.text.index
354 self_text_mark_set = self.text.mark_set
355 edges_table = ("sel.first+1c", "sel.last-1c")
356 def move_at_edge(event):
357 if (event.state & 5) == 0: # no shift(==1) or control(==4) pressed
358 try:
359 self_text_index("sel.first")
360 self_text_mark_set("insert", edges_table[edge_index])
361 except TclError:
362 pass
363 return move_at_edge
364
Steven M. Gavac5976402002-01-04 03:06:08 +0000365 def find_event(self, event):
366 SearchDialog.find(self.text)
367 return "break"
368
369 def find_again_event(self, event):
370 SearchDialog.find_again(self.text)
371 return "break"
372
373 def find_selection_event(self, event):
374 SearchDialog.find_selection(self.text)
375 return "break"
376
377 def find_in_files_event(self, event):
378 GrepDialog.grep(self.text, self.io, self.flist)
379 return "break"
380
381 def replace_event(self, event):
382 ReplaceDialog.replace(self.text)
383 return "break"
384
385 def goto_line_event(self, event):
386 text = self.text
387 lineno = tkSimpleDialog.askinteger("Goto",
388 "Go to line number:",parent=text)
389 if lineno is None:
390 return "break"
391 if lineno <= 0:
392 text.bell()
393 return "break"
394 text.mark_set("insert", "%d.0" % lineno)
395 text.see("insert")
396
David Scherer7aced172000-08-15 01:13:23 +0000397 def open_module(self, event=None):
398 # XXX Shouldn't this be in IOBinding or in FileList?
399 try:
400 name = self.text.get("sel.first", "sel.last")
401 except TclError:
402 name = ""
403 else:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000404 name = name.strip()
Guido van Rossum852f35b2003-06-05 11:36:55 +0000405 name = tkSimpleDialog.askstring("Module",
406 "Enter the name of a Python module\n"
407 "to search on sys.path and open:",
408 parent=self.text, initialvalue=name)
409 if name:
410 name = name.strip()
David Scherer7aced172000-08-15 01:13:23 +0000411 if not name:
Guido van Rossum852f35b2003-06-05 11:36:55 +0000412 return
David Scherer7aced172000-08-15 01:13:23 +0000413 # XXX Ought to insert current file's directory in front of path
414 try:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000415 (f, file, (suffix, mode, type)) = _find_module(name)
David Scherer7aced172000-08-15 01:13:23 +0000416 except (NameError, ImportError), msg:
417 tkMessageBox.showerror("Import error", str(msg), parent=self.text)
418 return
419 if type != imp.PY_SOURCE:
420 tkMessageBox.showerror("Unsupported type",
421 "%s is not a source module" % name, parent=self.text)
422 return
423 if f:
424 f.close()
425 if self.flist:
426 self.flist.open(file)
427 else:
428 self.io.loadfile(file)
429
430 def open_class_browser(self, event=None):
431 filename = self.io.filename
432 if not filename:
433 tkMessageBox.showerror(
434 "No filename",
435 "This buffer has no associated filename",
436 master=self.text)
437 self.text.focus_set()
438 return None
439 head, tail = os.path.split(filename)
440 base, ext = os.path.splitext(tail)
441 import ClassBrowser
442 ClassBrowser.ClassBrowser(self.flist, base, [head])
443
444 def open_path_browser(self, event=None):
445 import PathBrowser
446 PathBrowser.PathBrowser(self.flist)
447
448 def gotoline(self, lineno):
449 if lineno is not None and lineno > 0:
450 self.text.mark_set("insert", "%d.0" % lineno)
451 self.text.tag_remove("sel", "1.0", "end")
452 self.text.tag_add("sel", "insert", "insert +1l")
453 self.center()
454
455 def ispythonsource(self, filename):
456 if not filename:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000457 return True
David Scherer7aced172000-08-15 01:13:23 +0000458 base, ext = os.path.splitext(os.path.basename(filename))
459 if os.path.normcase(ext) in (".py", ".pyw"):
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000460 return True
David Scherer7aced172000-08-15 01:13:23 +0000461 try:
462 f = open(filename)
463 line = f.readline()
464 f.close()
465 except IOError:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000466 return False
Kurt B. Kaiser83a35602002-12-20 17:18:03 +0000467 return line.startswith('#!') and line.find('python') >= 0
David Scherer7aced172000-08-15 01:13:23 +0000468
469 def close_hook(self):
470 if self.flist:
471 self.flist.close_edit(self)
472
473 def set_close_hook(self, close_hook):
474 self.close_hook = close_hook
475
476 def filename_change_hook(self):
477 if self.flist:
478 self.flist.filename_changed_edit(self)
479 self.saved_change_hook()
Kurt B. Kaiser260cb902003-06-06 21:58:38 +0000480 self.top.update_windowlist_registry(self)
David Scherer7aced172000-08-15 01:13:23 +0000481 if self.ispythonsource(self.io.filename):
482 self.addcolorizer()
483 else:
484 self.rmcolorizer()
485
486 def addcolorizer(self):
487 if self.color:
488 return
David Scherer7aced172000-08-15 01:13:23 +0000489 self.per.removefilter(self.undo)
490 self.color = self.ColorDelegator()
491 self.per.insertfilter(self.color)
492 self.per.insertfilter(self.undo)
493
494 def rmcolorizer(self):
495 if not self.color:
496 return
David Scherer7aced172000-08-15 01:13:23 +0000497 self.per.removefilter(self.undo)
498 self.per.removefilter(self.color)
499 self.color = None
500 self.per.insertfilter(self.undo)
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000501
Steven M. Gavab77d3432002-03-02 07:16:21 +0000502 def ResetColorizer(self):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000503 "Update the colour theme if it is changed"
504 # Called from configDialog.py
Steven M. Gavab77d3432002-03-02 07:16:21 +0000505 if self.color:
506 self.color = self.ColorDelegator()
507 self.per.insertfilter(self.color)
Kurt B. Kaiser73360a32004-03-08 18:15:31 +0000508 theme = idleConf.GetOption('main','Theme','name')
509 self.text.config(idleConf.GetHighlight(theme, "normal"))
David Scherer7aced172000-08-15 01:13:23 +0000510
Steven M. Gavab1585412002-03-12 00:21:56 +0000511 def ResetFont(self):
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000512 "Update the text widgets' font if it is changed"
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000513 # Called from configDialog.py
Steven M. Gavab1585412002-03-12 00:21:56 +0000514 fontWeight='normal'
515 if idleConf.GetOption('main','EditorWindow','font-bold',type='bool'):
516 fontWeight='bold'
517 self.text.config(font=(idleConf.GetOption('main','EditorWindow','font'),
518 idleConf.GetOption('main','EditorWindow','font-size'),
519 fontWeight))
520
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000521 def ResetKeybindings(self):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000522 "Update the keybindings if they are changed"
523 # Called from configDialog.py
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000524 self.Bindings.default_keydefs=idleConf.GetCurrentKeySet()
525 keydefs = self.Bindings.default_keydefs
526 for event, keylist in keydefs.items():
527 self.text.event_delete(event)
528 self.apply_bindings()
529 #update menu accelerators
530 menuEventDict={}
531 for menu in self.Bindings.menudefs:
532 menuEventDict[menu[0]]={}
533 for item in menu[1]:
534 if item:
535 menuEventDict[menu[0]][prepstr(item[0])[1]]=item[1]
536 for menubarItem in self.menudict.keys():
537 menu=self.menudict[menubarItem]
538 end=menu.index(END)+1
539 for index in range(0,end):
540 if menu.type(index)=='command':
541 accel=menu.entrycget(index,'accelerator')
542 if accel:
543 itemName=menu.entrycget(index,'label')
544 event=''
545 if menuEventDict.has_key(menubarItem):
546 if menuEventDict[menubarItem].has_key(itemName):
547 event=menuEventDict[menubarItem][itemName]
548 if event:
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000549 accel=get_accelerator(keydefs, event)
550 menu.entryconfig(index,accelerator=accel)
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000551
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000552 def reset_help_menu_entries(self):
553 "Update the additional help entries on the Help menu"
554 help_list = idleConf.GetAllExtraHelpSourcesList()
555 helpmenu = self.menudict['help']
556 # first delete the extra help entries, if any
557 helpmenu_length = helpmenu.index(END)
558 if helpmenu_length > self.base_helpmenu_length:
559 helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length)
560 # then rebuild them
561 if help_list:
562 helpmenu.add_separator()
563 for entry in help_list:
564 cmd = self.__extra_help_callback(entry[1])
565 helpmenu.add_command(label=entry[0], command=cmd)
566 # and update the menu dictionary
567 self.menudict['help'] = helpmenu
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000568
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000569 def __extra_help_callback(self, helpfile):
570 "Create a callback with the helpfile value frozen at definition time"
571 def display_extra_help(helpfile=helpfile):
Kurt B. Kaiser8aa23922004-07-15 04:54:57 +0000572 if not (helpfile.startswith('www') or helpfile.startswith('http')):
573 url = os.path.normpath(helpfile)
574 if sys.platform[:3] == 'win':
575 os.startfile(helpfile)
576 else:
577 webbrowser.open(helpfile)
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000578 return display_extra_help
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000579
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000580 def update_recent_files_list(self, new_file=None):
581 "Load and update the recent files list and menus"
582 rf_list = []
583 if os.path.exists(self.recent_files_path):
584 rf_list_file = open(self.recent_files_path,'r')
Steven M. Gava1d46e402002-03-27 08:40:46 +0000585 try:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000586 rf_list = rf_list_file.readlines()
Steven M. Gava1d46e402002-03-27 08:40:46 +0000587 finally:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000588 rf_list_file.close()
589 if new_file:
590 new_file = os.path.abspath(new_file) + '\n'
591 if new_file in rf_list:
592 rf_list.remove(new_file) # move to top
593 rf_list.insert(0, new_file)
594 # clean and save the recent files list
595 bad_paths = []
596 for path in rf_list:
597 if '\0' in path or not os.path.exists(path[0:-1]):
598 bad_paths.append(path)
599 rf_list = [path for path in rf_list if path not in bad_paths]
600 ulchars = "1234567890ABCDEFGHIJK"
601 rf_list = rf_list[0:len(ulchars)]
602 rf_file = open(self.recent_files_path, 'w')
Steven M. Gava1d46e402002-03-27 08:40:46 +0000603 try:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000604 rf_file.writelines(rf_list)
Steven M. Gava1d46e402002-03-27 08:40:46 +0000605 finally:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000606 rf_file.close()
607 # for each edit window instance, construct the recent files menu
608 for instance in self.top.instance_dict.keys():
609 menu = instance.recent_files_menu
610 menu.delete(1, END) # clear, and rebuild:
611 for i, file in zip(count(), rf_list):
612 file_name = file[0:-1] # zap \n
613 callback = instance.__recent_file_callback(file_name)
614 menu.add_command(label=ulchars[i] + " " + file_name,
615 command=callback,
616 underline=0)
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000617
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000618 def __recent_file_callback(self, file_name):
619 def open_recent_file(fn_closure=file_name):
620 self.io.open(editFile=fn_closure)
621 return open_recent_file
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000622
David Scherer7aced172000-08-15 01:13:23 +0000623 def saved_change_hook(self):
624 short = self.short_title()
625 long = self.long_title()
626 if short and long:
627 title = short + " - " + long
628 elif short:
629 title = short
630 elif long:
631 title = long
632 else:
633 title = "Untitled"
634 icon = short or long or title
635 if not self.get_saved():
636 title = "*%s*" % title
637 icon = "*%s" % icon
638 self.top.wm_title(title)
639 self.top.wm_iconname(icon)
640
641 def get_saved(self):
642 return self.undo.get_saved()
643
644 def set_saved(self, flag):
645 self.undo.set_saved(flag)
646
647 def reset_undo(self):
648 self.undo.reset_undo()
649
650 def short_title(self):
651 filename = self.io.filename
652 if filename:
653 filename = os.path.basename(filename)
654 return filename
655
656 def long_title(self):
657 return self.io.filename or ""
658
659 def center_insert_event(self, event):
660 self.center()
661
662 def center(self, mark="insert"):
663 text = self.text
664 top, bot = self.getwindowlines()
665 lineno = self.getlineno(mark)
666 height = bot - top
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000667 newtop = max(1, lineno - height//2)
David Scherer7aced172000-08-15 01:13:23 +0000668 text.yview(float(newtop))
669
670 def getwindowlines(self):
671 text = self.text
672 top = self.getlineno("@0,0")
673 bot = self.getlineno("@0,65535")
674 if top == bot and text.winfo_height() == 1:
675 # Geometry manager hasn't run yet
676 height = int(text['height'])
677 bot = top + height - 1
678 return top, bot
679
680 def getlineno(self, mark="insert"):
681 text = self.text
682 return int(float(text.index(mark)))
683
Kurt B. Kaiser1061e722003-01-04 01:43:53 +0000684 def get_geometry(self):
685 "Return (width, height, x, y)"
686 geom = self.top.wm_geometry()
687 m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom)
688 tuple = (map(int, m.groups()))
689 return tuple
690
David Scherer7aced172000-08-15 01:13:23 +0000691 def close_event(self, event):
692 self.close()
693
694 def maybesave(self):
695 if self.io:
Steven M. Gava67716b52002-02-26 02:31:03 +0000696 if not self.get_saved():
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000697 if self.top.state()!='normal':
Steven M. Gava67716b52002-02-26 02:31:03 +0000698 self.top.deiconify()
699 self.top.lower()
700 self.top.lift()
David Scherer7aced172000-08-15 01:13:23 +0000701 return self.io.maybesave()
702
703 def close(self):
David Scherer7aced172000-08-15 01:13:23 +0000704 reply = self.maybesave()
705 if reply != "cancel":
706 self._close()
707 return reply
708
709 def _close(self):
Steven M. Gava1d46e402002-03-27 08:40:46 +0000710 if self.io.filename:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000711 self.update_recent_files_list(new_file=self.io.filename)
David Scherer7aced172000-08-15 01:13:23 +0000712 WindowList.unregister_callback(self.postwindowsmenu)
713 if self.close_hook:
714 self.close_hook()
715 self.flist = None
716 colorizing = 0
717 self.unload_extensions()
718 self.io.close(); self.io = None
719 self.undo = None # XXX
720 if self.color:
721 colorizing = self.color.colorizing
722 doh = colorizing and self.top
723 self.color.close(doh) # Cancel colorization
724 self.text = None
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000725 self.tkinter_vars = None
David Scherer7aced172000-08-15 01:13:23 +0000726 self.per.close(); self.per = None
727 if not colorizing:
728 self.top.destroy()
729
730 def load_extensions(self):
731 self.extensions = {}
732 self.load_standard_extensions()
733
734 def unload_extensions(self):
735 for ins in self.extensions.values():
736 if hasattr(ins, "close"):
737 ins.close()
738 self.extensions = {}
739
740 def load_standard_extensions(self):
741 for name in self.get_standard_extension_names():
742 try:
743 self.load_extension(name)
744 except:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000745 print "Failed to load extension", repr(name)
David Scherer7aced172000-08-15 01:13:23 +0000746 import traceback
747 traceback.print_exc()
748
749 def get_standard_extension_names(self):
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +0000750 return idleConf.GetExtensions(editor_only=True)
David Scherer7aced172000-08-15 01:13:23 +0000751
752 def load_extension(self, name):
753 mod = __import__(name, globals(), locals(), [])
754 cls = getattr(mod, name)
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +0000755 keydefs = idleConf.GetExtensionBindings(name)
756 if hasattr(cls, "menudefs"):
757 self.fill_menus(cls.menudefs, keydefs)
David Scherer7aced172000-08-15 01:13:23 +0000758 ins = cls(self)
759 self.extensions[name] = ins
David Scherer7aced172000-08-15 01:13:23 +0000760 if keydefs:
761 self.apply_bindings(keydefs)
762 for vevent in keydefs.keys():
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000763 methodname = vevent.replace("-", "_")
David Scherer7aced172000-08-15 01:13:23 +0000764 while methodname[:1] == '<':
765 methodname = methodname[1:]
766 while methodname[-1:] == '>':
767 methodname = methodname[:-1]
768 methodname = methodname + "_event"
769 if hasattr(ins, methodname):
770 self.text.bind(vevent, getattr(ins, methodname))
David Scherer7aced172000-08-15 01:13:23 +0000771 return ins
772
773 def apply_bindings(self, keydefs=None):
774 if keydefs is None:
775 keydefs = self.Bindings.default_keydefs
776 text = self.text
777 text.keydefs = keydefs
778 for event, keylist in keydefs.items():
779 if keylist:
Raymond Hettinger931237e2003-07-09 18:48:24 +0000780 text.event_add(event, *keylist)
David Scherer7aced172000-08-15 01:13:23 +0000781
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000782 def fill_menus(self, menudefs=None, keydefs=None):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000783 """Add appropriate entries to the menus and submenus
784
785 Menus that are absent or None in self.menudict are ignored.
786 """
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000787 if menudefs is None:
788 menudefs = self.Bindings.menudefs
David Scherer7aced172000-08-15 01:13:23 +0000789 if keydefs is None:
790 keydefs = self.Bindings.default_keydefs
791 menudict = self.menudict
792 text = self.text
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000793 for mname, entrylist in menudefs:
David Scherer7aced172000-08-15 01:13:23 +0000794 menu = menudict.get(mname)
795 if not menu:
796 continue
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000797 for entry in entrylist:
798 if not entry:
David Scherer7aced172000-08-15 01:13:23 +0000799 menu.add_separator()
800 else:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000801 label, eventname = entry
David Scherer7aced172000-08-15 01:13:23 +0000802 checkbutton = (label[:1] == '!')
803 if checkbutton:
804 label = label[1:]
805 underline, label = prepstr(label)
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000806 accelerator = get_accelerator(keydefs, eventname)
807 def command(text=text, eventname=eventname):
808 text.event_generate(eventname)
David Scherer7aced172000-08-15 01:13:23 +0000809 if checkbutton:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000810 var = self.get_var_obj(eventname, BooleanVar)
David Scherer7aced172000-08-15 01:13:23 +0000811 menu.add_checkbutton(label=label, underline=underline,
812 command=command, accelerator=accelerator,
813 variable=var)
814 else:
815 menu.add_command(label=label, underline=underline,
Kurt B. Kaiser84f48032002-09-26 22:13:22 +0000816 command=command,
817 accelerator=accelerator)
David Scherer7aced172000-08-15 01:13:23 +0000818
819 def getvar(self, name):
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000820 var = self.get_var_obj(name)
David Scherer7aced172000-08-15 01:13:23 +0000821 if var:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000822 value = var.get()
823 return value
824 else:
825 raise NameError, name
David Scherer7aced172000-08-15 01:13:23 +0000826
827 def setvar(self, name, value, vartype=None):
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000828 var = self.get_var_obj(name, vartype)
David Scherer7aced172000-08-15 01:13:23 +0000829 if var:
830 var.set(value)
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000831 else:
832 raise NameError, name
David Scherer7aced172000-08-15 01:13:23 +0000833
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000834 def get_var_obj(self, name, vartype=None):
835 var = self.tkinter_vars.get(name)
David Scherer7aced172000-08-15 01:13:23 +0000836 if not var and vartype:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000837 # create a Tkinter variable object with self.text as master:
838 self.tkinter_vars[name] = var = vartype(self.text)
David Scherer7aced172000-08-15 01:13:23 +0000839 return var
840
841 # Tk implementations of "virtual text methods" -- each platform
842 # reusing IDLE's support code needs to define these for its GUI's
843 # flavor of widget.
844
845 # Is character at text_index in a Python string? Return 0 for
846 # "guaranteed no", true for anything else. This info is expensive
847 # to compute ab initio, but is probably already known by the
848 # platform's colorizer.
849
850 def is_char_in_string(self, text_index):
851 if self.color:
852 # Return true iff colorizer hasn't (re)gotten this far
853 # yet, or the character is tagged as being in a string
854 return self.text.tag_prevrange("TODO", text_index) or \
855 "STRING" in self.text.tag_names(text_index)
856 else:
857 # The colorizer is missing: assume the worst
858 return 1
859
860 # If a selection is defined in the text widget, return (start,
861 # end) as Tkinter text indices, otherwise return (None, None)
862 def get_selection_indices(self):
863 try:
864 first = self.text.index("sel.first")
865 last = self.text.index("sel.last")
866 return first, last
867 except TclError:
868 return None, None
869
870 # Return the text widget's current view of what a tab stop means
871 # (equivalent width in spaces).
872
873 def get_tabwidth(self):
874 current = self.text['tabs'] or TK_TABWIDTH_DEFAULT
875 return int(current)
876
877 # Set the text widget's current view of what a tab stop means.
878
879 def set_tabwidth(self, newtabwidth):
880 text = self.text
881 if self.get_tabwidth() != newtabwidth:
882 pixels = text.tk.call("font", "measure", text["font"],
883 "-displayof", text.master,
Kurt B. Kaiserafdf71b2001-07-13 03:35:32 +0000884 "n" * newtabwidth)
David Scherer7aced172000-08-15 01:13:23 +0000885 text.configure(tabs=pixels)
886
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000887### begin autoindent code ###
888
889 # usetabs true -> literal tab characters are used by indent and
890 # dedent cmds, possibly mixed with spaces if
891 # indentwidth is not a multiple of tabwidth
892 # false -> tab characters are converted to spaces by indent
893 # and dedent cmds, and ditto TAB keystrokes
894 # indentwidth is the number of characters per logical indent level.
895 # tabwidth is the display width of a literal tab character.
896 # CAUTION: telling Tk to use anything other than its default
897 # tab setting causes it to use an entirely different tabbing algorithm,
898 # treating tab stops as fixed distances from the left margin.
899 # Nobody expects this, so for now tabwidth should never be changed.
900 usetabs = 0
901 indentwidth = 4
902 tabwidth = 8 # for IDLE use, must remain 8 until Tk is fixed
903
904 # If context_use_ps1 is true, parsing searches back for a ps1 line;
905 # else searches for a popular (if, def, ...) Python stmt.
906 context_use_ps1 = 0
907
908 # When searching backwards for a reliable place to begin parsing,
909 # first start num_context_lines[0] lines back, then
910 # num_context_lines[1] lines back if that didn't work, and so on.
911 # The last value should be huge (larger than the # of lines in a
912 # conceivable file).
913 # Making the initial values larger slows things down more often.
914 num_context_lines = 50, 500, 5000000
915
916 def config(self, **options):
917 for key, value in options.items():
918 if key == 'usetabs':
919 self.usetabs = value
920 elif key == 'indentwidth':
921 self.indentwidth = value
922 elif key == 'tabwidth':
923 self.tabwidth = value
924 elif key == 'context_use_ps1':
925 self.context_use_ps1 = value
926 else:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000927 raise KeyError, "bad option name: %r" % (key,)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000928
929 # If ispythonsource and guess are true, guess a good value for
930 # indentwidth based on file content (if possible), and if
931 # indentwidth != tabwidth set usetabs false.
932 # In any case, adjust the Text widget's view of what a tab
933 # character means.
934
935 def set_indentation_params(self, ispythonsource, guess=1):
936 if guess and ispythonsource:
937 i = self.guess_indent()
938 if 2 <= i <= 8:
939 self.indentwidth = i
940 if self.indentwidth != self.tabwidth:
941 self.usetabs = 0
942
943 self.set_tabwidth(self.tabwidth)
944
945 def smart_backspace_event(self, event):
946 text = self.text
947 first, last = self.get_selection_indices()
948 if first and last:
949 text.delete(first, last)
950 text.mark_set("insert", first)
951 return "break"
952 # Delete whitespace left, until hitting a real char or closest
953 # preceding virtual tab stop.
954 chars = text.get("insert linestart", "insert")
955 if chars == '':
956 if text.compare("insert", ">", "1.0"):
957 # easy: delete preceding newline
958 text.delete("insert-1c")
959 else:
960 text.bell() # at start of buffer
961 return "break"
962 if chars[-1] not in " \t":
963 # easy: delete preceding real char
964 text.delete("insert-1c")
965 return "break"
966 # Ick. It may require *inserting* spaces if we back up over a
967 # tab character! This is written to be clear, not fast.
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +0000968 tabwidth = self.tabwidth
969 have = len(chars.expandtabs(tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000970 assert have > 0
971 want = ((have - 1) // self.indentwidth) * self.indentwidth
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +0000972 # Debug prompt is multilined....
973 last_line_of_prompt = sys.ps1.split('\n')[-1]
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000974 ncharsdeleted = 0
975 while 1:
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +0000976 if chars == last_line_of_prompt:
Kurt B. Kaiser1bdca5e2002-12-16 22:25:10 +0000977 break
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000978 chars = chars[:-1]
979 ncharsdeleted = ncharsdeleted + 1
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +0000980 have = len(chars.expandtabs(tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000981 if have <= want or chars[-1] not in " \t":
982 break
983 text.undo_block_start()
984 text.delete("insert-%dc" % ncharsdeleted, "insert")
985 if have < want:
986 text.insert("insert", ' ' * (want - have))
987 text.undo_block_stop()
988 return "break"
989
990 def smart_indent_event(self, event):
991 # if intraline selection:
992 # delete it
993 # elif multiline selection:
994 # do indent-region & return
995 # indent one level
996 text = self.text
997 first, last = self.get_selection_indices()
998 text.undo_block_start()
999 try:
1000 if first and last:
1001 if index2line(first) != index2line(last):
1002 return self.indent_region_event(event)
1003 text.delete(first, last)
1004 text.mark_set("insert", first)
1005 prefix = text.get("insert linestart", "insert")
1006 raw, effective = classifyws(prefix, self.tabwidth)
1007 if raw == len(prefix):
1008 # only whitespace to the left
1009 self.reindent_to(effective + self.indentwidth)
1010 else:
1011 if self.usetabs:
1012 pad = '\t'
1013 else:
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001014 effective = len(prefix.expandtabs(self.tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001015 n = self.indentwidth
1016 pad = ' ' * (n - effective % n)
1017 text.insert("insert", pad)
1018 text.see("insert")
1019 return "break"
1020 finally:
1021 text.undo_block_stop()
1022
1023 def newline_and_indent_event(self, event):
1024 text = self.text
1025 first, last = self.get_selection_indices()
1026 text.undo_block_start()
1027 try:
1028 if first and last:
1029 text.delete(first, last)
1030 text.mark_set("insert", first)
1031 line = text.get("insert linestart", "insert")
1032 i, n = 0, len(line)
1033 while i < n and line[i] in " \t":
1034 i = i+1
1035 if i == n:
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001036 # the cursor is in or at leading indentation in a continuation
1037 # line; just inject an empty line at the start
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001038 text.insert("insert linestart", '\n')
1039 return "break"
1040 indent = line[:i]
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001041 # strip whitespace before insert point unless it's in the prompt
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001042 i = 0
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001043 last_line_of_prompt = sys.ps1.split('\n')[-1]
1044 while line and line[-1] in " \t" and line != last_line_of_prompt:
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001045 line = line[:-1]
1046 i = i+1
1047 if i:
1048 text.delete("insert - %d chars" % i, "insert")
1049 # strip whitespace after insert point
1050 while text.get("insert") in " \t":
1051 text.delete("insert")
1052 # start new line
1053 text.insert("insert", '\n')
1054
1055 # adjust indentation for continuations and block
1056 # open/close first need to find the last stmt
1057 lno = index2line(text.index('insert'))
1058 y = PyParse.Parser(self.indentwidth, self.tabwidth)
1059 for context in self.num_context_lines:
1060 startat = max(lno - context, 1)
Walter Dörwald70a6b492004-02-12 17:35:32 +00001061 startatindex = repr(startat) + ".0"
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001062 rawtext = text.get(startatindex, "insert")
1063 y.set_str(rawtext)
1064 bod = y.find_good_parse_start(
1065 self.context_use_ps1,
1066 self._build_char_in_string_func(startatindex))
1067 if bod is not None or startat == 1:
1068 break
1069 y.set_lo(bod or 0)
1070 c = y.get_continuation_type()
1071 if c != PyParse.C_NONE:
1072 # The current stmt hasn't ended yet.
1073 if c == PyParse.C_STRING:
1074 # inside a string; just mimic the current indent
1075 text.insert("insert", indent)
1076 elif c == PyParse.C_BRACKET:
1077 # line up with the first (if any) element of the
1078 # last open bracket structure; else indent one
1079 # level beyond the indent of the line with the
1080 # last open bracket
1081 self.reindent_to(y.compute_bracket_indent())
1082 elif c == PyParse.C_BACKSLASH:
1083 # if more than one line in this stmt already, just
1084 # mimic the current indent; else if initial line
1085 # has a start on an assignment stmt, indent to
1086 # beyond leftmost =; else to beyond first chunk of
1087 # non-whitespace on initial line
1088 if y.get_num_lines_in_stmt() > 1:
1089 text.insert("insert", indent)
1090 else:
1091 self.reindent_to(y.compute_backslash_indent())
1092 else:
Walter Dörwald70a6b492004-02-12 17:35:32 +00001093 assert 0, "bogus continuation type %r" % (c,)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001094 return "break"
1095
1096 # This line starts a brand new stmt; indent relative to
1097 # indentation of initial line of closest preceding
1098 # interesting stmt.
1099 indent = y.get_base_indent_string()
1100 text.insert("insert", indent)
1101 if y.is_block_opener():
1102 self.smart_indent_event(event)
1103 elif indent and y.is_block_closer():
1104 self.smart_backspace_event(event)
1105 return "break"
1106 finally:
1107 text.see("insert")
1108 text.undo_block_stop()
1109
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001110 # Our editwin provides a is_char_in_string function that works
1111 # with a Tk text index, but PyParse only knows about offsets into
1112 # a string. This builds a function for PyParse that accepts an
1113 # offset.
1114
1115 def _build_char_in_string_func(self, startindex):
1116 def inner(offset, _startindex=startindex,
1117 _icis=self.is_char_in_string):
1118 return _icis(_startindex + "+%dc" % offset)
1119 return inner
1120
1121 def indent_region_event(self, event):
1122 head, tail, chars, lines = self.get_region()
1123 for pos in range(len(lines)):
1124 line = lines[pos]
1125 if line:
1126 raw, effective = classifyws(line, self.tabwidth)
1127 effective = effective + self.indentwidth
1128 lines[pos] = self._make_blanks(effective) + line[raw:]
1129 self.set_region(head, tail, chars, lines)
1130 return "break"
1131
1132 def dedent_region_event(self, event):
1133 head, tail, chars, lines = self.get_region()
1134 for pos in range(len(lines)):
1135 line = lines[pos]
1136 if line:
1137 raw, effective = classifyws(line, self.tabwidth)
1138 effective = max(effective - self.indentwidth, 0)
1139 lines[pos] = self._make_blanks(effective) + line[raw:]
1140 self.set_region(head, tail, chars, lines)
1141 return "break"
1142
1143 def comment_region_event(self, event):
1144 head, tail, chars, lines = self.get_region()
1145 for pos in range(len(lines) - 1):
1146 line = lines[pos]
1147 lines[pos] = '##' + line
1148 self.set_region(head, tail, chars, lines)
1149
1150 def uncomment_region_event(self, event):
1151 head, tail, chars, lines = self.get_region()
1152 for pos in range(len(lines)):
1153 line = lines[pos]
1154 if not line:
1155 continue
1156 if line[:2] == '##':
1157 line = line[2:]
1158 elif line[:1] == '#':
1159 line = line[1:]
1160 lines[pos] = line
1161 self.set_region(head, tail, chars, lines)
1162
1163 def tabify_region_event(self, event):
1164 head, tail, chars, lines = self.get_region()
1165 tabwidth = self._asktabwidth()
1166 for pos in range(len(lines)):
1167 line = lines[pos]
1168 if line:
1169 raw, effective = classifyws(line, tabwidth)
1170 ntabs, nspaces = divmod(effective, tabwidth)
1171 lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]
1172 self.set_region(head, tail, chars, lines)
1173
1174 def untabify_region_event(self, event):
1175 head, tail, chars, lines = self.get_region()
1176 tabwidth = self._asktabwidth()
1177 for pos in range(len(lines)):
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001178 lines[pos] = lines[pos].expandtabs(tabwidth)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001179 self.set_region(head, tail, chars, lines)
1180
1181 def toggle_tabs_event(self, event):
1182 if self.askyesno(
1183 "Toggle tabs",
1184 "Turn tabs " + ("on", "off")[self.usetabs] + "?",
1185 parent=self.text):
1186 self.usetabs = not self.usetabs
1187 return "break"
1188
1189 # XXX this isn't bound to anything -- see class tabwidth comments
1190 def change_tabwidth_event(self, event):
1191 new = self._asktabwidth()
1192 if new != self.tabwidth:
1193 self.tabwidth = new
1194 self.set_indentation_params(0, guess=0)
1195 return "break"
1196
1197 def change_indentwidth_event(self, event):
1198 new = self.askinteger(
1199 "Indent width",
1200 "New indent width (2-16)",
1201 parent=self.text,
1202 initialvalue=self.indentwidth,
1203 minvalue=2,
1204 maxvalue=16)
1205 if new and new != self.indentwidth:
1206 self.indentwidth = new
1207 return "break"
1208
1209 def get_region(self):
1210 text = self.text
1211 first, last = self.get_selection_indices()
1212 if first and last:
1213 head = text.index(first + " linestart")
1214 tail = text.index(last + "-1c lineend +1c")
1215 else:
1216 head = text.index("insert linestart")
1217 tail = text.index("insert lineend +1c")
1218 chars = text.get(head, tail)
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001219 lines = chars.split("\n")
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001220 return head, tail, chars, lines
1221
1222 def set_region(self, head, tail, chars, lines):
1223 text = self.text
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001224 newchars = "\n".join(lines)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001225 if newchars == chars:
1226 text.bell()
1227 return
1228 text.tag_remove("sel", "1.0", "end")
1229 text.mark_set("insert", head)
1230 text.undo_block_start()
1231 text.delete(head, tail)
1232 text.insert(head, newchars)
1233 text.undo_block_stop()
1234 text.tag_add("sel", head, "insert")
1235
1236 # Make string that displays as n leading blanks.
1237
1238 def _make_blanks(self, n):
1239 if self.usetabs:
1240 ntabs, nspaces = divmod(n, self.tabwidth)
1241 return '\t' * ntabs + ' ' * nspaces
1242 else:
1243 return ' ' * n
1244
1245 # Delete from beginning of line to insert point, then reinsert
1246 # column logical (meaning use tabs if appropriate) spaces.
1247
1248 def reindent_to(self, column):
1249 text = self.text
1250 text.undo_block_start()
1251 if text.compare("insert linestart", "!=", "insert"):
1252 text.delete("insert linestart", "insert")
1253 if column:
1254 text.insert("insert", self._make_blanks(column))
1255 text.undo_block_stop()
1256
1257 def _asktabwidth(self):
1258 return self.askinteger(
1259 "Tab width",
1260 "Spaces per tab? (2-16)",
1261 parent=self.text,
1262 initialvalue=self.indentwidth,
1263 minvalue=2,
1264 maxvalue=16) or self.tabwidth
1265
1266 # Guess indentwidth from text content.
1267 # Return guessed indentwidth. This should not be believed unless
1268 # it's in a reasonable range (e.g., it will be 0 if no indented
1269 # blocks are found).
1270
1271 def guess_indent(self):
1272 opener, indented = IndentSearcher(self.text, self.tabwidth).run()
1273 if opener and indented:
1274 raw, indentsmall = classifyws(opener, self.tabwidth)
1275 raw, indentlarge = classifyws(indented, self.tabwidth)
1276 else:
1277 indentsmall = indentlarge = 0
1278 return indentlarge - indentsmall
1279
1280# "line.col" -> line, as an int
1281def index2line(index):
1282 return int(float(index))
1283
1284# Look at the leading whitespace in s.
1285# Return pair (# of leading ws characters,
1286# effective # of leading blanks after expanding
1287# tabs to width tabwidth)
1288
1289def classifyws(s, tabwidth):
1290 raw = effective = 0
1291 for ch in s:
1292 if ch == ' ':
1293 raw = raw + 1
1294 effective = effective + 1
1295 elif ch == '\t':
1296 raw = raw + 1
1297 effective = (effective // tabwidth + 1) * tabwidth
1298 else:
1299 break
1300 return raw, effective
1301
1302import tokenize
1303_tokenize = tokenize
1304del tokenize
1305
1306class IndentSearcher:
1307
1308 # .run() chews over the Text widget, looking for a block opener
1309 # and the stmt following it. Returns a pair,
1310 # (line containing block opener, line containing stmt)
1311 # Either or both may be None.
1312
1313 def __init__(self, text, tabwidth):
1314 self.text = text
1315 self.tabwidth = tabwidth
1316 self.i = self.finished = 0
1317 self.blkopenline = self.indentedline = None
1318
1319 def readline(self):
1320 if self.finished:
1321 return ""
1322 i = self.i = self.i + 1
Walter Dörwald70a6b492004-02-12 17:35:32 +00001323 mark = repr(i) + ".0"
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001324 if self.text.compare(mark, ">=", "end"):
1325 return ""
1326 return self.text.get(mark, mark + " lineend+1c")
1327
1328 def tokeneater(self, type, token, start, end, line,
1329 INDENT=_tokenize.INDENT,
1330 NAME=_tokenize.NAME,
1331 OPENERS=('class', 'def', 'for', 'if', 'try', 'while')):
1332 if self.finished:
1333 pass
1334 elif type == NAME and token in OPENERS:
1335 self.blkopenline = line
1336 elif type == INDENT and self.blkopenline:
1337 self.indentedline = line
1338 self.finished = 1
1339
1340 def run(self):
1341 save_tabsize = _tokenize.tabsize
1342 _tokenize.tabsize = self.tabwidth
1343 try:
1344 try:
1345 _tokenize.tokenize(self.readline, self.tokeneater)
1346 except _tokenize.TokenError:
1347 # since we cut off the tokenizer early, we can trigger
1348 # spurious errors
1349 pass
1350 finally:
1351 _tokenize.tabsize = save_tabsize
1352 return self.blkopenline, self.indentedline
1353
1354### end autoindent code ###
1355
David Scherer7aced172000-08-15 01:13:23 +00001356def prepstr(s):
1357 # Helper to extract the underscore from a string, e.g.
1358 # prepstr("Co_py") returns (2, "Copy").
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +00001359 i = s.find('_')
David Scherer7aced172000-08-15 01:13:23 +00001360 if i >= 0:
1361 s = s[:i] + s[i+1:]
1362 return i, s
1363
1364
1365keynames = {
1366 'bracketleft': '[',
1367 'bracketright': ']',
1368 'slash': '/',
1369}
1370
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +00001371def get_accelerator(keydefs, eventname):
1372 keylist = keydefs.get(eventname)
David Scherer7aced172000-08-15 01:13:23 +00001373 if not keylist:
1374 return ""
1375 s = keylist[0]
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +00001376 s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s)
David Scherer7aced172000-08-15 01:13:23 +00001377 s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s)
1378 s = re.sub("Key-", "", s)
1379 s = re.sub("Cancel","Ctrl-Break",s) # dscherer@cmu.edu
1380 s = re.sub("Control-", "Ctrl-", s)
1381 s = re.sub("-", "+", s)
1382 s = re.sub("><", " ", s)
1383 s = re.sub("<", "", s)
1384 s = re.sub(">", "", s)
1385 return s
1386
1387
1388def fixwordbreaks(root):
1389 # Make sure that Tk's double-click and next/previous word
1390 # operations use our definition of a word (i.e. an identifier)
1391 tk = root.tk
1392 tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
1393 tk.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]')
1394 tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]')
1395
1396
1397def test():
1398 root = Tk()
1399 fixwordbreaks(root)
1400 root.withdraw()
1401 if sys.argv[1:]:
1402 filename = sys.argv[1]
1403 else:
1404 filename = None
1405 edit = EditorWindow(root=root, filename=filename)
1406 edit.set_close_hook(root.quit)
1407 root.mainloop()
1408 root.destroy()
1409
1410if __name__ == '__main__':
1411 test()