17db96d56Sopenharmony_ci"""Line numbering implementation for IDLE as an extension.
27db96d56Sopenharmony_ciIncludes BaseSideBar which can be extended for other sidebar based extensions
37db96d56Sopenharmony_ci"""
47db96d56Sopenharmony_ciimport contextlib
57db96d56Sopenharmony_ciimport functools
67db96d56Sopenharmony_ciimport itertools
77db96d56Sopenharmony_ci
87db96d56Sopenharmony_ciimport tkinter as tk
97db96d56Sopenharmony_cifrom tkinter.font import Font
107db96d56Sopenharmony_cifrom idlelib.config import idleConf
117db96d56Sopenharmony_cifrom idlelib.delegator import Delegator
127db96d56Sopenharmony_cifrom idlelib import macosx
137db96d56Sopenharmony_ci
147db96d56Sopenharmony_ci
157db96d56Sopenharmony_cidef get_lineno(text, index):
167db96d56Sopenharmony_ci    """Return the line number of an index in a Tk text widget."""
177db96d56Sopenharmony_ci    text_index = text.index(index)
187db96d56Sopenharmony_ci    return int(float(text_index)) if text_index else None
197db96d56Sopenharmony_ci
207db96d56Sopenharmony_ci
217db96d56Sopenharmony_cidef get_end_linenumber(text):
227db96d56Sopenharmony_ci    """Return the number of the last line in a Tk text widget."""
237db96d56Sopenharmony_ci    return get_lineno(text, 'end-1c')
247db96d56Sopenharmony_ci
257db96d56Sopenharmony_ci
267db96d56Sopenharmony_cidef get_displaylines(text, index):
277db96d56Sopenharmony_ci    """Display height, in lines, of a logical line in a Tk text widget."""
287db96d56Sopenharmony_ci    res = text.count(f"{index} linestart",
297db96d56Sopenharmony_ci                     f"{index} lineend",
307db96d56Sopenharmony_ci                     "displaylines")
317db96d56Sopenharmony_ci    return res[0] if res else 0
327db96d56Sopenharmony_ci
337db96d56Sopenharmony_cidef get_widget_padding(widget):
347db96d56Sopenharmony_ci    """Get the total padding of a Tk widget, including its border."""
357db96d56Sopenharmony_ci    # TODO: use also in codecontext.py
367db96d56Sopenharmony_ci    manager = widget.winfo_manager()
377db96d56Sopenharmony_ci    if manager == 'pack':
387db96d56Sopenharmony_ci        info = widget.pack_info()
397db96d56Sopenharmony_ci    elif manager == 'grid':
407db96d56Sopenharmony_ci        info = widget.grid_info()
417db96d56Sopenharmony_ci    else:
427db96d56Sopenharmony_ci        raise ValueError(f"Unsupported geometry manager: {manager}")
437db96d56Sopenharmony_ci
447db96d56Sopenharmony_ci    # All values are passed through getint(), since some
457db96d56Sopenharmony_ci    # values may be pixel objects, which can't simply be added to ints.
467db96d56Sopenharmony_ci    padx = sum(map(widget.tk.getint, [
477db96d56Sopenharmony_ci        info['padx'],
487db96d56Sopenharmony_ci        widget.cget('padx'),
497db96d56Sopenharmony_ci        widget.cget('border'),
507db96d56Sopenharmony_ci    ]))
517db96d56Sopenharmony_ci    pady = sum(map(widget.tk.getint, [
527db96d56Sopenharmony_ci        info['pady'],
537db96d56Sopenharmony_ci        widget.cget('pady'),
547db96d56Sopenharmony_ci        widget.cget('border'),
557db96d56Sopenharmony_ci    ]))
567db96d56Sopenharmony_ci    return padx, pady
577db96d56Sopenharmony_ci
587db96d56Sopenharmony_ci
597db96d56Sopenharmony_ci@contextlib.contextmanager
607db96d56Sopenharmony_cidef temp_enable_text_widget(text):
617db96d56Sopenharmony_ci    text.configure(state=tk.NORMAL)
627db96d56Sopenharmony_ci    try:
637db96d56Sopenharmony_ci        yield
647db96d56Sopenharmony_ci    finally:
657db96d56Sopenharmony_ci        text.configure(state=tk.DISABLED)
667db96d56Sopenharmony_ci
677db96d56Sopenharmony_ci
687db96d56Sopenharmony_ciclass BaseSideBar:
697db96d56Sopenharmony_ci    """A base class for sidebars using Text."""
707db96d56Sopenharmony_ci    def __init__(self, editwin):
717db96d56Sopenharmony_ci        self.editwin = editwin
727db96d56Sopenharmony_ci        self.parent = editwin.text_frame
737db96d56Sopenharmony_ci        self.text = editwin.text
747db96d56Sopenharmony_ci
757db96d56Sopenharmony_ci        self.is_shown = False
767db96d56Sopenharmony_ci
777db96d56Sopenharmony_ci        self.main_widget = self.init_widgets()
787db96d56Sopenharmony_ci
797db96d56Sopenharmony_ci        self.bind_events()
807db96d56Sopenharmony_ci
817db96d56Sopenharmony_ci        self.update_font()
827db96d56Sopenharmony_ci        self.update_colors()
837db96d56Sopenharmony_ci
847db96d56Sopenharmony_ci    def init_widgets(self):
857db96d56Sopenharmony_ci        """Initialize the sidebar's widgets, returning the main widget."""
867db96d56Sopenharmony_ci        raise NotImplementedError
877db96d56Sopenharmony_ci
887db96d56Sopenharmony_ci    def update_font(self):
897db96d56Sopenharmony_ci        """Update the sidebar text font, usually after config changes."""
907db96d56Sopenharmony_ci        raise NotImplementedError
917db96d56Sopenharmony_ci
927db96d56Sopenharmony_ci    def update_colors(self):
937db96d56Sopenharmony_ci        """Update the sidebar text colors, usually after config changes."""
947db96d56Sopenharmony_ci        raise NotImplementedError
957db96d56Sopenharmony_ci
967db96d56Sopenharmony_ci    def grid(self):
977db96d56Sopenharmony_ci        """Layout the widget, always using grid layout."""
987db96d56Sopenharmony_ci        raise NotImplementedError
997db96d56Sopenharmony_ci
1007db96d56Sopenharmony_ci    def show_sidebar(self):
1017db96d56Sopenharmony_ci        if not self.is_shown:
1027db96d56Sopenharmony_ci            self.grid()
1037db96d56Sopenharmony_ci            self.is_shown = True
1047db96d56Sopenharmony_ci
1057db96d56Sopenharmony_ci    def hide_sidebar(self):
1067db96d56Sopenharmony_ci        if self.is_shown:
1077db96d56Sopenharmony_ci            self.main_widget.grid_forget()
1087db96d56Sopenharmony_ci            self.is_shown = False
1097db96d56Sopenharmony_ci
1107db96d56Sopenharmony_ci    def yscroll_event(self, *args, **kwargs):
1117db96d56Sopenharmony_ci        """Hook for vertical scrolling for sub-classes to override."""
1127db96d56Sopenharmony_ci        raise NotImplementedError
1137db96d56Sopenharmony_ci
1147db96d56Sopenharmony_ci    def redirect_yscroll_event(self, *args, **kwargs):
1157db96d56Sopenharmony_ci        """Redirect vertical scrolling to the main editor text widget.
1167db96d56Sopenharmony_ci
1177db96d56Sopenharmony_ci        The scroll bar is also updated.
1187db96d56Sopenharmony_ci        """
1197db96d56Sopenharmony_ci        self.editwin.vbar.set(*args)
1207db96d56Sopenharmony_ci        return self.yscroll_event(*args, **kwargs)
1217db96d56Sopenharmony_ci
1227db96d56Sopenharmony_ci    def redirect_focusin_event(self, event):
1237db96d56Sopenharmony_ci        """Redirect focus-in events to the main editor text widget."""
1247db96d56Sopenharmony_ci        self.text.focus_set()
1257db96d56Sopenharmony_ci        return 'break'
1267db96d56Sopenharmony_ci
1277db96d56Sopenharmony_ci    def redirect_mousebutton_event(self, event, event_name):
1287db96d56Sopenharmony_ci        """Redirect mouse button events to the main editor text widget."""
1297db96d56Sopenharmony_ci        self.text.focus_set()
1307db96d56Sopenharmony_ci        self.text.event_generate(event_name, x=0, y=event.y)
1317db96d56Sopenharmony_ci        return 'break'
1327db96d56Sopenharmony_ci
1337db96d56Sopenharmony_ci    def redirect_mousewheel_event(self, event):
1347db96d56Sopenharmony_ci        """Redirect mouse wheel events to the editwin text widget."""
1357db96d56Sopenharmony_ci        self.text.event_generate('<MouseWheel>',
1367db96d56Sopenharmony_ci                                 x=0, y=event.y, delta=event.delta)
1377db96d56Sopenharmony_ci        return 'break'
1387db96d56Sopenharmony_ci
1397db96d56Sopenharmony_ci    def bind_events(self):
1407db96d56Sopenharmony_ci        self.text['yscrollcommand'] = self.redirect_yscroll_event
1417db96d56Sopenharmony_ci
1427db96d56Sopenharmony_ci        # Ensure focus is always redirected to the main editor text widget.
1437db96d56Sopenharmony_ci        self.main_widget.bind('<FocusIn>', self.redirect_focusin_event)
1447db96d56Sopenharmony_ci
1457db96d56Sopenharmony_ci        # Redirect mouse scrolling to the main editor text widget.
1467db96d56Sopenharmony_ci        #
1477db96d56Sopenharmony_ci        # Note that without this, scrolling with the mouse only scrolls
1487db96d56Sopenharmony_ci        # the line numbers.
1497db96d56Sopenharmony_ci        self.main_widget.bind('<MouseWheel>', self.redirect_mousewheel_event)
1507db96d56Sopenharmony_ci
1517db96d56Sopenharmony_ci        # Redirect mouse button events to the main editor text widget,
1527db96d56Sopenharmony_ci        # except for the left mouse button (1).
1537db96d56Sopenharmony_ci        #
1547db96d56Sopenharmony_ci        # Note: X-11 sends Button-4 and Button-5 events for the scroll wheel.
1557db96d56Sopenharmony_ci        def bind_mouse_event(event_name, target_event_name):
1567db96d56Sopenharmony_ci            handler = functools.partial(self.redirect_mousebutton_event,
1577db96d56Sopenharmony_ci                                        event_name=target_event_name)
1587db96d56Sopenharmony_ci            self.main_widget.bind(event_name, handler)
1597db96d56Sopenharmony_ci
1607db96d56Sopenharmony_ci        for button in [2, 3, 4, 5]:
1617db96d56Sopenharmony_ci            for event_name in (f'<Button-{button}>',
1627db96d56Sopenharmony_ci                               f'<ButtonRelease-{button}>',
1637db96d56Sopenharmony_ci                               f'<B{button}-Motion>',
1647db96d56Sopenharmony_ci                               ):
1657db96d56Sopenharmony_ci                bind_mouse_event(event_name, target_event_name=event_name)
1667db96d56Sopenharmony_ci
1677db96d56Sopenharmony_ci            # Convert double- and triple-click events to normal click events,
1687db96d56Sopenharmony_ci            # since event_generate() doesn't allow generating such events.
1697db96d56Sopenharmony_ci            for event_name in (f'<Double-Button-{button}>',
1707db96d56Sopenharmony_ci                               f'<Triple-Button-{button}>',
1717db96d56Sopenharmony_ci                               ):
1727db96d56Sopenharmony_ci                bind_mouse_event(event_name,
1737db96d56Sopenharmony_ci                                 target_event_name=f'<Button-{button}>')
1747db96d56Sopenharmony_ci
1757db96d56Sopenharmony_ci        # start_line is set upon <Button-1> to allow selecting a range of rows
1767db96d56Sopenharmony_ci        # by dragging.  It is cleared upon <ButtonRelease-1>.
1777db96d56Sopenharmony_ci        start_line = None
1787db96d56Sopenharmony_ci
1797db96d56Sopenharmony_ci        # last_y is initially set upon <B1-Leave> and is continuously updated
1807db96d56Sopenharmony_ci        # upon <B1-Motion>, until <B1-Enter> or the mouse button is released.
1817db96d56Sopenharmony_ci        # It is used in text_auto_scroll(), which is called repeatedly and
1827db96d56Sopenharmony_ci        # does have a mouse event available.
1837db96d56Sopenharmony_ci        last_y = None
1847db96d56Sopenharmony_ci
1857db96d56Sopenharmony_ci        # auto_scrolling_after_id is set whenever text_auto_scroll is
1867db96d56Sopenharmony_ci        # scheduled via .after().  It is used to stop the auto-scrolling
1877db96d56Sopenharmony_ci        # upon <B1-Enter>, as well as to avoid scheduling the function several
1887db96d56Sopenharmony_ci        # times in parallel.
1897db96d56Sopenharmony_ci        auto_scrolling_after_id = None
1907db96d56Sopenharmony_ci
1917db96d56Sopenharmony_ci        def drag_update_selection_and_insert_mark(y_coord):
1927db96d56Sopenharmony_ci            """Helper function for drag and selection event handlers."""
1937db96d56Sopenharmony_ci            lineno = get_lineno(self.text, f"@0,{y_coord}")
1947db96d56Sopenharmony_ci            a, b = sorted([start_line, lineno])
1957db96d56Sopenharmony_ci            self.text.tag_remove("sel", "1.0", "end")
1967db96d56Sopenharmony_ci            self.text.tag_add("sel", f"{a}.0", f"{b+1}.0")
1977db96d56Sopenharmony_ci            self.text.mark_set("insert",
1987db96d56Sopenharmony_ci                               f"{lineno if lineno == a else lineno + 1}.0")
1997db96d56Sopenharmony_ci
2007db96d56Sopenharmony_ci        def b1_mousedown_handler(event):
2017db96d56Sopenharmony_ci            nonlocal start_line
2027db96d56Sopenharmony_ci            nonlocal last_y
2037db96d56Sopenharmony_ci            start_line = int(float(self.text.index(f"@0,{event.y}")))
2047db96d56Sopenharmony_ci            last_y = event.y
2057db96d56Sopenharmony_ci
2067db96d56Sopenharmony_ci            drag_update_selection_and_insert_mark(event.y)
2077db96d56Sopenharmony_ci        self.main_widget.bind('<Button-1>', b1_mousedown_handler)
2087db96d56Sopenharmony_ci
2097db96d56Sopenharmony_ci        def b1_mouseup_handler(event):
2107db96d56Sopenharmony_ci            # On mouse up, we're no longer dragging.  Set the shared persistent
2117db96d56Sopenharmony_ci            # variables to None to represent this.
2127db96d56Sopenharmony_ci            nonlocal start_line
2137db96d56Sopenharmony_ci            nonlocal last_y
2147db96d56Sopenharmony_ci            start_line = None
2157db96d56Sopenharmony_ci            last_y = None
2167db96d56Sopenharmony_ci            self.text.event_generate('<ButtonRelease-1>', x=0, y=event.y)
2177db96d56Sopenharmony_ci        self.main_widget.bind('<ButtonRelease-1>', b1_mouseup_handler)
2187db96d56Sopenharmony_ci
2197db96d56Sopenharmony_ci        def b1_drag_handler(event):
2207db96d56Sopenharmony_ci            nonlocal last_y
2217db96d56Sopenharmony_ci            if last_y is None:  # i.e. if not currently dragging
2227db96d56Sopenharmony_ci                return
2237db96d56Sopenharmony_ci            last_y = event.y
2247db96d56Sopenharmony_ci            drag_update_selection_and_insert_mark(event.y)
2257db96d56Sopenharmony_ci        self.main_widget.bind('<B1-Motion>', b1_drag_handler)
2267db96d56Sopenharmony_ci
2277db96d56Sopenharmony_ci        def text_auto_scroll():
2287db96d56Sopenharmony_ci            """Mimic Text auto-scrolling when dragging outside of it."""
2297db96d56Sopenharmony_ci            # See: https://github.com/tcltk/tk/blob/064ff9941b4b80b85916a8afe86a6c21fd388b54/library/text.tcl#L670
2307db96d56Sopenharmony_ci            nonlocal auto_scrolling_after_id
2317db96d56Sopenharmony_ci            y = last_y
2327db96d56Sopenharmony_ci            if y is None:
2337db96d56Sopenharmony_ci                self.main_widget.after_cancel(auto_scrolling_after_id)
2347db96d56Sopenharmony_ci                auto_scrolling_after_id = None
2357db96d56Sopenharmony_ci                return
2367db96d56Sopenharmony_ci            elif y < 0:
2377db96d56Sopenharmony_ci                self.text.yview_scroll(-1 + y, 'pixels')
2387db96d56Sopenharmony_ci                drag_update_selection_and_insert_mark(y)
2397db96d56Sopenharmony_ci            elif y > self.main_widget.winfo_height():
2407db96d56Sopenharmony_ci                self.text.yview_scroll(1 + y - self.main_widget.winfo_height(),
2417db96d56Sopenharmony_ci                                       'pixels')
2427db96d56Sopenharmony_ci                drag_update_selection_and_insert_mark(y)
2437db96d56Sopenharmony_ci            auto_scrolling_after_id = \
2447db96d56Sopenharmony_ci                self.main_widget.after(50, text_auto_scroll)
2457db96d56Sopenharmony_ci
2467db96d56Sopenharmony_ci        def b1_leave_handler(event):
2477db96d56Sopenharmony_ci            # Schedule the initial call to text_auto_scroll(), if not already
2487db96d56Sopenharmony_ci            # scheduled.
2497db96d56Sopenharmony_ci            nonlocal auto_scrolling_after_id
2507db96d56Sopenharmony_ci            if auto_scrolling_after_id is None:
2517db96d56Sopenharmony_ci                nonlocal last_y
2527db96d56Sopenharmony_ci                last_y = event.y
2537db96d56Sopenharmony_ci                auto_scrolling_after_id = \
2547db96d56Sopenharmony_ci                    self.main_widget.after(0, text_auto_scroll)
2557db96d56Sopenharmony_ci        self.main_widget.bind('<B1-Leave>', b1_leave_handler)
2567db96d56Sopenharmony_ci
2577db96d56Sopenharmony_ci        def b1_enter_handler(event):
2587db96d56Sopenharmony_ci            # Cancel the scheduling of text_auto_scroll(), if it exists.
2597db96d56Sopenharmony_ci            nonlocal auto_scrolling_after_id
2607db96d56Sopenharmony_ci            if auto_scrolling_after_id is not None:
2617db96d56Sopenharmony_ci                self.main_widget.after_cancel(auto_scrolling_after_id)
2627db96d56Sopenharmony_ci                auto_scrolling_after_id = None
2637db96d56Sopenharmony_ci        self.main_widget.bind('<B1-Enter>', b1_enter_handler)
2647db96d56Sopenharmony_ci
2657db96d56Sopenharmony_ci
2667db96d56Sopenharmony_ciclass EndLineDelegator(Delegator):
2677db96d56Sopenharmony_ci    """Generate callbacks with the current end line number.
2687db96d56Sopenharmony_ci
2697db96d56Sopenharmony_ci    The provided callback is called after every insert and delete.
2707db96d56Sopenharmony_ci    """
2717db96d56Sopenharmony_ci    def __init__(self, changed_callback):
2727db96d56Sopenharmony_ci        Delegator.__init__(self)
2737db96d56Sopenharmony_ci        self.changed_callback = changed_callback
2747db96d56Sopenharmony_ci
2757db96d56Sopenharmony_ci    def insert(self, index, chars, tags=None):
2767db96d56Sopenharmony_ci        self.delegate.insert(index, chars, tags)
2777db96d56Sopenharmony_ci        self.changed_callback(get_end_linenumber(self.delegate))
2787db96d56Sopenharmony_ci
2797db96d56Sopenharmony_ci    def delete(self, index1, index2=None):
2807db96d56Sopenharmony_ci        self.delegate.delete(index1, index2)
2817db96d56Sopenharmony_ci        self.changed_callback(get_end_linenumber(self.delegate))
2827db96d56Sopenharmony_ci
2837db96d56Sopenharmony_ci
2847db96d56Sopenharmony_ciclass LineNumbers(BaseSideBar):
2857db96d56Sopenharmony_ci    """Line numbers support for editor windows."""
2867db96d56Sopenharmony_ci    def __init__(self, editwin):
2877db96d56Sopenharmony_ci        super().__init__(editwin)
2887db96d56Sopenharmony_ci
2897db96d56Sopenharmony_ci        end_line_delegator = EndLineDelegator(self.update_sidebar_text)
2907db96d56Sopenharmony_ci        # Insert the delegator after the undo delegator, so that line numbers
2917db96d56Sopenharmony_ci        # are properly updated after undo and redo actions.
2927db96d56Sopenharmony_ci        self.editwin.per.insertfilterafter(end_line_delegator,
2937db96d56Sopenharmony_ci                                           after=self.editwin.undo)
2947db96d56Sopenharmony_ci
2957db96d56Sopenharmony_ci    def init_widgets(self):
2967db96d56Sopenharmony_ci        _padx, pady = get_widget_padding(self.text)
2977db96d56Sopenharmony_ci        self.sidebar_text = tk.Text(self.parent, width=1, wrap=tk.NONE,
2987db96d56Sopenharmony_ci                                    padx=2, pady=pady,
2997db96d56Sopenharmony_ci                                    borderwidth=0, highlightthickness=0)
3007db96d56Sopenharmony_ci        self.sidebar_text.config(state=tk.DISABLED)
3017db96d56Sopenharmony_ci
3027db96d56Sopenharmony_ci        self.prev_end = 1
3037db96d56Sopenharmony_ci        self._sidebar_width_type = type(self.sidebar_text['width'])
3047db96d56Sopenharmony_ci        with temp_enable_text_widget(self.sidebar_text):
3057db96d56Sopenharmony_ci            self.sidebar_text.insert('insert', '1', 'linenumber')
3067db96d56Sopenharmony_ci        self.sidebar_text.config(takefocus=False, exportselection=False)
3077db96d56Sopenharmony_ci        self.sidebar_text.tag_config('linenumber', justify=tk.RIGHT)
3087db96d56Sopenharmony_ci
3097db96d56Sopenharmony_ci        end = get_end_linenumber(self.text)
3107db96d56Sopenharmony_ci        self.update_sidebar_text(end)
3117db96d56Sopenharmony_ci
3127db96d56Sopenharmony_ci        return self.sidebar_text
3137db96d56Sopenharmony_ci
3147db96d56Sopenharmony_ci    def grid(self):
3157db96d56Sopenharmony_ci        self.sidebar_text.grid(row=1, column=0, sticky=tk.NSEW)
3167db96d56Sopenharmony_ci
3177db96d56Sopenharmony_ci    def update_font(self):
3187db96d56Sopenharmony_ci        font = idleConf.GetFont(self.text, 'main', 'EditorWindow')
3197db96d56Sopenharmony_ci        self.sidebar_text['font'] = font
3207db96d56Sopenharmony_ci
3217db96d56Sopenharmony_ci    def update_colors(self):
3227db96d56Sopenharmony_ci        """Update the sidebar text colors, usually after config changes."""
3237db96d56Sopenharmony_ci        colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber')
3247db96d56Sopenharmony_ci        foreground = colors['foreground']
3257db96d56Sopenharmony_ci        background = colors['background']
3267db96d56Sopenharmony_ci        self.sidebar_text.config(
3277db96d56Sopenharmony_ci            fg=foreground, bg=background,
3287db96d56Sopenharmony_ci            selectforeground=foreground, selectbackground=background,
3297db96d56Sopenharmony_ci            inactiveselectbackground=background,
3307db96d56Sopenharmony_ci        )
3317db96d56Sopenharmony_ci
3327db96d56Sopenharmony_ci    def update_sidebar_text(self, end):
3337db96d56Sopenharmony_ci        """
3347db96d56Sopenharmony_ci        Perform the following action:
3357db96d56Sopenharmony_ci        Each line sidebar_text contains the linenumber for that line
3367db96d56Sopenharmony_ci        Synchronize with editwin.text so that both sidebar_text and
3377db96d56Sopenharmony_ci        editwin.text contain the same number of lines"""
3387db96d56Sopenharmony_ci        if end == self.prev_end:
3397db96d56Sopenharmony_ci            return
3407db96d56Sopenharmony_ci
3417db96d56Sopenharmony_ci        width_difference = len(str(end)) - len(str(self.prev_end))
3427db96d56Sopenharmony_ci        if width_difference:
3437db96d56Sopenharmony_ci            cur_width = int(float(self.sidebar_text['width']))
3447db96d56Sopenharmony_ci            new_width = cur_width + width_difference
3457db96d56Sopenharmony_ci            self.sidebar_text['width'] = self._sidebar_width_type(new_width)
3467db96d56Sopenharmony_ci
3477db96d56Sopenharmony_ci        with temp_enable_text_widget(self.sidebar_text):
3487db96d56Sopenharmony_ci            if end > self.prev_end:
3497db96d56Sopenharmony_ci                new_text = '\n'.join(itertools.chain(
3507db96d56Sopenharmony_ci                    [''],
3517db96d56Sopenharmony_ci                    map(str, range(self.prev_end + 1, end + 1)),
3527db96d56Sopenharmony_ci                ))
3537db96d56Sopenharmony_ci                self.sidebar_text.insert(f'end -1c', new_text, 'linenumber')
3547db96d56Sopenharmony_ci            else:
3557db96d56Sopenharmony_ci                self.sidebar_text.delete(f'{end+1}.0 -1c', 'end -1c')
3567db96d56Sopenharmony_ci
3577db96d56Sopenharmony_ci        self.prev_end = end
3587db96d56Sopenharmony_ci
3597db96d56Sopenharmony_ci    def yscroll_event(self, *args, **kwargs):
3607db96d56Sopenharmony_ci        self.sidebar_text.yview_moveto(args[0])
3617db96d56Sopenharmony_ci        return 'break'
3627db96d56Sopenharmony_ci
3637db96d56Sopenharmony_ci
3647db96d56Sopenharmony_ciclass WrappedLineHeightChangeDelegator(Delegator):
3657db96d56Sopenharmony_ci    def __init__(self, callback):
3667db96d56Sopenharmony_ci        """
3677db96d56Sopenharmony_ci        callback - Callable, will be called when an insert, delete or replace
3687db96d56Sopenharmony_ci                   action on the text widget may require updating the shell
3697db96d56Sopenharmony_ci                   sidebar.
3707db96d56Sopenharmony_ci        """
3717db96d56Sopenharmony_ci        Delegator.__init__(self)
3727db96d56Sopenharmony_ci        self.callback = callback
3737db96d56Sopenharmony_ci
3747db96d56Sopenharmony_ci    def insert(self, index, chars, tags=None):
3757db96d56Sopenharmony_ci        is_single_line = '\n' not in chars
3767db96d56Sopenharmony_ci        if is_single_line:
3777db96d56Sopenharmony_ci            before_displaylines = get_displaylines(self, index)
3787db96d56Sopenharmony_ci
3797db96d56Sopenharmony_ci        self.delegate.insert(index, chars, tags)
3807db96d56Sopenharmony_ci
3817db96d56Sopenharmony_ci        if is_single_line:
3827db96d56Sopenharmony_ci            after_displaylines = get_displaylines(self, index)
3837db96d56Sopenharmony_ci            if after_displaylines == before_displaylines:
3847db96d56Sopenharmony_ci                return  # no need to update the sidebar
3857db96d56Sopenharmony_ci
3867db96d56Sopenharmony_ci        self.callback()
3877db96d56Sopenharmony_ci
3887db96d56Sopenharmony_ci    def delete(self, index1, index2=None):
3897db96d56Sopenharmony_ci        if index2 is None:
3907db96d56Sopenharmony_ci            index2 = index1 + "+1c"
3917db96d56Sopenharmony_ci        is_single_line = get_lineno(self, index1) == get_lineno(self, index2)
3927db96d56Sopenharmony_ci        if is_single_line:
3937db96d56Sopenharmony_ci            before_displaylines = get_displaylines(self, index1)
3947db96d56Sopenharmony_ci
3957db96d56Sopenharmony_ci        self.delegate.delete(index1, index2)
3967db96d56Sopenharmony_ci
3977db96d56Sopenharmony_ci        if is_single_line:
3987db96d56Sopenharmony_ci            after_displaylines = get_displaylines(self, index1)
3997db96d56Sopenharmony_ci            if after_displaylines == before_displaylines:
4007db96d56Sopenharmony_ci                return  # no need to update the sidebar
4017db96d56Sopenharmony_ci
4027db96d56Sopenharmony_ci        self.callback()
4037db96d56Sopenharmony_ci
4047db96d56Sopenharmony_ci
4057db96d56Sopenharmony_ciclass ShellSidebar(BaseSideBar):
4067db96d56Sopenharmony_ci    """Sidebar for the PyShell window, for prompts etc."""
4077db96d56Sopenharmony_ci    def __init__(self, editwin):
4087db96d56Sopenharmony_ci        self.canvas = None
4097db96d56Sopenharmony_ci        self.line_prompts = {}
4107db96d56Sopenharmony_ci
4117db96d56Sopenharmony_ci        super().__init__(editwin)
4127db96d56Sopenharmony_ci
4137db96d56Sopenharmony_ci        change_delegator = \
4147db96d56Sopenharmony_ci            WrappedLineHeightChangeDelegator(self.change_callback)
4157db96d56Sopenharmony_ci        # Insert the TextChangeDelegator after the last delegator, so that
4167db96d56Sopenharmony_ci        # the sidebar reflects final changes to the text widget contents.
4177db96d56Sopenharmony_ci        d = self.editwin.per.top
4187db96d56Sopenharmony_ci        if d.delegate is not self.text:
4197db96d56Sopenharmony_ci            while d.delegate is not self.editwin.per.bottom:
4207db96d56Sopenharmony_ci                d = d.delegate
4217db96d56Sopenharmony_ci        self.editwin.per.insertfilterafter(change_delegator, after=d)
4227db96d56Sopenharmony_ci
4237db96d56Sopenharmony_ci        self.is_shown = True
4247db96d56Sopenharmony_ci
4257db96d56Sopenharmony_ci    def init_widgets(self):
4267db96d56Sopenharmony_ci        self.canvas = tk.Canvas(self.parent, width=30,
4277db96d56Sopenharmony_ci                                borderwidth=0, highlightthickness=0,
4287db96d56Sopenharmony_ci                                takefocus=False)
4297db96d56Sopenharmony_ci        self.update_sidebar()
4307db96d56Sopenharmony_ci        self.grid()
4317db96d56Sopenharmony_ci        return self.canvas
4327db96d56Sopenharmony_ci
4337db96d56Sopenharmony_ci    def bind_events(self):
4347db96d56Sopenharmony_ci        super().bind_events()
4357db96d56Sopenharmony_ci
4367db96d56Sopenharmony_ci        self.main_widget.bind(
4377db96d56Sopenharmony_ci            # AquaTk defines <2> as the right button, not <3>.
4387db96d56Sopenharmony_ci            "<Button-2>" if macosx.isAquaTk() else "<Button-3>",
4397db96d56Sopenharmony_ci            self.context_menu_event,
4407db96d56Sopenharmony_ci        )
4417db96d56Sopenharmony_ci
4427db96d56Sopenharmony_ci    def context_menu_event(self, event):
4437db96d56Sopenharmony_ci        rmenu = tk.Menu(self.main_widget, tearoff=0)
4447db96d56Sopenharmony_ci        has_selection = bool(self.text.tag_nextrange('sel', '1.0'))
4457db96d56Sopenharmony_ci        def mkcmd(eventname):
4467db96d56Sopenharmony_ci            return lambda: self.text.event_generate(eventname)
4477db96d56Sopenharmony_ci        rmenu.add_command(label='Copy',
4487db96d56Sopenharmony_ci                          command=mkcmd('<<copy>>'),
4497db96d56Sopenharmony_ci                          state='normal' if has_selection else 'disabled')
4507db96d56Sopenharmony_ci        rmenu.add_command(label='Copy with prompts',
4517db96d56Sopenharmony_ci                          command=mkcmd('<<copy-with-prompts>>'),
4527db96d56Sopenharmony_ci                          state='normal' if has_selection else 'disabled')
4537db96d56Sopenharmony_ci        rmenu.tk_popup(event.x_root, event.y_root)
4547db96d56Sopenharmony_ci        return "break"
4557db96d56Sopenharmony_ci
4567db96d56Sopenharmony_ci    def grid(self):
4577db96d56Sopenharmony_ci        self.canvas.grid(row=1, column=0, sticky=tk.NSEW, padx=2, pady=0)
4587db96d56Sopenharmony_ci
4597db96d56Sopenharmony_ci    def change_callback(self):
4607db96d56Sopenharmony_ci        if self.is_shown:
4617db96d56Sopenharmony_ci            self.update_sidebar()
4627db96d56Sopenharmony_ci
4637db96d56Sopenharmony_ci    def update_sidebar(self):
4647db96d56Sopenharmony_ci        text = self.text
4657db96d56Sopenharmony_ci        text_tagnames = text.tag_names
4667db96d56Sopenharmony_ci        canvas = self.canvas
4677db96d56Sopenharmony_ci        line_prompts = self.line_prompts = {}
4687db96d56Sopenharmony_ci
4697db96d56Sopenharmony_ci        canvas.delete(tk.ALL)
4707db96d56Sopenharmony_ci
4717db96d56Sopenharmony_ci        index = text.index("@0,0")
4727db96d56Sopenharmony_ci        if index.split('.', 1)[1] != '0':
4737db96d56Sopenharmony_ci            index = text.index(f'{index}+1line linestart')
4747db96d56Sopenharmony_ci        while (lineinfo := text.dlineinfo(index)) is not None:
4757db96d56Sopenharmony_ci            y = lineinfo[1]
4767db96d56Sopenharmony_ci            prev_newline_tagnames = text_tagnames(f"{index} linestart -1c")
4777db96d56Sopenharmony_ci            prompt = (
4787db96d56Sopenharmony_ci                '>>>' if "console" in prev_newline_tagnames else
4797db96d56Sopenharmony_ci                '...' if "stdin" in prev_newline_tagnames else
4807db96d56Sopenharmony_ci                None
4817db96d56Sopenharmony_ci            )
4827db96d56Sopenharmony_ci            if prompt:
4837db96d56Sopenharmony_ci                canvas.create_text(2, y, anchor=tk.NW, text=prompt,
4847db96d56Sopenharmony_ci                                   font=self.font, fill=self.colors[0])
4857db96d56Sopenharmony_ci                lineno = get_lineno(text, index)
4867db96d56Sopenharmony_ci                line_prompts[lineno] = prompt
4877db96d56Sopenharmony_ci            index = text.index(f'{index}+1line')
4887db96d56Sopenharmony_ci
4897db96d56Sopenharmony_ci    def yscroll_event(self, *args, **kwargs):
4907db96d56Sopenharmony_ci        """Redirect vertical scrolling to the main editor text widget.
4917db96d56Sopenharmony_ci
4927db96d56Sopenharmony_ci        The scroll bar is also updated.
4937db96d56Sopenharmony_ci        """
4947db96d56Sopenharmony_ci        self.change_callback()
4957db96d56Sopenharmony_ci        return 'break'
4967db96d56Sopenharmony_ci
4977db96d56Sopenharmony_ci    def update_font(self):
4987db96d56Sopenharmony_ci        """Update the sidebar text font, usually after config changes."""
4997db96d56Sopenharmony_ci        font = idleConf.GetFont(self.text, 'main', 'EditorWindow')
5007db96d56Sopenharmony_ci        tk_font = Font(self.text, font=font)
5017db96d56Sopenharmony_ci        char_width = max(tk_font.measure(char) for char in ['>', '.'])
5027db96d56Sopenharmony_ci        self.canvas.configure(width=char_width * 3 + 4)
5037db96d56Sopenharmony_ci        self.font = font
5047db96d56Sopenharmony_ci        self.change_callback()
5057db96d56Sopenharmony_ci
5067db96d56Sopenharmony_ci    def update_colors(self):
5077db96d56Sopenharmony_ci        """Update the sidebar text colors, usually after config changes."""
5087db96d56Sopenharmony_ci        linenumbers_colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber')
5097db96d56Sopenharmony_ci        prompt_colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'console')
5107db96d56Sopenharmony_ci        foreground = prompt_colors['foreground']
5117db96d56Sopenharmony_ci        background = linenumbers_colors['background']
5127db96d56Sopenharmony_ci        self.colors = (foreground, background)
5137db96d56Sopenharmony_ci        self.canvas.configure(background=background)
5147db96d56Sopenharmony_ci        self.change_callback()
5157db96d56Sopenharmony_ci
5167db96d56Sopenharmony_ci
5177db96d56Sopenharmony_cidef _linenumbers_drag_scrolling(parent):  # htest #
5187db96d56Sopenharmony_ci    from idlelib.idle_test.test_sidebar import Dummy_editwin
5197db96d56Sopenharmony_ci
5207db96d56Sopenharmony_ci    toplevel = tk.Toplevel(parent)
5217db96d56Sopenharmony_ci    text_frame = tk.Frame(toplevel)
5227db96d56Sopenharmony_ci    text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
5237db96d56Sopenharmony_ci    text_frame.rowconfigure(1, weight=1)
5247db96d56Sopenharmony_ci    text_frame.columnconfigure(1, weight=1)
5257db96d56Sopenharmony_ci
5267db96d56Sopenharmony_ci    font = idleConf.GetFont(toplevel, 'main', 'EditorWindow')
5277db96d56Sopenharmony_ci    text = tk.Text(text_frame, width=80, height=24, wrap=tk.NONE, font=font)
5287db96d56Sopenharmony_ci    text.grid(row=1, column=1, sticky=tk.NSEW)
5297db96d56Sopenharmony_ci
5307db96d56Sopenharmony_ci    editwin = Dummy_editwin(text)
5317db96d56Sopenharmony_ci    editwin.vbar = tk.Scrollbar(text_frame)
5327db96d56Sopenharmony_ci
5337db96d56Sopenharmony_ci    linenumbers = LineNumbers(editwin)
5347db96d56Sopenharmony_ci    linenumbers.show_sidebar()
5357db96d56Sopenharmony_ci
5367db96d56Sopenharmony_ci    text.insert('1.0', '\n'.join('a'*i for i in range(1, 101)))
5377db96d56Sopenharmony_ci
5387db96d56Sopenharmony_ci
5397db96d56Sopenharmony_ciif __name__ == '__main__':
5407db96d56Sopenharmony_ci    from unittest import main
5417db96d56Sopenharmony_ci    main('idlelib.idle_test.test_sidebar', verbosity=2, exit=False)
5427db96d56Sopenharmony_ci
5437db96d56Sopenharmony_ci    from idlelib.idle_test.htest import run
5447db96d56Sopenharmony_ci    run(_linenumbers_drag_scrolling)
545