mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 13:41:24 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			544 lines
		
	
	
	
		
			20 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			544 lines
		
	
	
	
		
			20 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| """Line numbering implementation for IDLE as an extension.
 | |
| Includes BaseSideBar which can be extended for other sidebar based extensions
 | |
| """
 | |
| import contextlib
 | |
| import functools
 | |
| import itertools
 | |
| 
 | |
| import tkinter as tk
 | |
| from tkinter.font import Font
 | |
| from idlelib.config import idleConf
 | |
| from idlelib.delegator import Delegator
 | |
| from idlelib import macosx
 | |
| 
 | |
| 
 | |
| def get_lineno(text, index):
 | |
|     """Return the line number of an index in a Tk text widget."""
 | |
|     text_index = text.index(index)
 | |
|     return int(float(text_index)) if text_index else None
 | |
| 
 | |
| 
 | |
| def get_end_linenumber(text):
 | |
|     """Return the number of the last line in a Tk text widget."""
 | |
|     return get_lineno(text, 'end-1c')
 | |
| 
 | |
| 
 | |
| def get_displaylines(text, index):
 | |
|     """Display height, in lines, of a logical line in a Tk text widget."""
 | |
|     res = text.count(f"{index} linestart",
 | |
|                      f"{index} lineend",
 | |
|                      "displaylines")
 | |
|     return res[0] if res else 0
 | |
| 
 | |
| def get_widget_padding(widget):
 | |
|     """Get the total padding of a Tk widget, including its border."""
 | |
|     # TODO: use also in codecontext.py
 | |
|     manager = widget.winfo_manager()
 | |
|     if manager == 'pack':
 | |
|         info = widget.pack_info()
 | |
|     elif manager == 'grid':
 | |
|         info = widget.grid_info()
 | |
|     else:
 | |
|         raise ValueError(f"Unsupported geometry manager: {manager}")
 | |
| 
 | |
|     # All values are passed through getint(), since some
 | |
|     # values may be pixel objects, which can't simply be added to ints.
 | |
|     padx = sum(map(widget.tk.getint, [
 | |
|         info['padx'],
 | |
|         widget.cget('padx'),
 | |
|         widget.cget('border'),
 | |
|     ]))
 | |
|     pady = sum(map(widget.tk.getint, [
 | |
|         info['pady'],
 | |
|         widget.cget('pady'),
 | |
|         widget.cget('border'),
 | |
|     ]))
 | |
|     return padx, pady
 | |
| 
 | |
| 
 | |
| @contextlib.contextmanager
 | |
| def temp_enable_text_widget(text):
 | |
|     text.configure(state=tk.NORMAL)
 | |
|     try:
 | |
|         yield
 | |
|     finally:
 | |
|         text.configure(state=tk.DISABLED)
 | |
| 
 | |
| 
 | |
| class BaseSideBar:
 | |
|     """A base class for sidebars using Text."""
 | |
|     def __init__(self, editwin):
 | |
|         self.editwin = editwin
 | |
|         self.parent = editwin.text_frame
 | |
|         self.text = editwin.text
 | |
| 
 | |
|         self.is_shown = False
 | |
| 
 | |
|         self.main_widget = self.init_widgets()
 | |
| 
 | |
|         self.bind_events()
 | |
| 
 | |
|         self.update_font()
 | |
|         self.update_colors()
 | |
| 
 | |
|     def init_widgets(self):
 | |
|         """Initialize the sidebar's widgets, returning the main widget."""
 | |
|         raise NotImplementedError
 | |
| 
 | |
|     def update_font(self):
 | |
|         """Update the sidebar text font, usually after config changes."""
 | |
|         raise NotImplementedError
 | |
| 
 | |
|     def update_colors(self):
 | |
|         """Update the sidebar text colors, usually after config changes."""
 | |
|         raise NotImplementedError
 | |
| 
 | |
|     def grid(self):
 | |
|         """Layout the widget, always using grid layout."""
 | |
|         raise NotImplementedError
 | |
| 
 | |
|     def show_sidebar(self):
 | |
|         if not self.is_shown:
 | |
|             self.grid()
 | |
|             self.is_shown = True
 | |
| 
 | |
|     def hide_sidebar(self):
 | |
|         if self.is_shown:
 | |
|             self.main_widget.grid_forget()
 | |
|             self.is_shown = False
 | |
