blob: 5e0a57158a373e511873548b304c9686e218f0c2 [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
Kurt B. Kaiserdcba6622004-12-21 22:10:32 +000040class EditorWindow(object):
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)
Kurt B. Kaiser183403a2004-08-22 05:14:32 +000078 self.top = top = WindowList.ListedToplevel(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') )
Kurt B. Kaiser183403a2004-08-22 05:14:32 +0000105 self.top.focused_widget = self.text
David Scherer7aced172000-08-15 01:13:23 +0000106
107 self.createmenubar()
108 self.apply_bindings()
109
110 self.top.protocol("WM_DELETE_WINDOW", self.close)
111 self.top.bind("<<close-window>>", self.close_event)
Kurt B. Kaiser84f48032002-09-26 22:13:22 +0000112 text.bind("<<cut>>", self.cut)
113 text.bind("<<copy>>", self.copy)
114 text.bind("<<paste>>", self.paste)
David Scherer7aced172000-08-15 01:13:23 +0000115 text.bind("<<center-insert>>", self.center_insert_event)
116 text.bind("<<help>>", self.help_dialog)
David Scherer7aced172000-08-15 01:13:23 +0000117 text.bind("<<python-docs>>", self.python_docs)
118 text.bind("<<about-idle>>", self.about_dialog)
Steven M. Gava3b55a892001-11-21 05:56:26 +0000119 text.bind("<<open-config-dialog>>", self.config_dialog)
David Scherer7aced172000-08-15 01:13:23 +0000120 text.bind("<<open-module>>", self.open_module)
121 text.bind("<<do-nothing>>", lambda event: "break")
122 text.bind("<<select-all>>", self.select_all)
123 text.bind("<<remove-selection>>", self.remove_selection)
Steven M. Gavac5976402002-01-04 03:06:08 +0000124 text.bind("<<find>>", self.find_event)
125 text.bind("<<find-again>>", self.find_again_event)
126 text.bind("<<find-in-files>>", self.find_in_files_event)
127 text.bind("<<find-selection>>", self.find_selection_event)
128 text.bind("<<replace>>", self.replace_event)
129 text.bind("<<goto-line>>", self.goto_line_event)
David Scherer7aced172000-08-15 01:13:23 +0000130 text.bind("<3>", self.right_menu_event)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000131 text.bind("<<smart-backspace>>",self.smart_backspace_event)
132 text.bind("<<newline-and-indent>>",self.newline_and_indent_event)
133 text.bind("<<smart-indent>>",self.smart_indent_event)
134 text.bind("<<indent-region>>",self.indent_region_event)
135 text.bind("<<dedent-region>>",self.dedent_region_event)
136 text.bind("<<comment-region>>",self.comment_region_event)
137 text.bind("<<uncomment-region>>",self.uncomment_region_event)
138 text.bind("<<tabify-region>>",self.tabify_region_event)
139 text.bind("<<untabify-region>>",self.untabify_region_event)
140 text.bind("<<toggle-tabs>>",self.toggle_tabs_event)
141 text.bind("<<change-indentwidth>>",self.change_indentwidth_event)
Kurt B. Kaiser5ec186b2003-01-17 04:04:06 +0000142 text.bind("<Left>", self.move_at_edge_if_selection(0))
143 text.bind("<Right>", self.move_at_edge_if_selection(1))
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000144
David Scherer7aced172000-08-15 01:13:23 +0000145 if flist:
146 flist.inversedict[self] = key
147 if key:
148 flist.dict[key] = self
Kurt B. Kaiserd2f48612003-06-05 02:34:04 +0000149 text.bind("<<open-new-window>>", self.new_callback)
David Scherer7aced172000-08-15 01:13:23 +0000150 text.bind("<<close-all-windows>>", self.flist.close_all_callback)
151 text.bind("<<open-class-browser>>", self.open_class_browser)
152 text.bind("<<open-path-browser>>", self.open_path_browser)
153
Steven M. Gava898a3652001-10-07 11:10:44 +0000154 self.set_status_bar()
David Scherer7aced172000-08-15 01:13:23 +0000155 vbar['command'] = text.yview
156 vbar.pack(side=RIGHT, fill=Y)
David Scherer7aced172000-08-15 01:13:23 +0000157 text['yscrollcommand'] = vbar.set
Steven M. Gavab1585412002-03-12 00:21:56 +0000158 fontWeight='normal'
159 if idleConf.GetOption('main','EditorWindow','font-bold',type='bool'):
160 fontWeight='bold'
Steven M. Gavadc72f482002-01-03 11:51:07 +0000161 text.config(font=(idleConf.GetOption('main','EditorWindow','font'),
Steven M. Gavab1585412002-03-12 00:21:56 +0000162 idleConf.GetOption('main','EditorWindow','font-size'),
163 fontWeight))
David Scherer7aced172000-08-15 01:13:23 +0000164 text_frame.pack(side=LEFT, fill=BOTH, expand=1)
165 text.pack(side=TOP, fill=BOTH, expand=1)
166 text.focus_set()
167
168 self.per = per = self.Percolator(text)
169 if self.ispythonsource(filename):
Kurt B. Kaiserdc1e7092002-07-11 04:33:41 +0000170 self.color = color = self.ColorDelegator()
171 per.insertfilter(color)
David Scherer7aced172000-08-15 01:13:23 +0000172 else:
David Scherer7aced172000-08-15 01:13:23 +0000173 self.color = None
Kurt B. Kaiserdc1e7092002-07-11 04:33:41 +0000174
175 self.undo = undo = self.UndoDelegator()
176 per.insertfilter(undo)
177 text.undo_block_start = undo.undo_block_start
178 text.undo_block_stop = undo.undo_block_stop
179 undo.set_saved_change_hook(self.saved_change_hook)
180
181 # IOBinding implements file I/O and printing functionality
David Scherer7aced172000-08-15 01:13:23 +0000182 self.io = io = self.IOBinding(self)
Kurt B. Kaiserdc1e7092002-07-11 04:33:41 +0000183 io.set_filename_change_hook(self.filename_change_hook)
184
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000185 # Create the recent files submenu
186 self.recent_files_menu = Menu(self.menubar)
187 self.menudict['file'].insert_cascade(3, label='Recent Files',
188 underline=0,
189 menu=self.recent_files_menu)
190 self.update_recent_files_list()
David Scherer7aced172000-08-15 01:13:23 +0000191
David Scherer7aced172000-08-15 01:13:23 +0000192 if filename:
Kurt B. Kaiserd2f48612003-06-05 02:34:04 +0000193 if os.path.exists(filename) and not os.path.isdir(filename):
David Scherer7aced172000-08-15 01:13:23 +0000194 io.loadfile(filename)
195 else:
196 io.set_filename(filename)
David Scherer7aced172000-08-15 01:13:23 +0000197 self.saved_change_hook()
198
199 self.load_extensions()
200
201 menu = self.menudict.get('windows')
202 if menu:
203 end = menu.index("end")
204 if end is None:
205 end = -1
206 if end >= 0:
207 menu.add_separator()
208 end = end + 1
209 self.wmenu_end = end
210 WindowList.register_callback(self.postwindowsmenu)
211
212 # Some abstractions so IDLE extensions are cross-IDE
213 self.askyesno = tkMessageBox.askyesno
214 self.askinteger = tkSimpleDialog.askinteger
215 self.showerror = tkMessageBox.showerror
216
217 if self.extensions.has_key('AutoIndent'):
218 self.extensions['AutoIndent'].set_indentation_params(
219 self.ispythonsource(filename))
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000220
Kurt B. Kaiserd2f48612003-06-05 02:34:04 +0000221 def new_callback(self, event):
222 dirname, basename = self.io.defaultfilename()
223 self.flist.new(dirname)
224 return "break"
225
David Scherer7aced172000-08-15 01:13:23 +0000226 def set_status_bar(self):
Steven M. Gava898a3652001-10-07 11:10:44 +0000227 self.status_bar = self.MultiStatusBar(self.top)
David Scherer7aced172000-08-15 01:13:23 +0000228 self.status_bar.set_label('column', 'Col: ?', side=RIGHT)
229 self.status_bar.set_label('line', 'Ln: ?', side=RIGHT)
230 self.status_bar.pack(side=BOTTOM, fill=X)
231 self.text.bind('<KeyRelease>', self.set_line_and_column)
232 self.text.bind('<ButtonRelease>', self.set_line_and_column)
233 self.text.after_idle(self.set_line_and_column)
234
235 def set_line_and_column(self, event=None):
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000236 line, column = self.text.index(INSERT).split('.')
David Scherer7aced172000-08-15 01:13:23 +0000237 self.status_bar.set_label('column', 'Col: %s' % column)
238 self.status_bar.set_label('line', 'Ln: %s' % line)
239
David Scherer7aced172000-08-15 01:13:23 +0000240 menu_specs = [
241 ("file", "_File"),
242 ("edit", "_Edit"),
243 ("format", "F_ormat"),
244 ("run", "_Run"),
Kurt B. Kaiser1061e722003-01-04 01:43:53 +0000245 ("options", "_Options"),
David Scherer7aced172000-08-15 01:13:23 +0000246 ("windows", "_Windows"),
247 ("help", "_Help"),
248 ]
249
250 def createmenubar(self):
251 mbar = self.menubar
252 self.menudict = menudict = {}
253 for name, label in self.menu_specs:
254 underline, label = prepstr(label)
255 menudict[name] = menu = Menu(mbar, name=name)
256 mbar.add_cascade(label=label, menu=menu, underline=underline)
257 self.fill_menus()
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000258 self.base_helpmenu_length = self.menudict['help'].index(END)
259 self.reset_help_menu_entries()
David Scherer7aced172000-08-15 01:13:23 +0000260
261 def postwindowsmenu(self):
262 # Only called when Windows menu exists
David Scherer7aced172000-08-15 01:13:23 +0000263 menu = self.menudict['windows']
264 end = menu.index("end")
265 if end is None:
266 end = -1
267 if end > self.wmenu_end:
268 menu.delete(self.wmenu_end+1, end)
269 WindowList.add_windows_to_menu(menu)
270
271 rmenu = None
272
273 def right_menu_event(self, event):
274 self.text.tag_remove("sel", "1.0", "end")
275 self.text.mark_set("insert", "@%d,%d" % (event.x, event.y))
276 if not self.rmenu:
277 self.make_rmenu()
278 rmenu = self.rmenu
279 self.event = event
280 iswin = sys.platform[:3] == 'win'
281 if iswin:
282 self.text.config(cursor="arrow")
283 rmenu.tk_popup(event.x_root, event.y_root)
284 if iswin:
285 self.text.config(cursor="ibeam")
286
287 rmenu_specs = [
288 # ("Label", "<<virtual-event>>"), ...
289 ("Close", "<<close-window>>"), # Example
290 ]
291
292 def make_rmenu(self):
293 rmenu = Menu(self.text, tearoff=0)
294 for label, eventname in self.rmenu_specs:
295 def command(text=self.text, eventname=eventname):
296 text.event_generate(eventname)
297 rmenu.add_command(label=label, command=command)
298 self.rmenu = rmenu
299
300 def about_dialog(self, event=None):
Kurt B. Kaiserd78b2302003-06-12 04:03:49 +0000301 aboutDialog.AboutDialog(self.top,'About IDLE')
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000302
Steven M. Gava3b55a892001-11-21 05:56:26 +0000303 def config_dialog(self, event=None):
304 configDialog.ConfigDialog(self.top,'Settings')
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000305
David Scherer7aced172000-08-15 01:13:23 +0000306 def help_dialog(self, event=None):
Steven M. Gavab9d07b52001-07-31 11:11:38 +0000307 fn=os.path.join(os.path.abspath(os.path.dirname(__file__)),'help.txt')
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000308 textView.TextViewer(self.top,'Help',fn)
309
Kurt B. Kaiser114713d2003-01-10 05:07:24 +0000310 def python_docs(self, event=None):
Kurt B. Kaiser8aa23922004-07-15 04:54:57 +0000311 if sys.platform[:3] == 'win':
Steven M. Gava931625d2002-04-22 00:38:26 +0000312 os.startfile(self.help_url)
Kurt B. Kaiser114713d2003-01-10 05:07:24 +0000313 else:
314 webbrowser.open(self.help_url)
Kurt B. Kaiser8aa23922004-07-15 04:54:57 +0000315 return "break"
David Scherer7aced172000-08-15 01:13:23 +0000316
Kurt B. Kaiser84f48032002-09-26 22:13:22 +0000317 def cut(self,event):
318 self.text.event_generate("<<Cut>>")
319 return "break"
320
321 def copy(self,event):
322 self.text.event_generate("<<Copy>>")
323 return "break"
324
325 def paste(self,event):
326 self.text.event_generate("<<Paste>>")
327 return "break"
328
David Scherer7aced172000-08-15 01:13:23 +0000329 def select_all(self, event=None):
330 self.text.tag_add("sel", "1.0", "end-1c")
331 self.text.mark_set("insert", "1.0")
332 self.text.see("insert")
333 return "break"
334
335 def remove_selection(self, event=None):
336 self.text.tag_remove("sel", "1.0", "end")
337 self.text.see("insert")
338
Kurt B. Kaiser5ec186b2003-01-17 04:04:06 +0000339 def move_at_edge_if_selection(self, edge_index):
340 """Cursor move begins at start or end of selection
341
342 When a left/right cursor key is pressed create and return to Tkinter a
343 function which causes a cursor move from the associated edge of the
344 selection.
345
346 """
347 self_text_index = self.text.index
348 self_text_mark_set = self.text.mark_set
349 edges_table = ("sel.first+1c", "sel.last-1c")
350 def move_at_edge(event):
351 if (event.state & 5) == 0: # no shift(==1) or control(==4) pressed
352 try:
353 self_text_index("sel.first")
354 self_text_mark_set("insert", edges_table[edge_index])
355 except TclError:
356 pass
357 return move_at_edge
358
Steven M. Gavac5976402002-01-04 03:06:08 +0000359 def find_event(self, event):
360 SearchDialog.find(self.text)
361 return "break"
362
363 def find_again_event(self, event):
364 SearchDialog.find_again(self.text)
365 return "break"
366
367 def find_selection_event(self, event):
368 SearchDialog.find_selection(self.text)
369 return "break"
370
371 def find_in_files_event(self, event):
372 GrepDialog.grep(self.text, self.io, self.flist)
373 return "break"
374
375 def replace_event(self, event):
376 ReplaceDialog.replace(self.text)
377 return "break"
378
379 def goto_line_event(self, event):
380 text = self.text
381 lineno = tkSimpleDialog.askinteger("Goto",
382 "Go to line number:",parent=text)
383 if lineno is None:
384 return "break"
385 if lineno <= 0:
386 text.bell()
387 return "break"
388 text.mark_set("insert", "%d.0" % lineno)
389 text.see("insert")
390
David Scherer7aced172000-08-15 01:13:23 +0000391 def open_module(self, event=None):
392 # XXX Shouldn't this be in IOBinding or in FileList?
393 try:
394 name = self.text.get("sel.first", "sel.last")
395 except TclError:
396 name = ""
397 else:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000398 name = name.strip()
Guido van Rossum852f35b2003-06-05 11:36:55 +0000399 name = tkSimpleDialog.askstring("Module",
400 "Enter the name of a Python module\n"
401 "to search on sys.path and open:",
402 parent=self.text, initialvalue=name)
403 if name:
404 name = name.strip()
David Scherer7aced172000-08-15 01:13:23 +0000405 if not name:
Guido van Rossum852f35b2003-06-05 11:36:55 +0000406 return
David Scherer7aced172000-08-15 01:13:23 +0000407 # XXX Ought to insert current file's directory in front of path
408 try:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000409 (f, file, (suffix, mode, type)) = _find_module(name)
David Scherer7aced172000-08-15 01:13:23 +0000410 except (NameError, ImportError), msg:
411 tkMessageBox.showerror("Import error", str(msg), parent=self.text)
412 return
413 if type != imp.PY_SOURCE:
414 tkMessageBox.showerror("Unsupported type",
415 "%s is not a source module" % name, parent=self.text)
416 return
417 if f:
418 f.close()
419 if self.flist:
420 self.flist.open(file)
421 else:
422 self.io.loadfile(file)
423
424 def open_class_browser(self, event=None):
425 filename = self.io.filename
426 if not filename:
427 tkMessageBox.showerror(
428 "No filename",
429 "This buffer has no associated filename",
430 master=self.text)
431 self.text.focus_set()
432 return None
433 head, tail = os.path.split(filename)
434 base, ext = os.path.splitext(tail)
435 import ClassBrowser
436 ClassBrowser.ClassBrowser(self.flist, base, [head])
437
438 def open_path_browser(self, event=None):
439 import PathBrowser
440 PathBrowser.PathBrowser(self.flist)
441
442 def gotoline(self, lineno):
443 if lineno is not None and lineno > 0:
444 self.text.mark_set("insert", "%d.0" % lineno)
445 self.text.tag_remove("sel", "1.0", "end")
446 self.text.tag_add("sel", "insert", "insert +1l")
447 self.center()
448
449 def ispythonsource(self, filename):
450 if not filename:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000451 return True
David Scherer7aced172000-08-15 01:13:23 +0000452 base, ext = os.path.splitext(os.path.basename(filename))
453 if os.path.normcase(ext) in (".py", ".pyw"):
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000454 return True
David Scherer7aced172000-08-15 01:13:23 +0000455 try:
456 f = open(filename)
457 line = f.readline()
458 f.close()
459 except IOError:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000460 return False
Kurt B. Kaiser83a35602002-12-20 17:18:03 +0000461 return line.startswith('#!') and line.find('python') >= 0
David Scherer7aced172000-08-15 01:13:23 +0000462
463 def close_hook(self):
464 if self.flist:
465 self.flist.close_edit(self)
466
467 def set_close_hook(self, close_hook):
468 self.close_hook = close_hook
469
470 def filename_change_hook(self):
471 if self.flist:
472 self.flist.filename_changed_edit(self)
473 self.saved_change_hook()
Kurt B. Kaiser260cb902003-06-06 21:58:38 +0000474 self.top.update_windowlist_registry(self)
David Scherer7aced172000-08-15 01:13:23 +0000475 if self.ispythonsource(self.io.filename):
476 self.addcolorizer()
477 else:
478 self.rmcolorizer()
479
480 def addcolorizer(self):
481 if self.color:
482 return
David Scherer7aced172000-08-15 01:13:23 +0000483 self.per.removefilter(self.undo)
484 self.color = self.ColorDelegator()
485 self.per.insertfilter(self.color)
486 self.per.insertfilter(self.undo)
487
488 def rmcolorizer(self):
489 if not self.color:
490 return
David Scherer7aced172000-08-15 01:13:23 +0000491 self.per.removefilter(self.undo)
492 self.per.removefilter(self.color)
493 self.color = None
494 self.per.insertfilter(self.undo)
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000495
Steven M. Gavab77d3432002-03-02 07:16:21 +0000496 def ResetColorizer(self):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000497 "Update the colour theme if it is changed"
498 # Called from configDialog.py
Steven M. Gavab77d3432002-03-02 07:16:21 +0000499 if self.color:
500 self.color = self.ColorDelegator()
501 self.per.insertfilter(self.color)
Kurt B. Kaiser73360a32004-03-08 18:15:31 +0000502 theme = idleConf.GetOption('main','Theme','name')
503 self.text.config(idleConf.GetHighlight(theme, "normal"))
David Scherer7aced172000-08-15 01:13:23 +0000504
Steven M. Gavab1585412002-03-12 00:21:56 +0000505 def ResetFont(self):
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000506 "Update the text widgets' font if it is changed"
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000507 # Called from configDialog.py
Steven M. Gavab1585412002-03-12 00:21:56 +0000508 fontWeight='normal'
509 if idleConf.GetOption('main','EditorWindow','font-bold',type='bool'):
510 fontWeight='bold'
511 self.text.config(font=(idleConf.GetOption('main','EditorWindow','font'),
512 idleConf.GetOption('main','EditorWindow','font-size'),
513 fontWeight))
514
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000515 def ResetKeybindings(self):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000516 "Update the keybindings if they are changed"
517 # Called from configDialog.py
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000518 self.Bindings.default_keydefs=idleConf.GetCurrentKeySet()
519 keydefs = self.Bindings.default_keydefs
520 for event, keylist in keydefs.items():
521 self.text.event_delete(event)
522 self.apply_bindings()
523 #update menu accelerators
524 menuEventDict={}
525 for menu in self.Bindings.menudefs:
526 menuEventDict[menu[0]]={}
527 for item in menu[1]:
528 if item:
529 menuEventDict[menu[0]][prepstr(item[0])[1]]=item[1]
530 for menubarItem in self.menudict.keys():
531 menu=self.menudict[menubarItem]
532 end=menu.index(END)+1
533 for index in range(0,end):
534 if menu.type(index)=='command':
535 accel=menu.entrycget(index,'accelerator')
536 if accel:
537 itemName=menu.entrycget(index,'label')
538 event=''
539 if menuEventDict.has_key(menubarItem):
540 if menuEventDict[menubarItem].has_key(itemName):
541 event=menuEventDict[menubarItem][itemName]
542 if event:
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000543 accel=get_accelerator(keydefs, event)
544 menu.entryconfig(index,accelerator=accel)
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000545
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000546 def reset_help_menu_entries(self):
547 "Update the additional help entries on the Help menu"
548 help_list = idleConf.GetAllExtraHelpSourcesList()
549 helpmenu = self.menudict['help']
550 # first delete the extra help entries, if any
551 helpmenu_length = helpmenu.index(END)
552 if helpmenu_length > self.base_helpmenu_length:
553 helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length)
554 # then rebuild them
555 if help_list:
556 helpmenu.add_separator()
557 for entry in help_list:
558 cmd = self.__extra_help_callback(entry[1])
559 helpmenu.add_command(label=entry[0], command=cmd)
560 # and update the menu dictionary
561 self.menudict['help'] = helpmenu
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000562
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000563 def __extra_help_callback(self, helpfile):
564 "Create a callback with the helpfile value frozen at definition time"
565 def display_extra_help(helpfile=helpfile):
Kurt B. Kaiser8aa23922004-07-15 04:54:57 +0000566 if not (helpfile.startswith('www') or helpfile.startswith('http')):
567 url = os.path.normpath(helpfile)
568 if sys.platform[:3] == 'win':
569 os.startfile(helpfile)
570 else:
571 webbrowser.open(helpfile)
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000572 return display_extra_help
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000573
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000574 def update_recent_files_list(self, new_file=None):
575 "Load and update the recent files list and menus"
576 rf_list = []
577 if os.path.exists(self.recent_files_path):
578 rf_list_file = open(self.recent_files_path,'r')
Steven M. Gava1d46e402002-03-27 08:40:46 +0000579 try:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000580 rf_list = rf_list_file.readlines()
Steven M. Gava1d46e402002-03-27 08:40:46 +0000581 finally:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000582 rf_list_file.close()
583 if new_file:
584 new_file = os.path.abspath(new_file) + '\n'
585 if new_file in rf_list:
586 rf_list.remove(new_file) # move to top
587 rf_list.insert(0, new_file)
588 # clean and save the recent files list
589 bad_paths = []
590 for path in rf_list:
591 if '\0' in path or not os.path.exists(path[0:-1]):
592 bad_paths.append(path)
593 rf_list = [path for path in rf_list if path not in bad_paths]
594 ulchars = "1234567890ABCDEFGHIJK"
595 rf_list = rf_list[0:len(ulchars)]
596 rf_file = open(self.recent_files_path, 'w')
Steven M. Gava1d46e402002-03-27 08:40:46 +0000597 try:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000598 rf_file.writelines(rf_list)
Steven M. Gava1d46e402002-03-27 08:40:46 +0000599 finally:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000600 rf_file.close()
601 # for each edit window instance, construct the recent files menu
602 for instance in self.top.instance_dict.keys():
603 menu = instance.recent_files_menu
604 menu.delete(1, END) # clear, and rebuild:
605 for i, file in zip(count(), rf_list):
606 file_name = file[0:-1] # zap \n
607 callback = instance.__recent_file_callback(file_name)
608 menu.add_command(label=ulchars[i] + " " + file_name,
609 command=callback,
610 underline=0)
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000611
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000612 def __recent_file_callback(self, file_name):
613 def open_recent_file(fn_closure=file_name):
614 self.io.open(editFile=fn_closure)
615 return open_recent_file
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000616
David Scherer7aced172000-08-15 01:13:23 +0000617 def saved_change_hook(self):
618 short = self.short_title()
619 long = self.long_title()
620 if short and long:
621 title = short + " - " + long
622 elif short:
623 title = short
624 elif long:
625 title = long
626 else:
627 title = "Untitled"
628 icon = short or long or title
629 if not self.get_saved():
630 title = "*%s*" % title
631 icon = "*%s" % icon
632 self.top.wm_title(title)
633 self.top.wm_iconname(icon)
634
635 def get_saved(self):
636 return self.undo.get_saved()
637
638 def set_saved(self, flag):
639 self.undo.set_saved(flag)
640
641 def reset_undo(self):
642 self.undo.reset_undo()
643
644 def short_title(self):
645 filename = self.io.filename
646 if filename:
647 filename = os.path.basename(filename)
648 return filename
649
650 def long_title(self):
651 return self.io.filename or ""
652
653 def center_insert_event(self, event):
654 self.center()
655
656 def center(self, mark="insert"):
657 text = self.text
658 top, bot = self.getwindowlines()
659 lineno = self.getlineno(mark)
660 height = bot - top
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000661 newtop = max(1, lineno - height//2)
David Scherer7aced172000-08-15 01:13:23 +0000662 text.yview(float(newtop))
663
664 def getwindowlines(self):
665 text = self.text
666 top = self.getlineno("@0,0")
667 bot = self.getlineno("@0,65535")
668 if top == bot and text.winfo_height() == 1:
669 # Geometry manager hasn't run yet
670 height = int(text['height'])
671 bot = top + height - 1
672 return top, bot
673
674 def getlineno(self, mark="insert"):
675 text = self.text
676 return int(float(text.index(mark)))
677
Kurt B. Kaiser1061e722003-01-04 01:43:53 +0000678 def get_geometry(self):
679 "Return (width, height, x, y)"
680 geom = self.top.wm_geometry()
681 m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom)
682 tuple = (map(int, m.groups()))
683 return tuple
684
David Scherer7aced172000-08-15 01:13:23 +0000685 def close_event(self, event):
686 self.close()
687
688 def maybesave(self):
689 if self.io:
Steven M. Gava67716b52002-02-26 02:31:03 +0000690 if not self.get_saved():
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000691 if self.top.state()!='normal':
Steven M. Gava67716b52002-02-26 02:31:03 +0000692 self.top.deiconify()
693 self.top.lower()
694 self.top.lift()
David Scherer7aced172000-08-15 01:13:23 +0000695 return self.io.maybesave()
696
697 def close(self):
David Scherer7aced172000-08-15 01:13:23 +0000698 reply = self.maybesave()
699 if reply != "cancel":
700 self._close()
701 return reply
702
703 def _close(self):
Steven M. Gava1d46e402002-03-27 08:40:46 +0000704 if self.io.filename:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000705 self.update_recent_files_list(new_file=self.io.filename)
David Scherer7aced172000-08-15 01:13:23 +0000706 WindowList.unregister_callback(self.postwindowsmenu)
707 if self.close_hook:
708 self.close_hook()
709 self.flist = None
710 colorizing = 0
711 self.unload_extensions()
712 self.io.close(); self.io = None
713 self.undo = None # XXX
714 if self.color:
715 colorizing = self.color.colorizing
716 doh = colorizing and self.top
717 self.color.close(doh) # Cancel colorization
718 self.text = None
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000719 self.tkinter_vars = None
David Scherer7aced172000-08-15 01:13:23 +0000720 self.per.close(); self.per = None
721 if not colorizing:
722 self.top.destroy()
723
724 def load_extensions(self):
725 self.extensions = {}
726 self.load_standard_extensions()
727
728 def unload_extensions(self):
729 for ins in self.extensions.values():
730 if hasattr(ins, "close"):
731 ins.close()
732 self.extensions = {}
733
734 def load_standard_extensions(self):
735 for name in self.get_standard_extension_names():
736 try:
737 self.load_extension(name)
738 except:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000739 print "Failed to load extension", repr(name)
David Scherer7aced172000-08-15 01:13:23 +0000740 import traceback
741 traceback.print_exc()
742
743 def get_standard_extension_names(self):
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +0000744 return idleConf.GetExtensions(editor_only=True)
David Scherer7aced172000-08-15 01:13:23 +0000745
746 def load_extension(self, name):
Kurt B. Kaiserb00e89f2005-01-18 00:54:58 +0000747 try:
748 mod = __import__(name, globals(), locals(), [])
749 except ImportError:
750 print "\nFailed to import extension: ", name
751 return
David Scherer7aced172000-08-15 01:13:23 +0000752 cls = getattr(mod, name)
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +0000753 keydefs = idleConf.GetExtensionBindings(name)
754 if hasattr(cls, "menudefs"):
755 self.fill_menus(cls.menudefs, keydefs)
David Scherer7aced172000-08-15 01:13:23 +0000756 ins = cls(self)
757 self.extensions[name] = ins
David Scherer7aced172000-08-15 01:13:23 +0000758 if keydefs:
759 self.apply_bindings(keydefs)
760 for vevent in keydefs.keys():
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000761 methodname = vevent.replace("-", "_")
David Scherer7aced172000-08-15 01:13:23 +0000762 while methodname[:1] == '<':
763 methodname = methodname[1:]
764 while methodname[-1:] == '>':
765 methodname = methodname[:-1]
766 methodname = methodname + "_event"
767 if hasattr(ins, methodname):
768 self.text.bind(vevent, getattr(ins, methodname))
David Scherer7aced172000-08-15 01:13:23 +0000769
770 def apply_bindings(self, keydefs=None):
771 if keydefs is None:
772 keydefs = self.Bindings.default_keydefs
773 text = self.text
774 text.keydefs = keydefs
775 for event, keylist in keydefs.items():
776 if keylist:
Raymond Hettinger931237e2003-07-09 18:48:24 +0000777 text.event_add(event, *keylist)
David Scherer7aced172000-08-15 01:13:23 +0000778
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000779 def fill_menus(self, menudefs=None, keydefs=None):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000780 """Add appropriate entries to the menus and submenus
781
782 Menus that are absent or None in self.menudict are ignored.
783 """
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000784 if menudefs is None:
785 menudefs = self.Bindings.menudefs
David Scherer7aced172000-08-15 01:13:23 +0000786 if keydefs is None:
787 keydefs = self.Bindings.default_keydefs
788 menudict = self.menudict
789 text = self.text
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000790 for mname, entrylist in menudefs:
David Scherer7aced172000-08-15 01:13:23 +0000791 menu = menudict.get(mname)
792 if not menu:
793 continue
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000794 for entry in entrylist:
795 if not entry:
David Scherer7aced172000-08-15 01:13:23 +0000796 menu.add_separator()
797 else:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000798 label, eventname = entry
David Scherer7aced172000-08-15 01:13:23 +0000799 checkbutton = (label[:1] == '!')
800 if checkbutton:
801 label = label[1:]
802 underline, label = prepstr(label)
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000803 accelerator = get_accelerator(keydefs, eventname)
804 def command(text=text, eventname=eventname):
805 text.event_generate(eventname)
David Scherer7aced172000-08-15 01:13:23 +0000806 if checkbutton:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000807 var = self.get_var_obj(eventname, BooleanVar)
David Scherer7aced172000-08-15 01:13:23 +0000808 menu.add_checkbutton(label=label, underline=underline,
809 command=command, accelerator=accelerator,
810 variable=var)
811 else:
812 menu.add_command(label=label, underline=underline,
Kurt B. Kaiser84f48032002-09-26 22:13:22 +0000813 command=command,
814 accelerator=accelerator)
David Scherer7aced172000-08-15 01:13:23 +0000815
816 def getvar(self, name):
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000817 var = self.get_var_obj(name)
David Scherer7aced172000-08-15 01:13:23 +0000818 if var:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000819 value = var.get()
820 return value
821 else:
822 raise NameError, name
David Scherer7aced172000-08-15 01:13:23 +0000823
824 def setvar(self, name, value, vartype=None):
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000825 var = self.get_var_obj(name, vartype)
David Scherer7aced172000-08-15 01:13:23 +0000826 if var:
827 var.set(value)
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000828 else:
829 raise NameError, name
David Scherer7aced172000-08-15 01:13:23 +0000830
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000831 def get_var_obj(self, name, vartype=None):
832 var = self.tkinter_vars.get(name)
David Scherer7aced172000-08-15 01:13:23 +0000833 if not var and vartype:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000834 # create a Tkinter variable object with self.text as master:
835 self.tkinter_vars[name] = var = vartype(self.text)
David Scherer7aced172000-08-15 01:13:23 +0000836 return var
837
838 # Tk implementations of "virtual text methods" -- each platform
839 # reusing IDLE's support code needs to define these for its GUI's
840 # flavor of widget.
841
842 # Is character at text_index in a Python string? Return 0 for
843 # "guaranteed no", true for anything else. This info is expensive
844 # to compute ab initio, but is probably already known by the
845 # platform's colorizer.
846
847 def is_char_in_string(self, text_index):
848 if self.color:
849 # Return true iff colorizer hasn't (re)gotten this far
850 # yet, or the character is tagged as being in a string
851 return self.text.tag_prevrange("TODO", text_index) or \
852 "STRING" in self.text.tag_names(text_index)
853 else:
854 # The colorizer is missing: assume the worst
855 return 1
856
857 # If a selection is defined in the text widget, return (start,
858 # end) as Tkinter text indices, otherwise return (None, None)
859 def get_selection_indices(self):
860 try:
861 first = self.text.index("sel.first")
862 last = self.text.index("sel.last")
863 return first, last
864 except TclError:
865 return None, None
866
867 # Return the text widget's current view of what a tab stop means
868 # (equivalent width in spaces).
869
870 def get_tabwidth(self):
871 current = self.text['tabs'] or TK_TABWIDTH_DEFAULT
872 return int(current)
873
874 # Set the text widget's current view of what a tab stop means.
875
876 def set_tabwidth(self, newtabwidth):
877 text = self.text
878 if self.get_tabwidth() != newtabwidth:
879 pixels = text.tk.call("font", "measure", text["font"],
880 "-displayof", text.master,
Kurt B. Kaiserafdf71b2001-07-13 03:35:32 +0000881 "n" * newtabwidth)
David Scherer7aced172000-08-15 01:13:23 +0000882 text.configure(tabs=pixels)
883
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000884### begin autoindent code ###
885
886 # usetabs true -> literal tab characters are used by indent and
887 # dedent cmds, possibly mixed with spaces if
888 # indentwidth is not a multiple of tabwidth
889 # false -> tab characters are converted to spaces by indent
890 # and dedent cmds, and ditto TAB keystrokes
891 # indentwidth is the number of characters per logical indent level.
892 # tabwidth is the display width of a literal tab character.
893 # CAUTION: telling Tk to use anything other than its default
894 # tab setting causes it to use an entirely different tabbing algorithm,
895 # treating tab stops as fixed distances from the left margin.
896 # Nobody expects this, so for now tabwidth should never be changed.
897 usetabs = 0
898 indentwidth = 4
899 tabwidth = 8 # for IDLE use, must remain 8 until Tk is fixed
900
901 # If context_use_ps1 is true, parsing searches back for a ps1 line;
902 # else searches for a popular (if, def, ...) Python stmt.
903 context_use_ps1 = 0
904
905 # When searching backwards for a reliable place to begin parsing,
906 # first start num_context_lines[0] lines back, then
907 # num_context_lines[1] lines back if that didn't work, and so on.
908 # The last value should be huge (larger than the # of lines in a
909 # conceivable file).
910 # Making the initial values larger slows things down more often.
911 num_context_lines = 50, 500, 5000000
912
913 def config(self, **options):
914 for key, value in options.items():
915 if key == 'usetabs':
916 self.usetabs = value
917 elif key == 'indentwidth':
918 self.indentwidth = value
919 elif key == 'tabwidth':
920 self.tabwidth = value
921 elif key == 'context_use_ps1':
922 self.context_use_ps1 = value
923 else:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000924 raise KeyError, "bad option name: %r" % (key,)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000925
926 # If ispythonsource and guess are true, guess a good value for
927 # indentwidth based on file content (if possible), and if
928 # indentwidth != tabwidth set usetabs false.
929 # In any case, adjust the Text widget's view of what a tab
930 # character means.
931
932 def set_indentation_params(self, ispythonsource, guess=1):
933 if guess and ispythonsource:
934 i = self.guess_indent()
935 if 2 <= i <= 8:
936 self.indentwidth = i
937 if self.indentwidth != self.tabwidth:
938 self.usetabs = 0
939
940 self.set_tabwidth(self.tabwidth)
941
942 def smart_backspace_event(self, event):
943 text = self.text
944 first, last = self.get_selection_indices()
945 if first and last:
946 text.delete(first, last)
947 text.mark_set("insert", first)
948 return "break"
949 # Delete whitespace left, until hitting a real char or closest
950 # preceding virtual tab stop.
951 chars = text.get("insert linestart", "insert")
952 if chars == '':
953 if text.compare("insert", ">", "1.0"):
954 # easy: delete preceding newline
955 text.delete("insert-1c")
956 else:
957 text.bell() # at start of buffer
958 return "break"
959 if chars[-1] not in " \t":
960 # easy: delete preceding real char
961 text.delete("insert-1c")
962 return "break"
963 # Ick. It may require *inserting* spaces if we back up over a
964 # tab character! This is written to be clear, not fast.
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +0000965 tabwidth = self.tabwidth
966 have = len(chars.expandtabs(tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000967 assert have > 0
968 want = ((have - 1) // self.indentwidth) * self.indentwidth
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +0000969 # Debug prompt is multilined....
970 last_line_of_prompt = sys.ps1.split('\n')[-1]
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000971 ncharsdeleted = 0
972 while 1:
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +0000973 if chars == last_line_of_prompt:
Kurt B. Kaiser1bdca5e2002-12-16 22:25:10 +0000974 break
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000975 chars = chars[:-1]
976 ncharsdeleted = ncharsdeleted + 1
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +0000977 have = len(chars.expandtabs(tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000978 if have <= want or chars[-1] not in " \t":
979 break
980 text.undo_block_start()
981 text.delete("insert-%dc" % ncharsdeleted, "insert")
982 if have < want:
983 text.insert("insert", ' ' * (want - have))
984 text.undo_block_stop()
985 return "break"
986
987 def smart_indent_event(self, event):
988 # if intraline selection:
989 # delete it
990 # elif multiline selection:
991 # do indent-region & return
992 # indent one level
993 text = self.text
994 first, last = self.get_selection_indices()
995 text.undo_block_start()
996 try:
997 if first and last:
998 if index2line(first) != index2line(last):
999 return self.indent_region_event(event)
1000 text.delete(first, last)
1001 text.mark_set("insert", first)
1002 prefix = text.get("insert linestart", "insert")
1003 raw, effective = classifyws(prefix, self.tabwidth)
1004 if raw == len(prefix):
1005 # only whitespace to the left
1006 self.reindent_to(effective + self.indentwidth)
1007 else:
1008 if self.usetabs:
1009 pad = '\t'
1010 else:
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001011 effective = len(prefix.expandtabs(self.tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001012 n = self.indentwidth
1013 pad = ' ' * (n - effective % n)
1014 text.insert("insert", pad)
1015 text.see("insert")
1016 return "break"
1017 finally:
1018 text.undo_block_stop()
1019
1020 def newline_and_indent_event(self, event):
1021 text = self.text
1022 first, last = self.get_selection_indices()
1023 text.undo_block_start()
1024 try:
1025 if first and last:
1026 text.delete(first, last)
1027 text.mark_set("insert", first)
1028 line = text.get("insert linestart", "insert")
1029 i, n = 0, len(line)
1030 while i < n and line[i] in " \t":
1031 i = i+1
1032 if i == n:
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001033 # the cursor is in or at leading indentation in a continuation
1034 # line; just inject an empty line at the start
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001035 text.insert("insert linestart", '\n')
1036 return "break"
1037 indent = line[:i]
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001038 # strip whitespace before insert point unless it's in the prompt
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001039 i = 0
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001040 last_line_of_prompt = sys.ps1.split('\n')[-1]
1041 while line and line[-1] in " \t" and line != last_line_of_prompt:
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001042 line = line[:-1]
1043 i = i+1
1044 if i:
1045 text.delete("insert - %d chars" % i, "insert")
1046 # strip whitespace after insert point
1047 while text.get("insert") in " \t":
1048 text.delete("insert")
1049 # start new line
1050 text.insert("insert", '\n')
1051
1052 # adjust indentation for continuations and block
1053 # open/close first need to find the last stmt
1054 lno = index2line(text.index('insert'))
1055 y = PyParse.Parser(self.indentwidth, self.tabwidth)
1056 for context in self.num_context_lines:
1057 startat = max(lno - context, 1)
Walter Dörwald70a6b492004-02-12 17:35:32 +00001058 startatindex = repr(startat) + ".0"
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001059 rawtext = text.get(startatindex, "insert")
1060 y.set_str(rawtext)
1061 bod = y.find_good_parse_start(
1062 self.context_use_ps1,
1063 self._build_char_in_string_func(startatindex))
1064 if bod is not None or startat == 1:
1065 break
1066 y.set_lo(bod or 0)
1067 c = y.get_continuation_type()
1068 if c != PyParse.C_NONE:
1069 # The current stmt hasn't ended yet.
1070 if c == PyParse.C_STRING:
1071 # inside a string; just mimic the current indent
1072 text.insert("insert", indent)
1073 elif c == PyParse.C_BRACKET:
1074 # line up with the first (if any) element of the
1075 # last open bracket structure; else indent one
1076 # level beyond the indent of the line with the
1077 # last open bracket
1078 self.reindent_to(y.compute_bracket_indent())
1079 elif c == PyParse.C_BACKSLASH:
1080 # if more than one line in this stmt already, just
1081 # mimic the current indent; else if initial line
1082 # has a start on an assignment stmt, indent to
1083 # beyond leftmost =; else to beyond first chunk of
1084 # non-whitespace on initial line
1085 if y.get_num_lines_in_stmt() > 1:
1086 text.insert("insert", indent)
1087 else:
1088 self.reindent_to(y.compute_backslash_indent())
1089 else:
Walter Dörwald70a6b492004-02-12 17:35:32 +00001090 assert 0, "bogus continuation type %r" % (c,)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001091 return "break"
1092
1093 # This line starts a brand new stmt; indent relative to
1094 # indentation of initial line of closest preceding
1095 # interesting stmt.
1096 indent = y.get_base_indent_string()
1097 text.insert("insert", indent)
1098 if y.is_block_opener():
1099 self.smart_indent_event(event)
1100 elif indent and y.is_block_closer():
1101 self.smart_backspace_event(event)
1102 return "break"
1103 finally:
1104 text.see("insert")
1105 text.undo_block_stop()
1106
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001107 # Our editwin provides a is_char_in_string function that works
1108 # with a Tk text index, but PyParse only knows about offsets into
1109 # a string. This builds a function for PyParse that accepts an
1110 # offset.
1111
1112 def _build_char_in_string_func(self, startindex):
1113 def inner(offset, _startindex=startindex,
1114 _icis=self.is_char_in_string):
1115 return _icis(_startindex + "+%dc" % offset)
1116 return inner
1117
1118 def indent_region_event(self, event):
1119 head, tail, chars, lines = self.get_region()
1120 for pos in range(len(lines)):
1121 line = lines[pos]
1122 if line:
1123 raw, effective = classifyws(line, self.tabwidth)
1124 effective = effective + self.indentwidth
1125 lines[pos] = self._make_blanks(effective) + line[raw:]
1126 self.set_region(head, tail, chars, lines)
1127 return "break"
1128
1129 def dedent_region_event(self, event):
1130 head, tail, chars, lines = self.get_region()
1131 for pos in range(len(lines)):
1132 line = lines[pos]
1133 if line:
1134 raw, effective = classifyws(line, self.tabwidth)
1135 effective = max(effective - self.indentwidth, 0)
1136 lines[pos] = self._make_blanks(effective) + line[raw:]
1137 self.set_region(head, tail, chars, lines)
1138 return "break"
1139
1140 def comment_region_event(self, event):
1141 head, tail, chars, lines = self.get_region()
1142 for pos in range(len(lines) - 1):
1143 line = lines[pos]
1144 lines[pos] = '##' + line
1145 self.set_region(head, tail, chars, lines)
1146
1147 def uncomment_region_event(self, event):
1148 head, tail, chars, lines = self.get_region()
1149 for pos in range(len(lines)):
1150 line = lines[pos]
1151 if not line:
1152 continue
1153 if line[:2] == '##':
1154 line = line[2:]
1155 elif line[:1] == '#':
1156 line = line[1:]
1157 lines[pos] = line
1158 self.set_region(head, tail, chars, lines)
1159
1160 def tabify_region_event(self, event):
1161 head, tail, chars, lines = self.get_region()
1162 tabwidth = self._asktabwidth()
1163 for pos in range(len(lines)):
1164 line = lines[pos]
1165 if line:
1166 raw, effective = classifyws(line, tabwidth)
1167 ntabs, nspaces = divmod(effective, tabwidth)
1168 lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]
1169 self.set_region(head, tail, chars, lines)
1170
1171 def untabify_region_event(self, event):
1172 head, tail, chars, lines = self.get_region()
1173 tabwidth = self._asktabwidth()
1174 for pos in range(len(lines)):
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001175 lines[pos] = lines[pos].expandtabs(tabwidth)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001176 self.set_region(head, tail, chars, lines)
1177
1178 def toggle_tabs_event(self, event):
1179 if self.askyesno(
1180 "Toggle tabs",
1181 "Turn tabs " + ("on", "off")[self.usetabs] + "?",
1182 parent=self.text):
1183 self.usetabs = not self.usetabs
1184 return "break"
1185
1186 # XXX this isn't bound to anything -- see class tabwidth comments
1187 def change_tabwidth_event(self, event):
1188 new = self._asktabwidth()
1189 if new != self.tabwidth:
1190 self.tabwidth = new
1191 self.set_indentation_params(0, guess=0)
1192 return "break"
1193
1194 def change_indentwidth_event(self, event):
1195 new = self.askinteger(
1196 "Indent width",
1197 "New indent width (2-16)",
1198 parent=self.text,
1199 initialvalue=self.indentwidth,
1200 minvalue=2,
1201 maxvalue=16)
1202 if new and new != self.indentwidth:
1203 self.indentwidth = new
1204 return "break"
1205
1206 def get_region(self):
1207 text = self.text
1208 first, last = self.get_selection_indices()
1209 if first and last:
1210 head = text.index(first + " linestart")
1211 tail = text.index(last + "-1c lineend +1c")
1212 else:
1213 head = text.index("insert linestart")
1214 tail = text.index("insert lineend +1c")
1215 chars = text.get(head, tail)
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001216 lines = chars.split("\n")
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001217 return head, tail, chars, lines
1218
1219 def set_region(self, head, tail, chars, lines):
1220 text = self.text
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001221 newchars = "\n".join(lines)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001222 if newchars == chars:
1223 text.bell()
1224 return
1225 text.tag_remove("sel", "1.0", "end")
1226 text.mark_set("insert", head)
1227 text.undo_block_start()
1228 text.delete(head, tail)
1229 text.insert(head, newchars)
1230 text.undo_block_stop()
1231 text.tag_add("sel", head, "insert")
1232
1233 # Make string that displays as n leading blanks.
1234
1235 def _make_blanks(self, n):
1236 if self.usetabs:
1237 ntabs, nspaces = divmod(n, self.tabwidth)
1238 return '\t' * ntabs + ' ' * nspaces
1239 else:
1240 return ' ' * n
1241
1242 # Delete from beginning of line to insert point, then reinsert
1243 # column logical (meaning use tabs if appropriate) spaces.
1244
1245 def reindent_to(self, column):
1246 text = self.text
1247 text.undo_block_start()
1248 if text.compare("insert linestart", "!=", "insert"):
1249 text.delete("insert linestart", "insert")
1250 if column:
1251 text.insert("insert", self._make_blanks(column))
1252 text.undo_block_stop()
1253
1254 def _asktabwidth(self):
1255 return self.askinteger(
1256 "Tab width",
1257 "Spaces per tab? (2-16)",
1258 parent=self.text,
1259 initialvalue=self.indentwidth,
1260 minvalue=2,
1261 maxvalue=16) or self.tabwidth
1262
1263 # Guess indentwidth from text content.
1264 # Return guessed indentwidth. This should not be believed unless
1265 # it's in a reasonable range (e.g., it will be 0 if no indented
1266 # blocks are found).
1267
1268 def guess_indent(self):
1269 opener, indented = IndentSearcher(self.text, self.tabwidth).run()
1270 if opener and indented:
1271 raw, indentsmall = classifyws(opener, self.tabwidth)
1272 raw, indentlarge = classifyws(indented, self.tabwidth)
1273 else:
1274 indentsmall = indentlarge = 0
1275 return indentlarge - indentsmall
1276
1277# "line.col" -> line, as an int
1278def index2line(index):
1279 return int(float(index))
1280
1281# Look at the leading whitespace in s.
1282# Return pair (# of leading ws characters,
1283# effective # of leading blanks after expanding
1284# tabs to width tabwidth)
1285
1286def classifyws(s, tabwidth):
1287 raw = effective = 0
1288 for ch in s:
1289 if ch == ' ':
1290 raw = raw + 1
1291 effective = effective + 1
1292 elif ch == '\t':
1293 raw = raw + 1
1294 effective = (effective // tabwidth + 1) * tabwidth
1295 else:
1296 break
1297 return raw, effective
1298
1299import tokenize
1300_tokenize = tokenize
1301del tokenize
1302
Kurt B. Kaiserdcba6622004-12-21 22:10:32 +00001303class IndentSearcher(object):
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001304
1305 # .run() chews over the Text widget, looking for a block opener
1306 # and the stmt following it. Returns a pair,
1307 # (line containing block opener, line containing stmt)
1308 # Either or both may be None.
1309
1310 def __init__(self, text, tabwidth):
1311 self.text = text
1312 self.tabwidth = tabwidth
1313 self.i = self.finished = 0
1314 self.blkopenline = self.indentedline = None
1315
1316 def readline(self):
1317 if self.finished:
1318 return ""
1319 i = self.i = self.i + 1
Walter Dörwald70a6b492004-02-12 17:35:32 +00001320 mark = repr(i) + ".0"
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001321 if self.text.compare(mark, ">=", "end"):
1322 return ""
1323 return self.text.get(mark, mark + " lineend+1c")
1324
1325 def tokeneater(self, type, token, start, end, line,
1326 INDENT=_tokenize.INDENT,
1327 NAME=_tokenize.NAME,
1328 OPENERS=('class', 'def', 'for', 'if', 'try', 'while')):
1329 if self.finished:
1330 pass
1331 elif type == NAME and token in OPENERS:
1332 self.blkopenline = line
1333 elif type == INDENT and self.blkopenline:
1334 self.indentedline = line
1335 self.finished = 1
1336
1337 def run(self):
1338 save_tabsize = _tokenize.tabsize
1339 _tokenize.tabsize = self.tabwidth
1340 try:
1341 try:
1342 _tokenize.tokenize(self.readline, self.tokeneater)
1343 except _tokenize.TokenError:
1344 # since we cut off the tokenizer early, we can trigger
1345 # spurious errors
1346 pass
1347 finally:
1348 _tokenize.tabsize = save_tabsize
1349 return self.blkopenline, self.indentedline
1350
1351### end autoindent code ###
1352
David Scherer7aced172000-08-15 01:13:23 +00001353def prepstr(s):
1354 # Helper to extract the underscore from a string, e.g.
1355 # prepstr("Co_py") returns (2, "Copy").
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +00001356 i = s.find('_')
David Scherer7aced172000-08-15 01:13:23 +00001357 if i >= 0:
1358 s = s[:i] + s[i+1:]
1359 return i, s
1360
1361
1362keynames = {
1363 'bracketleft': '[',
1364 'bracketright': ']',
1365 'slash': '/',
1366}
1367
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +00001368def get_accelerator(keydefs, eventname):
1369 keylist = keydefs.get(eventname)
David Scherer7aced172000-08-15 01:13:23 +00001370 if not keylist:
1371 return ""
1372 s = keylist[0]
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +00001373 s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s)
David Scherer7aced172000-08-15 01:13:23 +00001374 s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s)
1375 s = re.sub("Key-", "", s)
1376 s = re.sub("Cancel","Ctrl-Break",s) # dscherer@cmu.edu
1377 s = re.sub("Control-", "Ctrl-", s)
1378 s = re.sub("-", "+", s)
1379 s = re.sub("><", " ", s)
1380 s = re.sub("<", "", s)
1381 s = re.sub(">", "", s)
1382 return s
1383
1384
1385def fixwordbreaks(root):
1386 # Make sure that Tk's double-click and next/previous word
1387 # operations use our definition of a word (i.e. an identifier)
1388 tk = root.tk
1389 tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
1390 tk.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]')
1391 tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]')
1392
1393
1394def test():
1395 root = Tk()
1396 fixwordbreaks(root)
1397 root.withdraw()
1398 if sys.argv[1:]:
1399 filename = sys.argv[1]
1400 else:
1401 filename = None
1402 edit = EditorWindow(root=root, filename=filename)
1403 edit.set_close_hook(root.quit)
1404 root.mainloop()
1405 root.destroy()
1406
1407if __name__ == '__main__':
1408 test()