You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			957 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			957 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			Python
		
	
"""
 | 
						|
User interface Controls for the layout.
 | 
						|
"""
 | 
						|
 | 
						|
from __future__ import annotations
 | 
						|
 | 
						|
import time
 | 
						|
from abc import ABCMeta, abstractmethod
 | 
						|
from typing import TYPE_CHECKING, Callable, Hashable, Iterable, NamedTuple
 | 
						|
 | 
						|
from prompt_toolkit.application.current import get_app
 | 
						|
from prompt_toolkit.buffer import Buffer
 | 
						|
from prompt_toolkit.cache import SimpleCache
 | 
						|
from prompt_toolkit.data_structures import Point
 | 
						|
from prompt_toolkit.document import Document
 | 
						|
from prompt_toolkit.filters import FilterOrBool, to_filter
 | 
						|
from prompt_toolkit.formatted_text import (
 | 
						|
    AnyFormattedText,
 | 
						|
    StyleAndTextTuples,
 | 
						|
    to_formatted_text,
 | 
						|
)
 | 
						|
from prompt_toolkit.formatted_text.utils import (
 | 
						|
    fragment_list_to_text,
 | 
						|
    fragment_list_width,
 | 
						|
    split_lines,
 | 
						|
)
 | 
						|
from prompt_toolkit.lexers import Lexer, SimpleLexer
 | 
						|
from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType
 | 
						|
from prompt_toolkit.search import SearchState
 | 
						|
from prompt_toolkit.selection import SelectionType
 | 
						|
from prompt_toolkit.utils import get_cwidth
 | 
						|
 | 
						|
from .processors import (
 | 
						|
    DisplayMultipleCursors,
 | 
						|
    HighlightIncrementalSearchProcessor,
 | 
						|
    HighlightSearchProcessor,
 | 
						|
    HighlightSelectionProcessor,
 | 
						|
    Processor,
 | 
						|
    TransformationInput,
 | 
						|
    merge_processors,
 | 
						|
)
 | 
						|
 | 
						|
if TYPE_CHECKING:
 | 
						|
    from prompt_toolkit.key_binding.key_bindings import (
 | 
						|
        KeyBindingsBase,
 | 
						|
        NotImplementedOrNone,
 | 
						|
    )
 | 
						|
    from prompt_toolkit.utils import Event
 | 
						|
 | 
						|
 | 
						|
__all__ = [
 | 
						|
    "BufferControl",
 | 
						|
    "SearchBufferControl",
 | 
						|
    "DummyControl",
 | 
						|
    "FormattedTextControl",
 | 
						|
    "UIControl",
 | 
						|
    "UIContent",
 | 
						|
]
 | 
						|
 | 
						|
GetLinePrefixCallable = Callable[[int, int], AnyFormattedText]
 | 
						|
 | 
						|
 | 
						|
class UIControl(metaclass=ABCMeta):
 | 
						|
    """
 | 
						|
    Base class for all user interface controls.
 | 
						|
    """
 | 
						|
 | 
						|
    def reset(self) -> None:
 | 
						|
        # Default reset. (Doesn't have to be implemented.)
 | 
						|
        pass
 | 
						|
 | 
						|
    def preferred_width(self, max_available_width: int) -> int | None:
 | 
						|
        return None
 | 
						|
 | 
						|
    def preferred_height(
 | 
						|
        self,
 | 
						|
        width: int,
 | 
						|
        max_available_height: int,
 | 
						|
        wrap_lines: bool,
 | 
						|
        get_line_prefix: GetLinePrefixCallable | None,
 | 
						|
    ) -> int | None:
 | 
						|
        return None
 | 
						|
 | 
						|
    def is_focusable(self) -> bool:
 | 
						|
        """
 | 
						|
        Tell whether this user control is focusable.
 | 
						|
        """
 | 
						|
        return False
 | 
						|
 | 
						|
    @abstractmethod
 | 
						|
    def create_content(self, width: int, height: int) -> UIContent:
 | 
						|
        """
 | 
						|
        Generate the content for this user control.
 | 
						|
 | 
						|
        Returns a :class:`.UIContent` instance.
 | 
						|
        """
 | 
						|
 | 
						|
    def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
 | 
						|
        """
 | 
						|
        Handle mouse events.
 | 
						|
 | 
						|
        When `NotImplemented` is returned, it means that the given event is not
 | 
						|
        handled by the `UIControl` itself. The `Window` or key bindings can
 | 
						|
        decide to handle this event as scrolling or changing focus.
 | 
						|
 | 
						|
        :param mouse_event: `MouseEvent` instance.
 | 
						|
        """
 | 
						|
        return NotImplemented
 | 
						|
 | 
						|
    def move_cursor_down(self) -> None:
 | 
						|
        """
 | 
						|
        Request to move the cursor down.
 | 
						|
        This happens when scrolling down and the cursor is completely at the
 | 
						|
        top.
 | 
						|
        """
 | 
						|
 | 
						|
    def move_cursor_up(self) -> None:
 | 
						|
        """
 | 
						|
        Request to move the cursor up.
 | 
						|
        """
 | 
						|
 | 
						|
    def get_key_bindings(self) -> KeyBindingsBase | None:
 | 
						|
        """
 | 
						|
        The key bindings that are specific for this user control.
 | 
						|
 | 
						|
        Return a :class:`.KeyBindings` object if some key bindings are
 | 
						|
        specified, or `None` otherwise.
 | 
						|
        """
 | 
						|
 | 
						|
    def get_invalidate_events(self) -> Iterable[Event[object]]:
 | 
						|
        """
 | 
						|
        Return a list of `Event` objects. This can be a generator.
 | 
						|
        (The application collects all these events, in order to bind redraw
 | 
						|
        handlers to these events.)
 | 
						|
        """
 | 
						|
        return []
 | 
						|
 | 
						|
 | 
						|