| 
 | |
|     def yscroll_event(self, *args, **kwargs):
 | |
|         """Hook for vertical scrolling for sub-classes to override."""
 | |
|         raise NotImplementedError
 | |
| 
 | |
|     def redirect_yscroll_event(self, *args, **kwargs):
 | |
|         """Redirect vertical scrolling to the main editor text widget.
 | |
| 
 | |
|         The scroll bar is also updated.
 | |
|         """
 | |
|         self.editwin.vbar.set(*args)
 | |
|         return self.yscroll_event(*args, **kwargs)
 | |
| 
 | |
|     def redirect_focusin_event(self, event):
 | |
|         """Redirect focus-in events to the main editor text widget."""
 | |
|         self.text.focus_set()
 | |
|         return 'break'
 | |
| 
 | |
|     def redirect_mousebutton_event(self, event, event_name):
 | |
|         """Redirect mouse button events to the main editor text widget."""
 | |
|         self.text.focus_set()
 | |
|         self.text.event_generate(event_name, x=0, y=event.y)
 | |
|         return 'break'
 | |
| 
 | |
|     def redirect_mousewheel_event(self, event):
 | |
|         """Redirect mouse wheel events to the editwin text widget."""
 | |
|         self.text.event_generate('<MouseWheel>',
 | |
|                                  x=0, y=event.y, delta=event.delta)
 | |
|         return 'break'
 | |
| 
 | |
|     def bind_events(self):
 | |
|         self.text['yscrollcommand'] = self.redirect_yscroll_event
 | |
| 
 | |
|         # Ensure focus is always redirected to the main editor text widget.
 | |
|         self.main_widget.bind('<FocusIn>', self.redirect_focusin_event)
 | |
| 
 | |
|         # Redirect mouse scrolling to the main editor text widget.
 | |
|         #
 | |
|         # Note that without this, scrolling with the mouse only scrolls
 | |
|         # the line numbers.
 | |
|         self.main_widget.bind('<MouseWheel>', self.redirect_mousewheel_event)
 | |
| 
 | |
|         # Redirect mouse button events to the main editor text widget,
 | |
|         # except for the left mouse button (1).
 | |
|         #
 | |
|         # Note: X-11 sends Button-4 and Button-5 events for the scroll wheel.
 | |
|         def bind_mouse_event(event_name, target_event_name):
 | |
|             handler = functools.partial(self.redirect_mousebutton_event,
 | |
|                                         event_name=target_event_name)
 | |
|             self.main_widget.bind(event_name, handler)
 | |
| 
 | |
|         for button in [2, 3, 4, 5]:
 | |
|             for event_name in (f'<Button-{button}>',
 | |
|                                f'<ButtonRelease-{button}>',
 | |
|                                f'<B{button}-Motion>',
 | |
|                                ):
 | |
|                 bind_mouse_event(event_name, target_event_name=event_name)
 | |
| 
 | |
|             # Convert double- and triple-click events to normal click events,
 | |
|             # since event_generate() doesn't allow generating such events.
 | |
|             for event_name in (f'<Double-Button-{button}>',
 | |
|                                f'<Triple-Button-{button}>',
 | |
|                                ):
 | |
|                 bind_mouse_event(event_name,
 | |
|                                  target_event_name=f'<Button-{button}>')
 | |
| 
 | |
|         # start_line is set upon <Button-1> to allow selecting a range of rows
 | |
|         # by dragging.  It is cleared upon <ButtonRelease-1>.
 | |
|         start_line = None
 | |
| 
 | |
|         # last_y is initially set upon <B1-Leave> and is continuously updated
 | |
|         # upon <B1-Motion>, until <B1-Enter> or the mouse button is released.
 | |
|         # It is used in text_auto_scroll(), which is called repeatedly and
 | |
|         # does have a mouse event available.
 | |
|         last_y = None
 | |
| 
 | |
|         # auto_scrolling_after_id is set whenever text_auto_scroll is
 | |
|         # scheduled via .after().  It is used to stop the auto-scrolling
 | |
|         # upon <B1-Enter>, as well as to avoid scheduling the function several
 | |
|         # times in parallel.
 | |
|         auto_scrolling_after_id = None
 | |
| 
 | |
|         def drag_update_selection_and_insert_mark(y_coord):
 | |
|             """Helper function for drag and selection event handlers."""
 | |
