comparison CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/idlelib/editor.py @ 68:5028fdace37b

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