class UIContent:
 | 
						|
    """
 | 
						|
    Content generated by a user control. This content consists of a list of
 | 
						|
    lines.
 | 
						|
 | 
						|
    :param get_line: Callable that takes a line number and returns the current
 | 
						|
        line. This is a list of (style_str, text) tuples.
 | 
						|
    :param line_count: The number of lines.
 | 
						|
    :param cursor_position: a :class:`.Point` for the cursor position.
 | 
						|
    :param menu_position: a :class:`.Point` for the menu position.
 | 
						|
    :param show_cursor: Make the cursor visible.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        get_line: Callable[[int], StyleAndTextTuples] = (lambda i: []),
 | 
						|
        line_count: int = 0,
 | 
						|
        cursor_position: Point | None = None,
 | 
						|
        menu_position: Point | None = None,
 | 
						|
        show_cursor: bool = True,
 | 
						|
    ):
 | 
						|
        self.get_line = get_line
 | 
						|
        self.line_count = line_count
 | 
						|
        self.cursor_position = cursor_position or Point(x=0, y=0)
 | 
						|
        self.menu_position = menu_position
 | 
						|
        self.show_cursor = show_cursor
 | 
						|
 | 
						|
        # Cache for line heights. Maps cache key -> height
 | 
						|
        self._line_heights_cache: dict[Hashable, int] = {}
 | 
						|
 | 
						|
    def __getitem__(self, lineno: int) -> StyleAndTextTuples:
 | 
						|
        "Make it iterable (iterate line by line)."
 | 
						|
        if lineno < self.line_count:
 | 
						|
            return self.get_line(lineno)
 | 
						|
        else:
 | 
						|
            raise IndexError
 | 
						|
 | 
						|
    def get_height_for_line(
 | 
						|
        self,
 | 
						|
        lineno: int,
 | 
						|
        width: int,
 | 
						|
        get_line_prefix: GetLinePrefixCallable | None,
 | 
						|
        slice_stop: int | None = None,
 | 
						|
    ) -> int:
 | 
						|
        """
 | 
						|
        Return the height that a given line would need if it is rendered in a
 | 
						|
        space with the given width (using line wrapping).
 | 
						|
 | 
						|
        :param get_line_prefix: None or a `Window.get_line_prefix` callable
 | 
						|
            that returns the prefix to be inserted before this line.
 | 
						|
        :param slice_stop: Wrap only "line[:slice_stop]" and return that
 | 
						|
            partial result. This is needed for scrolling the window correctly
 | 
						|
            when line wrapping.
 | 
						|
        :returns: The computed height.
 | 
						|
        """
 | 
						|
        # Instead of using `get_line_prefix` as key, we use render_counter
 | 
						|
        # instead. This is more reliable, because this function could still be
 | 
						|
        # the same, while the content would change over time.
 | 
						|
        key = get_app().render_counter, lineno, width, slice_stop
 | 
						|
 | 
						|
        try:
 | 
						|
            return self._line_heights_cache[key]
 | 
						|
        except KeyError:
 | 
						|
            if width == 0:
 | 
						|
                height = 10**8
 | 
						|
            else:
 | 
						|
                # Calculate line width first.
 | 
						|
                line = fragment_list_to_text(self.get_line(lineno))[:slice_stop]
 | 
						|
                text_width = get_cwidth(line)
 | 
						|
 | 
						|
                if get_line_prefix:
 | 
						|
                    # Add prefix width.
 | 
						|
                    text_width += fragment_list_width(
 | 
						|
                        to_formatted_text(get_line_prefix(lineno, 0))
 | 
						|
                    )
 | 
						|
 | 
						|
                    # Slower path: compute path when there's a line prefix.
 | 
						|
                    height = 1
 | 
						|
 | 
						|
                    # Keep wrapping as long as the line doesn't fit.
 | 
						|
                    # Keep adding new prefixes for every wrapped line.
 | 
						|
                    while text_width > width:
 | 
						|
                        height += 1
 | 
						|
                        text_width -= width
 | 
						|
 | 
						|
                        fragments2 = to_formatted_text(
 | 
						|
                            get_line_prefix(lineno, height - 1)
 | 
						|
                        )
 | 
						|
                        prefix_width = get_cwidth(fragment_list_to_text(fragments2))
 | 
						|
 | 
						|
                        if prefix_width >= width:  # Prefix doesn't fit.
 | 
						|
                            height = 10**8
 | 
						|
                            break
 | 
						|
 | 
						|
                        text_width += prefix_width
 | 
						|
                else:
 | 
						|
                    # Fast path: compute height when there's no line prefix.
 | 
						|
                    try:
 | 
						|
                        quotient, remainder = divmod(text_width, width)
 | 
						|
                    except ZeroDivisionError:
 | 
						|
                        height = 10**8
 | 
						|
                    else:
 | 
						|
                        if remainder:
 | 
						|
                            quotient += 1  # Like math.ceil.
 | 
						|
                        height = max(1, quotient)
 | 
						|
 | 
						|
            # Cache and return
 | 
						|
            self._line_heights_cache[key] = height
 | 
						|
            return height
 | 
						|
 | 
						|
 | 
						|
class FormattedTextControl(UIControl):
 | 
						|
    """
 | 
						|
    Control that displays formatted text. This can be either plain text, an
 | 
						|
    :class:`~prompt_toolkit.formatted_text.HTML` object an
 | 
						|
    :class:`~prompt_toolkit.formatted_text.ANSI` object, a list of ``(style_str,
 | 
						|
    text)`` tuples or a callable that takes no argument and returns one of
 | 
						|
    those, depending on how you prefer to do the formatting. See
 | 
						|
    ``prompt_toolkit.layout.formatted_text`` for more information.
 | 
						|
 | 
						|
    (It's mostly optimized for rather small widgets, like toolbars, menus, etc...)
 | 
						|
 | 
						|
    When this UI control has the focus, the cursor will be shown in the upper
 | 
						|
    left corner of this control by default. There are two ways for specifying
 | 
						|
    the cursor position:
 | 
						|
 | 
						|
    - Pass a `get_cursor_position` function which returns a `Point` instance
 | 
						|
      with the current cursor position.
 | 
						|
 | 
						|
    - If the (formatted) text is passed as a list of ``(style, text)`` tuples
 | 
						|
      and there is one that looks like ``('[SetCursorPosition]', '')``, then
 | 
						|
      this will specify the cursor position.
 | 
						|
 | 
						|
    Mouse support:
 | 
						|
 | 
						|
        The list of fragments can also contain tuples of three items, looking like:
 | 
						|
        (style_str, text, handler). When mouse support is enabled and the user
 | 
						|
        clicks on this fragment, then the given handler is called. That handler
 | 
						|
        should accept two inputs: (Application, MouseEvent) and it should
 | 
						|
        either handle the event or return `NotImplemented` in case we want the
 | 
						|
        containing Window to handle this event.
 | 
						|
 | 
						|
    :param focusable: `bool` or :class:`.Filter`: Tell whether this control is
 | 
						|
        focusable.
 | 
						|
 | 
						|
    :param text: Text or formatted text to be displayed.
 | 
						|
    :param style: Style string applied to the content. (If you want to style
 | 
						|
        the whole :class:`~prompt_toolkit.layout.Window`, pass the style to the
 | 
						|
        :class:`~prompt_toolkit.layout.Window` instead.)
 | 
						|
    :param key_bindings: a :class:`.KeyBindings` object.
 | 
						|
    :param get_cursor_position: A callable that returns the cursor position as
 | 
						|
        a `Point` instance.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        text: AnyFormattedText = "",
 | 
						|
        style: str = "",
 | 
						|
        focusable: FilterOrBool = False,
 | 
						|
        key_bindings: KeyBindingsBase | None = None,
 | 
						|
        show_cursor: bool = True,
 | 
						|
        modal: bool = False,
 | 
						|
        get_cursor_position: Callable[[], Point | None] | None = None,
 | 
						|
    ) -> None:
 | 
						|
        self.text = text  # No type check on 'text'. This is done dynamically.
 | 
						|
        self.style = style
 | 
						|
        self.focusable = to_filter(focusable)
 | 
						|
 | 
						|
        # Key bindings.
 | 
						|
        self.key_bindings = key_bindings
 | 
						|
        self.show_cursor = show_cursor
 | 
						|
        self.modal = modal
 | 
						|
        self.get_cursor_position = get_cursor_position
 | 
						|
 | 
						|
        #: Cache for the content.
 | 
						|
        self._content_cache: SimpleCache[Hashable, UIContent] = SimpleCache(maxsize=18)
 | 
						|
        self._fragment_cache: SimpleCache[int, StyleAndTextTuples] = SimpleCache(
 | 
						|
            maxsize=1
 | 
						|
        )
 | 
						|
        # Only cache one fragment list. We don't need the previous item.
 | 
						|
 | 
						|
        # Render info for the mouse support.
 | 
						|
        self._fragments: StyleAndTextTuples | None = None
 | 
						|
 | 
						|
    def reset(self) -> None:
 | 
						|
        self._fragments = None
 | 
						|
 | 
						|
    def is_focusable(self) -> bool:
 | 
						|
        return self.focusable()
 | 
						|
 | 
						|
    def __repr__(self) -> str:
 | 
						|
        return f"{self.__class__.__name__}({self.text!r})"
 | 
						|
 | 
						|
    def _get_formatted_text_cached(self) -> StyleAndTextTuples:
 | 
						|
        """
 | 
						|
        Get fragments, but only retrieve fragments once during one render run.
 | 
						|
        (This function is called several times during one rendering, because
 | 
						|
        we also need those for calculating the dimensions.)
 | 
						|
        """
 | 
						|
        return self._fragment_cache.get(
 | 
						|
            get_app().render_counter, lambda: to_formatted_text(self.text, self.style)
 | 
						|
        )
 | 
						|
 | 
						|
    def preferred_width(self, max_available_width: int) -> int:
 | 
						|
        """
 | 
						|
        Return the preferred width for this control.
 | 
						|
        That is the width of the longest line.
 | 
						|
        """
 | 
						|
        text = fragment_list_to_text(self._get_formatted_text_cached())
 | 
						|
        line_lengths = [get_cwidth(l) for l in text.split("\n")]
 | 
						|
        return max(line_lengths)
 | 
						|
 | 
						|
    def preferred_height(
 | 
						|
        self,
 | 
						|
        width: int,
 | 
						|
        max_available_height: int,
 | 
						|
        wrap_lines: bool,
 | 
						|
        get_line_prefix: GetLinePrefixCallable | None,
 | 
						|
    ) -> int | None:
 | 
						|
        """
 | 
						|
        Return the preferred height for this control.
 | 
						|
        """
 | 
						|
        content = self.create_content(width, None)
 | 
						|
        if wrap_lines:
 | 
						|
            height = 0
 | 
						|
            for i in range(content.line_count):
 | 
						|
                height += content.get_height_for_line(i, width, get_line_prefix)
 | 
						|
                if height >= max_available_height:
 | 
						|
                    return max_available_height
 | 
						|
            return height
 | 
						|
        else:
 | 
						|
            return content.line_count
 | 
						|
 | 
						|
    def create_content(self, width: int, height: int | None) -> UIContent:
 | 
						|
        # Get fragments
 | 
						|
        fragments_with_mouse_handlers = self._get_formatted_text_cached()
 | 
						|
        fragment_lines_with_mouse_handlers = list(
 | 
						|
            split_lines(fragments_with_mouse_handlers)
 | 
						|
        )
 | 
						|
 | 
						|
        # Strip mouse handlers from fragments.
 | 
						|
        fragment_lines: list[StyleAndTextTuples] = [
 | 
						|
            [(item[0], item[1]) for item in line]
 | 
						|
            for line in fragment_lines_with_mouse_handlers
 | 
						|
        ]
 | 
						|
 | 
						|
        # Keep track of the fragments with mouse handler, for later use in
 | 
						|
        # `mouse_handler`.
 | 
						|
        self._fragments = fragments_with_mouse_handlers
 | 
						|
 | 
						|
        # If there is a `[SetCursorPosition]` in the fragment list, set the
 | 
						|
        # cursor position here.
 | 
						|
        def get_cursor_position(
 | 
						|
            fragment: str = "[SetCursorPosition]",
 | 
						|
        ) -> Point | None:
 | 
						|
            for y, line in enumerate(fragment_lines):
 | 
						|
                x = 0
 | 
						|
                for style_str, text, *_ in line:
 | 
						|
                    if fragment in style_str:
 | 
						|
                        return Point(x=x, y=y)
 | 
						|
                    x += len(text)
 | 
						|
            return None
 | 
						|
 | 
						|
        # If there is a `[SetMenuPosition]`, set the menu over here.
 | 
						|
        def get_menu_position() -> Point | None:
 | 
						|
            return get_cursor_position("[SetMenuPosition]")
 | 
						|
 | 
						|
        cursor_position = (self.get_cursor_position or get_cursor_position)()
 | 
						|
 | 
						|
        # Create content, or take it from the cache.
 | 
						|
        key = (tuple(fragments_with_mouse_handlers), width, cursor_position)
 | 
						|
 | 
						|
        def get_content() -> UIContent:
 | 
						|
            return UIContent(
 | 
						|
                get_line=lambda i: fragment_lines[i],
 | 
						|
                line_count=len(fragment_lines),
 | 
						|
                show_cursor=self.show_cursor,
 | 
						|
                cursor_position=cursor_position,
 | 
						|
                menu_position=get_menu_position(),
 | 
						|
            )
 | 
						|
 | 
						|
        return self._content_cache.get(key, get_content)
 | 
						|
 | 
						|
    def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
 | 
						|
        """
 | 
						|
        Handle mouse events.
 | 
						|
 | 
						|
        (When the fragment list contained mouse handlers and the user clicked on
 | 
						|
        on any of these, the matching handler is called. This handler can still
 | 
						|
        return `NotImplemented` in case we want the
 | 
						|
        :class:`~prompt_toolkit.layout.Window` to handle this particular
 | 
						|
        event.)
 | 
						|
        """
 | 
						|
        if self._fragments:
 | 
						|
            # Read the generator.
 | 
						|
            fragments_for_line = list(split_lines(self._fragments))
 | 
						|
 | 
						|
            try:
 | 
						|
                fragments = fragments_for_line[mouse_event.position.y]
 | 
						|
            except IndexError:
 | 
						|
                return NotImplemented
 | 
						|
            else:
 | 
						|
                # Find position in the fragment list.
 | 
						|
                xpos = mouse_event.position.x
 | 
						|
 | 
						|
                # Find mouse handler for this character.
 | 
						|
                count = 0
 | 
						|
                for item in fragments:
 | 
						|
                    count += len(item[1])
 | 
						|
                    if count > xpos:
 | 
						|
                        if len(item) >= 3:
 | 
						|
                            # Handler found. Call it.
 | 
						|
                            # (Handler can return NotImplemented, so return
 | 
						|
                            # that result.)
 | 
						|
                            handler = item[2]
 | 
						|
                            return handler(mouse_event)
 | 
						|
                        else:
 | 
						|
                            break
 | 
						|
 | 
						|
        # Otherwise, don't handle here.
 | 
						|
        return NotImplemented
 | 
						|
 | 
						|
    def is_modal(self) -> bool:
 | 
						|
        return self.modal
 | 
						|
 | 
						|
    def get_key_bindings(self) -> KeyBindingsBase | None:
 | 
						|
        return self.key_bindings
 | 
						|
 | 
						|
 | 
						|