|             lineno = get_lineno(self.text, f"@0,{y_coord}")
 | |
|             a, b = sorted([start_line, lineno])
 | |
|             self.text.tag_remove("sel", "1.0", "end")
 | |
|             self.text.tag_add("sel", f"{a}.0", f"{b+1}.0")
 | |
|             self.text.mark_set("insert",
 | |
|                                f"{lineno if lineno == a else lineno + 1}.0")
 | |
| 
 | |
|         def b1_mousedown_handler(event):
 | |
|             nonlocal start_line
 | |
|             nonlocal last_y
 | |
|             start_line = int(float(self.text.index(f"@0,{event.y}")))
 | |
|             last_y = event.y
 | |
| 
 | |
|             drag_update_selection_and_insert_mark(event.y)
 | |
|         self.main_widget.bind('<Button-1>', b1_mousedown_handler)
 | |
| 
 | |
|         def b1_mouseup_handler(event):
 | |
|             # On mouse up, we're no longer dragging.  Set the shared persistent
 | |
|             # variables to None to represent this.
 | |
|             nonlocal start_line
 | |
|             nonlocal last_y
 | |
|             start_line = None
 | |
|             last_y = None
 | |
|             self.text.event_generate('<ButtonRelease-1>', x=0, y=event.y)
 | |
|         self.main_widget.bind('<ButtonRelease-1>', b1_mouseup_handler)
 | |
| 
 | |
|         def b1_drag_handler(event):
 | |
|             nonlocal last_y
 | |
|             if last_y is None:  # i.e. if not currently dragging
 | |
|                 return
 | |
|             last_y = event.y
 | |
|             drag_update_selection_and_insert_mark(event.y)
 | |
|         self.main_widget.bind('<B1-Motion>', b1_drag_handler)
 | |
| 
 | |
|         def text_auto_scroll():
 | |
|             """Mimic Text auto-scrolling when dragging outside of it."""
 | |
|             # See: https://github.com/tcltk/tk/blob/064ff9941b4b80b85916a8afe86a6c21fd388b54/library/text.tcl#L670
 | |
|             nonlocal auto_scrolling_after_id
 | |
|             y = last_y
 | |
|             if y is None:
 | |
|                 self.main_widget.after_cancel(auto_scrolling_after_id)
 | |
|                 auto_scrolling_after_id = None
 | |
|                 return
 | |
|             elif y < 0:
 | |
|                 self.text.yview_scroll(-1 + y, 'pixels')
 | |
|                 drag_update_selection_and_insert_mark(y)
 | |
|             elif y > self.main_widget.winfo_height():
 | |
|                 self.text.yview_scroll(1 + y - self.main_widget.winfo_height(),
 | |
|                                        'pixels')
 | |
|                 drag_update_selection_and_insert_mark(y)
 | |
|             auto_scrolling_after_id = \
 | |
|                 self.main_widget.after(50, text_auto_scroll)
 | |
| 
 | |
|         def b1_leave_handler(event):
 | |
|             # Schedule the initial call to text_auto_scroll(), if not already
 | |
|             # scheduled.
 | |
|             nonlocal auto_scrolling_after_id
 | |
|             if auto_scrolling_after_id is None:
 | |
|                 nonlocal last_y
 | |
|                 last_y = event.y
 | |
|                 auto_scrolling_after_id = \
 | |
|                     self.main_widget.after(0, text_auto_scroll)
 | |
|         self.main_widget.bind('<B1-Leave>', b1_leave_handler)
 | |
| 
 | |
|         def b1_enter_handler(event):
 | |
|             # Cancel the scheduling of text_auto_scroll(), if it exists.
 | |
|             nonlocal auto_scrolling_after_id
 | |
|             if auto_scrolling_after_id is not None:
 | |
|                 self.main_widget.after_cancel(auto_scrolling_after_id)
 | |
|                 auto_scrolling_after_id = None
 | |
|         self.main_widget.bind('<B1-Enter>', b1_enter_handler)
 | |
| 
 | |
| 
 | |
| class EndLineDelegator(Delegator):
 | |
|     """Generate callbacks with the current end line number.
 | |
| 
 | |
|     The provided callback is called after every insert and delete.
 | |
|     """
 | |
|     def __init__(self, changed_callback):
 | |
|         Delegator.__init__(self)
 | |
