Coverage for editor.py: 11%
1204 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 13:22 -0700
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 13:22 -0700
1import importlib.abc
2import importlib.util
3import os
4import platform
5import re
6import string
7import sys
8import tokenize
9import traceback
10import webbrowser
11import ast
13from tkinter import *
14from tkinter.font import Font
15from tkinter.ttk import Scrollbar
16from tkinter import simpledialog
17from tkinter import messagebox
19from idlelib.config import idleConf
20from idlelib import configdialog
21from idlelib import grep
22from idlelib import help
23from idlelib import help_about
24from idlelib import macosx
25from idlelib.multicall import MultiCallCreator
26from idlelib import pyparse
27from idlelib import query
28from idlelib import replace
29from idlelib import search
30from idlelib.tree import wheel_event
31from idlelib.util import py_extensions
32from idlelib import window
33from code_outline import ClassFuncNodeVisitor
35# The default tab setting for a Text widget, in average-width characters.
36TK_TABWIDTH_DEFAULT = 8
37_py_version = ' (%s)' % platform.python_version()
38darwin = sys.platform == 'darwin'
40def _sphinx_version():
41 "Format sys.version_info to produce the Sphinx version string used to install the chm docs"
42 major, minor, micro, level, serial = sys.version_info
43 release = '%s%s' % (major, minor)
44 release += '%s' % (micro,)
45 if level == 'candidate':
46 release += 'rc%s' % (serial,)
47 elif level != 'final':
48 release += '%s%s' % (level[0], serial)
49 return release
52class EditorWindow:
53 from idlelib.percolator import Percolator
54 from idlelib.colorizer import ColorDelegator, color_config
55 from idlelib.undo import UndoDelegator
56 from idlelib.iomenu import IOBinding, encoding
57 from idlelib import mainmenu
58 from idlelib.statusbar import MultiStatusBar
59 from idlelib.autocomplete import AutoComplete
60 from idlelib.autoexpand import AutoExpand
61 from idlelib.calltip import Calltip
62 from idlelib.codecontext import CodeContext
63 from idlelib.sidebar import LineNumbers
64 from idlelib.format import FormatParagraph, FormatRegion, Indents, Rstrip
65 from idlelib.parenmatch import ParenMatch
66 from idlelib.zoomheight import ZoomHeight
68 filesystemencoding = sys.getfilesystemencoding() # for file names
69 help_url = None
71 allow_code_context = True
72 allow_line_numbers = True
73 user_input_insert_tags = None
75 def __init__(self, flist=None, filename=None, key=None, root=None):
76 # Delay import: runscript imports pyshell imports EditorWindow.
77 from idlelib.runscript import ScriptBinding
79 if EditorWindow.help_url is None:
80 dochome = os.path.join(sys.base_prefix, 'Doc', 'index.html')
81 if sys.platform.count('linux'):
82 # look for html docs in a couple of standard places
83 pyver = 'python-docs-' + '%s.%s.%s' % sys.version_info[:3]
84 if os.path.isdir('/var/www/html/python/'): # "python2" rpm
85 dochome = '/var/www/html/python/index.html'
86 else:
87 basepath = '/usr/share/doc/' # standard location
88 dochome = os.path.join(basepath, pyver,
89 'Doc', 'index.html')
90 elif sys.platform[:3] == 'win':
91 import winreg # Windows only, block only executed once.
92 docfile = ''
93 KEY = (rf"Software\Python\PythonCore\{sys.winver}"
94 r"\Help\Main Python Documentation")
95 try:
96 docfile = winreg.QueryValue(winreg.HKEY_CURRENT_USER, KEY)
97 except FileNotFoundError:
98 try:
99 docfile = winreg.QueryValue(winreg.HKEY_LOCAL_MACHINE,
100 KEY)
101 except FileNotFoundError:
102 pass
103 if os.path.isfile(docfile):
104 dochome = docfile
105 elif sys.platform == 'darwin':
106 # documentation may be stored inside a python framework
107 dochome = os.path.join(sys.base_prefix,
108 'Resources/English.lproj/Documentation/index.html')
109 dochome = os.path.normpath(dochome)
110 if os.path.isfile(dochome):
111 EditorWindow.help_url = dochome
112 if sys.platform == 'darwin':
113 # Safari requires real file:-URLs
114 EditorWindow.help_url = 'file://' + EditorWindow.help_url
115 else:
116 EditorWindow.help_url = ("https://docs.python.org/%d.%d/"
117 % sys.version_info[:2])
118 self.flist = flist
119 root = root or flist.root
120 self.root = root
121 self.menubar = Menu(root)
122 self.top = top = window.ListedToplevel(root, menu=self.menubar)
123 if flist:
124 self.tkinter_vars = flist.vars
125 #self.top.instance_dict makes flist.inversedict available to
126 #configdialog.py so it can access all EditorWindow instances
127 self.top.instance_dict = flist.inversedict
128 else:
129 self.tkinter_vars = {} # keys: Tkinter event names
130 # values: Tkinter variable instances
131 self.top.instance_dict = {}
132 self.recent_files_path = idleConf.userdir and os.path.join(
133 idleConf.userdir, 'recent-files.lst')
135 self.prompt_last_line = '' # Override in PyShell
136 self.text_frame = text_frame = Frame(top)
137 self.vbar = vbar = Scrollbar(text_frame, name='vbar')
138 width = idleConf.GetOption('main', 'EditorWindow', 'width', type='int')
139 text_options = {
140 'name': 'text',
141 'padx': 5,
142 'wrap': 'none',
143 'highlightthickness': 0,
144 'width': width,
145 'tabstyle': 'wordprocessor', # new in 8.5
146 'height': idleConf.GetOption(
147 'main', 'EditorWindow', 'height', type='int'),
148 }
149 self.text = text = MultiCallCreator(Text)(text_frame, **text_options)
150 self.top.focused_widget = self.text
152 self.createmenubar()
153 self.apply_bindings()
155 self.top.protocol("WM_DELETE_WINDOW", self.close)
156 self.top.bind("<<close-window>>", self.close_event)
157 if macosx.isAquaTk():
158 # Command-W on editor windows doesn't work without this.
159 text.bind('<<close-window>>', self.close_event)
160 # Some OS X systems have only one mouse button, so use
161 # control-click for popup context menus there. For two
162 # buttons, AquaTk defines <2> as the right button, not <3>.
163 text.bind("<Control-Button-1>",self.right_menu_event)
164 text.bind("<2>", self.right_menu_event)
165 else:
166 # Elsewhere, use right-click for popup menus.
167 text.bind("<3>",self.right_menu_event)
169 text.bind('<MouseWheel>', wheel_event)
170 text.bind('<Button-4>', wheel_event)
171 text.bind('<Button-5>', wheel_event)
172 text.bind('<Configure>', self.handle_winconfig)
173 text.bind("<<cut>>", self.cut)
174 text.bind("<<copy>>", self.copy)
175 text.bind("<<paste>>", self.paste)
176 text.bind("<<center-insert>>", self.center_insert_event)
177 text.bind("<<help>>", self.help_dialog)
178 text.bind("<<python-docs>>", self.python_docs)
179 text.bind("<<about-idle>>", self.about_dialog)
180 text.bind("<<open-config-dialog>>", self.config_dialog)
181 text.bind("<<open-module>>", self.open_module_event)
182 text.bind("<<do-nothing>>", lambda event: "break")
183 text.bind("<<select-all>>", self.select_all)
184 text.bind("<<remove-selection>>", self.remove_selection)
185 text.bind("<<find>>", self.find_event)
186 text.bind("<<find-again>>", self.find_again_event)
187 text.bind("<<find-in-files>>", self.find_in_files_event)
188 text.bind("<<find-selection>>", self.find_selection_event)
189 text.bind("<<replace>>", self.replace_event)
190 text.bind("<<goto-line>>", self.goto_line_event)
191 text.bind("<<show-code-outline>>", self.show_code_outline_event)
192 text.bind("<<smart-backspace>>",self.smart_backspace_event)
193 text.bind("<<newline-and-indent>>",self.newline_and_indent_event)
194 text.bind("<<smart-indent>>",self.smart_indent_event)
195 self.fregion = fregion = self.FormatRegion(self)
196 # self.fregion used in smart_indent_event to access indent_region.
197 text.bind("<<indent-region>>", fregion.indent_region_event)
198 text.bind("<<dedent-region>>", fregion.dedent_region_event)
199 text.bind("<<comment-region>>", fregion.comment_region_event)
200 text.bind("<<uncomment-region>>", fregion.uncomment_region_event)
201 text.bind("<<tabify-region>>", fregion.tabify_region_event)
202 text.bind("<<untabify-region>>", fregion.untabify_region_event)
203 indents = self.Indents(self)
204 text.bind("<<toggle-tabs>>", indents.toggle_tabs_event)
205 text.bind("<<change-indentwidth>>", indents.change_indentwidth_event)
206 text.bind("<Left>", self.move_at_edge_if_selection(0))
207 text.bind("<Right>", self.move_at_edge_if_selection(1))
208 text.bind("<<del-word-left>>", self.del_word_left)
209 text.bind("<<del-word-right>>", self.del_word_right)
210 text.bind("<<beginning-of-line>>", self.home_callback)
212 if flist:
213 flist.inversedict[self] = key
214 if key:
215 flist.dict[key] = self
216 text.bind("<<open-new-window>>", self.new_callback)
217 text.bind("<<close-all-windows>>", self.flist.close_all_callback)
218 text.bind("<<open-class-browser>>", self.open_module_browser)
219 text.bind("<<open-path-browser>>", self.open_path_browser)
220 text.bind("<<open-turtle-demo>>", self.open_turtle_demo)
222 self.set_status_bar()
223 text_frame.pack(side=LEFT, fill=BOTH, expand=1)
224 text_frame.rowconfigure(1, weight=1)
225 text_frame.columnconfigure(1, weight=1)
226 vbar['command'] = self.handle_yview
227 vbar.grid(row=1, column=2, sticky=NSEW)
228 text['yscrollcommand'] = vbar.set
229 text['font'] = idleConf.GetFont(self.root, 'main', 'EditorWindow')
230 text.grid(row=1, column=1, sticky=NSEW)
231 text.focus_set()
232 self.set_width()
234 # usetabs true -> literal tab characters are used by indent and
235 # dedent cmds, possibly mixed with spaces if
236 # indentwidth is not a multiple of tabwidth,
237 # which will cause Tabnanny to nag!
238 # false -> tab characters are converted to spaces by indent
239 # and dedent cmds, and ditto TAB keystrokes
240 # Although use-spaces=0 can be configured manually in config-main.def,
241 # configuration of tabs v. spaces is not supported in the configuration
242 # dialog. IDLE promotes the preferred Python indentation: use spaces!
243 usespaces = idleConf.GetOption('main', 'Indent',
244 'use-spaces', type='bool')
245 self.usetabs = not usespaces
247 # tabwidth is the display width of a literal tab character.
248 # CAUTION: telling Tk to use anything other than its default
249 # tab setting causes it to use an entirely different tabbing algorithm,
250 # treating tab stops as fixed distances from the left margin.
251 # Nobody expects this, so for now tabwidth should never be changed.
252 self.tabwidth = 8 # must remain 8 until Tk is fixed.
254 # indentwidth is the number of screen characters per indent level.
255 # The recommended Python indentation is four spaces.
256 self.indentwidth = self.tabwidth
257 self.set_notabs_indentwidth()
259 # Store the current value of the insertofftime now so we can restore
260 # it if needed.
261 if not hasattr(idleConf, 'blink_off_time'):
262 idleConf.blink_off_time = self.text['insertofftime']
263 self.update_cursor_blink()
265 # When searching backwards for a reliable place to begin parsing,
266 # first start num_context_lines[0] lines back, then
267 # num_context_lines[1] lines back if that didn't work, and so on.
268 # The last value should be huge (larger than the # of lines in a
269 # conceivable file).
270 # Making the initial values larger slows things down more often.
271 self.num_context_lines = 50, 500, 5000000
272 self.per = per = self.Percolator(text)
273 self.undo = undo = self.UndoDelegator()
274 per.insertfilter(undo)
275 text.undo_block_start = undo.undo_block_start
276 text.undo_block_stop = undo.undo_block_stop
277 undo.set_saved_change_hook(self.saved_change_hook)
278 # IOBinding implements file I/O and printing functionality
279 self.io = io = self.IOBinding(self)
280 io.set_filename_change_hook(self.filename_change_hook)
281 self.good_load = False
282 self.set_indentation_params(False)
283 self.color = None # initialized below in self.ResetColorizer
284 self.code_context = None # optionally initialized later below
285 self.line_numbers = None # optionally initialized later below
286 if filename:
287 if os.path.exists(filename) and not os.path.isdir(filename):
288 if io.loadfile(filename):
289 self.good_load = True
290 is_py_src = self.ispythonsource(filename)
291 self.set_indentation_params(is_py_src)
292 else:
293 io.set_filename(filename)
294 self.good_load = True
295 self.ResetColorizer()
296 self.saved_change_hook()
297 self.update_recent_files_list()
298 self.load_extensions()
299 menu = self.menudict.get('window')
300 if menu:
301 end = menu.index("end")
302 if end is None:
303 end = -1
304 if end >= 0:
305 menu.add_separator()
306 end = end + 1
307 self.wmenu_end = end
308 window.register_callback(self.postwindowsmenu)
310 # Some abstractions so IDLE extensions are cross-IDE
311 self.askinteger = simpledialog.askinteger
312 self.askyesno = messagebox.askyesno
313 self.showerror = messagebox.showerror
315 # Add pseudoevents for former extension fixed keys.
316 # (This probably needs to be done once in the process.)
317 text.event_add('<<autocomplete>>', '<Key-Tab>')
318 text.event_add('<<try-open-completions>>', '<KeyRelease-period>',
319 '<KeyRelease-slash>', '<KeyRelease-backslash>')
320 text.event_add('<<try-open-calltip>>', '<KeyRelease-parenleft>')
321 text.event_add('<<refresh-calltip>>', '<KeyRelease-parenright>')
322 text.event_add('<<paren-closed>>', '<KeyRelease-parenright>',
323 '<KeyRelease-bracketright>', '<KeyRelease-braceright>')
325 # Former extension bindings depends on frame.text being packed
326 # (called from self.ResetColorizer()).
327 autocomplete = self.AutoComplete(self, self.user_input_insert_tags)
328 text.bind("<<autocomplete>>", autocomplete.autocomplete_event)
329 text.bind("<<try-open-completions>>",
330 autocomplete.try_open_completions_event)
331 text.bind("<<force-open-completions>>",
332 autocomplete.force_open_completions_event)
333 text.bind("<<expand-word>>", self.AutoExpand(self).expand_word_event)
334 text.bind("<<format-paragraph>>",
335 self.FormatParagraph(self).format_paragraph_event)
336 parenmatch = self.ParenMatch(self)
337 text.bind("<<flash-paren>>", parenmatch.flash_paren_event)
338 text.bind("<<paren-closed>>", parenmatch.paren_closed_event)
339 scriptbinding = ScriptBinding(self)
340 text.bind("<<check-module>>", scriptbinding.check_module_event)
341 text.bind("<<run-module>>", scriptbinding.run_module_event)
342 text.bind("<<run-custom>>", scriptbinding.run_custom_event)
343 text.bind("<<do-rstrip>>", self.Rstrip(self).do_rstrip)
344 self.ctip = ctip = self.Calltip(self)
345 text.bind("<<try-open-calltip>>", ctip.try_open_calltip_event)
346 #refresh-calltip must come after paren-closed to work right
347 text.bind("<<refresh-calltip>>", ctip.refresh_calltip_event)
348 text.bind("<<force-open-calltip>>", ctip.force_open_calltip_event)
349 text.bind("<<zoom-height>>", self.ZoomHeight(self).zoom_height_event)
350 if self.allow_code_context:
351 self.code_context = self.CodeContext(self)
352 text.bind("<<toggle-code-context>>",
353 self.code_context.toggle_code_context_event)
354 else:
355 self.update_menu_state('options', '*ode*ontext', 'disabled')
356 if self.allow_line_numbers:
357 self.line_numbers = self.LineNumbers(self)
358 if idleConf.GetOption('main', 'EditorWindow',
359 'line-numbers-default', type='bool'):
360 self.toggle_line_numbers_event()
361 text.bind("<<toggle-line-numbers>>", self.toggle_line_numbers_event)
362 else:
363 self.update_menu_state('options', '*ine*umbers', 'disabled')
365 def handle_winconfig(self, event=None):
366 self.set_width()
368 def set_width(self):
369 text = self.text
370 inner_padding = sum(map(text.tk.getint, [text.cget('border'),
371 text.cget('padx')]))
372 pixel_width = text.winfo_width() - 2 * inner_padding
374 # Divide the width of the Text widget by the font width,
375 # which is taken to be the width of '0' (zero).
376 # http://www.tcl.tk/man/tcl8.6/TkCmd/text.htm#M21
377 zero_char_width = \
378 Font(text, font=text.cget('font')).measure('0')
379 self.width = pixel_width // zero_char_width
381 def new_callback(self, event):
382 dirname, basename = self.io.defaultfilename()
383 self.flist.new(dirname)
384 return "break"
386 def home_callback(self, event):
387 if (event.state & 4) != 0 and event.keysym == "Home":
388 # state&4==Control. If <Control-Home>, use the Tk binding.
389 return None
390 if self.text.index("iomark") and \
391 self.text.compare("iomark", "<=", "insert lineend") and \
392 self.text.compare("insert linestart", "<=", "iomark"):
393 # In Shell on input line, go to just after prompt
394 insertpt = int(self.text.index("iomark").split(".")[1])
395 else:
396 line = self.text.get("insert linestart", "insert lineend")
397 for insertpt in range(len(line)):
398 if line[insertpt] not in (' ','\t'):
399 break
400 else:
401 insertpt=len(line)
402 lineat = int(self.text.index("insert").split('.')[1])
403 if insertpt == lineat:
404 insertpt = 0
405 dest = "insert linestart+"+str(insertpt)+"c"
406 if (event.state&1) == 0:
407 # shift was not pressed
408 self.text.tag_remove("sel", "1.0", "end")
409 else:
410 if not self.text.index("sel.first"):
411 # there was no previous selection
412 self.text.mark_set("my_anchor", "insert")
413 else:
414 if self.text.compare(self.text.index("sel.first"), "<",
415 self.text.index("insert")):
416 self.text.mark_set("my_anchor", "sel.first") # extend back
417 else:
418 self.text.mark_set("my_anchor", "sel.last") # extend forward
419 first = self.text.index(dest)
420 last = self.text.index("my_anchor")
421 if self.text.compare(first,">",last):
422 first,last = last,first
423 self.text.tag_remove("sel", "1.0", "end")
424 self.text.tag_add("sel", first, last)
425 self.text.mark_set("insert", dest)
426 self.text.see("insert")
427 return "break"
429 def set_status_bar(self):
430 self.status_bar = self.MultiStatusBar(self.top)
431 sep = Frame(self.top, height=1, borderwidth=1, background='grey75')
432 if sys.platform == "darwin":
433 # Insert some padding to avoid obscuring some of the statusbar
434 # by the resize widget.
435 self.status_bar.set_label('_padding1', ' ', side=RIGHT)
436 self.status_bar.set_label('column', 'Col: ?', side=RIGHT)
437 self.status_bar.set_label('line', 'Ln: ?', side=RIGHT)
438 self.status_bar.pack(side=BOTTOM, fill=X)
439 sep.pack(side=BOTTOM, fill=X)
440 self.text.bind("<<set-line-and-column>>", self.set_line_and_column)
441 self.text.event_add("<<set-line-and-column>>",
442 "<KeyRelease>", "<ButtonRelease>")
443 self.text.after_idle(self.set_line_and_column)
445 def set_line_and_column(self, event=None):
446 line, column = self.text.index(INSERT).split('.')
447 self.status_bar.set_label('column', 'Col: %s' % column)
448 self.status_bar.set_label('line', 'Ln: %s' % line)
450 menu_specs = [
451 ("file", "_File"),
452 ("edit", "_Edit"),
453 ("format", "F_ormat"),
454 ("run", "_Run"),
455 ("options", "_Options"),
456 ("window", "_Window"),
457 ("help", "_Help"),
458 ]
461 def createmenubar(self):
462 mbar = self.menubar
463 self.menudict = menudict = {}
464 for name, label in self.menu_specs:
465 underline, label = prepstr(label)
466 postcommand = getattr(self, f'{name}_menu_postcommand', None)
467 menudict[name] = menu = Menu(mbar, name=name, tearoff=0,
468 postcommand=postcommand)
469 mbar.add_cascade(label=label, menu=menu, underline=underline)
470 if macosx.isCarbonTk():
471 # Insert the application menu
472 menudict['application'] = menu = Menu(mbar, name='apple',
473 tearoff=0)
474 mbar.add_cascade(label='IDLE', menu=menu)
475 self.fill_menus()
476 self.recent_files_menu = Menu(self.menubar, tearoff=0)
477 self.menudict['file'].insert_cascade(3, label='Recent Files',
478 underline=0,
479 menu=self.recent_files_menu)
480 self.base_helpmenu_length = self.menudict['help'].index(END)
481 self.reset_help_menu_entries()
483 def postwindowsmenu(self):
484 # Only called when Window menu exists
485 menu = self.menudict['window']
486 end = menu.index("end")
487 if end is None:
488 end = -1
489 if end > self.wmenu_end:
490 menu.delete(self.wmenu_end+1, end)
491 window.add_windows_to_menu(menu)
493 def update_menu_label(self, menu, index, label):
494 "Update label for menu item at index."
495 menuitem = self.menudict[menu]
496 menuitem.entryconfig(index, label=label)
498 def update_menu_state(self, menu, index, state):
499 "Update state for menu item at index."
500 menuitem = self.menudict[menu]
501 menuitem.entryconfig(index, state=state)
503 def handle_yview(self, event, *args):
504 "Handle scrollbar."
505 if event == 'moveto':
506 fraction = float(args[0])
507 lines = (round(self.getlineno('end') * fraction) -
508 self.getlineno('@0,0'))
509 event = 'scroll'
510 args = (lines, 'units')
511 self.text.yview(event, *args)
512 return 'break'
514 rmenu = None
516 def right_menu_event(self, event):
517 text = self.text
518 newdex = text.index(f'@{event.x},{event.y}')
519 try:
520 in_selection = (text.compare('sel.first', '<=', newdex) and
521 text.compare(newdex, '<=', 'sel.last'))
522 except TclError:
523 in_selection = False
524 if not in_selection:
525 text.tag_remove("sel", "1.0", "end")
526 text.mark_set("insert", newdex)
527 if not self.rmenu:
528 self.make_rmenu()
529 rmenu = self.rmenu
530 self.event = event
531 iswin = sys.platform[:3] == 'win'
532 if iswin:
533 text.config(cursor="arrow")
535 for item in self.rmenu_specs:
536 try:
537 label, eventname, verify_state = item
538 except ValueError: # see issue1207589
539 continue
541 if verify_state is None:
542 continue
543 state = getattr(self, verify_state)()
544 rmenu.entryconfigure(label, state=state)
546 rmenu.tk_popup(event.x_root, event.y_root)
547 if iswin:
548 self.text.config(cursor="ibeam")
549 return "break"
551 rmenu_specs = [
552 # ("Label", "<<virtual-event>>", "statefuncname"), ...
553 ("Close", "<<close-window>>", None), # Example
554 ]
556 def make_rmenu(self):
557 rmenu = Menu(self.text, tearoff=0)
558 for item in self.rmenu_specs:
559 label, eventname = item[0], item[1]
560 if label is not None:
561 def command(text=self.text, eventname=eventname):
562 text.event_generate(eventname)
563 rmenu.add_command(label=label, command=command)
564 else:
565 rmenu.add_separator()
566 self.rmenu = rmenu
568 def rmenu_check_cut(self):
569 return self.rmenu_check_copy()
571 def rmenu_check_copy(self):
572 try:
573 indx = self.text.index('sel.first')
574 except TclError:
575 return 'disabled'
576 else:
577 return 'normal' if indx else 'disabled'
579 def rmenu_check_paste(self):
580 try:
581 self.text.tk.call('tk::GetSelection', self.text, 'CLIPBOARD')
582 except TclError:
583 return 'disabled'
584 else:
585 return 'normal'
587 def about_dialog(self, event=None):
588 "Handle Help 'About IDLE' event."
589 # Synchronize with macosx.overrideRootMenu.about_dialog.
590 help_about.AboutDialog(self.top)
591 return "break"
593 def config_dialog(self, event=None):
594 "Handle Options 'Configure IDLE' event."
595 # Synchronize with macosx.overrideRootMenu.config_dialog.
596 configdialog.ConfigDialog(self.top,'Settings')
597 return "break"
599 def help_dialog(self, event=None):
600 "Handle Help 'IDLE Help' event."
601 # Synchronize with macosx.overrideRootMenu.help_dialog.
602 if self.root:
603 parent = self.root
604 else:
605 parent = self.top
606 help.show_idlehelp(parent)
607 return "break"
609 def python_docs(self, event=None):
610 if sys.platform[:3] == 'win':
611 try:
612 os.startfile(self.help_url)
613 except OSError as why:
614 messagebox.showerror(title='Document Start Failure',
615 message=str(why), parent=self.text)
616 else:
617 webbrowser.open(self.help_url)
618 return "break"
620 def cut(self,event):
621 self.text.event_generate("<<Cut>>")
622 return "break"
624 def copy(self,event):
625 if not self.text.tag_ranges("sel"):
626 # There is no selection, so do nothing and maybe interrupt.
627 return None
628 self.text.event_generate("<<Copy>>")
629 return "break"
631 def paste(self,event):
632 self.text.event_generate("<<Paste>>")
633 self.text.see("insert")
634 return "break"
636 def select_all(self, event=None):
637 self.text.tag_add("sel", "1.0", "end-1c")
638 self.text.mark_set("insert", "1.0")
639 self.text.see("insert")
640 return "break"
642 def remove_selection(self, event=None):
643 self.text.tag_remove("sel", "1.0", "end")
644 self.text.see("insert")
645 return "break"
647 def move_at_edge_if_selection(self, edge_index):
648 """Cursor move begins at start or end of selection
650 When a left/right cursor key is pressed create and return to Tkinter a
651 function which causes a cursor move from the associated edge of the
652 selection.
654 """
655 self_text_index = self.text.index
656 self_text_mark_set = self.text.mark_set
657 edges_table = ("sel.first+1c", "sel.last-1c")
658 def move_at_edge(event):
659 if (event.state & 5) == 0: # no shift(==1) or control(==4) pressed
660 try:
661 self_text_index("sel.first")
662 self_text_mark_set("insert", edges_table[edge_index])
663 except TclError:
664 pass
665 return move_at_edge
667 def del_word_left(self, event):
668 self.text.event_generate('<Meta-Delete>')
669 return "break"
671 def del_word_right(self, event):
672 self.text.event_generate('<Meta-d>')
673 return "break"
675 def find_event(self, event):
676 search.find(self.text)
677 return "break"
679 def find_again_event(self, event):
680 search.find_again(self.text)
681 return "break"
683 def find_selection_event(self, event):
684 search.find_selection(self.text)
685 return "break"
687 def find_in_files_event(self, event):
688 grep.grep(self.text, self.io, self.flist)
689 return "break"
691 def replace_event(self, event):
692 replace.replace(self.text)
693 return "break"
695 def goto_line(self, query):
696 """
697 Jump to a line in the text widget.
698 The query must be either a line number, or a string of the format
699 "line:char", where both `line` and `char` are valid character and line
700 indices present in the current Text widget.
701 """
702 # Get the total line count of the Text widget. This helps us
703 # index negatively.
704 line_count = int(self.text.index(END).split(".")[0]) - 1
706 # If, by any chance, we errored out before, don't do anything. Otherwise...
707 if query is not None:
708 # Assume we are only updating the line index.
709 line = query
710 char = "0"
711 # Check if we are character indexing as well.
712 if ":" in query:
713 line = query.split(":")[0]
714 char = query.split(":")[1]
715 # If we got a positive integer, just jump to line.
716 # And char, if we happened to change it.
717 if int(line) > 0:
718 self.text.tag_remove("sel", "1.0", "end")
719 self.text.mark_set("insert", f'{line}.{char}')
720 self.text.see("insert")
721 self.set_line_and_column()
722 else:
723 # Otherwise, add it to the total line count to index negatively.
724 # TOTAL_LINE_COUNT + (-some_line) = TOTAL_LINE_COUNT - some_line
725 # so this checks out for negative indexing functionality.
726 self.text.tag_remove("sel", "1.0", "end")
727 self.text.mark_set("insert", f'{line_count + int(line)}.{char}')
728 self.text.see("insert")
729 self.set_line_and_column()
730 return "break"
732 def goto_line_event(self, event):
733 text = self.text
734 # Get the full line count of the editor window.
735 # Note this returns the location in the format "line.char",
736 # according to index's documentation, so we need to pull the
737 # line count out later.
738 count_query = text.index(END)
739 # Get the location of the cursor in the window.
740 mark_location = text.index(INSERT)
741 # Pull the line count.
742 line_count = count_query.split(".")[0]
743 # The line count returned includes EOF. Remove it.
744 line_count = int(line_count) - 1
746 # Create dialog box clarifying the range of possible values.
747 # We will pass through the negative indexing feature as well
748 # as the current line, to make it easier to know where to jump.
749 lineno = query.Goto(
750 text, "Go To Line",
751 f"Type a line number between 1 and {str(line_count)} or use negative indexing (-1 to -{str(line_count)})\n"
752 f"You are at line {mark_location.split('.')[0]}",
753 linecount=line_count,
754 editwin=self
755 ).result
756 # Update the line.
757 self.goto_line(lineno)
758 return "break"
760 def show_code_outline_event(self, event):
761 """Event handler function for when user clicks 'Show Code Outline' in the menu bar.
763 This simply prints the function and classes containing the highlit region in the
764 PyShell window.
765 """
766 def write_to_shell(shell, text):
767 shell.resetoutput()
768 shell.text.insert(INSERT, text)
769 shell.showprompt()
771 text = self.text
772 # Check if user accidentally clicked "Show Code Outline" in PyShell.
773 if self.flist.pyshell == self:
774 write_to_shell(self, "Can't parse code from a shell window!")
775 return "break"
776 # Get lines of selection. If no line was selected, "select" entire file.
777 (line_start, line_end) = (text.index("sel.first"), text.index("sel.last"))
778 if line_start == "" and line_end == "":
779 line_start = 1
780 line_end = int(text.index(END).split(".")[0]) - 1
781 else:
782 line_start = int(line_start.split(".")[0])
783 line_end = int(line_end.split(".")[0])
785 # Process the file's source code using `ast.parse`
786 tree = ast.AST
787 try:
788 tree = ast.parse(text.get("1.0", "end"))
789 except SyntaxError:
790 write_to_shell(self.flist.pyshell, "Incorrect source code in original file!")
791 return "break"
793 # First, we'll find all the existent classes and function definitions in the program.
794 # We'll do a BFS of the tree and, for every entry, note the information about the node
795 # and its level of indentation (for the outline display later). This should be enough
796 # for us to process later.
797 visitor = ClassFuncNodeVisitor()
798 visitor.visit(tree)
799 node_list = visitor.get_node_list()
801 # Now we need to only keep code relevant to what we want to display. We'll do this
802 # by filtering for any node whose line ranges overlap with the line range of our selection.
803 # We'll also sort by the first line, since this will order the nodes depending on how they
804 # showed up in the file, which is about how it should show up in the outline later.
805 contains_selection = lambda node: node.line_start <= line_end and line_start <= node.line_end
806 outline_nodes = sorted([x for x in node_list if contains_selection(x)], key=lambda x: x.line_start)
808 # We'll use a string builder and just add everything in one go. We'll begin with the name
809 # of the file, since we'll want to print that even if there is absolutely nothing in the file
810 # but unindented code. Afterwards, print the type of definition and then the name.
811 code_outline = f"{os.path.basename(self.io.filename)}:\n"
812 for node in outline_nodes:
813 # Indent with 4 spaces depending on whatever level of indentation the node has.
814 # Add 1, because we want to separate from the filename as well.
815 #
816 # Also check if tabs are used. If they are, replace the col_offset for tabs with 4 spaces,
817 # since by default tabs only indent by one column in `ast`'s module.
818 indent = (" " * 4) * (node.indentation + 1) if self.usetabs else " " * (node.indentation + 4)
819 code_outline += indent + f"{node.type}: {node.name}\n"
820 write_to_shell(self.flist.pyshell, code_outline)
821 return "break"
823 def open_module(self):
824 """Get module name from user and open it.
826 Return module path or None for calls by open_module_browser
827 when latter is not invoked in named editor window.
828 """
829 # XXX This, open_module_browser, and open_path_browser
830 # would fit better in iomenu.IOBinding.
831 try:
832 name = self.text.get("sel.first", "sel.last").strip()
833 except TclError:
834 name = ''
835 file_path = query.ModuleName(
836 self.text, "Open Module",
837 "Enter the name of a Python module\n"
838 "to search on sys.path and open:",
839 name).result
840 if file_path is not None:
841 if self.flist:
842 self.flist.open(file_path)
843 else:
844 self.io.loadfile(file_path)
845 return file_path
847 def open_module_event(self, event):
848 self.open_module()
849 return "break"
851 def open_module_browser(self, event=None):
852 filename = self.io.filename
853 if not (self.__class__.__name__ == 'PyShellEditorWindow'
854 and filename):
855 filename = self.open_module()
856 if filename is None:
857 return "break"
858 from idlelib import browser
859 browser.ModuleBrowser(self.root, filename)
860 return "break"
862 def open_path_browser(self, event=None):
863 from idlelib import pathbrowser
864 pathbrowser.PathBrowser(self.root)
865 return "break"
867 def open_turtle_demo(self, event = None):
868 import subprocess
870 cmd = [sys.executable,
871 '-c',
872 'from turtledemo.__main__ import main; main()']
873 subprocess.Popen(cmd, shell=False)
874 return "break"
876 def gotoline(self, lineno):
877 if lineno is not None and lineno > 0:
878 self.text.mark_set("insert", "%d.0" % lineno)
879 self.text.tag_remove("sel", "1.0", "end")
880 self.text.tag_add("sel", "insert", "insert +1l")
881 self.center()
883 def ispythonsource(self, filename):
884 if not filename or os.path.isdir(filename):
885 return True
886 base, ext = os.path.splitext(os.path.basename(filename))
887 if os.path.normcase(ext) in py_extensions:
888 return True
889 line = self.text.get('1.0', '1.0 lineend')
890 return line.startswith('#!') and 'python' in line
892 def close_hook(self):
893 if self.flist:
894 self.flist.unregister_maybe_terminate(self)
895 self.flist = None
897 def set_close_hook(self, close_hook):
898 self.close_hook = close_hook
900 def filename_change_hook(self):
901 if self.flist:
902 self.flist.filename_changed_edit(self)
903 self.saved_change_hook()
904 self.top.update_windowlist_registry(self)
905 self.ResetColorizer()
907 def _addcolorizer(self):
908 if self.color:
909 return
910 if self.ispythonsource(self.io.filename):
911 self.color = self.ColorDelegator()
912 # can add more colorizers here...
913 if self.color:
914 self.per.insertfilterafter(filter=self.color, after=self.undo)
916 def _rmcolorizer(self):
917 if not self.color:
918 return
919 self.color.removecolors()
920 self.per.removefilter(self.color)
921 self.color = None
923 def ResetColorizer(self):
924 "Update the color theme"
925 # Called from self.filename_change_hook and from configdialog.py
926 self._rmcolorizer()
927 self._addcolorizer()
928 EditorWindow.color_config(self.text)
930 if self.code_context is not None:
931 self.code_context.update_highlight_colors()
933 if self.line_numbers is not None:
934 self.line_numbers.update_colors()
936 IDENTCHARS = string.ascii_letters + string.digits + "_"
938 def colorize_syntax_error(self, text, pos):
939 text.tag_add("ERROR", pos)
940 char = text.get(pos)
941 if char and char in self.IDENTCHARS:
942 text.tag_add("ERROR", pos + " wordstart", pos)
943 if '\n' == text.get(pos): # error at line end
944 text.mark_set("insert", pos)
945 else:
946 text.mark_set("insert", pos + "+1c")
947 text.see(pos)
949 def update_cursor_blink(self):
950 "Update the cursor blink configuration."
951 cursorblink = idleConf.GetOption(
952 'main', 'EditorWindow', 'cursor-blink', type='bool')
953 if not cursorblink:
954 self.text['insertofftime'] = 0
955 else:
956 # Restore the original value
957 self.text['insertofftime'] = idleConf.blink_off_time
959 def ResetFont(self):
960 "Update the text widgets' font if it is changed"
961 # Called from configdialog.py
963 # Update the code context widget first, since its height affects
964 # the height of the text widget. This avoids double re-rendering.
965 if self.code_context is not None:
966 self.code_context.update_font()
967 # Next, update the line numbers widget, since its width affects
968 # the width of the text widget.
969 if self.line_numbers is not None:
970 self.line_numbers.update_font()
971 # Finally, update the main text widget.
972 new_font = idleConf.GetFont(self.root, 'main', 'EditorWindow')
973 self.text['font'] = new_font
974 self.set_width()
976 def RemoveKeybindings(self):
977 "Remove the keybindings before they are changed."
978 # Called from configdialog.py
979 self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet()
980 for event, keylist in keydefs.items():
981 self.text.event_delete(event, *keylist)
982 for extensionName in self.get_standard_extension_names():
983 xkeydefs = idleConf.GetExtensionBindings(extensionName)
984 if xkeydefs:
985 for event, keylist in xkeydefs.items():
986 self.text.event_delete(event, *keylist)
988 def ApplyKeybindings(self):
989 "Update the keybindings after they are changed"
990 # Called from configdialog.py
991 self.mainmenu.default_keydefs = keydefs = idleConf.GetCurrentKeySet()
992 self.apply_bindings()
993 for extensionName in self.get_standard_extension_names():
994 xkeydefs = idleConf.GetExtensionBindings(extensionName)
995 if xkeydefs:
996 self.apply_bindings(xkeydefs)
997 #update menu accelerators
998 menuEventDict = {}
999 for menu in self.mainmenu.menudefs:
1000 menuEventDict[menu[0]] = {}
1001 for item in menu[1]:
1002 if item:
1003 menuEventDict[menu[0]][prepstr(item[0])[1]] = item[1]
1004 for menubarItem in self.menudict:
1005 menu = self.menudict[menubarItem]
1006 end = menu.index(END)
1007 if end is None:
1008 # Skip empty menus
1009 continue
1010 end += 1
1011 for index in range(0, end):
1012 if menu.type(index) == 'command':
1013 accel = menu.entrycget(index, 'accelerator')
1014 if accel:
1015 itemName = menu.entrycget(index, 'label')
1016 event = ''
1017 if menubarItem in menuEventDict:
1018 if itemName in menuEventDict[menubarItem]:
1019 event = menuEventDict[menubarItem][itemName]
1020 if event:
1021 accel = get_accelerator(keydefs, event)
1022 menu.entryconfig(index, accelerator=accel)
1024 def set_notabs_indentwidth(self):
1025 "Update the indentwidth if changed and not using tabs in this window"
1026 # Called from configdialog.py
1027 if not self.usetabs:
1028 self.indentwidth = idleConf.GetOption('main', 'Indent','num-spaces',
1029 type='int')
1031 def reset_help_menu_entries(self):
1032 "Update the additional help entries on the Help menu"
1033 help_list = idleConf.GetAllExtraHelpSourcesList()
1034 helpmenu = self.menudict['help']
1035 # first delete the extra help entries, if any
1036 helpmenu_length = helpmenu.index(END)
1037 if helpmenu_length > self.base_helpmenu_length:
1038 helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length)
1039 # then rebuild them
1040 if help_list:
1041 helpmenu.add_separator()
1042 for entry in help_list:
1043 cmd = self.__extra_help_callback(entry[1])
1044 helpmenu.add_command(label=entry[0], command=cmd)
1045 # and update the menu dictionary
1046 self.menudict['help'] = helpmenu
1048 def __extra_help_callback(self, helpfile):
1049 "Create a callback with the helpfile value frozen at definition time"
1050 def display_extra_help(helpfile=helpfile):
1051 if not helpfile.startswith(('www', 'http')):
1052 helpfile = os.path.normpath(helpfile)
1053 if sys.platform[:3] == 'win':
1054 try:
1055 os.startfile(helpfile)
1056 except OSError as why:
1057 messagebox.showerror(title='Document Start Failure',
1058 message=str(why), parent=self.text)
1059 else:
1060 webbrowser.open(helpfile)
1061 return display_extra_help
1063 def update_recent_files_list(self, new_file=None):
1064 "Load and update the recent files list and menus"
1065 # TODO: move to iomenu.
1066 rf_list = []
1067 file_path = self.recent_files_path
1068 if file_path and os.path.exists(file_path):
1069 with open(file_path, 'r',
1070 encoding='utf_8', errors='replace') as rf_list_file:
1071 rf_list = rf_list_file.readlines()
1072 if new_file:
1073 new_file = os.path.abspath(new_file) + '\n'
1074 if new_file in rf_list:
1075 rf_list.remove(new_file) # move to top
1076 rf_list.insert(0, new_file)
1077 # clean and save the recent files list
1078 bad_paths = []
1079 for path in rf_list:
1080 if '\0' in path or not os.path.exists(path[0:-1]):
1081 bad_paths.append(path)
1082 rf_list = [path for path in rf_list if path not in bad_paths]
1083 ulchars = "1234567890ABCDEFGHIJK"
1084 rf_list = rf_list[0:len(ulchars)]
1085 if file_path:
1086 try:
1087 with open(file_path, 'w',
1088 encoding='utf_8', errors='replace') as rf_file:
1089 rf_file.writelines(rf_list)
1090 except OSError as err:
1091 if not getattr(self.root, "recentfiles_message", False):
1092 self.root.recentfiles_message = True
1093 messagebox.showwarning(title='IDLE Warning',
1094 message="Cannot save Recent Files list to disk.\n"
1095 f" {err}\n"
1096 "Select OK to continue.",
1097 parent=self.text)
1098 # for each edit window instance, construct the recent files menu
1099 for instance in self.top.instance_dict:
1100 menu = instance.recent_files_menu
1101 menu.delete(0, END) # clear, and rebuild:
1102 for i, file_name in enumerate(rf_list):
1103 file_name = file_name.rstrip() # zap \n
1104 callback = instance.__recent_file_callback(file_name)
1105 menu.add_command(label=ulchars[i] + " " + file_name,
1106 command=callback,
1107 underline=0)
1109 def __recent_file_callback(self, file_name):
1110 def open_recent_file(fn_closure=file_name):
1111 self.io.open(editFile=fn_closure)
1112 return open_recent_file
1114 def saved_change_hook(self):
1115 short = self.short_title()
1116 long = self.long_title()
1117 if short and long:
1118 title = short + " - " + long + _py_version
1119 elif short:
1120 title = short
1121 elif long:
1122 title = long
1123 else:
1124 title = "untitled"
1125 icon = short or long or title
1126 if not self.get_saved():
1127 title = "*%s*" % title
1128 icon = "*%s" % icon
1129 self.top.wm_title(title)
1130 self.top.wm_iconname(icon)
1132 def get_saved(self):
1133 return self.undo.get_saved()
1135 def set_saved(self, flag):
1136 self.undo.set_saved(flag)
1138 def reset_undo(self):
1139 self.undo.reset_undo()
1141 def short_title(self):
1142 filename = self.io.filename
1143 return os.path.basename(filename) if filename else "untitled"
1145 def long_title(self):
1146 return self.io.filename or ""
1148 def center_insert_event(self, event):
1149 self.center()
1150 return "break"
1152 def center(self, mark="insert"):
1153 text = self.text
1154 top, bot = self.getwindowlines()
1155 lineno = self.getlineno(mark)
1156 height = bot - top
1157 newtop = max(1, lineno - height//2)
1158 text.yview(float(newtop))
1160 def getwindowlines(self):
1161 text = self.text
1162 top = self.getlineno("@0,0")
1163 bot = self.getlineno("@0,65535")
1164 if top == bot and text.winfo_height() == 1:
1165 # Geometry manager hasn't run yet
1166 height = int(text['height'])
1167 bot = top + height - 1
1168 return top, bot
1170 def getlineno(self, mark="insert"):
1171 text = self.text
1172 return int(float(text.index(mark)))
1174 def get_geometry(self):
1175 "Return (width, height, x, y)"
1176 geom = self.top.wm_geometry()
1177 m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom)
1178 return list(map(int, m.groups()))
1180 def close_event(self, event):
1181 self.close()
1182 return "break"
1184 def maybesave(self):
1185 if self.io:
1186 if not self.get_saved():
1187 if self.top.state()!='normal':
1188 self.top.deiconify()
1189 self.top.lower()
1190 self.top.lift()
1191 return self.io.maybesave()
1193 def close(self):
1194 try:
1195 reply = self.maybesave()
1196 if str(reply) != "cancel":
1197 self._close()
1198 return reply
1199 except AttributeError: # bpo-35379: close called twice
1200 pass
1202 def _close(self):
1203 if self.io.filename:
1204 self.update_recent_files_list(new_file=self.io.filename)
1205 window.unregister_callback(self.postwindowsmenu)
1206 self.unload_extensions()
1207 self.io.close()
1208 self.io = None
1209 self.undo = None
1210 if self.color:
1211 self.color.close()
1212 self.color = None
1213 self.text = None
1214 self.tkinter_vars = None
1215 self.per.close()
1216 self.per = None
1217 self.top.destroy()
1218 if self.close_hook:
1219 # unless override: unregister from flist, terminate if last window
1220 self.close_hook()
1222 def load_extensions(self):
1223 self.extensions = {}
1224 self.load_standard_extensions()
1226 def unload_extensions(self):
1227 for ins in list(self.extensions.values()):
1228 if hasattr(ins, "close"):
1229 ins.close()
1230 self.extensions = {}
1232 def load_standard_extensions(self):
1233 for name in self.get_standard_extension_names():
1234 try:
1235 self.load_extension(name)
1236 except:
1237 print("Failed to load extension", repr(name))
1238 traceback.print_exc()
1240 def get_standard_extension_names(self):
1241 return idleConf.GetExtensions(editor_only=True)
1243 extfiles = { # Map built-in config-extension section names to file names.
1244 'ZzDummy': 'zzdummy',
1245 }
1247 def load_extension(self, name):
1248 fname = self.extfiles.get(name, name)
1249 try:
1250 try:
1251 mod = importlib.import_module('.' + fname, package=__package__)
1252 except (ImportError, TypeError):
1253 mod = importlib.import_module(fname)
1254 except ImportError:
1255 print("\nFailed to import extension: ", name)
1256 raise
1257 cls = getattr(mod, name)
1258 keydefs = idleConf.GetExtensionBindings(name)
1259 if hasattr(cls, "menudefs"):
1260 self.fill_menus(cls.menudefs, keydefs)
1261 ins = cls(self)
1262 self.extensions[name] = ins
1263 if keydefs:
1264 self.apply_bindings(keydefs)
1265 for vevent in keydefs:
1266 methodname = vevent.replace("-", "_")
1267 while methodname[:1] == '<':
1268 methodname = methodname[1:]
1269 while methodname[-1:] == '>':
1270 methodname = methodname[:-1]
1271 methodname = methodname + "_event"
1272 if hasattr(ins, methodname):
1273 self.text.bind(vevent, getattr(ins, methodname))
1275 def apply_bindings(self, keydefs=None):
1276 if keydefs is None:
1277 keydefs = self.mainmenu.default_keydefs
1278 text = self.text
1279 text.keydefs = keydefs
1280 for event, keylist in keydefs.items():
1281 if keylist:
1282 text.event_add(event, *keylist)
1284 def fill_menus(self, menudefs=None, keydefs=None):
1285 """Add appropriate entries to the menus and submenus
1287 Menus that are absent or None in self.menudict are ignored.
1288 """
1289 if menudefs is None:
1290 menudefs = self.mainmenu.menudefs
1291 if keydefs is None:
1292 keydefs = self.mainmenu.default_keydefs
1293 menudict = self.menudict
1294 text = self.text
1295 for mname, entrylist in menudefs:
1296 menu = menudict.get(mname)
1297 if not menu:
1298 continue
1299 for entry in entrylist:
1300 if not entry:
1301 menu.add_separator()
1302 else:
1303 label, eventname = entry
1304 checkbutton = (label[:1] == '!')
1305 if checkbutton:
1306 label = label[1:]
1307 underline, label = prepstr(label)
1308 accelerator = get_accelerator(keydefs, eventname)
1309 def command(text=text, eventname=eventname):
1310 text.event_generate(eventname)
1311 if checkbutton:
1312 var = self.get_var_obj(eventname, BooleanVar)
1313 menu.add_checkbutton(label=label, underline=underline,
1314 command=command, accelerator=accelerator,
1315 variable=var)
1316 else:
1317 menu.add_command(label=label, underline=underline,
1318 command=command,
1319 accelerator=accelerator)
1321 def getvar(self, name):
1322 var = self.get_var_obj(name)
1323 if var:
1324 value = var.get()
1325 return value
1326 else:
1327 raise NameError(name)
1329 def setvar(self, name, value, vartype=None):
1330 var = self.get_var_obj(name, vartype)
1331 if var:
1332 var.set(value)
1333 else:
1334 raise NameError(name)
1336 def get_var_obj(self, name, vartype=None):
1337 var = self.tkinter_vars.get(name)
1338 if not var and vartype:
1339 # create a Tkinter variable object with self.text as master:
1340 self.tkinter_vars[name] = var = vartype(self.text)
1341 return var
1343 # Tk implementations of "virtual text methods" -- each platform
1344 # reusing IDLE's support code needs to define these for its GUI's
1345 # flavor of widget.
1347 # Is character at text_index in a Python string? Return 0 for
1348 # "guaranteed no", true for anything else. This info is expensive
1349 # to compute ab initio, but is probably already known by the
1350 # platform's colorizer.
1352 def is_char_in_string(self, text_index):
1353 if self.color:
1354 # Return true iff colorizer hasn't (re)gotten this far
1355 # yet, or the character is tagged as being in a string
1356 return self.text.tag_prevrange("TODO", text_index) or \
1357 "STRING" in self.text.tag_names(text_index)
1358 else:
1359 # The colorizer is missing: assume the worst
1360 return 1
1362 # If a selection is defined in the text widget, return (start,
1363 # end) as Tkinter text indices, otherwise return (None, None)
1364 def get_selection_indices(self):
1365 try:
1366 first = self.text.index("sel.first")
1367 last = self.text.index("sel.last")
1368 return first, last
1369 except TclError:
1370 return None, None
1372 # Return the text widget's current view of what a tab stop means
1373 # (equivalent width in spaces).
1375 def get_tk_tabwidth(self):
1376 current = self.text['tabs'] or TK_TABWIDTH_DEFAULT
1377 return int(current)
1379 # Set the text widget's current view of what a tab stop means.
1381 def set_tk_tabwidth(self, newtabwidth):
1382 text = self.text
1383 if self.get_tk_tabwidth() != newtabwidth:
1384 # Set text widget tab width
1385 pixels = text.tk.call("font", "measure", text["font"],
1386 "-displayof", text.master,
1387 "n" * newtabwidth)
1388 text.configure(tabs=pixels)
1390### begin autoindent code ### (configuration was moved to beginning of class)
1392 def set_indentation_params(self, is_py_src, guess=True):
1393 if is_py_src and guess:
1394 i = self.guess_indent()
1395 if 2 <= i <= 8:
1396 self.indentwidth = i
1397 if self.indentwidth != self.tabwidth:
1398 self.usetabs = False
1399 self.set_tk_tabwidth(self.tabwidth)
1401 def smart_backspace_event(self, event):
1402 text = self.text
1403 first, last = self.get_selection_indices()
1404 if first and last:
1405 text.delete(first, last)
1406 text.mark_set("insert", first)
1407 return "break"
1408 # Delete whitespace left, until hitting a real char or closest
1409 # preceding virtual tab stop.
1410 chars = text.get("insert linestart", "insert")
1411 if chars == '':
1412 if text.compare("insert", ">", "1.0"):
1413 # easy: delete preceding newline
1414 text.delete("insert-1c")
1415 else:
1416 text.bell() # at start of buffer
1417 return "break"
1418 if chars[-1] not in " \t":
1419 # easy: delete preceding real char
1420 text.delete("insert-1c")
1421 return "break"
1422 # Ick. It may require *inserting* spaces if we back up over a
1423 # tab character! This is written to be clear, not fast.
1424 tabwidth = self.tabwidth
1425 have = len(chars.expandtabs(tabwidth))
1426 assert have > 0
1427 want = ((have - 1) // self.indentwidth) * self.indentwidth
1428 # Debug prompt is multilined....
1429 ncharsdeleted = 0
1430 while True:
1431 chars = chars[:-1]
1432 ncharsdeleted = ncharsdeleted + 1
1433 have = len(chars.expandtabs(tabwidth))
1434 if have <= want or chars[-1] not in " \t":
1435 break
1436 text.undo_block_start()
1437 text.delete("insert-%dc" % ncharsdeleted, "insert")
1438 if have < want:
1439 text.insert("insert", ' ' * (want - have),
1440 self.user_input_insert_tags)
1441 text.undo_block_stop()
1442 return "break"
1444 def smart_indent_event(self, event):
1445 # if intraline selection:
1446 # delete it
1447 # elif multiline selection:
1448 # do indent-region
1449 # else:
1450 # indent one level
1451 text = self.text
1452 first, last = self.get_selection_indices()
1453 text.undo_block_start()
1454 try:
1455 if first and last:
1456 if index2line(first) != index2line(last):
1457 return self.fregion.indent_region_event(event)
1458 text.delete(first, last)
1459 text.mark_set("insert", first)
1460 prefix = text.get("insert linestart", "insert")
1461 raw, effective = get_line_indent(prefix, self.tabwidth)
1462 if raw == len(prefix):
1463 # only whitespace to the left
1464 self.reindent_to(effective + self.indentwidth)
1465 else:
1466 # tab to the next 'stop' within or to right of line's text:
1467 if self.usetabs:
1468 pad = '\t'
1469 else:
1470 effective = len(prefix.expandtabs(self.tabwidth))
1471 n = self.indentwidth
1472 pad = ' ' * (n - effective % n)
1473 text.insert("insert", pad, self.user_input_insert_tags)
1474 text.see("insert")
1475 return "break"
1476 finally:
1477 text.undo_block_stop()
1479 def newline_and_indent_event(self, event):
1480 """Insert a newline and indentation after Enter keypress event.
1482 Properly position the cursor on the new line based on information
1483 from the current line. This takes into account if the current line
1484 is a shell prompt, is empty, has selected text, contains a block
1485 opener, contains a block closer, is a continuation line, or
1486 is inside a string.
1487 """
1488 text = self.text
1489 first, last = self.get_selection_indices()
1490 text.undo_block_start()
1491 try: # Close undo block and expose new line in finally clause.
1492 if first and last:
1493 text.delete(first, last)
1494 text.mark_set("insert", first)
1495 line = text.get("insert linestart", "insert")
1497 # Count leading whitespace for indent size.
1498 i, n = 0, len(line)
1499 while i < n and line[i] in " \t":
1500 i += 1
1501 if i == n:
1502 # The cursor is in or at leading indentation in a continuation
1503 # line; just inject an empty line at the start.
1504 text.insert("insert linestart", '\n',
1505 self.user_input_insert_tags)
1506 return "break"
1507 indent = line[:i]
1509 # Strip whitespace before insert point unless it's in the prompt.
1510 i = 0
1511 while line and line[-1] in " \t":
1512 line = line[:-1]
1513 i += 1
1514 if i:
1515 text.delete("insert - %d chars" % i, "insert")
1517 # Strip whitespace after insert point.
1518 while text.get("insert") in " \t":
1519 text.delete("insert")
1521 # Insert new line.
1522 text.insert("insert", '\n', self.user_input_insert_tags)
1524 # Adjust indentation for continuations and block open/close.
1525 # First need to find the last statement.
1526 lno = index2line(text.index('insert'))
1527 y = pyparse.Parser(self.indentwidth, self.tabwidth)
1528 if not self.prompt_last_line:
1529 for context in self.num_context_lines:
1530 startat = max(lno - context, 1)
1531 startatindex = repr(startat) + ".0"
1532 rawtext = text.get(startatindex, "insert")
1533 y.set_code(rawtext)
1534 bod = y.find_good_parse_start(
1535 self._build_char_in_string_func(startatindex))
1536 if bod is not None or startat == 1:
1537 break
1538 y.set_lo(bod or 0)
1539 else:
1540 r = text.tag_prevrange("console", "insert")
1541 if r:
1542 startatindex = r[1]
1543 else:
1544 startatindex = "1.0"
1545 rawtext = text.get(startatindex, "insert")
1546 y.set_code(rawtext)
1547 y.set_lo(0)
1549 c = y.get_continuation_type()
1550 if c != pyparse.C_NONE:
1551 # The current statement hasn't ended yet.
1552 if c == pyparse.C_STRING_FIRST_LINE:
1553 # After the first line of a string do not indent at all.
1554 pass
1555 elif c == pyparse.C_STRING_NEXT_LINES:
1556 # Inside a string which started before this line;
1557 # just mimic the current indent.
1558 text.insert("insert", indent, self.user_input_insert_tags)
1559 elif c == pyparse.C_BRACKET:
1560 # Line up with the first (if any) element of the
1561 # last open bracket structure; else indent one
1562 # level beyond the indent of the line with the
1563 # last open bracket.
1564 self.reindent_to(y.compute_bracket_indent())
1565 elif c == pyparse.C_BACKSLASH:
1566 # If more than one line in this statement already, just
1567 # mimic the current indent; else if initial line
1568 # has a start on an assignment stmt, indent to
1569 # beyond leftmost =; else to beyond first chunk of
1570 # non-whitespace on initial line.
1571 if y.get_num_lines_in_stmt() > 1:
1572 text.insert("insert", indent,
1573 self.user_input_insert_tags)
1574 else:
1575 self.reindent_to(y.compute_backslash_indent())
1576 else:
1577 assert 0, "bogus continuation type %r" % (c,)
1578 return "break"
1580 # This line starts a brand new statement; indent relative to
1581 # indentation of initial line of closest preceding
1582 # interesting statement.
1583 indent = y.get_base_indent_string()
1584 text.insert("insert", indent, self.user_input_insert_tags)
1585 if y.is_block_opener():
1586 self.smart_indent_event(event)
1587 elif indent and y.is_block_closer():
1588 self.smart_backspace_event(event)
1589 return "break"
1590 finally:
1591 text.see("insert")
1592 text.undo_block_stop()
1594 # Our editwin provides an is_char_in_string function that works
1595 # with a Tk text index, but PyParse only knows about offsets into
1596 # a string. This builds a function for PyParse that accepts an
1597 # offset.
1599 def _build_char_in_string_func(self, startindex):
1600 def inner(offset, _startindex=startindex,
1601 _icis=self.is_char_in_string):
1602 return _icis(_startindex + "+%dc" % offset)
1603 return inner
1605 # XXX this isn't bound to anything -- see tabwidth comments
1606## def change_tabwidth_event(self, event):
1607## new = self._asktabwidth()
1608## if new != self.tabwidth:
1609## self.tabwidth = new
1610## self.set_indentation_params(0, guess=0)
1611## return "break"
1613 # Make string that displays as n leading blanks.
1615 def _make_blanks(self, n):
1616 if self.usetabs:
1617 ntabs, nspaces = divmod(n, self.tabwidth)
1618 return '\t' * ntabs + ' ' * nspaces
1619 else:
1620 return ' ' * n
1622 # Delete from beginning of line to insert point, then reinsert
1623 # column logical (meaning use tabs if appropriate) spaces.
1625 def reindent_to(self, column):
1626 text = self.text
1627 text.undo_block_start()
1628 if text.compare("insert linestart", "!=", "insert"):
1629 text.delete("insert linestart", "insert")
1630 if column:
1631 text.insert("insert", self._make_blanks(column),
1632 self.user_input_insert_tags)
1633 text.undo_block_stop()
1635 # Guess indentwidth from text content.
1636 # Return guessed indentwidth. This should not be believed unless
1637 # it's in a reasonable range (e.g., it will be 0 if no indented
1638 # blocks are found).
1640 def guess_indent(self):
1641 opener, indented = IndentSearcher(self.text, self.tabwidth).run()
1642 if opener and indented:
1643 raw, indentsmall = get_line_indent(opener, self.tabwidth)
1644 raw, indentlarge = get_line_indent(indented, self.tabwidth)
1645 else:
1646 indentsmall = indentlarge = 0
1647 return indentlarge - indentsmall
1649 def toggle_line_numbers_event(self, event=None):
1650 if self.line_numbers is None:
1651 return
1653 if self.line_numbers.is_shown:
1654 self.line_numbers.hide_sidebar()
1655 menu_label = "Show"
1656 else:
1657 self.line_numbers.show_sidebar()
1658 menu_label = "Hide"
1659 self.update_menu_label(menu='options', index='*ine*umbers',
1660 label=f'{menu_label} Line Numbers')
1662# "line.col" -> line, as an int
1663def index2line(index):
1664 return int(float(index))
1667_line_indent_re = re.compile(r'[ \t]*')
1668def get_line_indent(line, tabwidth):
1669 """Return a line's indentation as (# chars, effective # of spaces).
1671 The effective # of spaces is the length after properly "expanding"
1672 the tabs into spaces, as done by str.expandtabs(tabwidth).
1673 """
1674 m = _line_indent_re.match(line) 1bcd
1675 return m.end(), len(m.group().expandtabs(tabwidth)) 1bcd
1678class IndentSearcher:
1680 # .run() chews over the Text widget, looking for a block opener
1681 # and the stmt following it. Returns a pair,
1682 # (line containing block opener, line containing stmt)
1683 # Either or both may be None.
1685 def __init__(self, text, tabwidth):
1686 self.text = text
1687 self.tabwidth = tabwidth
1688 self.i = self.finished = 0
1689 self.blkopenline = self.indentedline = None
1691 def readline(self):
1692 if self.finished:
1693 return ""
1694 i = self.i = self.i + 1
1695 mark = repr(i) + ".0"
1696 if self.text.compare(mark, ">=", "end"):
1697 return ""
1698 return self.text.get(mark, mark + " lineend+1c")
1700 def tokeneater(self, type, token, start, end, line,
1701 INDENT=tokenize.INDENT,
1702 NAME=tokenize.NAME,
1703 OPENERS=('class', 'def', 'for', 'if', 'try', 'while')):
1704 if self.finished:
1705 pass
1706 elif type == NAME and token in OPENERS:
1707 self.blkopenline = line
1708 elif type == INDENT and self.blkopenline:
1709 self.indentedline = line
1710 self.finished = 1
1712 def run(self):
1713 save_tabsize = tokenize.tabsize
1714 tokenize.tabsize = self.tabwidth
1715 try:
1716 try:
1717 tokens = tokenize.generate_tokens(self.readline)
1718 for token in tokens:
1719 self.tokeneater(*token)
1720 except (tokenize.TokenError, SyntaxError):
1721 # since we cut off the tokenizer early, we can trigger
1722 # spurious errors
1723 pass
1724 finally:
1725 tokenize.tabsize = save_tabsize
1726 return self.blkopenline, self.indentedline
1728### end autoindent code ###
1730def prepstr(s):
1731 # Helper to extract the underscore from a string, e.g.
1732 # prepstr("Co_py") returns (2, "Copy").
1733 i = s.find('_')
1734 if i >= 0:
1735 s = s[:i] + s[i+1:]
1736 return i, s
1739keynames = {
1740 'bracketleft': '[',
1741 'bracketright': ']',
1742 'slash': '/',
1743}
1745def get_accelerator(keydefs, eventname):
1746 keylist = keydefs.get(eventname)
1747 # issue10940: temporary workaround to prevent hang with OS X Cocoa Tk 8.5
1748 # if not keylist:
1749 if (not keylist) or (macosx.isCocoaTk() and eventname in {
1750 "<<open-module>>",
1751 "<<goto-line>>",
1752 "<<change-indentwidth>>"}):
1753 return ""
1754 s = keylist[0]
1755 s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s)
1756 s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s)
1757 s = re.sub("Key-", "", s)
1758 s = re.sub("Cancel","Ctrl-Break",s) # dscherer@cmu.edu
1759 s = re.sub("Control-", "Ctrl-", s)
1760 s = re.sub("-", "+", s)
1761 s = re.sub("><", " ", s)
1762 s = re.sub("<", "", s)
1763 s = re.sub(">", "", s)
1764 return s
1767def fixwordbreaks(root):
1768 # On Windows, tcl/tk breaks 'words' only on spaces, as in Command Prompt.
1769 # We want Motif style everywhere. See #21474, msg218992 and followup.
1770 tk = root.tk
1771 tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
1772 tk.call('set', 'tcl_wordchars', r'\w')
1773 tk.call('set', 'tcl_nonwordchars', r'\W')
1776def _editor_window(parent): # htest #
1777 # error if close master window first - timer event, after script
1778 root = parent
1779 fixwordbreaks(root)
1780 if sys.argv[1:]:
1781 filename = sys.argv[1]
1782 else:
1783 filename = None
1784 macosx.setupApp(root, None)
1785 edit = EditorWindow(root=root, filename=filename)
1786 text = edit.text
1787 text['height'] = 10
1788 for i in range(20):
1789 text.insert('insert', ' '*i + str(i) + '\n')
1790 # text.bind("<<close-all-windows>>", edit.close_event)
1791 # Does not stop error, neither does following
1792 # edit.text.bind("<<close-window>>", edit.close_event)
1794if __name__ == '__main__': 1794 ↛ 1795line 1794 didn't jump to line 1795, because the condition on line 1794 was never true
1795 from unittest import main
1796 main('idlelib.idle_test.test_editor', verbosity=2, exit=False)
1798 from idlelib.idle_test.htest import run
1799 run(_editor_window)