class DummyControl(UIControl):
 | 
						|
    """
 | 
						|
    A dummy control object that doesn't paint any content.
 | 
						|
 | 
						|
    Useful for filling a :class:`~prompt_toolkit.layout.Window`. (The
 | 
						|
    `fragment` and `char` attributes of the `Window` class can be used to
 | 
						|
    define the filling.)
 | 
						|
    """
 | 
						|
 | 
						|
    def create_content(self, width: int, height: int) -> UIContent:
 | 
						|
        def get_line(i: int) -> StyleAndTextTuples:
 | 
						|
            return []
 | 
						|
 | 
						|
        return UIContent(get_line=get_line, line_count=100**100)  # Something very big.
 | 
						|
 | 
						|
    def is_focusable(self) -> bool:
 | 
						|
        return False
 | 
						|
 | 
						|
 | 
						|
class _ProcessedLine(NamedTuple):
 | 
						|
    fragments: StyleAndTextTuples
 | 
						|
    source_to_display: Callable[[int], int]
 | 
						|
    display_to_source: Callable[[int], int]
 | 
						|
 | 
						|
 | 
						|
class BufferControl(UIControl):
 | 
						|
    """
 | 
						|
    Control for visualizing the content of a :class:`.Buffer`.
 | 
						|
 | 
						|
    :param buffer: The :class:`.Buffer` object to be displayed.
 | 
						|
    :param input_processors: A list of
 | 
						|
        :class:`~prompt_toolkit.layout.processors.Processor` objects.
 | 
						|
    :param include_default_input_processors: When True, include the default
 | 
						|
        processors for highlighting of selection, search and displaying of
 | 
						|
        multiple cursors.
 | 
						|
    :param lexer: :class:`.Lexer` instance for syntax highlighting.
 | 
						|
    :param preview_search: `bool` or :class:`.Filter`: Show search while
 | 
						|
        typing. When this is `True`, probably you want to add a
 | 
						|
        ``HighlightIncrementalSearchProcessor`` as well. Otherwise only the
 | 
						|
        cursor position will move, but the text won't be highlighted.
 | 
						|
    :param focusable: `bool` or :class:`.Filter`: Tell whether this control is focusable.
 | 
						|
    :param focus_on_click: Focus this buffer when it's click, but not yet focused.
 | 
						|
    :param key_bindings: a :class:`.KeyBindings` object.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        buffer: Buffer | None = None,
 | 
						|
        input_processors: list[Processor] | None = None,
 | 
						|
        include_default_input_processors: bool = True,
 | 
						|
        lexer: Lexer | None = None,
 | 
						|
        preview_search: FilterOrBool = False,
 | 
						|
        focusable: FilterOrBool = True,
 | 
						|
        search_buffer_control: (
 | 
						|
            None | SearchBufferControl | Callable[[], SearchBufferControl]
 | 
						|
        ) = None,
 | 
						|
        menu_position: Callable[[], int | None] | None = None,
 | 
						|
        focus_on_click: FilterOrBool = False,
 | 
						|
        key_bindings: KeyBindingsBase | None = None,
 | 
						|
    ):
 | 
						|
        self.input_processors = input_processors
 | 
						|
        self.include_default_input_processors = include_default_input_processors
 | 
						|
 | 
						|
        self.default_input_processors = [
 | 
						|
            HighlightSearchProcessor(),
 | 
						|
            HighlightIncrementalSearchProcessor(),
 | 
						|
            HighlightSelectionProcessor(),
 | 
						|
            DisplayMultipleCursors(),
 | 
						|
        ]
 | 
						|
 | 
						|
        self.preview_search = to_filter(preview_search)
 | 
						|
        self.focusable = to_filter(focusable)
 | 
						|
        self.focus_on_click = to_filter(focus_on_click)
 | 
						|
 | 
						|
        self.buffer = buffer or Buffer()
 | 
						|
        self.menu_position = menu_position
 | 
						|
        self.lexer = lexer or SimpleLexer()
 | 
						|
        self.key_bindings = key_bindings
 | 
						|
        self._search_buffer_control = search_buffer_control
 | 
						|
 | 
						|
        #: Cache for the lexer.
 | 
						|
        #: Often, due to cursor movement, undo/redo and window resizing
 | 
						|
        #: operations, it happens that a short time, the same document has to be
 | 
						|
        #: lexed. This is a fairly easy way to cache such an expensive operation.
 | 
						|
        self._fragment_cache: SimpleCache[
 | 
						|
            Hashable, Callable[[int], StyleAndTextTuples]
 | 
						|
        ] = SimpleCache(maxsize=8)
 | 
						|
 | 
						|
        self._last_click_timestamp: float | None = None
 | 
						|
        self._last_get_processed_line: Callable[[int], _ProcessedLine] | None = None
 | 
						|
 | 
						|
    def __repr__(self) -> str:
 | 
						|
        return f"<{self.__class__.__name__} buffer={self.buffer!r} at {id(self)!r}>"
 | 
						|
 | 
						|
    @property
 | 
						|
    def search_buffer_control(self) -> SearchBufferControl | None:
 | 
						|
        result: SearchBufferControl | None
 | 
						|
 | 
						|
        if callable(self._search_buffer_control):
 | 
						|
            result = self._search_buffer_control()
 | 
						|
        else:
 | 
						|
            result = self._search_buffer_control
 | 
						|
 | 
						|
        assert result is None or isinstance(result, SearchBufferControl)
 | 
						|
        return result
 | 
						|
 | 
						|
    @property
 | 
						|
    def search_buffer(self) -> Buffer | None:
 | 
						|
        control = self.search_buffer_control
 | 
						|
        if control is not None:
 | 
						|
            return control.buffer
 | 
						|
        return None
 | 
						|
 | 
						|
    @property
 | 
						|
    def search_state(self) -> SearchState:
 | 
						|
        """
 | 
						|
        Return the `SearchState` for searching this `BufferControl`. This is
 | 
						|
        always associated with the search control. If one search bar is used
 | 
						|
        for searching multiple `BufferControls`, then they share the same
 | 
						|
        `SearchState`.
 | 
						|
        """
 | 
						|
        search_buffer_control = self.search_buffer_control
 | 
						|
        if search_buffer_control:
 | 
						|
            return search_buffer_control.searcher_search_state
 | 
						|
        else:
 | 
						|
            return SearchState()
 | 
						|
 | 
						|
    def is_focusable(self) -> bool:
 | 
						|
        return self.focusable()
 | 
						|
 | 
						|
    def preferred_width(self, max_available_width: int) -> int | None:
 | 
						|
        """
 | 
						|
        This should return the preferred width.
 | 
						|
 | 
						|
        Note: We don't specify a preferred width according to the content,
 | 
						|
              because it would be too expensive. Calculating the preferred
 | 
						|
              width can be done by calculating the longest line, but this would
 | 
						|
              require applying all the processors to each line. This is
 | 
						|
              unfeasible for a larger document, and doing it for small
 | 
						|
              documents only would result in inconsistent behavior.
 | 
						|
        """
 | 
						|
        return None
 | 
						|
 | 
						|
    def preferred_height(
 | 
						|
        self,
 | 
						|
        width: int,
 | 
						|
        max_available_height: int,
 | 
						|
        wrap_lines: bool,
 | 
						|
        get_line_prefix: GetLinePrefixCallable | None,
 | 
						|
    ) -> int | None:
 | 
						|
        # Calculate the content height, if it was drawn on a screen with the
 | 
						|
        # given width.
 | 
						|
        height = 0
 | 
						|
        content = self.create_content(width, height=1)  # Pass a dummy '1' as height.
 | 
						|
 | 
						|
        # When line wrapping is off, the height should be equal to the amount
 | 
						|
        # of lines.
 | 
						|
        if not wrap_lines:
 | 
						|
            return content.line_count
 | 
						|
 | 
						|
        # When the number of lines exceeds the max_available_height, just
 | 
						|
        # return max_available_height. No need to calculate anything.
 | 
						|
        if content.line_count >= max_available_height:
 | 
						|
            return max_available_height
 | 
						|
 | 
						|
        for i in range(content.line_count):
 | 
						|
            height += content.get_height_for_line(i, width, get_line_prefix)
 | 
						|
 | 
						|
            if height >= max_available_height:
 | 
						|
                return max_available_height
 | 
						|
 | 
						|
        return height
 | 
						|
 | 
						|
    def _get_formatted_text_for_line_func(
 | 
						|
        self, document: Document
 | 
						|
    ) -> Callable[[int], StyleAndTextTuples]:
 | 
						|
        """
 | 
						|
        Create a function that returns the fragments for a given line.
 | 
						|
        """
 | 
						|
 | 
						|
        # Cache using `document.text`.
 | 
						|
        def get_formatted_text_for_line() -> Callable[[int], StyleAndTextTuples]:
 | 
						|
            return self.lexer.lex_document(document)
 | 
						|
 | 
						|
        key = (document.text, self.lexer.invalidation_hash())
 | 
						|
        return self._fragment_cache.get(key, get_formatted_text_for_line)
 | 
						|
 | 
						|
    def _create_get_processed_line_func(
 | 
						|
        self, document: Document, width: int, height: int
 | 
						|
    ) -> Callable[[int], _ProcessedLine]:
 | 
						|
        """
 | 
						|
        Create a function that takes a line number of the current document and
 | 
						|
        returns a _ProcessedLine(processed_fragments, source_to_display, display_to_source)
 | 
						|
        tuple.
 | 
						|
        """
 | 
						|
        # Merge all input processors together.
 | 
						|
        input_processors = self.input_processors or []
 | 
						|
        if self.include_default_input_processors:
 | 
						|
            input_processors = self.default_input_processors + input_processors
 | 
						|
 | 
						|
        merged_processor = merge_processors(input_processors)
 | 
						|
 | 
						|
        def transform(
 | 
						|
            lineno: int,
 | 
						|
            fragments: StyleAndTextTuples,
 | 
						|
            get_line: Callable[[int], StyleAndTextTuples],
 | 
						|
        ) -> _ProcessedLine:
 | 
						|
            "Transform the fragments for a given line number."
 | 
						|
 | 
						|
            # Get cursor position at this line.
 | 
						|
            def source_to_display(i: int) -> int:
 | 
						|
                """X position from the buffer to the x position in the
 | 
						|
                processed fragment list. By default, we start from the 'identity'
 | 
						|
                operation."""
 | 
						|
                return i
 | 
						|
 | 
						|
            transformation = merged_processor.apply_transformation(
 | 
						|
                TransformationInput(
 | 
						|
                    self,
 | 
						|
                    document,
 | 
						|
                    lineno,
 | 
						|
                    source_to_display,
 | 
						|
                    fragments,
 | 
						|
                    width,
 | 
						|
                    height,
 | 
						|
                    get_line,
 | 
						|
                )
 | 
						|
            )
 | 
						|
 | 
						|
            return _ProcessedLine(
 | 
						|
                transformation.fragments,
 | 
						|
                transformation.source_to_display,
 | 
						|
                transformation.display_to_source,
 | 
						|
            )
 | 
						|
 | 
						|
        def create_func() -> Callable[[int], _ProcessedLine]:
 | 
						|
            get_line = self._get_formatted_text_for_line_func(document)
 | 
						|
            cache: dict[int, _ProcessedLine] = {}
 | 
						|
 | 
						|
            def get_processed_line(i: int) -> _ProcessedLine:
 | 
						|
                try:
 | 
						|
                    return cache[i]
 | 
						|
                except KeyError:
 | 
						|
                    processed_line = transform(i, get_line(i), get_line)
 | 
						|
                    cache[i] = processed_line
 | 
						|
                    return processed_line
 | 
						|
 | 
						|
            return get_processed_line
 | 
						|
 | 
						|
        return create_func()
 | 
						|
 | 
						|
    def create_content(
 | 
						|
        self, width: int, height: int, preview_search: bool = False
 | 
						|
    ) -> UIContent:
 | 
						|
        """
 | 
						|
        Create a UIContent.
 | 
						|
        """
 | 
						|
        buffer = self.buffer
 | 
						|
 | 
						|
        # Trigger history loading of the buffer. We do this during the
 | 
						|
        # rendering of the UI here, because it needs to happen when an
 | 
						|
        # `Application` with its event loop is running. During the rendering of
 | 
						|
        # the buffer control is the earliest place we can achieve this, where
 | 
						|
        # we're sure the right event loop is active, and don't require user
 | 
						|
        # interaction (like in a key binding).
 | 
						|
        buffer.load_history_if_not_yet_loaded()
 | 
						|
 | 
						|
        # Get the document to be shown. If we are currently searching (the
 | 
						|
        # search buffer has focus, and the preview_search filter is enabled),
 | 
						|
        # then use the search document, which has possibly a different
 | 
						|
        # text/cursor position.)
 | 
						|
        search_control = self.search_buffer_control
 | 
						|
        preview_now = preview_search or bool(
 | 
						|
            # Only if this feature is enabled.
 | 
						|
            self.preview_search()
 | 
						|
            and
 | 
						|
            # And something was typed in the associated search field.
 | 
						|
            search_control
 | 
						|
            and search_control.buffer.text
 | 
						|
            and
 | 
						|
            # And we are searching in this control. (Many controls can point to
 | 
						|
            # the same search field, like in Pyvim.)
 | 
						|
            get_app().layout.search_target_buffer_control == self
 | 
						|
        )
 | 
						|
 | 
						|
        if preview_now and search_control is not None:
 | 
						|
            ss = self.search_state
 | 
						|
 | 
						|
            document = buffer.document_for_search(
 | 
						|
                SearchState(
 | 
						|
                    text=search_control.buffer.text,
 | 
						|
                    direction=ss.direction,
 | 
						|
                    ignore_case=ss.ignore_case,
 | 
						|
                )
 | 
						|
            )
 | 
						|
        else:
 | 
						|
            document = buffer.document
 | 
						|
 | 
						|
        get_processed_line = self._create_get_processed_line_func(
 | 
						|
            document, width, height
 | 
						|
        )
 | 
						|
        self._last_get_processed_line = get_processed_line
 | 
						|
 | 
						|
        def translate_rowcol(row: int, col: int) -> Point:
 | 
						|
            "Return the content column for this coordinate."
 | 
						|
            return Point(x=get_processed_line(row).source_to_display(col), y=row)
 | 
						|
 | 
						|
        def get_line(i: int) -> StyleAndTextTuples:
 | 
						|
            "Return the fragments for a given line number."
 | 
						|
            fragments = get_processed_line(i).fragments
 | 
						|
 | 
						|
            # Add a space at the end, because that is a possible cursor
 | 
						|
            # position. (When inserting after the input.) We should do this on
 | 
						|
            # all the lines, not just the line containing the cursor. (Because
 | 
						|
            # otherwise, line wrapping/scrolling could change when moving the
 | 
						|
            # cursor around.)
 | 
						|
            fragments = fragments + [("", " ")]
 | 
						|
            return fragments
 | 
						|
 | 
						|
        content = UIContent(
 | 
						|
            get_line=get_line,
 | 
						|
            line_count=document.line_count,
 | 
						|
            cursor_position=translate_rowcol(
 | 
						|
                document.cursor_position_row, document.cursor_position_col
 | 
						|
            ),
 | 
						|
        )
 | 
						|
 | 
						|
        # If there is an auto completion going on, use that start point for a
 | 
						|
        # pop-up menu position. (But only when this buffer has the focus --
 | 
						|
        # there is only one place for a menu, determined by the focused buffer.)
 | 
						|
        if get_app().layout.current_control == self:
 | 
						|
            menu_position = self.menu_position() if self.menu_position else None
 | 
						|
            if menu_position is not None:
 | 
						|
                assert isinstance(menu_position, int)
 | 
						|
                menu_row, menu_col = buffer.document.translate_index_to_position(
 | 
						|
                    menu_position
 | 
						|
                )
 | 
						|
                content.menu_position = translate_rowcol(menu_row, menu_col)
 | 
						|
            elif buffer.complete_state:
 | 
						|
                # Position for completion menu.
 | 
						|
                # Note: We use 'min', because the original cursor position could be
 | 
						|
                #       behind the input string when the actual completion is for
 | 
						|
                #       some reason shorter than the text we had before. (A completion
 | 
						|
                #       can change and shorten the input.)
 | 
						|
                menu_row, menu_col = buffer.document.translate_index_to_position(
 | 
						|
                    min(
 | 
						|
                        buffer.cursor_position,
 | 
						|
                        buffer.complete_state.original_document.cursor_position,
 | 
						|
                    )
 | 
						|
                )
 | 
						|
                content.menu_position = translate_rowcol(menu_row, menu_col)
 | 
						|
            else:
 | 
						|
                content.menu_position = None
 | 
						|
 | 
						|
        return content
 | 
						|
 | 
						|
    def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
 | 
						|
        """
 | 
						|
        Mouse handler for this control.
 | 
						|
        """
 | 
						|
        buffer = self.buffer
 | 
						|
        position = mouse_event.position
 | 
						|
 | 
						|
        # Focus buffer when clicked.
 | 
						|
        if get_app().layout.current_control == self:
 | 
						|
            if self._last_get_processed_line:
 | 
						|
                processed_line = self._last_get_processed_line(position.y)
 | 
						|
 | 
						|
                # Translate coordinates back to the cursor position of the
 | 
						|
                # original input.
 | 
						|
                xpos = processed_line.display_to_source(position.x)
 | 
						|
                index = buffer.document.translate_row_col_to_index(position.y, xpos)
 | 
						|
 | 
						|
                # Set the cursor position.
 | 
						|
                if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
 | 
						|
                    buffer.exit_selection()
 | 
						|
                    buffer.cursor_position = index
 | 
						|
 | 
						|
                elif (
 | 
						|
                    mouse_event.event_type == MouseEventType.MOUSE_MOVE
 | 
						|
                    and mouse_event.button != MouseButton.NONE
 | 
						|
                ):
 | 
						|
                    # Click and drag to highlight a selection
 | 
						|
                    if (
 | 
						|
                        buffer.selection_state is None
 | 
						|
                        and abs(buffer.cursor_position - index) > 0
 | 
						|
                    ):
 | 
						|
                        buffer.start_selection(selection_type=SelectionType.CHARACTERS)
 | 
						|
                    buffer.cursor_position = index
 | 
						|
 | 
						|
                elif mouse_event.event_type == MouseEventType.MOUSE_UP:
 | 
						|
                    # When the cursor was moved to another place, select the text.
 | 
						|
                    # (The >1 is actually a small but acceptable workaround for
 | 
						|
                    # selecting text in Vi navigation mode. In navigation mode,
 | 
						|
                    # the cursor can never be after the text, so the cursor
 | 
						|
                    # will be repositioned automatically.)
 | 
						|
                    if abs(buffer.cursor_position - index) > 1:
 | 
						|
                        if buffer.selection_state is None:
 | 
						|
                            buffer.start_selection(
 | 
						|
                                selection_type=SelectionType.CHARACTERS
 | 
						|
                            )
 | 
						|
                        buffer.cursor_position = index
 | 
						|
 | 
						|
                    # Select word around cursor on double click.
 | 
						|
                    # Two MOUSE_UP events in a short timespan are considered a double click.
 | 
						|
                    double_click = (
 | 
						|
                        self._last_click_timestamp
 | 
						|
                        and time.time() - self._last_click_timestamp < 0.3
 | 
						|
                    )
 | 
						|
                    self._last_click_timestamp = time.time()
 | 
						|
 | 
						|
                    if double_click:
 | 
						|
                        start, end = buffer.document.find_boundaries_of_current_word()
 | 
						|
                        buffer.cursor_position += start
 | 
						|
                        buffer.start_selection(selection_type=SelectionType.CHARACTERS)
 | 
						|
                        buffer.cursor_position += end - start
 | 
						|
                else:
 | 
						|
                    # Don't handle scroll events here.
 | 
						|
                    return NotImplemented
 | 
						|
 | 
						|
        # Not focused, but focusing on click events.
 | 
						|
        else:
 | 
						|
            if (
 | 
						|
                self.focus_on_click()
 | 
						|
                and mouse_event.event_type == MouseEventType.MOUSE_UP
 | 
						|
            ):
 | 
						|
                # Focus happens on mouseup. (If we did this on mousedown, the
 | 
						|
                # up event will be received at the point where this widget is
 | 
						|
                # focused and be handled anyway.)
 | 
						|
                get_app().layout.current_control = self
 | 
						|
            else:
 | 
						|
                return NotImplemented
 | 
						|
 | 
						|
        return None
 | 
						|
 | 
						|
    def move_cursor_down(self) -> None:
 | 
						|
        b = self.buffer
 | 
						|
        b.cursor_position += b.document.get_cursor_down_position()
 | 
						|
 | 
						|
    def move_cursor_up(self) -> None:
 | 
						|
        b = self.buffer
 | 
						|
        b.cursor_position += b.document.get_cursor_up_position()
 | 
						|
 | 
						|
    def get_key_bindings(self) -> KeyBindingsBase | None:
 | 
						|
        """
 | 
						|
        When additional key bindings are given. Return these.
 | 
						|
        """
 | 
						|
        return self.key_bindings
 | 
						|
 | 
						|
    def get_invalidate_events(self) -> Iterable[Event[object]]:
 | 
						|
        """
 | 
						|
        Return the Window invalidate events.
 | 
						|
        """
 | 
						|
        # Whenever the buffer changes, the UI has to be updated.
 | 
						|
        yield self.buffer.on_text_changed
 | 
						|
        yield self.buffer.on_cursor_position_changed
 | 
						|
 | 
						|
        yield self.buffer.on_completions_changed
 | 
						|
        yield self.buffer.on_suggestion_set
 | 
						|
 | 
						|
 | 
						|
class SearchBufferControl(BufferControl):
 | 
						|
    """
 | 
						|
    :class:`.BufferControl` which is used for searching another
 | 
						|
    :class:`.BufferControl`.
 | 
						|
 | 
						|
    :param ignore_case: Search case insensitive.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        buffer: Buffer | None = None,
 | 
						|
        input_processors: list[Processor] | None = None,
 | 
						|
        lexer: Lexer | None = None,
 | 
						|
        focus_on_click: FilterOrBool = False,
 | 
						|
        key_bindings: KeyBindingsBase | None = None,
 | 
						|
        ignore_case: FilterOrBool = False,
 | 
						|
    ):
 | 
						|
        super().__init__(
 | 
						|
            buffer=buffer,
 | 
						|
            input_processors=input_processors,
 | 
						|
            lexer=lexer,
 | 
						|
            focus_on_click=focus_on_click,
 | 
						|
            key_bindings=key_bindings,
 | 
						|
        )
 | 
						|
 | 
						|
        # If this BufferControl is used as a search field for one or more other
 | 
						|
        # BufferControls, then represents the search state.
 | 
						|
        self.searcher_search_state = SearchState(ignore_case=ignore_case)
 |