|         self.changed_callback = changed_callback
 | |
| 
 | |
|     def insert(self, index, chars, tags=None):
 | |
|         self.delegate.insert(index, chars, tags)
 | |
|         self.changed_callback(get_end_linenumber(self.delegate))
 | |
| 
 | |
|     def delete(self, index1, index2=None):
 | |
|         self.delegate.delete(index1, index2)
 | |
|         self.changed_callback(get_end_linenumber(self.delegate))
 | |
| 
 | |
| 
 | |
| class LineNumbers(BaseSideBar):
 | |
|     """Line numbers support for editor windows."""
 | |
|     def __init__(self, editwin):
 | |
|         super().__init__(editwin)
 | |
| 
 | |
|         end_line_delegator = EndLineDelegator(self.update_sidebar_text)
 | |
|         # Insert the delegator after the undo delegator, so that line numbers
 | |
|         # are properly updated after undo and redo actions.
 | |
|         self.editwin.per.insertfilterafter(end_line_delegator,
 | |
|                                            after=self.editwin.undo)
 | |
| 
 | |
|     def init_widgets(self):
 | |
|         _padx, pady = get_widget_padding(self.text)
 | |
|         self.sidebar_text = tk.Text(self.parent, width=1, wrap=tk.NONE,
 | |
|                                     padx=2, pady=pady,
 | |
|                                     borderwidth=0, highlightthickness=0)
 | |
|         self.sidebar_text.config(state=tk.DISABLED)
 | |
| 
 | |
|         self.prev_end = 1
 | |
|         self._sidebar_width_type = type(self.sidebar_text['width'])
 | |
|         with temp_enable_text_widget(self.sidebar_text):
 | |
|             self.sidebar_text.insert('insert', '1', 'linenumber')
 | |
|         self.sidebar_text.config(takefocus=False, exportselection=False)
 | |
|         self.sidebar_text.tag_config('linenumber', justify=tk.RIGHT)
 | |
| 
 | |
|         end = get_end_linenumber(self.text)
 | |
|         self.update_sidebar_text(end)
 | |
| 
 | |
|         return self.sidebar_text
 | |
| 
 | |
|     def grid(self):
 | |
|         self.sidebar_text.grid(row=1, column=0, sticky=tk.NSEW)
 | |
| 
 | |
|     def update_font(self):
 | |
|         font = idleConf.GetFont(self.text, 'main', 'EditorWindow')
 | |
|         self.sidebar_text['font'] = font
 | |
| 
 | |
|     def update_colors(self):
 | |
|         """Update the sidebar text colors, usually after config changes."""
 | |
|         colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber')
 | |
|         foreground = colors['foreground']
 | |
|         background = colors['background']
 | |
|         self.sidebar_text.config(
 | |
|             fg=foreground, bg=background,
 | |
|             selectforeground=foreground, selectbackground=background,
 | |
|             inactiveselectbackground=background,
 | |
|         )
 | |
| 
 | |
|     def update_sidebar_text(self, end):
 | |
|         """
 | |
|         Perform the following action:
 | |
|         Each line sidebar_text contains the linenumber for that line
 | |
|         Synchronize with editwin.text so that both sidebar_text and
 | |
|         editwin.text contain the same number of lines"""
 | |
|         if end == self.prev_end:
 | |
|             return
 | |
| 
 | |
|         width_difference = len(str(end)) - len(str(self.prev_end))
 | |
|         if width_difference:
 | |
|             cur_width = int(float(self.sidebar_text['width']))
 | |
|             new_width = cur_width + width_difference
 | |
|             self.sidebar_text['width'] = self._sidebar_width_type(new_width)
 | |
| 
 | |
|         with temp_enable_text_widget(self.sidebar_text):
 | |
|             if end > self.prev_end:
 | |
|                 new_text = '\n'.join(itertools.chain(
 | |
|                     [''],
 | |
|                     map(str, range(self.prev_end + 1, end + 1)),
 | |
|                 ))
 | |
|                 self.sidebar_text.insert(f'end -1c', new_text, 'linenumber')
 | |
|             else:
 | |
|                 self.sidebar_text.delete(f'{end+1}.0 -1c', 'end -1c')
 | |
| 
 | |
|         self.prev_end = end
 | |
| 
 | |
|     def yscroll_event(self, *args, **kwargs):
 | |
