You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1655 lines
62 KiB

26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
26 years ago
  1. import sys
  2. import os
  3. import re
  4. import imp
  5. from Tkinter import *
  6. import tkSimpleDialog
  7. import tkMessageBox
  8. import webbrowser
  9. from idlelib.MultiCall import MultiCallCreator
  10. from idlelib import idlever
  11. from idlelib import WindowList
  12. from idlelib import SearchDialog
  13. from idlelib import GrepDialog
  14. from idlelib import ReplaceDialog
  15. from idlelib import PyParse
  16. from idlelib.configHandler import idleConf
  17. from idlelib import aboutDialog, textView, configDialog
  18. from idlelib import macosxSupport
  19. # The default tab setting for a Text widget, in average-width characters.
  20. TK_TABWIDTH_DEFAULT = 8
  21. def _sphinx_version():
  22. "Format sys.version_info to produce the Sphinx version string used to install the chm docs"
  23. major, minor, micro, level, serial = sys.version_info
  24. release = '%s%s' % (major, minor)
  25. if micro:
  26. release += '%s' % (micro,)
  27. if level == 'candidate':
  28. release += 'rc%s' % (serial,)
  29. elif level != 'final':
  30. release += '%s%s' % (level[0], serial)
  31. return release
  32. def _find_module(fullname, path=None):
  33. """Version of imp.find_module() that handles hierarchical module names"""
  34. file = None
  35. for tgt in fullname.split('.'):
  36. if file is not None:
  37. file.close() # close intermediate files
  38. (file, filename, descr) = imp.find_module(tgt, path)
  39. if descr[2] == imp.PY_SOURCE:
  40. break # find but not load the source file
  41. module = imp.load_module(tgt, file, filename, descr)
  42. try:
  43. path = module.__path__
  44. except AttributeError:
  45. raise ImportError, 'No source for module ' + module.__name__
  46. if descr[2] != imp.PY_SOURCE:
  47. # If all of the above fails and didn't raise an exception,fallback
  48. # to a straight import which can find __init__.py in a package.
  49. m = __import__(fullname)
  50. try:
  51. filename = m.__file__
  52. except AttributeError:
  53. pass
  54. else:
  55. file = None
  56. base, ext = os.path.splitext(filename)
  57. if ext == '.pyc':
  58. ext = '.py'
  59. filename = base + ext
  60. descr = filename, None, imp.PY_SOURCE
  61. return file, filename, descr
  62. class HelpDialog(object):
  63. def __init__(self):
  64. self.parent = None # parent of help window
  65. self.dlg = None # the help window iteself
  66. def display(self, parent, near=None):
  67. """ Display the help dialog.
  68. parent - parent widget for the help window
  69. near - a Toplevel widget (e.g. EditorWindow or PyShell)
  70. to use as a reference for placing the help window
  71. """
  72. if self.dlg is None:
  73. self.show_dialog(parent)
  74. if near:
  75. self.nearwindow(near)
  76. def show_dialog(self, parent):
  77. self.parent = parent
  78. fn=os.path.join(os.path.abspath(os.path.dirname(__file__)),'help.txt')
  79. self.dlg = dlg = textView.view_file(parent,'Help',fn, modal=False)
  80. dlg.bind('<Destroy>', self.destroy, '+')
  81. def nearwindow(self, near):
  82. # Place the help dialog near the window specified by parent.
  83. # Note - this may not reposition the window in Metacity
  84. # if "/apps/metacity/general/disable_workarounds" is enabled
  85. dlg = self.dlg
  86. geom = (near.winfo_rootx() + 10, near.winfo_rooty() + 10)
  87. dlg.withdraw()
  88. dlg.geometry("=+%d+%d" % geom)
  89. dlg.deiconify()
  90. dlg.lift()
  91. def destroy(self, ev=None):
  92. self.dlg = None
  93. self.parent = None
  94. helpDialog = HelpDialog() # singleton instance
  95. class EditorWindow(object):
  96. from idlelib.Percolator import Percolator
  97. from idlelib.ColorDelegator import ColorDelegator
  98. from idlelib.UndoDelegator import UndoDelegator
  99. from idlelib.IOBinding import IOBinding, filesystemencoding, encoding
  100. from idlelib import Bindings
  101. from Tkinter import Toplevel
  102. from idlelib.MultiStatusBar import MultiStatusBar
  103. help_url = None
  104. def __init__(self, flist=None, filename=None, key=None, root=None):
  105. if EditorWindow.help_url is None:
  106. dochome = os.path.join(sys.prefix, 'Doc', 'index.html')
  107. if sys.platform.count('linux'):
  108. # look for html docs in a couple of standard places
  109. pyver = 'python-docs-' + '%s.%s.%s' % sys.version_info[:3]
  110. if os.path.isdir('/var/www/html/python/'): # "python2" rpm
  111. dochome = '/var/www/html/python/index.html'
  112. else:
  113. basepath = '/usr/share/doc/' # standard location
  114. dochome = os.path.join(basepath, pyver,
  115. 'Doc', 'index.html')
  116. elif sys.platform[:3] == 'win':
  117. chmfile = os.path.join(sys.prefix, 'Doc',
  118. 'Python%s.chm' % _sphinx_version())
  119. if os.path.isfile(chmfile):
  120. dochome = chmfile
  121. elif macosxSupport.runningAsOSXApp():
  122. # documentation is stored inside the python framework
  123. dochome = os.path.join(sys.prefix,
  124. 'Resources/English.lproj/Documentation/index.html')
  125. dochome = os.path.normpath(dochome)
  126. if os.path.isfile(dochome):
  127. EditorWindow.help_url = dochome
  128. if sys.platform == 'darwin':
  129. # Safari requires real file:-URLs
  130. EditorWindow.help_url = 'file://' + EditorWindow.help_url
  131. else:
  132. EditorWindow.help_url = "http://docs.python.org/%d.%d" % sys.version_info[:2]
  133. currentTheme=idleConf.CurrentTheme()
  134. self.flist = flist
  135. root = root or flist.root
  136. self.root = root
  137. try:
  138. sys.ps1
  139. except AttributeError:
  140. sys.ps1 = '>>> '
  141. self.menubar = Menu(root)
  142. self.top = top = WindowList.ListedToplevel(root, menu=self.menubar)
  143. if flist:
  144. self.tkinter_vars = flist.vars
  145. #self.top.instance_dict makes flist.inversedict available to
  146. #configDialog.py so it can access all EditorWindow instances
  147. self.top.instance_dict = flist.inversedict
  148. else:
  149. self.tkinter_vars = {} # keys: Tkinter event names
  150. # values: Tkinter variable instances
  151. self.top.instance_dict = {}
  152. self.recent_files_path = os.path.join(idleConf.GetUserCfgDir(),
  153. 'recent-files.lst')
  154. self.text_frame = text_frame = Frame(top)
  155. self.vbar = vbar = Scrollbar(text_frame, name='vbar')
  156. self.width = idleConf.GetOption('main','EditorWindow','width')
  157. text_options = {
  158. 'name': 'text',
  159. 'padx': 5,
  160. 'wrap': 'none',
  161. 'width': self.width,
  162. 'height': idleConf.GetOption('main', 'EditorWindow', 'height')}
  163. if TkVersion >= 8.5:
  164. # Starting with tk 8.5 we have to set the new tabstyle option
  165. # to 'wordprocessor' to achieve the same display of tabs as in
  166. # older tk versions.
  167. text_options['tabstyle'] = 'wordprocessor'
  168. self.text = text = MultiCallCreator(Text)(text_frame, **text_options)
  169. self.top.focused_widget = self.text
  170. self.createmenubar()
  171. self.apply_bindings()
  172. self.top.protocol("WM_DELETE_WINDOW", self.close)
  173. self.top.bind("<<close-window>>", self.close_event)
  174. if macosxSupport.runningAsOSXApp():
  175. # Command-W on editorwindows doesn't work without this.
  176. text.bind('<<close-window>>', self.close_event)
  177. # Some OS X systems have only one mouse button,
  178. # so use control-click for pulldown menus there.
  179. # (Note, AquaTk defines <2> as the right button if
  180. # present and the Tk Text widget already binds <2>.)
  181. text.bind("<Control-Button-1>",self.right_menu_event)
  182. else:
  183. # Elsewhere, use right-click for pulldown menus.
  184. text.bind("<3>",self.right_menu_event)
  185. text.bind("<<cut>>", self.cut)
  186. text.bind("<<copy>>", self.copy)
  187. text.bind("<<paste>>", self.paste)
  188. text.bind("<<center-insert>>", self.center_insert_event)
  189. text.bind("<<help>>", self.help_dialog)
  190. text.bind("<<python-docs>>", self.python_docs)
  191. text.bind("<<about-idle>>", self.about_dialog)
  192. text.bind("<<open-config-dialog>>", self.config_dialog)
  193. text.bind("<<open-module>>", self.open_module)
  194. text.bind("<<do-nothing>>", lambda event: "break")
  195. text.bind("<<select-all>>", self.select_all)
  196. text.bind("<<remove-selection>>", self.remove_selection)
  197. text.bind("<<find>>", self.find_event)
  198. text.bind("<<find-again>>", self.find_again_event)
  199. text.bind("<<find-in-files>>", self.find_in_files_event)
  200. text.bind("<<find-selection>>", self.find_selection_event)
  201. text.bind("<<replace>>", self.replace_event)
  202. text.bind("<<goto-line>>", self.goto_line_event)
  203. text.bind("<<smart-backspace>>",self.smart_backspace_event)
  204. text.bind("<<newline-and-indent>>",self.newline_and_indent_event)
  205. text.bind("<<smart-indent>>",self.smart_indent_event)
  206. text.bind("<<indent-region>>",self.indent_region_event)
  207. text.bind("<<dedent-region>>",self.dedent_region_event)
  208. text.bind("<<comment-region>>",self.comment_region_event)
  209. text.bind("<<uncomment-region>>",self.uncomment_region_event)
  210. text.bind("<<tabify-region>>",self.tabify_region_event)
  211. text.bind("<<untabify-region>>",self.untabify_region_event)
  212. text.bind("<<toggle-tabs>>",self.toggle_tabs_event)
  213. text.bind("<<change-indentwidth>>",self.change_indentwidth_event)
  214. text.bind("<Left>", self.move_at_edge_if_selection(0))
  215. text.bind("<Right>", self.move_at_edge_if_selection(1))
  216. text.bind("<<del-word-left>>", self.del_word_left)
  217. text.bind("<<del-word-right>>", self.del_word_right)
  218. text.bind("<<beginning-of-line>>", self.home_callback)
  219. if flist:
  220. flist.inversedict[self] = key
  221. if key:
  222. flist.dict[key] = self
  223. text.bind("<<open-new-window>>", self.new_callback)
  224. text.bind("<<close-all-windows>>", self.flist.close_all_callback)
  225. text.bind("<<open-class-browser>>", self.open_class_browser)
  226. text.bind("<<open-path-browser>>", self.open_path_browser)
  227. self.set_status_bar()
  228. vbar['command'] = text.yview
  229. vbar.pack(side=RIGHT, fill=Y)
  230. text['yscrollcommand'] = vbar.set
  231. fontWeight = 'normal'
  232. if idleConf.GetOption('main', 'EditorWindow', 'font-bold', type='bool'):
  233. fontWeight='bold'
  234. text.config(font=(idleConf.GetOption('main', 'EditorWindow', 'font'),
  235. idleConf.GetOption('main', 'EditorWindow', 'font-size'),
  236. fontWeight))
  237. text_frame.pack(side=LEFT, fill=BOTH, expand=1)
  238. text.pack(side=TOP, fill=BOTH, expand=1)
  239. text.focus_set()
  240. # usetabs true -> literal tab characters are used by indent and
  241. # dedent cmds, possibly mixed with spaces if
  242. # indentwidth is not a multiple of tabwidth,
  243. # which will cause Tabnanny to nag!
  244. # false -> tab characters are converted to spaces by indent
  245. # and dedent cmds, and ditto TAB keystrokes
  246. # Although use-spaces=0 can be configured manually in config-main.def,
  247. # configuration of tabs v. spaces is not supported in the configuration
  248. # dialog. IDLE promotes the preferred Python indentation: use spaces!
  249. usespaces = idleConf.GetOption('main', 'Indent', 'use-spaces', type='bool')
  250. self.usetabs = not usespaces
  251. # tabwidth is the display width of a literal tab character.
  252. # CAUTION: telling Tk to use anything other than its default
  253. # tab setting causes it to use an entirely different tabbing algorithm,
  254. # treating tab stops as fixed distances from the left margin.
  255. # Nobody expects this, so for now tabwidth should never be changed.
  256. self.tabwidth = 8 # must remain 8 until Tk is fixed.
  257. # indentwidth is the number of screen characters per indent level.
  258. # The recommended Python indentation is four spaces.
  259. self.indentwidth = self.tabwidth
  260. self.set_notabs_indentwidth()
  261. # If context_use_ps1 is true, parsing searches back for a ps1 line;
  262. # else searches for a popular (if, def, ...) Python stmt.
  263. self.context_use_ps1 = False
  264. # When searching backwards for a reliable place to begin parsing,
  265. # first start num_context_lines[0] lines back, then
  266. # num_context_lines[1] lines back if that didn't work, and so on.
  267. # The last value should be huge (larger than the # of lines in a
  268. # conceivable file).
  269. # Making the initial values larger slows things down more often.
  270. self.num_context_lines = 50, 500, 5000000
  271. self.per = per = self.Percolator(text)
  272. self.undo = undo = self.UndoDelegator()
  273. per.insertfilter(undo)
  274. text.undo_block_start = undo.undo_block_start
  275. text.undo_block_stop = undo.undo_block_stop
  276. undo.set_saved_change_hook(self.saved_change_hook)
  277. # IOBinding implements file I/O and printing functionality
  278. self.io = io = self.IOBinding(self)
  279. io.set_filename_change_hook(self.filename_change_hook)
  280. # Create the recent files submenu
  281. self.recent_files_menu = Menu(self.menubar)
  282. self.menudict['file'].insert_cascade(3, label='Recent Files',
  283. underline=0,
  284. menu=self.recent_files_menu)
  285. self.update_recent_files_list()
  286. self.color = None # initialized below in self.ResetColorizer
  287. if filename:
  288. if os.path.exists(filename) and not os.path.isdir(filename):
  289. io.loadfile(filename)
  290. else:
  291. io.set_filename(filename)
  292. self.ResetColorizer()
  293. self.saved_change_hook()
  294. self.set_indentation_params(self.ispythonsource(filename))
  295. self.load_extensions()
  296. menu = self.menudict.get('windows')
  297. if menu:
  298. end = menu.index("end")
  299. if end is None:
  300. end = -1
  301. if end >= 0:
  302. menu.add_separator()
  303. end = end + 1
  304. self.wmenu_end = end
  305. WindowList.register_callback(self.postwindowsmenu)
  306. # Some abstractions so IDLE extensions are cross-IDE
  307. self.askyesno = tkMessageBox.askyesno
  308. self.askinteger = tkSimpleDialog.askinteger
  309. self.showerror = tkMessageBox.showerror
  310. def _filename_to_unicode(self, filename):
  311. """convert filename to unicode in order to display it in Tk"""
  312. if isinstance(filename, unicode) or not filename:
  313. return filename
  314. else:
  315. try:
  316. return filename.decode(self.filesystemencoding)
  317. except UnicodeDecodeError:
  318. # XXX
  319. try:
  320. return filename.decode(self.encoding)
  321. except UnicodeDecodeError:
  322. # byte-to-byte conversion
  323. return filename.decode('iso8859-1')
  324. def new_callback(self, event):
  325. dirname, basename = self.io.defaultfilename()
  326. self.flist.new(dirname)
  327. return "break"
  328. def home_callback(self, event):
  329. if (event.state & 4) != 0 and event.keysym == "Home":
  330. # state&4==Control. If <Control-Home>, use the Tk binding.
  331. return
  332. if self.text.index("iomark") and \
  333. self.text.compare("iomark", "<=", "insert lineend") and \
  334. self.text.compare("insert linestart", "<=", "iomark"):
  335. # In Shell on input line, go to just after prompt
  336. insertpt = int(self.text.index("iomark").split(".")[1])
  337. else:
  338. line = self.text.get("insert linestart", "insert lineend")
  339. for insertpt in xrange(len(line)):
  340. if line[insertpt] not in (' ','\t'):
  341. break
  342. else:
  343. insertpt=len(line)
  344. lineat = int(self.text.index("insert").split('.')[1])
  345. if insertpt == lineat:
  346. insertpt = 0
  347. dest = "insert linestart+"+str(insertpt)+"c"
  348. if (event.state&1) == 0:
  349. # shift was not pressed
  350. self.text.tag_remove("sel", "1.0", "end")
  351. else:
  352. if not self.text.index("sel.first"):
  353. self.text.mark_set("my_anchor", "insert") # there was no previous selection
  354. else:
  355. if self.text.compare(self.text.index("sel.first"), "<", self.text.index("insert")):
  356. self.text.mark_set("my_anchor", "sel.first") # extend back
  357. else:
  358. self.text.mark_set("my_anchor", "sel.last") # extend forward
  359. first = self.text.index(dest)
  360. last = self.text.index("my_anchor")
  361. if self.text.compare(first,">",last):
  362. first,last = last,first
  363. self.text.tag_remove("sel", "1.0", "end")
  364. self.text.tag_add("sel", first, last)
  365. self.text.mark_set("insert", dest)
  366. self.text.see("insert")
  367. return "break"
  368. def set_status_bar(self):
  369. self.status_bar = self.MultiStatusBar(self.top)
  370. if macosxSupport.runningAsOSXApp():
  371. # Insert some padding to avoid obscuring some of the statusbar
  372. # by the resize widget.
  373. self.status_bar.set_label('_padding1', ' ', side=RIGHT)
  374. self.status_bar.set_label('column', 'Col: ?', side=RIGHT)
  375. self.status_bar.set_label('line', 'Ln: ?', side=RIGHT)
  376. self.status_bar.pack(side=BOTTOM, fill=X)
  377. self.text.bind("<<set-line-and-column>>", self.set_line_and_column)
  378. self.text.event_add("<<set-line-and-column>>",
  379. "<KeyRelease>", "<ButtonRelease>")
  380. self.text.after_idle(self.set_line_and_column)
  381. def set_line_and_column(self, event=None):
  382. line, column = self.text.index(INSERT).split('.')
  383. self.status_bar.set_label('column', 'Col: %s' % column)
  384. self.status_bar.set_label('line', 'Ln: %s' % line)
  385. menu_specs = [
  386. ("file", "_File"),
  387. ("edit", "_Edit"),
  388. ("format", "F_ormat"),
  389. ("run", "_Run"),
  390. ("options", "_Options"),
  391. ("windows", "_Windows"),
  392. ("help", "_Help"),
  393. ]
  394. if macosxSupport.runningAsOSXApp():
  395. del menu_specs[-3]
  396. menu_specs[-2] = ("windows", "_Window")
  397. def createmenubar(self):
  398. mbar = self.menubar
  399. self.menudict = menudict = {}
  400. for name, label in self.menu_specs:
  401. underline, label = prepstr(label)
  402. menudict[name] = menu = Menu(mbar, name=name)
  403. mbar.add_cascade(label=label, menu=menu, underline=underline)
  404. if macosxSupport.isCarbonAquaTk(self.root):
  405. # Insert the application menu
  406. menudict['application'] = menu = Menu(mbar, name='apple')
  407. mbar.add_cascade(label='IDLE', menu=menu)
  408. self.fill_menus()
  409. self.base_helpmenu_length = self.menudict['help'].index(END)
  410. self.reset_help_menu_entries()
  411. def postwindowsmenu(self):
  412. # Only called when Windows menu exists
  413. menu = self.menudict['windows']
  414. end = menu.index("end")
  415. if end is None:
  416. end = -1
  417. if end > self.wmenu_end:
  418. menu.delete(self.wmenu_end+1, end)
  419. WindowList.add_windows_to_menu(menu)
  420. rmenu = None
  421. def right_menu_event(self, event):
  422. self.text.tag_remove("sel", "1.0", "end")
  423. self.text.mark_set("insert", "@%d,%d" % (event.x, event.y))
  424. if not self.rmenu:
  425. self.make_rmenu()
  426. rmenu = self.rmenu
  427. self.event = event
  428. iswin = sys.platform[:3] == 'win'
  429. if iswin:
  430. self.text.config(cursor="arrow")
  431. rmenu.tk_popup(event.x_root, event.y_root)
  432. if iswin:
  433. self.text.config(cursor="ibeam")
  434. rmenu_specs = [
  435. # ("Label", "<<virtual-event>>"), ...
  436. ("Close", "<<close-window>>"), # Example
  437. ]
  438. def make_rmenu(self):
  439. rmenu = Menu(self.text, tearoff=0)
  440. for label, eventname in self.rmenu_specs:
  441. def command(text=self.text, eventname=eventname):
  442. text.event_generate(eventname)
  443. rmenu.add_command(label=label, command=command)
  444. self.rmenu = rmenu
  445. def about_dialog(self, event=None):
  446. aboutDialog.AboutDialog(self.top,'About IDLE')
  447. def config_dialog(self, event=None):
  448. configDialog.ConfigDialog(self.top,'Settings')
  449. def help_dialog(self, event=None):
  450. if self.root:
  451. parent = self.root
  452. else:
  453. parent = self.top
  454. helpDialog.display(parent, near=self.top)
  455. def python_docs(self, event=None):
  456. if sys.platform[:3] == 'win':
  457. try:
  458. os.startfile(self.help_url)
  459. except WindowsError as why:
  460. tkMessageBox.showerror(title='Document Start Failure',
  461. message=str(why), parent=self.text)
  462. else:
  463. webbrowser.open(self.help_url)
  464. return "break"
  465. def cut(self,event):
  466. self.text.event_generate("<<Cut>>")
  467. return "break"
  468. def copy(self,event):
  469. if not self.text.tag_ranges("sel"):
  470. # There is no selection, so do nothing and maybe interrupt.
  471. return
  472. self.text.event_generate("<<Copy>>")
  473. return "break"
  474. def paste(self,event):
  475. self.text.event_generate("<<Paste>>")
  476. self.text.see("insert")
  477. return "break"
  478. def select_all(self, event=None):
  479. self.text.tag_add("sel", "1.0", "end-1c")
  480. self.text.mark_set("insert", "1.0")
  481. self.text.see("insert")
  482. return "break"
  483. def remove_selection(self, event=None):
  484. self.text.tag_remove("sel", "1.0", "end")
  485. self.text.see("insert")
  486. def move_at_edge_if_selection(self, edge_index):
  487. """Cursor move begins at start or end of selection
  488. When a left/right cursor key is pressed create and return to Tkinter a
  489. function which causes a cursor move from the associated edge of the
  490. selection.
  491. """
  492. self_text_index = self.text.index
  493. self_text_mark_set = self.text.mark_set
  494. edges_table = ("sel.first+1c", "sel.last-1c")
  495. def move_at_edge(event):
  496. if (event.state & 5) == 0: # no shift(==1) or control(==4) pressed
  497. try:
  498. self_text_index("sel.first")
  499. self_text_mark_set("insert", edges_table[edge_index])
  500. except TclError:
  501. pass
  502. return move_at_edge
  503. def del_word_left(self, event):
  504. self.text.event_generate('<Meta-Delete>')
  505. return "break"
  506. def del_word_right(self, event):
  507. self.text.event_generate('<Meta-d>')
  508. return "break"
  509. def find_event(self, event):
  510. SearchDialog.find(self.text)
  511. return "break"
  512. def find_again_event(self, event):
  513. SearchDialog.find_again(self.text)
  514. return "break"
  515. def find_selection_event(self, event):
  516. SearchDialog.find_selection(self.text)
  517. return "break"
  518. def find_in_files_event(self, event):
  519. GrepDialog.grep(self.text, self.io, self.flist)
  520. return "break"
  521. def replace_event(self, event):
  522. ReplaceDialog.replace(self.text)
  523. return "break"
  524. def goto_line_event(self, event):
  525. text = self.text
  526. lineno = tkSimpleDialog.askinteger("Goto",
  527. "Go to line number:",parent=text)
  528. if lineno is None:
  529. return "break"
  530. if lineno <= 0:
  531. text.bell()
  532. return "break"
  533. text.mark_set("insert", "%d.0" % lineno)
  534. text.see("insert")
  535. def open_module(self, event=None):
  536. # XXX Shouldn't this be in IOBinding or in FileList?
  537. try:
  538. name = self.text.get("sel.first", "sel.last")
  539. except TclError:
  540. name = ""
  541. else:
  542. name = name.strip()
  543. name = tkSimpleDialog.askstring("Module",
  544. "Enter the name of a Python module\n"
  545. "to search on sys.path and open:",
  546. parent=self.text, initialvalue=name)
  547. if name:
  548. name = name.strip()
  549. if not name:
  550. return
  551. # XXX Ought to insert current file's directory in front of path
  552. try:
  553. (f, file, (suffix, mode, type)) = _find_module(name)
  554. except (NameError, ImportError), msg:
  555. tkMessageBox.showerror("Import error", str(msg), parent=self.text)
  556. return
  557. if type != imp.PY_SOURCE:
  558. tkMessageBox.showerror("Unsupported type",
  559. "%s is not a source module" % name, parent=self.text)
  560. return
  561. if f:
  562. f.close()
  563. if self.flist:
  564. self.flist.open(file)
  565. else:
  566. self.io.loadfile(file)
  567. def open_class_browser(self, event=None):
  568. filename = self.io.filename
  569. if not filename:
  570. tkMessageBox.showerror(
  571. "No filename",
  572. "This buffer has no associated filename",
  573. master=self.text)
  574. self.text.focus_set()
  575. return None
  576. head, tail = os.path.split(filename)
  577. base, ext = os.path.splitext(tail)
  578. from idlelib import ClassBrowser
  579. ClassBrowser.ClassBrowser(self.flist, base, [head])
  580. def open_path_browser(self, event=None):
  581. from idlelib import PathBrowser
  582. PathBrowser.PathBrowser(self.flist)
  583. def gotoline(self, lineno):
  584. if lineno is not None and lineno > 0:
  585. self.text.mark_set("insert", "%d.0" % lineno)
  586. self.text.tag_remove("sel", "1.0", "end")
  587. self.text.tag_add("sel", "insert", "insert +1l")
  588. self.center()
  589. def ispythonsource(self, filename):
  590. if not filename or os.path.isdir(filename):
  591. return True
  592. base, ext = os.path.splitext(os.path.basename(filename))
  593. if os.path.normcase(ext) in (".py", ".pyw"):
  594. return True
  595. try:
  596. f = open(filename)
  597. line = f.readline()
  598. f.close()
  599. except IOError:
  600. return False
  601. return line.startswith('#!') and line.find('python') >= 0
  602. def close_hook(self):
  603. if self.flist:
  604. self.flist.unregister_maybe_terminate(self)
  605. self.flist = None
  606. def set_close_hook(self, close_hook):
  607. self.close_hook = close_hook
  608. def filename_change_hook(self):
  609. if self.flist:
  610. self.flist.filename_changed_edit(self)
  611. self.saved_change_hook()
  612. self.top.update_windowlist_registry(self)
  613. self.ResetColorizer()
  614. def _addcolorizer(self):
  615. if self.color:
  616. return
  617. if self.ispythonsource(self.io.filename):
  618. self.color = self.ColorDelegator()
  619. # can add more colorizers here...
  620. if self.color:
  621. self.per.removefilter(self.undo)
  622. self.per.insertfilter(self.color)
  623. self.per.insertfilter(self.undo)
  624. def _rmcolorizer(self):
  625. if not self.color:
  626. return
  627. self.color.removecolors()
  628. self.per.removefilter(self.color)
  629. self.color = None
  630. def ResetColorizer(self):
  631. "Update the colour theme"
  632. # Called from self.filename_change_hook and from configDialog.py
  633. self._rmcolorizer()
  634. self._addcolorizer()
  635. theme = idleConf.GetOption('main','Theme','name')
  636. normal_colors = idleConf.GetHighlight(theme, 'normal')
  637. cursor_color = idleConf.GetHighlight(theme, 'cursor', fgBg='fg')
  638. select_colors = idleConf.GetHighlight(theme, 'hilite')
  639. self.text.config(
  640. foreground=normal_colors['foreground'],
  641. background=normal_colors['background'],
  642. insertbackground=cursor_color,
  643. selectforeground=select_colors['foreground'],
  644. selectbackground=select_colors['background'],
  645. )
  646. def ResetFont(self):
  647. "Update the text widgets' font if it is changed"
  648. # Called from configDialog.py
  649. fontWeight='normal'
  650. if idleConf.GetOption('main','EditorWindow','font-bold',type='bool'):
  651. fontWeight='bold'
  652. self.text.config(font=(idleConf.GetOption('main','EditorWindow','font'),
  653. idleConf.GetOption('main','EditorWindow','font-size'),
  654. fontWeight))
  655. def RemoveKeybindings(self):
  656. "Remove the keybindings before they are changed."
  657. # Called from configDialog.py
  658. self.Bindings.default_keydefs = keydefs = idleConf.GetCurrentKeySet()
  659. for event, keylist in keydefs.items():
  660. self.text.event_delete(event, *keylist)
  661. for extensionName in self.get_standard_extension_names():
  662. xkeydefs = idleConf.GetExtensionBindings(extensionName)
  663. if xkeydefs:
  664. for event, keylist in xkeydefs.items():
  665. self.text.event_delete(event, *keylist)
  666. def ApplyKeybindings(self):
  667. "Update the keybindings after they are changed"
  668. # Called from configDialog.py
  669. self.Bindings.default_keydefs = keydefs = idleConf.GetCurrentKeySet()
  670. self.apply_bindings()
  671. for extensionName in self.get_standard_extension_names():
  672. xkeydefs = idleConf.GetExtensionBindings(extensionName)
  673. if xkeydefs:
  674. self.apply_bindings(xkeydefs)
  675. #update menu accelerators
  676. menuEventDict = {}
  677. for menu in self.Bindings.menudefs:
  678. menuEventDict[menu[0]] = {}
  679. for item in menu[1]:
  680. if item:
  681. menuEventDict[menu[0]][prepstr(item[0])[1]] = item[1]
  682. for menubarItem in self.menudict.keys():
  683. menu = self.menudict[menubarItem]
  684. end = menu.index(END) + 1
  685. for index in range(0, end):
  686. if menu.type(index) == 'command':
  687. accel = menu.entrycget(index, 'accelerator')
  688. if accel:
  689. itemName = menu.entrycget(index, 'label')
  690. event = ''
  691. if menubarItem in menuEventDict:
  692. if itemName in menuEventDict[menubarItem]:
  693. event = menuEventDict[menubarItem][itemName]
  694. if event:
  695. accel = get_accelerator(keydefs, event)
  696. menu.entryconfig(index, accelerator=accel)
  697. def set_notabs_indentwidth(self):
  698. "Update the indentwidth if changed and not using tabs in this window"
  699. # Called from configDialog.py
  700. if not self.usetabs:
  701. self.indentwidth = idleConf.GetOption('main', 'Indent','num-spaces',
  702. type='int')
  703. def reset_help_menu_entries(self):
  704. "Update the additional help entries on the Help menu"
  705. help_list = idleConf.GetAllExtraHelpSourcesList()
  706. helpmenu = self.menudict['help']
  707. # first delete the extra help entries, if any
  708. helpmenu_length = helpmenu.index(END)
  709. if helpmenu_length > self.base_helpmenu_length:
  710. helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length)
  711. # then rebuild them
  712. if help_list:
  713. helpmenu.add_separator()
  714. for entry in help_list:
  715. cmd = self.__extra_help_callback(entry[1])
  716. helpmenu.add_command(label=entry[0], command=cmd)
  717. # and update the menu dictionary
  718. self.menudict['help'] = helpmenu
  719. def __extra_help_callback(self, helpfile):
  720. "Create a callback with the helpfile value frozen at definition time"
  721. def display_extra_help(helpfile=helpfile):
  722. if not helpfile.startswith(('www', 'http')):
  723. helpfile = os.path.normpath(helpfile)
  724. if sys.platform[:3] == 'win':
  725. try:
  726. os.startfile(helpfile)
  727. except WindowsError as why:
  728. tkMessageBox.showerror(title='Document Start Failure',
  729. message=str(why), parent=self.text)
  730. else:
  731. webbrowser.open(helpfile)
  732. return display_extra_help
  733. def update_recent_files_list(self, new_file=None):
  734. "Load and update the recent files list and menus"
  735. rf_list = []
  736. if os.path.exists(self.recent_files_path):
  737. rf_list_file = open(self.recent_files_path,'r')
  738. try:
  739. rf_list = rf_list_file.readlines()
  740. finally:
  741. rf_list_file.close()
  742. if new_file:
  743. new_file = os.path.abspath(new_file) + '\n'
  744. if new_file in rf_list:
  745. rf_list.remove(new_file) # move to top
  746. rf_list.insert(0, new_file)
  747. # clean and save the recent files list
  748. bad_paths = []
  749. for path in rf_list:
  750. if '\0' in path or not os.path.exists(path[0:-1]):
  751. bad_paths.append(path)
  752. rf_list = [path for path in rf_list if path not in bad_paths]
  753. ulchars = "1234567890ABCDEFGHIJK"
  754. rf_list = rf_list[0:len(ulchars)]
  755. try:
  756. with open(self.recent_files_path, 'w') as rf_file:
  757. rf_file.writelines(rf_list)
  758. except IOError as err:
  759. if not getattr(self.root, "recentfilelist_error_displayed", False):
  760. self.root.recentfilelist_error_displayed = True
  761. tkMessageBox.showerror(title='IDLE Error',
  762. message='Unable to update Recent Files list:\n%s'
  763. % str(err),
  764. parent=self.text)
  765. # for each edit window instance, construct the recent files menu
  766. for instance in self.top.instance_dict.keys():
  767. menu = instance.recent_files_menu
  768. menu.delete(1, END) # clear, and rebuild:
  769. for i, file_name in enumerate(rf_list):
  770. file_name = file_name.rstrip() # zap \n
  771. # make unicode string to display non-ASCII chars correctly
  772. ufile_name = self._filename_to_unicode(file_name)
  773. callback = instance.__recent_file_callback(file_name)
  774. menu.add_command(label=ulchars[i] + " " + ufile_name,
  775. command=callback,
  776. underline=0)
  777. def __recent_file_callback(self, file_name):
  778. def open_recent_file(fn_closure=file_name):
  779. self.io.open(editFile=fn_closure)
  780. return open_recent_file
  781. def saved_change_hook(self):
  782. short = self.short_title()
  783. long = self.long_title()
  784. if short and long:
  785. title = short + " - " + long
  786. elif short:
  787. title = short
  788. elif long:
  789. title = long
  790. else:
  791. title = "Untitled"
  792. icon = short or long or title
  793. if not self.get_saved():
  794. title = "*%s*" % title
  795. icon = "*%s" % icon
  796. self.top.wm_title(title)
  797. self.top.wm_iconname(icon)
  798. def get_saved(self):
  799. return self.undo.get_saved()
  800. def set_saved(self, flag):
  801. self.undo.set_saved(flag)
  802. def reset_undo(self):
  803. self.undo.reset_undo()
  804. def short_title(self):
  805. filename = self.io.filename
  806. if filename:
  807. filename = os.path.basename(filename)
  808. # return unicode string to display non-ASCII chars correctly
  809. return self._filename_to_unicode(filename)
  810. def long_title(self):
  811. # return unicode string to display non-ASCII chars correctly
  812. return self._filename_to_unicode(self.io.filename or "")
  813. def center_insert_event(self, event):
  814. self.center()
  815. def center(self, mark="insert"):
  816. text = self.text
  817. top, bot = self.getwindowlines()
  818. lineno = self.getlineno(mark)
  819. height = bot - top
  820. newtop = max(1, lineno - height//2)
  821. text.yview(float(newtop))
  822. def getwindowlines(self):
  823. text = self.text
  824. top = self.getlineno("@0,0")
  825. bot = self.getlineno("@0,65535")
  826. if top == bot and text.winfo_height() == 1:
  827. # Geometry manager hasn't run yet
  828. height = int(text['height'])
  829. bot = top + height - 1
  830. return top, bot
  831. def getlineno(self, mark="insert"):
  832. text = self.text
  833. return int(float(text.index(mark)))
  834. def get_geometry(self):
  835. "Return (width, height, x, y)"
  836. geom = self.top.wm_geometry()
  837. m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom)
  838. tuple = (map(int, m.groups()))
  839. return tuple
  840. def close_event(self, event):
  841. self.close()
  842. def maybesave(self):
  843. if self.io:
  844. if not self.get_saved():
  845. if self.top.state()!='normal':
  846. self.top.deiconify()
  847. self.top.lower()
  848. self.top.lift()
  849. return self.io.maybesave()
  850. def close(self):
  851. reply = self.maybesave()
  852. if str(reply) != "cancel":
  853. self._close()
  854. return reply
  855. def _close(self):
  856. if self.io.filename:
  857. self.update_recent_files_list(new_file=self.io.filename)
  858. WindowList.unregister_callback(self.postwindowsmenu)
  859. self.unload_extensions()
  860. self.io.close()
  861. self.io = None
  862. self.undo = None
  863. if self.color:
  864. self.color.close(False)
  865. self.color = None
  866. self.text = None
  867. self.tkinter_vars = None
  868. self.per.close()
  869. self.per = None
  870. self.top.destroy()
  871. if self.close_hook:
  872. # unless override: unregister from flist, terminate if last window
  873. self.close_hook()
  874. def load_extensions(self):
  875. self.extensions = {}
  876. self.load_standard_extensions()
  877. def unload_extensions(self):
  878. for ins in self.extensions.values():
  879. if hasattr(ins, "close"):
  880. ins.close()
  881. self.extensions = {}
  882. def load_standard_extensions(self):
  883. for name in self.get_standard_extension_names():
  884. try:
  885. self.load_extension(name)
  886. except:
  887. print "Failed to load extension", repr(name)
  888. import traceback
  889. traceback.print_exc()
  890. def get_standard_extension_names(self):
  891. return idleConf.GetExtensions(editor_only=True)
  892. def load_extension(self, name):
  893. try:
  894. mod = __import__(name, globals(), locals(), [])
  895. except ImportError:
  896. print "\nFailed to import extension: ", name
  897. return
  898. cls = getattr(mod, name)
  899. keydefs = idleConf.GetExtensionBindings(name)
  900. if hasattr(cls, "menudefs"):
  901. self.fill_menus(cls.menudefs, keydefs)
  902. ins = cls(self)
  903. self.extensions[name] = ins
  904. if keydefs:
  905. self.apply_bindings(keydefs)
  906. for vevent in keydefs.keys():
  907. methodname = vevent.replace("-", "_")
  908. while methodname[:1] == '<':
  909. methodname = methodname[1:]
  910. while methodname[-1:] == '>':
  911. methodname = methodname[:-1]
  912. methodname = methodname + "_event"
  913. if hasattr(ins, methodname):
  914. self.text.bind(vevent, getattr(ins, methodname))
  915. def apply_bindings(self, keydefs=None):
  916. if keydefs is None:
  917. keydefs = self.Bindings.default_keydefs
  918. text = self.text
  919. text.keydefs = keydefs
  920. for event, keylist in keydefs.items():
  921. if keylist:
  922. text.event_add(event, *keylist)
  923. def fill_menus(self, menudefs=None, keydefs=None):
  924. """Add appropriate entries to the menus and submenus
  925. Menus that are absent or None in self.menudict are ignored.
  926. """
  927. if menudefs is None:
  928. menudefs = self.Bindings.menudefs
  929. if keydefs is None:
  930. keydefs = self.Bindings.default_keydefs
  931. menudict = self.menudict
  932. text = self.text
  933. for mname, entrylist in menudefs:
  934. menu = menudict.get(mname)
  935. if not menu:
  936. continue
  937. for entry in entrylist:
  938. if not entry:
  939. menu.add_separator()
  940. else:
  941. label, eventname = entry
  942. checkbutton = (label[:1] == '!')
  943. if checkbutton:
  944. label = label[1:]
  945. underline, label = prepstr(label)
  946. accelerator = get_accelerator(keydefs, eventname)
  947. def command(text=text, eventname=eventname):
  948. text.event_generate(eventname)
  949. if checkbutton:
  950. var = self.get_var_obj(eventname, BooleanVar)
  951. menu.add_checkbutton(label=label, underline=underline,
  952. command=command, accelerator=accelerator,
  953. variable=var)
  954. else:
  955. menu.add_command(label=label, underline=underline,
  956. command=command,
  957. accelerator=accelerator)
  958. def getvar(self, name):
  959. var = self.get_var_obj(name)
  960. if var:
  961. value = var.get()
  962. return value
  963. else:
  964. raise NameError, name
  965. def setvar(self, name, value, vartype=None):
  966. var = self.get_var_obj(name, vartype)
  967. if var:
  968. var.set(value)
  969. else:
  970. raise NameError, name
  971. def get_var_obj(self, name, vartype=None):
  972. var = self.tkinter_vars.get(name)
  973. if not var and vartype:
  974. # create a Tkinter variable object with self.text as master:
  975. self.tkinter_vars[name] = var = vartype(self.text)
  976. return var
  977. # Tk implementations of "virtual text methods" -- each platform
  978. # reusing IDLE's support code needs to define these for its GUI's
  979. # flavor of widget.
  980. # Is character at text_index in a Python string? Return 0 for
  981. # "guaranteed no", true for anything else. This info is expensive
  982. # to compute ab initio, but is probably already known by the
  983. # platform's colorizer.
  984. def is_char_in_string(self, text_index):
  985. if self.color:
  986. # Return true iff colorizer hasn't (re)gotten this far
  987. # yet, or the character is tagged as being in a string
  988. return self.text.tag_prevrange("TODO", text_index) or \
  989. "STRING" in self.text.tag_names(text_index)
  990. else:
  991. # The colorizer is missing: assume the worst
  992. return 1
  993. # If a selection is defined in the text widget, return (start,
  994. # end) as Tkinter text indices, otherwise return (None, None)
  995. def get_selection_indices(self):
  996. try:
  997. first = self.text.index("sel.first")
  998. last = self.text.index("sel.last")
  999. return first, last
  1000. except TclError:
  1001. return None, None
  1002. # Return the text widget's current view of what a tab stop means
  1003. # (equivalent width in spaces).
  1004. def get_tabwidth(self):
  1005. current = self.text['tabs'] or TK_TABWIDTH_DEFAULT
  1006. return int(current)
  1007. # Set the text widget's current view of what a tab stop means.
  1008. def set_tabwidth(self, newtabwidth):
  1009. text = self.text
  1010. if self.get_tabwidth() != newtabwidth:
  1011. pixels = text.tk.call("font", "measure", text["font"],
  1012. "-displayof", text.master,
  1013. "n" * newtabwidth)
  1014. text.configure(tabs=pixels)
  1015. # If ispythonsource and guess are true, guess a good value for
  1016. # indentwidth based on file content (if possible), and if
  1017. # indentwidth != tabwidth set usetabs false.
  1018. # In any case, adjust the Text widget's view of what a tab
  1019. # character means.
  1020. def set_indentation_params(self, ispythonsource, guess=True):
  1021. if guess and ispythonsource:
  1022. i = self.guess_indent()
  1023. if 2 <= i <= 8:
  1024. self.indentwidth = i
  1025. if self.indentwidth != self.tabwidth:
  1026. self.usetabs = False
  1027. self.set_tabwidth(self.tabwidth)
  1028. def smart_backspace_event(self, event):
  1029. text = self.text
  1030. first, last = self.get_selection_indices()
  1031. if first and last:
  1032. text.delete(first, last)
  1033. text.mark_set("insert", first)
  1034. return "break"
  1035. # Delete whitespace left, until hitting a real char or closest
  1036. # preceding virtual tab stop.
  1037. chars = text.get("insert linestart", "insert")
  1038. if chars == '':
  1039. if text.compare("insert", ">", "1.0"):
  1040. # easy: delete preceding newline
  1041. text.delete("insert-1c")
  1042. else:
  1043. text.bell() # at start of buffer
  1044. return "break"
  1045. if chars[-1] not in " \t":
  1046. # easy: delete preceding real char
  1047. text.delete("insert-1c")
  1048. return "break"
  1049. # Ick. It may require *inserting* spaces if we back up over a
  1050. # tab character! This is written to be clear, not fast.
  1051. tabwidth = self.tabwidth
  1052. have = len(chars.expandtabs(tabwidth))
  1053. assert have > 0
  1054. want = ((have - 1) // self.indentwidth) * self.indentwidth
  1055. # Debug prompt is multilined....
  1056. if self.context_use_ps1:
  1057. last_line_of_prompt = sys.ps1.split('\n')[-1]
  1058. else:
  1059. last_line_of_prompt = ''
  1060. ncharsdeleted = 0
  1061. while 1:
  1062. if chars == last_line_of_prompt:
  1063. break
  1064. chars = chars[:-1]
  1065. ncharsdeleted = ncharsdeleted + 1
  1066. have = len(chars.expandtabs(tabwidth))
  1067. if have <= want or chars[-1] not in " \t":
  1068. break
  1069. text.undo_block_start()
  1070. text.delete("insert-%dc" % ncharsdeleted, "insert")
  1071. if have < want:
  1072. text.insert("insert", ' ' * (want - have))
  1073. text.undo_block_stop()
  1074. return "break"
  1075. def smart_indent_event(self, event):
  1076. # if intraline selection:
  1077. # delete it
  1078. # elif multiline selection:
  1079. # do indent-region
  1080. # else:
  1081. # indent one level
  1082. text = self.text
  1083. first, last = self.get_selection_indices()
  1084. text.undo_block_start()
  1085. try:
  1086. if first and last:
  1087. if index2line(first) != index2line(last):
  1088. return self.indent_region_event(event)
  1089. text.delete(first, last)
  1090. text.mark_set("insert", first)
  1091. prefix = text.get("insert linestart", "insert")
  1092. raw, effective = classifyws(prefix, self.tabwidth)
  1093. if raw == len(prefix):
  1094. # only whitespace to the left
  1095. self.reindent_to(effective + self.indentwidth)
  1096. else:
  1097. # tab to the next 'stop' within or to right of line's text:
  1098. if self.usetabs:
  1099. pad = '\t'
  1100. else:
  1101. effective = len(prefix.expandtabs(self.tabwidth))
  1102. n = self.indentwidth
  1103. pad = ' ' * (n - effective % n)
  1104. text.insert("insert", pad)
  1105. text.see("insert")
  1106. return "break"
  1107. finally:
  1108. text.undo_block_stop()
  1109. def newline_and_indent_event(self, event):
  1110. text = self.text
  1111. first, last = self.get_selection_indices()
  1112. text.undo_block_start()
  1113. try:
  1114. if first and last:
  1115. text.delete(first, last)
  1116. text.mark_set("insert", first)
  1117. line = text.get("insert linestart", "insert")
  1118. i, n = 0, len(line)
  1119. while i < n and line[i] in " \t":
  1120. i = i+1
  1121. if i == n:
  1122. # the cursor is in or at leading indentation in a continuation
  1123. # line; just inject an empty line at the start
  1124. text.insert("insert linestart", '\n')
  1125. return "break"
  1126. indent = line[:i]
  1127. # strip whitespace before insert point unless it's in the prompt
  1128. i = 0
  1129. last_line_of_prompt = sys.ps1.split('\n')[-1]
  1130. while line and line[-1] in " \t" and line != last_line_of_prompt:
  1131. line = line[:-1]
  1132. i = i+1
  1133. if i:
  1134. text.delete("insert - %d chars" % i, "insert")
  1135. # strip whitespace after insert point
  1136. while text.get("insert") in " \t":
  1137. text.delete("insert")
  1138. # start new line
  1139. text.insert("insert", '\n')
  1140. # adjust indentation for continuations and block
  1141. # open/close first need to find the last stmt
  1142. lno = index2line(text.index('insert'))
  1143. y = PyParse.Parser(self.indentwidth, self.tabwidth)
  1144. if not self.context_use_ps1:
  1145. for context in self.num_context_lines:
  1146. startat = max(lno - context, 1)
  1147. startatindex = repr(startat) + ".0"
  1148. rawtext = text.get(startatindex, "insert")
  1149. y.set_str(rawtext)
  1150. bod = y.find_good_parse_start(
  1151. self.context_use_ps1,
  1152. self._build_char_in_string_func(startatindex))
  1153. if bod is not None or startat == 1:
  1154. break
  1155. y.set_lo(bod or 0)
  1156. else:
  1157. r = text.tag_prevrange("console", "insert")
  1158. if r:
  1159. startatindex = r[1]
  1160. else:
  1161. startatindex = "1.0"
  1162. rawtext = text.get(startatindex, "insert")
  1163. y.set_str(rawtext)
  1164. y.set_lo(0)
  1165. c = y.get_continuation_type()
  1166. if c != PyParse.C_NONE:
  1167. # The current stmt hasn't ended yet.
  1168. if c == PyParse.C_STRING_FIRST_LINE:
  1169. # after the first line of a string; do not indent at all
  1170. pass
  1171. elif c == PyParse.C_STRING_NEXT_LINES:
  1172. # inside a string which started before this line;
  1173. # just mimic the current indent
  1174. text.insert("insert", indent)
  1175. elif c == PyParse.C_BRACKET:
  1176. # line up with the first (if any) element of the
  1177. # last open bracket structure; else indent one
  1178. # level beyond the indent of the line with the
  1179. # last open bracket
  1180. self.reindent_to(y.compute_bracket_indent())
  1181. elif c == PyParse.C_BACKSLASH:
  1182. # if more than one line in this stmt already, just
  1183. # mimic the current indent; else if initial line
  1184. # has a start on an assignment stmt, indent to
  1185. # beyond leftmost =; else to beyond first chunk of
  1186. # non-whitespace on initial line
  1187. if y.get_num_lines_in_stmt() > 1:
  1188. text.insert("insert", indent)
  1189. else:
  1190. self.reindent_to(y.compute_backslash_indent())
  1191. else:
  1192. assert 0, "bogus continuation type %r" % (c,)
  1193. return "break"
  1194. # This line starts a brand new stmt; indent relative to
  1195. # indentation of initial line of closest preceding
  1196. # interesting stmt.
  1197. indent = y.get_base_indent_string()
  1198. text.insert("insert", indent)
  1199. if y.is_block_opener():
  1200. self.smart_indent_event(event)
  1201. elif indent and y.is_block_closer():
  1202. self.smart_backspace_event(event)
  1203. return "break"
  1204. finally:
  1205. text.see("insert")
  1206. text.undo_block_stop()
  1207. # Our editwin provides a is_char_in_string function that works
  1208. # with a Tk text index, but PyParse only knows about offsets into
  1209. # a string. This builds a function for PyParse that accepts an
  1210. # offset.
  1211. def _build_char_in_string_func(self, startindex):
  1212. def inner(offset, _startindex=startindex,
  1213. _icis=self.is_char_in_string):
  1214. return _icis(_startindex + "+%dc" % offset)
  1215. return inner
  1216. def indent_region_event(self, event):
  1217. head, tail, chars, lines = self.get_region()
  1218. for pos in range(len(lines)):
  1219. line = lines[pos]
  1220. if line:
  1221. raw, effective = classifyws(line, self.tabwidth)
  1222. effective = effective + self.indentwidth
  1223. lines[pos] = self._make_blanks(effective) + line[raw:]
  1224. self.set_region(head, tail, chars, lines)
  1225. return "break"
  1226. def dedent_region_event(self, event):
  1227. head, tail, chars, lines = self.get_region()
  1228. for pos in range(len(lines)):
  1229. line = lines[pos]
  1230. if line:
  1231. raw, effective = classifyws(line, self.tabwidth)
  1232. effective = max(effective - self.indentwidth, 0)
  1233. lines[pos] = self._make_blanks(effective) + line[raw:]
  1234. self.set_region(head, tail, chars, lines)
  1235. return "break"
  1236. def comment_region_event(self, event):
  1237. head, tail, chars, lines = self.get_region()
  1238. for pos in range(len(lines) - 1):
  1239. line = lines[pos]
  1240. lines[pos] = '##' + line
  1241. self.set_region(head, tail, chars, lines)
  1242. def uncomment_region_event(self, event):
  1243. head, tail, chars, lines = self.get_region()
  1244. for pos in range(len(lines)):
  1245. line = lines[pos]
  1246. if not line:
  1247. continue
  1248. if line[:2] == '##':
  1249. line = line[2:]
  1250. elif line[:1] == '#':
  1251. line = line[1:]
  1252. lines[pos] = line
  1253. self.set_region(head, tail, chars, lines)
  1254. def tabify_region_event(self, event):
  1255. head, tail, chars, lines = self.get_region()
  1256. tabwidth = self._asktabwidth()
  1257. for pos in range(len(lines)):
  1258. line = lines[pos]
  1259. if line:
  1260. raw, effective = classifyws(line, tabwidth)
  1261. ntabs, nspaces = divmod(effective, tabwidth)
  1262. lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]
  1263. self.set_region(head, tail, chars, lines)
  1264. def untabify_region_event(self, event):
  1265. head, tail, chars, lines = self.get_region()
  1266. tabwidth = self._asktabwidth()
  1267. for pos in range(len(lines)):
  1268. lines[pos] = lines[pos].expandtabs(tabwidth)
  1269. self.set_region(head, tail, chars, lines)
  1270. def toggle_tabs_event(self, event):
  1271. if self.askyesno(
  1272. "Toggle tabs",
  1273. "Turn tabs " + ("on", "off")[self.usetabs] +
  1274. "?\nIndent width " +
  1275. ("will be", "remains at")[self.usetabs] + " 8." +
  1276. "\n Note: a tab is always 8 columns",
  1277. parent=self.text):
  1278. self.usetabs = not self.usetabs
  1279. # Try to prevent inconsistent indentation.
  1280. # User must change indent width manually after using tabs.
  1281. self.indentwidth = 8
  1282. return "break"
  1283. # XXX this isn't bound to anything -- see tabwidth comments
  1284. ## def change_tabwidth_event(self, event):
  1285. ## new = self._asktabwidth()
  1286. ## if new != self.tabwidth:
  1287. ## self.tabwidth = new
  1288. ## self.set_indentation_params(0, guess=0)
  1289. ## return "break"
  1290. def change_indentwidth_event(self, event):
  1291. new = self.askinteger(
  1292. "Indent width",
  1293. "New indent width (2-16)\n(Always use 8 when using tabs)",
  1294. parent=self.text,
  1295. initialvalue=self.indentwidth,
  1296. minvalue=2,
  1297. maxvalue=16)
  1298. if new and new != self.indentwidth and not self.usetabs:
  1299. self.indentwidth = new
  1300. return "break"
  1301. def get_region(self):
  1302. text = self.text
  1303. first, last = self.get_selection_indices()
  1304. if first and last:
  1305. head = text.index(first + " linestart")
  1306. tail = text.index(last + "-1c lineend +1c")
  1307. else:
  1308. head = text.index("insert linestart")
  1309. tail = text.index("insert lineend +1c")
  1310. chars = text.get(head, tail)
  1311. lines = chars.split("\n")
  1312. return head, tail, chars, lines
  1313. def set_region(self, head, tail, chars, lines):
  1314. text = self.text
  1315. newchars = "\n".join(lines)
  1316. if newchars == chars:
  1317. text.bell()
  1318. return
  1319. text.tag_remove("sel", "1.0", "end")
  1320. text.mark_set("insert", head)
  1321. text.undo_block_start()
  1322. text.delete(head, tail)
  1323. text.insert(head, newchars)
  1324. text.undo_block_stop()
  1325. text.tag_add("sel", head, "insert")
  1326. # Make string that displays as n leading blanks.
  1327. def _make_blanks(self, n):
  1328. if self.usetabs:
  1329. ntabs, nspaces = divmod(n, self.tabwidth)
  1330. return '\t' * ntabs + ' ' * nspaces
  1331. else:
  1332. return ' ' * n
  1333. # Delete from beginning of line to insert point, then reinsert
  1334. # column logical (meaning use tabs if appropriate) spaces.
  1335. def reindent_to(self, column):
  1336. text = self.text
  1337. text.undo_block_start()
  1338. if text.compare("insert linestart", "!=", "insert"):
  1339. text.delete("insert linestart", "insert")
  1340. if column:
  1341. text.insert("insert", self._make_blanks(column))
  1342. text.undo_block_stop()
  1343. def _asktabwidth(self):
  1344. return self.askinteger(
  1345. "Tab width",
  1346. "Columns per tab? (2-16)",
  1347. parent=self.text,
  1348. initialvalue=self.indentwidth,
  1349. minvalue=2,
  1350. maxvalue=16) or self.tabwidth
  1351. # Guess indentwidth from text content.
  1352. # Return guessed indentwidth. This should not be believed unless
  1353. # it's in a reasonable range (e.g., it will be 0 if no indented
  1354. # blocks are found).
  1355. def guess_indent(self):
  1356. opener, indented = IndentSearcher(self.text, self.tabwidth).run()
  1357. if opener and indented:
  1358. raw, indentsmall = classifyws(opener, self.tabwidth)
  1359. raw, indentlarge = classifyws(indented, self.tabwidth)
  1360. else:
  1361. indentsmall = indentlarge = 0
  1362. return indentlarge - indentsmall
  1363. # "line.col" -> line, as an int
  1364. def index2line(index):
  1365. return int(float(index))
  1366. # Look at the leading whitespace in s.
  1367. # Return pair (# of leading ws characters,
  1368. # effective # of leading blanks after expanding
  1369. # tabs to width tabwidth)
  1370. def classifyws(s, tabwidth):
  1371. raw = effective = 0
  1372. for ch in s:
  1373. if ch == ' ':
  1374. raw = raw + 1
  1375. effective = effective + 1
  1376. elif ch == '\t':
  1377. raw = raw + 1
  1378. effective = (effective // tabwidth + 1) * tabwidth
  1379. else:
  1380. break
  1381. return raw, effective
  1382. import tokenize
  1383. _tokenize = tokenize
  1384. del tokenize
  1385. class IndentSearcher(object):
  1386. # .run() chews over the Text widget, looking for a block opener
  1387. # and the stmt following it. Returns a pair,
  1388. # (line containing block opener, line containing stmt)
  1389. # Either or both may be None.
  1390. def __init__(self, text, tabwidth):
  1391. self.text = text
  1392. self.tabwidth = tabwidth
  1393. self.i = self.finished = 0
  1394. self.blkopenline = self.indentedline = None
  1395. def readline(self):
  1396. if self.finished:
  1397. return ""
  1398. i = self.i = self.i + 1
  1399. mark = repr(i) + ".0"
  1400. if self.text.compare(mark, ">=", "end"):
  1401. return ""
  1402. return self.text.get(mark, mark + " lineend+1c")
  1403. def tokeneater(self, type, token, start, end, line,
  1404. INDENT=_tokenize.INDENT,
  1405. NAME=_tokenize.NAME,
  1406. OPENERS=('class', 'def', 'for', 'if', 'try', 'while')):
  1407. if self.finished:
  1408. pass
  1409. elif type == NAME and token in OPENERS:
  1410. self.blkopenline = line
  1411. elif type == INDENT and self.blkopenline:
  1412. self.indentedline = line
  1413. self.finished = 1
  1414. def run(self):
  1415. save_tabsize = _tokenize.tabsize
  1416. _tokenize.tabsize = self.tabwidth
  1417. try:
  1418. try:
  1419. _tokenize.tokenize(self.readline, self.tokeneater)
  1420. except _tokenize.TokenError:
  1421. # since we cut off the tokenizer early, we can trigger
  1422. # spurious errors
  1423. pass
  1424. finally:
  1425. _tokenize.tabsize = save_tabsize
  1426. return self.blkopenline, self.indentedline
  1427. ### end autoindent code ###
  1428. def prepstr(s):
  1429. # Helper to extract the underscore from a string, e.g.
  1430. # prepstr("Co_py") returns (2, "Copy").
  1431. i = s.find('_')
  1432. if i >= 0:
  1433. s = s[:i] + s[i+1:]
  1434. return i, s
  1435. keynames = {
  1436. 'bracketleft': '[',
  1437. 'bracketright': ']',
  1438. 'slash': '/',
  1439. }
  1440. def get_accelerator(keydefs, eventname):
  1441. keylist = keydefs.get(eventname)
  1442. # issue10940: temporary workaround to prevent hang with OS X Cocoa Tk 8.5
  1443. # if not keylist:
  1444. if (not keylist) or (macosxSupport.runningAsOSXApp() and eventname in {
  1445. "<<open-module>>",
  1446. "<<goto-line>>",
  1447. "<<change-indentwidth>>"}):
  1448. return ""
  1449. s = keylist[0]
  1450. s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s)
  1451. s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s)
  1452. s = re.sub("Key-", "", s)
  1453. s = re.sub("Cancel","Ctrl-Break",s) # dscherer@cmu.edu
  1454. s = re.sub("Control-", "Ctrl-", s)
  1455. s = re.sub("-", "+", s)
  1456. s = re.sub("><", " ", s)
  1457. s = re.sub("<", "", s)
  1458. s = re.sub(">", "", s)
  1459. return s
  1460. def fixwordbreaks(root):
  1461. # Make sure that Tk's double-click and next/previous word
  1462. # operations use our definition of a word (i.e. an identifier)
  1463. tk = root.tk
  1464. tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
  1465. tk.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]')
  1466. tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]')
  1467. def test():
  1468. root = Tk()
  1469. fixwordbreaks(root)
  1470. root.withdraw()
  1471. if sys.argv[1:]:
  1472. filename = sys.argv[1]
  1473. else:
  1474. filename = None
  1475. edit = EditorWindow(root=root, filename=filename)
  1476. edit.set_close_hook(root.quit)
  1477. edit.text.bind("<<close-all-windows>>", edit.close_event)
  1478. root.mainloop()
  1479. root.destroy()
  1480. if __name__ == '__main__':
  1481. test()