|         self.sidebar_text.yview_moveto(args[0])
 | |
|         return 'break'
 | |
| 
 | |
| 
 | |
| class WrappedLineHeightChangeDelegator(Delegator):
 | |
|     def __init__(self, callback):
 | |
|         """
 | |
|         callback - Callable, will be called when an insert, delete or replace
 | |
|                    action on the text widget may require updating the shell
 | |
|                    sidebar.
 | |
|         """
 | |
|         Delegator.__init__(self)
 | |
|         self.callback = callback
 | |
| 
 | |
|     def insert(self, index, chars, tags=None):
 | |
|         is_single_line = '\n' not in chars
 | |
|         if is_single_line:
 | |
|             before_displaylines = get_displaylines(self, index)
 | |
| 
 | |
|         self.delegate.insert(index, chars, tags)
 | |
| 
 | |
|         if is_single_line:
 | |
|             after_displaylines = get_displaylines(self, index)
 | |
|             if after_displaylines == before_displaylines:
 | |
|                 return  # no need to update the sidebar
 | |
| 
 | |
|         self.callback()
 | |
| 
 | |
|     def delete(self, index1, index2=None):
 | |
|         if index2 is None:
 | |
|             index2 = index1 + "+1c"
 | |
|         is_single_line = get_lineno(self, index1) == get_lineno(self, index2)
 | |
|         if is_single_line:
 | |
|             before_displaylines = get_displaylines(self, index1)
 | |
| 
 | |
|         self.delegate.delete(index1, index2)
 | |
| 
 | |
|         if is_single_line:
 | |
|             after_displaylines = get_displaylines(self, index1)
 | |
|             if after_displaylines == before_displaylines:
 | |
|                 return  # no need to update the sidebar
 | |
| 
 | |
|         self.callback()
 | |
| 
 | |
| 
 | |
| class ShellSidebar(BaseSideBar):
 | |
|     """Sidebar for the PyShell window, for prompts etc."""
 | |
|     def __init__(self, editwin):
 | |
|         self.canvas = None
 | |
|         self.line_prompts = {}
 | |
| 
 | |
|         super().__init__(editwin)
 | |
| 
 | |
|         change_delegator = \
 | |
|             WrappedLineHeightChangeDelegator(self.change_callback)
 | |
|         # Insert the TextChangeDelegator after the last delegator, so that
 | |
|         # the sidebar reflects final changes to the text widget contents.
 | |
|         d = self.editwin.per.top
 | |
|         if d.delegate is not self.text:
 | |
|             while d.delegate is not self.editwin.per.bottom:
 | |
|                 d = d.delegate
 | |
|         self.editwin.per.insertfilterafter(change_delegator, after=d)
 | |
| 
 | |
|         self.is_shown = True
 | |
| 
 | |
|     def init_widgets(self):
 | |
|         self.canvas = tk.Canvas(self.parent, width=30,
 | |
|                                 borderwidth=0, highlightthickness=0,
 | |
|                                 takefocus=False)
 | |
|         self.update_sidebar()
 | |
|         self.grid()
 | |
|         return self.canvas
 | |
| 
 | |
|     def bind_events(self):
 | |
|         super().bind_events()
 | |
| 
 | |
|         self.main_widget.bind(
 | |
|             # AquaTk defines <2> as the right button, not <3>.
 | |
|             "<Button-2>" if macosx.isAquaTk() else "<Button-3>",
 | |
|             self.context_menu_event,
 | |
|         )
 | |
| 
 | |
|     def context_menu_event(self, event):
 | |
|         rmenu = tk.Menu(self.main_widget, tearoff=0)
 | |
|         has_selection = bool(self.text.tag_nextrange('sel', '1.0'))
 | |
|         def mkcmd(eventname):
 | |
|             return lambda: self.text.event_generate(eventname)
 | |
|         rmenu.add_command(label='Copy',
 | |
|                           command=mkcmd('<<copy>>'),
 | |
|                           state='normal' if has_selection else 'disabled')
 | |
|         rmenu.add_command(label='Copy with prompts',
 | |
|                           command=mkcmd('<<copy-with-prompts>>'),
 | |
|                           state='normal' if has_selection else 'disabled')
 | |
|         rmenu.tk_popup(event.x_root, event.y_root)
 | |
|         return "break"
 | |
| 
 | |
|     def grid(self):
 | |
|         self.canvas.grid(row=1, column=0, sticky=tk.NSEW, padx=2, pady=0)
 | |
| 
 | |
|     def change_callback(self):
 | |
|         if self.is_shown:
 | |
|             self.update_sidebar()
 | |
| 
 | |
|     def update_sidebar(self):
 | |
|         text = self.text
 | |
|         text_tagnames = text.tag_names
 | |
|         canvas = self.canvas
 | |
|         line_prompts = self.line_prompts = {}
 | |
| 
 | |
|         canvas.delete(tk.ALL)
 | |
| 
 | |
|         index = text.index("@0,0")
 | |
|         if index.split('.', 1)[1] != '0':
 | |
|             index = text.index(f'{index}+1line linestart')
 | |
|         while (lineinfo := text.dlineinfo(index)) is not None:
 | |
|             y = lineinfo[1]
 | |
|             prev_newline_tagnames = text_tagnames(f"{index} linestart -1c")
 | |
|             prompt = (
 | |
|                 '>>>' if "console" in prev_newline_tagnames else
 | |
|                 '...' if "stdin" in prev_newline_tagnames else
 | |
|                 None
 | |
|             )
 | |
|             if prompt:
 | |
|                 canvas.create_text(2, y, anchor=tk.NW, text=prompt,
 | |
|                                    font=self.font, fill=self.colors[0])
 | |
|                 lineno = get_lineno(text, index)
 | |
|                 line_prompts[lineno] = prompt
 | |
|             index = text.index(f'{index}+1line')
 | |
| 
 | |
|     def yscroll_event(self, *args, **kwargs):
 | |
|         """Redirect vertical scrolling to the main editor text widget.
 | |
| 
 | |
|         The scroll bar is also updated.
 | |
|         """
 | |
|         self.change_callback()
 | |
|         return 'break'
 | |
| 
 | |
|     def update_font(self):
 | |
|         """Update the sidebar text font, usually after config changes."""
 | |
|         font = idleConf.GetFont(self.text, 'main', 'EditorWindow')
 | |
|         tk_font = Font(self.text, font=font)
 | |
|         char_width = max(tk_font.measure(char) for char in ['>', '.'])
 | |
|         self.canvas.configure(width=char_width * 3 + 4)
 | |
|         self.font = font
 | |
|         self.change_callback()
 | |
| 
 | |
|     def update_colors(self):
 | |
|         """Update the sidebar text colors, usually after config changes."""
 | |
|         linenumbers_colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber')
 | |
|         prompt_colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'console')
 | |
|         foreground = prompt_colors['foreground']
 | |
|         background = linenumbers_colors['background']
 | |
|         self.colors = (foreground, background)
 | |
|         self.canvas.configure(background=background)
 | |
|         self.change_callback()
 | |
| 
 | |
| 
 | |
| def _linenumbers_drag_scrolling(parent):  # htest #
 | |
|     from idlelib.idle_test.test_sidebar import Dummy_editwin
 | |
| 
 | |
|     toplevel = tk.Toplevel(parent)
 | |
|     text_frame = tk.Frame(toplevel)
 | |
|     text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
 | |
|     text_frame.rowconfigure(1, weight=1)
 | |
|     text_frame.columnconfigure(1, weight=1)
 | |
| 
 | |
|     font = idleConf.GetFont(toplevel, 'main', 'EditorWindow')
 | |
|     text = tk.Text(text_frame, width=80, height=24, wrap=tk.NONE, font=font)
 | |
|     text.grid(row=1, column=1, sticky=tk.NSEW)
 | |
| 
 | |
|     editwin = Dummy_editwin(text)
 | |
|     editwin.vbar = tk.Scrollbar(text_frame)
 | |
| 
 | |
|     linenumbers = LineNumbers(editwin)
 | |
|     linenumbers.show_sidebar()
 | |
| 
 | |
|     text.insert('1.0', '\n'.join('a'*i for i in range(1, 101)))
 | |
| 
 | |
| 
 | |
| if __name__ == '__main__':
 | |
|     from unittest import main
 | |
|     main('idlelib.idle_test.test_sidebar', verbosity=2, exit=False)
 | |
| 
 | |
|     from idlelib.idle_test.htest import run
 | |
|     run(_linenumbers_drag_scrolling)
 | 
