Пример #1
0
 def __init__(self) -> None:
     self._bindings: List[Binding] = []
     self._get_bindings_for_keys_cache: SimpleCache[KeysTuple, List[Binding]] = \
         SimpleCache(maxsize=10000)
     self._get_bindings_starting_with_keys_cache: SimpleCache[KeysTuple, List[Binding]] = \
         SimpleCache(maxsize=1000)
     self.__version = 0  # For cache invalidation.
Пример #2
0
    def __init__(self, get_tokens, default_char=None, get_default_char=None,
                 align_right=False, align_center=False,
                 has_focus=False, wrap_lines=True):
        assert default_char is None or isinstance(default_char, Char)
        assert get_default_char is None or callable(get_default_char)
        assert not (default_char and get_default_char)

        self.align_right = to_cli_filter(align_right)
        self.align_center = to_cli_filter(align_center)
        self._has_focus_filter = to_cli_filter(has_focus)
        self.wrap_lines = to_cli_filter(wrap_lines)

        self.get_tokens = get_tokens

        # Construct `get_default_char` callable.
        if default_char:
            get_default_char = lambda _: default_char
        elif not get_default_char:
            get_default_char = lambda _: Char(' ', Token)

        self.get_default_char = get_default_char

        #: Cache for rendered screens.
        self._screen_cache = SimpleCache(maxsize=18)
        self._token_cache = SimpleCache(maxsize=1)
            # Only cache one token list. We don't need the previous item.

        # Render info for the mouse support.
        self._tokens = None  # The last rendered tokens.
        self._pos_to_indexes = None  # Mapping from mouse positions (x,y) to
Пример #3
0
    def __init__(self, get_tokens, default_char=None, get_default_char=None,
                 align_right=False, align_center=False, has_focus=False):
        assert default_char is None or isinstance(default_char, Char)
        assert get_default_char is None or callable(get_default_char)
        assert not (default_char and get_default_char)

        self.align_right = to_cli_filter(align_right)
        self.align_center = to_cli_filter(align_center)
        self._has_focus_filter = to_cli_filter(has_focus)

        self.get_tokens = get_tokens

        # Construct `get_default_char` callable.
        if default_char:
            get_default_char = lambda _: default_char
        elif not get_default_char:
            get_default_char = lambda _: Char(' ', Token)

        self.get_default_char = get_default_char

        #: Cache for the content.
        self._content_cache = SimpleCache(maxsize=18)
        self._token_cache = SimpleCache(maxsize=1)
            # Only cache one token list. We don't need the previous item.

        # Render info for the mouse support.
        self._tokens = None
Пример #4
0
    def __init__(self,
                 text='',
                 style='',
                 focusable=False,
                 key_bindings=None,
                 show_cursor=True,
                 modal=False,
                 get_cursor_position=None):
        from prompt_toolkit.key_binding.key_bindings import KeyBindingsBase
        assert isinstance(style, six.text_type)
        assert key_bindings is None or isinstance(key_bindings,
                                                  KeyBindingsBase)
        assert isinstance(show_cursor, bool)
        assert isinstance(modal, bool)
        assert get_cursor_position is None or callable(get_cursor_position)

        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(maxsize=18)
        self._fragment_cache = SimpleCache(maxsize=1)
        # Only cache one fragment list. We don't need the previous item.

        # Render info for the mouse support.
        self._fragments = None
Пример #5
0
    def __init__(self,
                 get_tokens,
                 default_char=None,
                 get_default_char=None,
                 align_right=False,
                 align_center=False,
                 has_focus=False):
        assert callable(get_tokens)
        assert default_char is None or isinstance(default_char, Char)
        assert get_default_char is None or callable(get_default_char)
        assert not (default_char and get_default_char)

        self.align_right = to_cli_filter(align_right)
        self.align_center = to_cli_filter(align_center)
        self._has_focus_filter = to_cli_filter(has_focus)

        self.get_tokens = get_tokens

        # Construct `get_default_char` callable.
        if default_char:
            get_default_char = lambda _: default_char
        elif not get_default_char:
            get_default_char = lambda _: Char(' ', Token.Transparent)

        self.get_default_char = get_default_char

        #: Cache for the content.
        self._content_cache = SimpleCache(maxsize=18)
        self._token_cache = SimpleCache(maxsize=1)
        # Only cache one token list. We don't need the previous item.

        # Render info for the mouse support.
        self._tokens = None
Пример #6
0
    def __init__(
        self,
        text: AnyFormattedText = "",
        style: str = "",
        focusable: FilterOrBool = False,
        key_bindings: Optional["KeyBindingsBase"] = None,
        show_cursor: bool = True,
        modal: bool = False,
        get_cursor_position: Optional[Callable[[], Optional[Point]]] = 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: Optional[StyleAndTextTuples] = None
    def __init__(self,
                 get_tokens,
                 default_char=None,
                 get_default_char=None,
                 align_right=False,
                 align_center=False,
                 has_focus=False,
                 wrap_lines=True):
        assert default_char is None or isinstance(default_char, Char)
        assert get_default_char is None or callable(get_default_char)
        assert not (default_char and get_default_char)

        self.align_right = to_cli_filter(align_right)
        self.align_center = to_cli_filter(align_center)
        self._has_focus_filter = to_cli_filter(has_focus)
        self.wrap_lines = to_cli_filter(wrap_lines)

        self.get_tokens = get_tokens

        # Construct `get_default_char` callable.
        if default_char:
            get_default_char = lambda _: default_char
        elif not get_default_char:
            get_default_char = lambda _: Char(' ', Token)

        self.get_default_char = get_default_char

        #: Cache for rendered screens.
        self._screen_cache = SimpleCache(maxsize=18)
        self._token_cache = SimpleCache(maxsize=1)
        # Only cache one token list. We don't need the previous item.

        # Render info for the mouse support.
        self._tokens = None  # The last rendered tokens.
        self._pos_to_indexes = None  # Mapping from mouse positions (x,y) to
Пример #8
0
    def __init__(self,
                 buffer=None,
                 input_processors=None,
                 include_default_input_processors=True,
                 lexer=None,
                 preview_search=False,
                 focusable=True,
                 search_buffer_control=None,
                 menu_position=None,
                 focus_on_click=False,
                 key_bindings=None):
        from prompt_toolkit.key_binding.key_bindings import KeyBindingsBase
        assert buffer is None or isinstance(buffer, Buffer)
        assert input_processors is None or isinstance(input_processors, list)
        assert isinstance(include_default_input_processors, bool)
        assert menu_position is None or callable(menu_position)
        assert lexer is None or isinstance(lexer, Lexer)
        assert (search_buffer_control is None
                or callable(search_buffer_control)
                or isinstance(search_buffer_control, SearchBufferControl))
        assert key_bindings is None or isinstance(key_bindings,
                                                  KeyBindingsBase)

        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(maxsize=8)

        self._xy_to_cursor_position = None
        self._last_click_timestamp = None
        self._last_get_processed_line = None
Пример #9
0
    def __init__(self,
                 content,
                 width=None,
                 height=None,
                 get_width=None,
                 get_height=None,
                 dont_extend_width=False,
                 dont_extend_height=False,
                 left_margins=None,
                 right_margins=None,
                 scroll_offsets=None,
                 allow_scroll_beyond_bottom=False,
                 get_vertical_scroll=None,
                 get_horizontal_scroll=None,
                 always_hide_cursor=False):
        assert isinstance(content, UIControl)
        assert width is None or isinstance(width, LayoutDimension)
        assert height is None or isinstance(height, LayoutDimension)
        assert get_width is None or callable(get_width)
        assert get_height is None or callable(get_height)
        assert width is None or get_width is None
        assert height is None or get_height is None
        assert scroll_offsets is None or isinstance(scroll_offsets,
                                                    ScrollOffsets)
        assert left_margins is None or all(
            isinstance(m, Margin) for m in left_margins)
        assert right_margins is None or all(
            isinstance(m, Margin) for m in right_margins)
        assert get_vertical_scroll is None or callable(get_vertical_scroll)
        assert get_horizontal_scroll is None or callable(get_horizontal_scroll)

        self.allow_scroll_beyond_bottom = to_cli_filter(
            allow_scroll_beyond_bottom)
        self.always_hide_cursor = to_cli_filter(always_hide_cursor)

        self.content = content
        self.dont_extend_width = dont_extend_width
        self.dont_extend_height = dont_extend_height
        self.left_margins = left_margins or []
        self.right_margins = right_margins or []
        self.scroll_offsets = scroll_offsets or ScrollOffsets()
        self.get_vertical_scroll = get_vertical_scroll
        self.get_horizontal_scroll = get_horizontal_scroll
        self._width = get_width or (lambda cli: width)
        self._height = get_height or (lambda cli: height)

        # Cache for the screens generated by the margin.
        self._margin_cache = SimpleCache(maxsize=8)

        self.reset()
Пример #10
0
    def __init__(self,
                 buffer_name=DEFAULT_BUFFER,
                 input_processors=None,
                 highlighters=None,
                 lexer=None,
                 preview_search=False,
                 search_buffer_name=SEARCH_BUFFER,
                 get_search_state=None,
                 wrap_lines=True,
                 menu_position=None,
                 default_char=None,
                 focus_on_click=False):
        assert input_processors is None or all(isinstance(i, Processor) for i in input_processors)
        assert highlighters is None or all(isinstance(i, Highlighter) for i in highlighters)
        assert menu_position is None or callable(menu_position)
        assert lexer is None or isinstance(lexer, Lexer)
        assert get_search_state is None or callable(get_search_state)

        self.preview_search = to_cli_filter(preview_search)
        self.get_search_state = get_search_state
        self.wrap_lines = to_cli_filter(wrap_lines)
        self.focus_on_click = to_cli_filter(focus_on_click)

        self.input_processors = input_processors or []
        self.highlighters = highlighters or []
        self.buffer_name = buffer_name
        self.menu_position = menu_position
        self.lexer = lexer or SimpleLexer()
        self.default_char = default_char or Char(token=Token.Transparent)
        self.search_buffer_name = search_buffer_name

        #: 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 faily easy way to cache such an expensive operation.
        self._token_cache = SimpleCache(maxsize=8)

        #: Keep a similar cache for rendered screens. (when we scroll up/down
        #: through the screen, or when we change another buffer, we don't want
        #: to recreate the same screen again.)
        self._screen_cache = SimpleCache(maxsize=8)

        #: Highlight Cache.
        #: When nothing of the buffer content or processors has changed, but
        #: the highlighting of the selection/search changes,
        self._highlight_cache = SimpleCache(maxsize=8)

        self._xy_to_cursor_position = None
        self._last_click_timestamp = None
Пример #11
0
    def __init__(
        self, chars: str = "[](){}<>", max_cursor_distance: int = 1000
    ) -> None:
        self.chars = chars
        self.max_cursor_distance = max_cursor_distance

        self._positions_cache: SimpleCache[
            Hashable, List[Tuple[int, int]]
        ] = SimpleCache(maxsize=8)
Пример #12
0
    def __init__(self,
                 buffer_name=DEFAULT_BUFFER,
                 input_processors=None,
                 lexer=None,
                 preview_search=False,
                 search_buffer_name=SEARCH_BUFFER,
                 get_search_state=None,
                 menu_position=None,
                 default_char=None,
                 focus_on_click=False):
        assert input_processors is None or all(
            isinstance(i, Processor) for i in input_processors)
        assert menu_position is None or callable(menu_position)
        assert lexer is None or isinstance(lexer, Lexer)
        assert get_search_state is None or callable(get_search_state)
        assert default_char is None or isinstance(default_char, Char)

        self.preview_search = to_cli_filter(preview_search)
        self.get_search_state = get_search_state
        self.focus_on_click = to_cli_filter(focus_on_click)

        self.input_processors = input_processors or []
        self.buffer_name = buffer_name
        self.menu_position = menu_position
        self.lexer = lexer or SimpleLexer()
        self.default_char = default_char or Char(token=Token.Transparent)
        self.search_buffer_name = search_buffer_name

        #: 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 faily easy way to cache such an expensive operation.
        self._token_cache = SimpleCache(maxsize=8)

        self._xy_to_cursor_position = None
        self._last_click_timestamp = None
        self._last_get_processed_line = None
Пример #13
0
    def __init__(
        self,
        buffer: Optional[Buffer] = None,
        input_processors: Optional[List[Processor]] = None,
        include_default_input_processors: bool = True,
        lexer: Optional[Lexer] = None,
        preview_search: FilterOrBool = False,
        focusable: FilterOrBool = True,
        search_buffer_control: Union[None, "SearchBufferControl",
                                     Callable[[],
                                              "SearchBufferControl"]] = None,
        menu_position: Optional[Callable] = None,
        focus_on_click: FilterOrBool = False,
        key_bindings: Optional["KeyBindingsBase"] = 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: Optional[float] = None
        self._last_get_processed_line: Optional[Callable[
            [int], _ProcessedLine]] = None
    def __init__(self,
                 buffer_name=DEFAULT_BUFFER,
                 input_processors=None,
                 highlighters=None,
                 lexer=None,
                 preview_search=False,
                 search_buffer_name=SEARCH_BUFFER,
                 get_search_state=None,
                 wrap_lines=True,
                 menu_position=None,
                 default_char=None,
                 focus_on_click=False):
        assert input_processors is None or all(
            isinstance(i, Processor) for i in input_processors)
        assert highlighters is None or all(
            isinstance(i, Highlighter) for i in highlighters)
        assert menu_position is None or callable(menu_position)
        assert lexer is None or isinstance(lexer, Lexer)
        assert get_search_state is None or callable(get_search_state)

        self.preview_search = to_cli_filter(preview_search)
        self.get_search_state = get_search_state
        self.wrap_lines = to_cli_filter(wrap_lines)
        self.focus_on_click = to_cli_filter(focus_on_click)

        self.input_processors = input_processors or []
        self.highlighters = highlighters or []
        self.buffer_name = buffer_name
        self.menu_position = menu_position
        self.lexer = lexer or SimpleLexer()
        self.default_char = default_char or Char(token=Token.Transparent)
        self.search_buffer_name = search_buffer_name

        #: 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 faily easy way to cache such an expensive operation.
        self._token_cache = SimpleCache(maxsize=8)

        #: Keep a similar cache for rendered screens. (when we scroll up/down
        #: through the screen, or when we change another buffer, we don't want
        #: to recreate the same screen again.)
        self._screen_cache = SimpleCache(maxsize=8)

        #: Highlight Cache.
        #: When nothing of the buffer content or processors has changed, but
        #: the highlighting of the selection/search changes,
        self._highlight_cache = SimpleCache(maxsize=8)

        self._xy_to_cursor_position = None
        self._last_click_timestamp = None
Пример #15
0
class _MergedStyle(BaseStyle):
    """
    Merge multiple `Style` objects into one.
    This is supposed to ensure consistency: if any of the given styles changes,
    then this style will be updated.
    """

    # NOTE: previously, we used an algorithm where we did not generate the
    #       combined style. Instead this was a proxy that called one style
    #       after the other, passing the outcome of the previous style as the
    #       default for the next one. This did not work, because that way, the
    #       priorities like described in the `Style` class don't work.
    #       'class:aborted' was for instance never displayed in gray, because
    #       the next style specified a default color for any text. (The
    #       explicit styling of class:aborted should have taken priority,
    #       because it was more precise.)
    def __init__(self, styles):
        assert all(isinstance(style, BaseStyle) for style in styles)

        self.styles = styles
        self._style = SimpleCache(maxsize=1)

    @property
    def _merged_style(self):
        " The `Style` object that has the other styles merged together. "

        def get():
            return Style(self.style_rules)

        return self._style.get(self.invalidation_hash(), get)

    @property
    def style_rules(self):
        style_rules = []
        for s in self.styles:
            style_rules.extend(s.style_rules)
        return style_rules

    def get_attrs_for_style_str(self, style_str, default=DEFAULT_ATTRS):
        return self._merged_style.get_attrs_for_style_str(style_str, default)

    def invalidation_hash(self):
        return tuple(s.invalidation_hash() for s in self.styles)
class _MergedStyle(BaseStyle):
    """
    Merge multiple `Style` objects into one.
    This is supposed to ensure consistency: if any of the given styles changes,
    then this style will be updated.
    """
    # NOTE: previously, we used an algorithm where we did not generate the
    #       combined style. Instead this was a proxy that called one style
    #       after the other, passing the outcome of the previous style as the
    #       default for the next one. This did not work, because that way, the
    #       priorities like described in the `Style` class don't work.
    #       'class:aborted' was for instance never displayed in gray, because
    #       the next style specified a default color for any text. (The
    #       explicit styling of class:aborted should have taken priority,
    #       because it was more precise.)
    def __init__(self, styles):
        assert all(isinstance(style, BaseStyle) for style in styles)

        self.styles = styles
        self._style = SimpleCache(maxsize=1)

    @property
    def _merged_style(self):
        " The `Style` object that has the other styles merged together. "
        def get():
            return Style(self.style_rules)
        return self._style.get(self.invalidation_hash(), get)

    @property
    def style_rules(self):
        style_rules = []
        for s in self.styles:
            style_rules.extend(s.style_rules)
        return style_rules

    def get_attrs_for_style_str(self, style_str, default=DEFAULT_ATTRS):
        return self._merged_style.get_attrs_for_style_str(style_str, default)

    def invalidation_hash(self):
        return tuple(s.invalidation_hash() for s in self.styles)
Пример #17
0
    def __init__(self,
                 buffer_name=DEFAULT_BUFFER,
                 input_processors=None,
                 lexer=None,
                 preview_search=False,
                 search_buffer_name=SEARCH_BUFFER,
                 get_search_state=None,
                 menu_position=None,
                 default_char=None,
                 focus_on_click=False):
        assert input_processors is None or all(isinstance(i, Processor) for i in input_processors)
        assert menu_position is None or callable(menu_position)
        assert lexer is None or isinstance(lexer, Lexer)
        assert get_search_state is None or callable(get_search_state)
        assert default_char is None or isinstance(default_char, Char)

        self.preview_search = to_cli_filter(preview_search)
        self.get_search_state = get_search_state
        self.focus_on_click = to_cli_filter(focus_on_click)

        self.input_processors = input_processors or []
        self.buffer_name = buffer_name
        self.menu_position = menu_position
        self.lexer = lexer or SimpleLexer()
        self.default_char = default_char or Char(token=Token.Transparent)
        self.search_buffer_name = search_buffer_name

        #: 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 faily easy way to cache such an expensive operation.
        self._token_cache = SimpleCache(maxsize=8)
        self._processed_token_cache = SimpleCache(maxsize=8)

        self._xy_to_cursor_position = None
        self._last_click_timestamp = None
        self._last_get_processed_line = None
Пример #18
0
    def __init__(self, content, width=None, height=None, get_width=None,
                 get_height=None, dont_extend_width=False, dont_extend_height=False,
                 left_margins=None, right_margins=None, scroll_offsets=None,
                 allow_scroll_beyond_bottom=False,
                 get_vertical_scroll=None, get_horizontal_scroll=None, always_hide_cursor=False):
        assert isinstance(content, UIControl)
        assert width is None or isinstance(width, LayoutDimension)
        assert height is None or isinstance(height, LayoutDimension)
        assert get_width is None or callable(get_width)
        assert get_height is None or callable(get_height)
        assert width is None or get_width is None
        assert height is None or get_height is None
        assert scroll_offsets is None or isinstance(scroll_offsets, ScrollOffsets)
        assert left_margins is None or all(isinstance(m, Margin) for m in left_margins)
        assert right_margins is None or all(isinstance(m, Margin) for m in right_margins)
        assert get_vertical_scroll is None or callable(get_vertical_scroll)
        assert get_horizontal_scroll is None or callable(get_horizontal_scroll)

        self.allow_scroll_beyond_bottom = to_cli_filter(allow_scroll_beyond_bottom)
        self.always_hide_cursor = to_cli_filter(always_hide_cursor)

        self.content = content
        self.dont_extend_width = dont_extend_width
        self.dont_extend_height = dont_extend_height
        self.left_margins = left_margins or []
        self.right_margins = right_margins or []
        self.scroll_offsets = scroll_offsets or ScrollOffsets()
        self.get_vertical_scroll = get_vertical_scroll
        self.get_horizontal_scroll = get_horizontal_scroll
        self._width = get_width or (lambda cli: width)
        self._height = get_height or (lambda cli: height)

        # Cache for the screens generated by the margin.
        self._margin_cache = SimpleCache(maxsize=8)

        self.reset()
Пример #19
0
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 or a list of
    ``(style_str, text)`` tuples, 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='',
                 style='',
                 focusable=False,
                 key_bindings=None,
                 show_cursor=True,
                 modal=False,
                 get_cursor_position=None):
        from prompt_toolkit.key_binding.key_bindings import KeyBindingsBase
        assert isinstance(style, six.text_type)
        assert key_bindings is None or isinstance(key_bindings,
                                                  KeyBindingsBase)
        assert isinstance(show_cursor, bool)
        assert isinstance(modal, bool)
        assert get_cursor_position is None or callable(get_cursor_position)

        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(maxsize=18)
        self._fragment_cache = SimpleCache(maxsize=1)
        # Only cache one fragment list. We don't need the previous item.

        # Render info for the mouse support.
        self._fragments = None

    def reset(self):
        self._fragments = None

    def is_focusable(self):
        return self.focusable()

    def __repr__(self):
        return '%s(%r)' % (self.__class__.__name__, self.text)

    def _get_formatted_text_cached(self):
        """
        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):
        """
        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, max_available_height, wrap_lines):
        content = self.create_content(width, None)
        return content.line_count

    def create_content(self, width, height):
        # 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 = [[tuple(item[:2]) 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='[SetCursorPosition]'):
            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():
            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():
            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):
        """
        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):
        return self.modal

    def get_key_bindings(self):
        return self.key_bindings
Пример #20
0
 def __init__(self, app: Application[_AppResult]) -> None:
     self.app = app
     self._cache: SimpleCache[Tuple[Window, FrozenSet[UIControl]],
                              KeyBindingsBase] = SimpleCache()
Пример #21
0
class KeyBindings(KeyBindingsBase):
    """
    A container for a set of key bindings.

    Example usage::

        kb = KeyBindings()

        @kb.add('c-t')
        def _(event):
            print('Control-T pressed')

        @kb.add('c-a', 'c-b')
        def _(event):
            print('Control-A pressed, followed by Control-B')

        @kb.add('c-x', filter=is_searching)
        def _(event):
            print('Control-X pressed')  # Works only if we are searching.

    """

    def __init__(self):
        self.bindings = []
        self._get_bindings_for_keys_cache = SimpleCache(maxsize=10000)
        self._get_bindings_starting_with_keys_cache = SimpleCache(maxsize=1000)
        self.__version = 0  # For cache invalidation.

    def _clear_cache(self):
        self.__version += 1
        self._get_bindings_for_keys_cache.clear()
        self._get_bindings_starting_with_keys_cache.clear()

    @property
    def _version(self):
        return self.__version

    def add(self, *keys, **kwargs):
        """
        Decorator for adding a key bindings.

        :param filter: :class:`~prompt_toolkit.filters.Filter` to determine
            when this key binding is active.
        :param eager: :class:`~prompt_toolkit.filters.Filter` or `bool`.
            When True, ignore potential longer matches when this key binding is
            hit. E.g. when there is an active eager key binding for Ctrl-X,
            execute the handler immediately and ignore the key binding for
            Ctrl-X Ctrl-E of which it is a prefix.
        :param is_global: When this key bindings is added to a `Container` or
            `Control`, make it a global (always active) binding.
        :param save_before: Callable that takes an `Event` and returns True if
            we should save the current buffer, before handling the event.
            (That's the default.)
        :param record_in_macro: Record these key bindings when a macro is
            being recorded. (True by default.)
        """
        filter = to_filter(kwargs.pop('filter', True))
        eager = to_filter(kwargs.pop('eager', False))
        is_global = to_filter(kwargs.pop('is_global', False))
        save_before = kwargs.pop('save_before', lambda e: True)
        record_in_macro = to_filter(kwargs.pop('record_in_macro', True))

        assert not kwargs
        assert keys
        assert callable(save_before)

        keys = tuple(_check_and_expand_key(k) for k in keys)

        if isinstance(filter, Never):
            # When a filter is Never, it will always stay disabled, so in that
            # case don't bother putting it in the key bindings. It will slow
            # down every key press otherwise.
            def decorator(func):
                return func
        else:
            def decorator(func):
                if isinstance(func, _Binding):
                    # We're adding an existing _Binding object.
                    self.bindings.append(
                        _Binding(
                            keys, func.handler,
                            filter=func.filter & filter,
                            eager=eager | func.eager,
                            is_global=is_global | func.is_global,
                            save_before=func.save_before,
                            record_in_macro=func.record_in_macro))
                else:
                    self.bindings.append(
                        _Binding(keys, func, filter=filter, eager=eager,
                                 is_global=is_global, save_before=save_before,
                                 record_in_macro=record_in_macro))
                self._clear_cache()

                return func
        return decorator

    def remove(self, *args):
        """
        Remove a key binding.

        This expects either a function that was given to `add` method as
        parameter or a sequence of key bindings.

        Raises `ValueError` when no bindings was found.

        Usage::

            remove(handler)  # Pass handler.
            remove('c-x', 'c-a')  # Or pass the key bindings.
        """
        found = False

        if callable(args[0]):
            assert len(args) == 1
            function = args[0]

            # Remove the given function.
            for b in self.bindings:
                if b.handler == function:
                    self.bindings.remove(b)
                    found = True

        else:
            assert len(args) > 0

            # Remove this sequence of key bindings.
            keys = tuple(_check_and_expand_key(k) for k in args)

            for b in self.bindings:
                if b.keys == keys:
                    self.bindings.remove(b)
                    found = True

        if found:
            self._clear_cache()
        else:
            # No key binding found for this function. Raise ValueError.
            raise ValueError('Binding not found: %r' % (function,))

    # For backwards-compatibility.
    add_binding = add
    remove_binding = remove

    def get_bindings_for_keys(self, keys):
        """
        Return a list of key bindings that can handle this key.
        (This return also inactive bindings, so the `filter` still has to be
        called, for checking it.)

        :param keys: tuple of keys.
        """

        def get():
            result = []
            for b in self.bindings:
                if len(keys) == len(b.keys):
                    match = True
                    any_count = 0

                    for i, j in zip(b.keys, keys):
                        if i != j and i != Keys.Any:
                            match = False
                            break

                        if i == Keys.Any:
                            any_count += 1

                    if match:
                        result.append((any_count, b))

            # Place bindings that have more 'Any' occurrences in them at the end.
            result = sorted(result, key=lambda item: -item[0])

            return [item[1] for item in result]

        return self._get_bindings_for_keys_cache.get(keys, get)

    def get_bindings_starting_with_keys(self, keys):
        """
        Return a list of key bindings that handle a key sequence starting with
        `keys`. (It does only return bindings for which the sequences are
        longer than `keys`. And like `get_bindings_for_keys`, it also includes
        inactive bindings.)

        :param keys: tuple of keys.
        """

        def get():
            result = []
            for b in self.bindings:
                if len(keys) < len(b.keys):
                    match = True
                    for i, j in zip(b.keys, keys):
                        if i != j and i != Keys.Any:
                            match = False
                            break
                    if match:
                        result.append(b)
            return result

        return self._get_bindings_starting_with_keys_cache.get(keys, get)
Пример #22
0
    def __init__(self,
                 buffer=None,
                 input_processor=None,
                 lexer=None,
                 preview_search=False,
                 focusable=True,
                 search_buffer_control=None,
                 get_search_buffer_control=None,
                 get_search_state=None,
                 menu_position=None,
                 focus_on_click=False,
                 key_bindings=None):
        from prompt_toolkit.key_binding.key_bindings import KeyBindingsBase
        assert buffer is None or isinstance(buffer, Buffer)
        assert input_processor is None or isinstance(input_processor,
                                                     Processor)
        assert menu_position is None or callable(menu_position)
        assert lexer is None or isinstance(lexer, Lexer)
        assert search_buffer_control is None or isinstance(
            search_buffer_control, BufferControl)
        assert get_search_buffer_control is None or callable(
            get_search_buffer_control)
        assert not (search_buffer_control and get_search_buffer_control)
        assert get_search_state is None or callable(get_search_state)
        assert key_bindings is None or isinstance(key_bindings,
                                                  KeyBindingsBase)

        # Default search state.
        if get_search_state is None:
            search_state = SearchState()

            def get_search_state():
                return search_state

        # Default input processor (display search and selection by default.)
        if input_processor is None:
            input_processor = merge_processors([
                HighlightSearchProcessor(),
                HighlightSelectionProcessor(),
                DisplayMultipleCursors(),
            ])

        self.preview_search = to_filter(preview_search)
        self.focusable = to_filter(focusable)
        self.get_search_state = get_search_state
        self.focus_on_click = to_filter(focus_on_click)

        self.input_processor = input_processor
        self.buffer = buffer or Buffer()
        self.menu_position = menu_position
        self.lexer = lexer or SimpleLexer()
        self.get_search_buffer_control = get_search_buffer_control
        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(maxsize=8)

        self._xy_to_cursor_position = None
        self._last_click_timestamp = None
        self._last_get_processed_line = None
class _CombinedRegistry(KeyBindingsBase):
    """
    The `KeyBindings` of key bindings for a `Application`.
    This merges the global key bindings with the one of the current user
    control.
    """
    def __init__(self, app):
        self.app = app
        self._cache = SimpleCache()

    @property
    def _version(self):
        """ Not needed - this object is not going to be wrapped in another
        KeyBindings object. """
        raise NotImplementedError

    def _create_key_bindings(self, current_window, other_controls):
        """
        Create a `KeyBindings` object that merges the `KeyBindings` from the
        `UIControl` with all the parent controls and the global key bindings.
        """
        key_bindings = []
        collected_containers = set()

        # Collect key bindings from currently focused control and all parent
        # controls. Don't include key bindings of container parent controls.
        container = current_window
        while True:
            collected_containers.add(container)
            kb = container.get_key_bindings()
            if kb is not None:
                key_bindings.append(kb)

            if container.is_modal():
                break

            parent = self.app.layout.get_parent(container)
            if parent is None:
                break
            else:
                container = parent

        # Include global bindings (starting at the top-model container).
        for c in walk(container):
            if c not in collected_containers:
                kb = c.get_key_bindings()
                if kb is not None:
                    key_bindings.append(GlobalOnlyKeyBindings(kb))

        # Add App key bindings
        if self.app.key_bindings:
            key_bindings.append(self.app.key_bindings)

        # Add mouse bindings.
        key_bindings.append(ConditionalKeyBindings(
            self.app._page_navigation_bindings,
            self.app.enable_page_navigation_bindings))
        key_bindings.append(self.app._default_bindings)

        # Reverse this list. The current control's key bindings should come
        # last. They need priority.
        key_bindings = key_bindings[::-1]

        return merge_key_bindings(key_bindings)

    @property
    def _key_bindings(self):
        current_window = self.app.layout.current_window
        other_controls = list(self.app.layout.find_all_controls())
        key = current_window, frozenset(other_controls)

        return self._cache.get(
            key, lambda: self._create_key_bindings(current_window, other_controls))

    def get_bindings_for_keys(self, keys):
        return self._key_bindings.get_bindings_for_keys(keys)

    def get_bindings_starting_with_keys(self, keys):
        return self._key_bindings.get_bindings_starting_with_keys(keys)
Пример #24
0
class TokenListControl(UIControl):
    """
    Control that displays a list of (Token, text) tuples.
    (It's mostly optimized for rather small widgets, like toolbars, menus, etc...)

    Mouse support:

        The list of tokens can also contain tuples of three items, looking like:
        (Token, text, handler). When mouse support is enabled and the user
        clicks on this token, then the given handler is called. That handler
        should accept two inputs: (CommandLineInterface, MouseEvent) and it
        should either handle the event or return `NotImplemented` in case we
        want the containing Window to handle this event.

    :param get_tokens: Callable that takes a `CommandLineInterface` instance
        and returns the list of (Token, text) tuples to be displayed right now.
    :param default_char: default :class:`.Char` (character and Token) to use
        for the background when there is more space available than `get_tokens`
        returns.
    :param get_default_char: Like `default_char`, but this is a callable that
        takes a :class:`prompt_toolkit.interface.CommandLineInterface` and
        returns a :class:`.Char` instance.
    :param has_focus: `bool` or `CLIFilter`, when this evaluates to `True`,
        this UI control will take the focus. The cursor will be shown in the
        upper left corner of this control, unless `get_token` returns a
        ``Token.SetCursorPosition`` token somewhere in the token list, then the
        cursor will be shown there.
    """
    def __init__(self, get_tokens, default_char=None, get_default_char=None,
                 align_right=False, align_center=False, has_focus=False):
        assert default_char is None or isinstance(default_char, Char)
        assert get_default_char is None or callable(get_default_char)
        assert not (default_char and get_default_char)

        self.align_right = to_cli_filter(align_right)
        self.align_center = to_cli_filter(align_center)
        self._has_focus_filter = to_cli_filter(has_focus)

        self.get_tokens = get_tokens

        # Construct `get_default_char` callable.
        if default_char:
            get_default_char = lambda _: default_char
        elif not get_default_char:
            get_default_char = lambda _: Char(' ', Token)

        self.get_default_char = get_default_char

        #: Cache for the content.
        self._content_cache = SimpleCache(maxsize=18)
        self._token_cache = SimpleCache(maxsize=1)
            # Only cache one token list. We don't need the previous item.

        # Render info for the mouse support.
        self._tokens = None

    def reset(self):
        self._tokens = None

    def __repr__(self):
        return '%s(%r)' % (self.__class__.__name__, self.get_tokens)

    def _get_tokens_cached(self, cli):
        """
        Get tokens, but only retrieve tokens 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._token_cache.get(
            cli.render_counter, lambda: self.get_tokens(cli))

    def has_focus(self, cli):
        return self._has_focus_filter(cli)

    def preferred_width(self, cli, max_available_width):
        """
        Return the preferred width for this control.
        That is the width of the longest line.
        """
        text = token_list_to_text(self._get_tokens_cached(cli))
        line_lengths = [get_cwidth(l) for l in text.split('\n')]
        return max(line_lengths)

    def preferred_height(self, cli, width, max_available_height, wrap_lines):
        content = self.create_content(cli, width, None)
        return content.line_count

    def create_content(self, cli, width, height):
        # Get tokens
        tokens_with_mouse_handlers = self._get_tokens_cached(cli)

        default_char = self.get_default_char(cli)

        # Wrap/align right/center parameters.
        right = self.align_right(cli)
        center = self.align_center(cli)

        def process_line(line):
            " Center or right align a single line. "
            used_width = token_list_width(line)
            padding = width - used_width
            if center:
                padding = int(padding / 2)
            return [(default_char.token, default_char.char * padding)] + line

        if right or center:
            token_lines_with_mouse_handlers = []

            for line in split_lines(tokens_with_mouse_handlers):
                token_lines_with_mouse_handlers.append(process_line(line))
        else:
            token_lines_with_mouse_handlers = list(split_lines(tokens_with_mouse_handlers))

        # Strip mouse handlers from tokens.
        token_lines = [
            [tuple(item[:2]) for item in line]
            for line in token_lines_with_mouse_handlers
        ]

        # Keep track of the tokens with mouse handler, for later use in
        # `mouse_handler`.
        self._tokens = tokens_with_mouse_handlers

        # If there is a `Token.SetCursorPosition` in the token list, set the
        # cursor position here.
        def get_cursor_position():
            SetCursorPosition = Token.SetCursorPosition

            for y, line in enumerate(token_lines):
                x = 0
                for token, text in line:
                    if token == SetCursorPosition:
                        return Point(x=x, y=y)
                    x += len(text)
            return None

        # Create content, or take it from the cache.
        key = (default_char.char, default_char.token,
                tuple(tokens_with_mouse_handlers), width, right, center)

        def get_content():
            return UIContent(get_line=lambda i: token_lines[i],
                             line_count=len(token_lines),
                             default_char=default_char,
                             cursor_position=get_cursor_position())

        return self._content_cache.get(key, get_content)

    @classmethod
    def static(cls, tokens):
        def get_static_tokens(cli):
            return tokens
        return cls(get_static_tokens)

    def mouse_handler(self, cli, mouse_event):
        """
        Handle mouse events.

        (When the token 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 `Window` to handle this
        particular event.)
        """
        if self._tokens:
            # Read the generator.
            tokens_for_line = list(split_lines(self._tokens))

            try:
                tokens = tokens_for_line[mouse_event.position.y]
            except IndexError:
                return NotImplemented
            else:
                # Find position in the token list.
                xpos = mouse_event.position.x

                # Find mouse handler for this character.
                count = 0
                for item in tokens:
                    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(cli, mouse_event)
                        else:
                            break

        # Otherwise, don't handle here.
        return NotImplemented
Пример #25
0
 def __init__(self):
     self.key_bindings = []
     self._get_bindings_for_keys_cache = SimpleCache(maxsize=10000)
     self._get_bindings_starting_with_keys_cache = SimpleCache(maxsize=1000)
 def __init__(self):
     self.bindings = []
     self._get_bindings_for_keys_cache = SimpleCache(maxsize=10000)
     self._get_bindings_starting_with_keys_cache = SimpleCache(maxsize=1000)
     self.__version = 0  # For cache invalidation.
    def __init__(self, styles):
        assert all(isinstance(style, BaseStyle) for style in styles)

        self.styles = styles
        self._style = SimpleCache(maxsize=1)
Пример #28
0
class _CombinedRegistry(KeyBindingsBase):
    """
    The `KeyBindings` of key bindings for a `Application`.
    This merges the global key bindings with the one of the current user
    control.
    """
    def __init__(self, app):
        self.app = app
        self._cache = SimpleCache()

    @property
    def _version(self):
        """ Not needed - this object is not going to be wrapped in another
        KeyBindings object. """
        raise NotImplementedError

    def _create_key_bindings(self, current_window, other_controls):
        """
        Create a `KeyBindings` object that merges the `KeyBindings` from the
        `UIControl` with all the parent controls and the global key bindings.
        """
        key_bindings = []
        collected_containers = set()

        # Collect key bindings from currently focused control and all parent
        # controls. Don't include key bindings of container parent controls.
        container = current_window
        while True:
            collected_containers.add(container)
            kb = container.get_key_bindings()
            if kb is not None:
                key_bindings.append(kb)

            if container.is_modal():
                break

            parent = self.app.layout.get_parent(container)
            if parent is None:
                break
            else:
                container = parent

        # Include global bindings (starting at the top-model container).
        for c in walk(container):
            if c not in collected_containers:
                kb = c.get_key_bindings()
                if kb is not None:
                    key_bindings.append(GlobalOnlyKeyBindings(kb))

        # Add App key bindings
        if self.app.key_bindings:
            key_bindings.append(self.app.key_bindings)

        # Add mouse bindings.
        key_bindings.append(ConditionalKeyBindings(
            self.app._page_navigation_bindings,
            self.app.enable_page_navigation_bindings))
        key_bindings.append(self.app._default_bindings)

        # Reverse this list. The current control's key bindings should come
        # last. They need priority.
        key_bindings = key_bindings[::-1]

        return merge_key_bindings(key_bindings)

    @property
    def _key_bindings(self):
        current_window = self.app.layout.current_window
        other_controls = list(self.app.layout.find_all_controls())
        key = current_window, frozenset(other_controls)

        return self._cache.get(
            key, lambda: self._create_key_bindings(current_window, other_controls))

    def get_bindings_for_keys(self, keys):
        return self._key_bindings.get_bindings_for_keys(keys)

    def get_bindings_starting_with_keys(self, keys):
        return self._key_bindings.get_bindings_starting_with_keys(keys)
class HighlightMatchingBracketProcessor(Processor):
    """
    When the cursor is on or right after a bracket, it highlights the matching
    bracket.

    :param max_cursor_distance: Only highlight matching brackets when the
        cursor is within this distance. (From inside a `Processor`, we can't
        know which lines will be visible on the screen. But we also don't want
        to scan the whole document for matching brackets on each key press, so
        we limit to this value.)
    """
    _closing_braces = '])}>'

    def __init__(self, chars='[](){}<>', max_cursor_distance=1000):
        self.chars = chars
        self.max_cursor_distance = max_cursor_distance

        self._positions_cache = SimpleCache(maxsize=8)

    def _get_positions_to_highlight(self, document):
        """
        Return a list of (row, col) tuples that need to be highlighted.
        """
        # Try for the character under the cursor.
        if document.current_char and document.current_char in self.chars:
            pos = document.find_matching_bracket_position(
                start_pos=document.cursor_position - self.max_cursor_distance,
                end_pos=document.cursor_position + self.max_cursor_distance)

        # Try for the character before the cursor.
        elif (document.char_before_cursor
              and document.char_before_cursor in self._closing_braces
              and document.char_before_cursor in self.chars):
            document = Document(document.text, document.cursor_position - 1)

            pos = document.find_matching_bracket_position(
                start_pos=document.cursor_position - self.max_cursor_distance,
                end_pos=document.cursor_position + self.max_cursor_distance)
        else:
            pos = None

        # Return a list of (row, col) tuples that need to be highlighted.
        if pos:
            pos += document.cursor_position  # pos is relative.
            row, col = document.translate_index_to_position(pos)
            return [(row, col),
                    (document.cursor_position_row,
                     document.cursor_position_col)]
        else:
            return []

    def apply_transformation(self, cli, document, lineno, source_to_display,
                             tokens):
        # Get the highlight positions.
        key = (cli.render_counter, document.text, document.cursor_position)
        positions = self._positions_cache.get(
            key, lambda: self._get_positions_to_highlight(document))

        # Apply if positions were foun at this line.
        if positions:
            for row, col in positions:
                if row == lineno:
                    col = source_to_display(col)
                    tokens = explode_tokens(tokens)
                    tokens[col] = (Token.MatchingBracket, tokens[col][1])

        return Transformation(tokens)
Пример #30
0
    def __init__(self, chars='[](){}<>', max_cursor_distance=1000):
        self.chars = chars
        self.max_cursor_distance = max_cursor_distance

        self._positions_cache = SimpleCache(maxsize=8)
Пример #31
0
class HighlightMatchingBracketProcessor(Processor):
    """
    When the cursor is on or right after a bracket, it highlights the matching
    bracket.

    :param max_cursor_distance: Only highlight matching brackets when the
        cursor is within this distance. (From inside a `Processor`, we can't
        know which lines will be visible on the screen. But we also don't want
        to scan the whole document for matching brackets on each key press, so
        we limit to this value.)
    """
    _closing_braces = '])}>'

    def __init__(self, chars='[](){}<>', max_cursor_distance=1000):
        self.chars = chars
        self.max_cursor_distance = max_cursor_distance

        self._positions_cache = SimpleCache(maxsize=8)

    def _get_positions_to_highlight(self, document):
        """
        Return a list of (row, col) tuples that need to be highlighted.
        """
        # Try for the character under the cursor.
        if document.current_char and document.current_char in self.chars:
            pos = document.find_matching_bracket_position(
                    start_pos=document.cursor_position - self.max_cursor_distance,
                    end_pos=document.cursor_position + self.max_cursor_distance)

        # Try for the character before the cursor.
        elif (document.char_before_cursor and document.char_before_cursor in
              self._closing_braces and document.char_before_cursor in self.chars):
            document = Document(document.text, document.cursor_position - 1)

            pos = document.find_matching_bracket_position(
                    start_pos=document.cursor_position - self.max_cursor_distance,
                    end_pos=document.cursor_position + self.max_cursor_distance)
        else:
            pos = None

        # Return a list of (row, col) tuples that need to be highlighted.
        if pos:
            pos += document.cursor_position  # pos is relative.
            row, col = document.translate_index_to_position(pos)
            return [(row, col), (document.cursor_position_row, document.cursor_position_col)]
        else:
            return []

    def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
        # Get the highlight positions.
        key = (cli.render_counter, document.text, document.cursor_position)
        positions = self._positions_cache.get(
            key, lambda: self._get_positions_to_highlight(document))

        # Apply if positions were foun at this line.
        if positions:
            for row, col in positions:
                if row == lineno:
                    col = source_to_display(col)
                    tokens = explode_tokens(tokens)
                    tokens[col] = (Token.MatchingBracket, tokens[col][1])

        return Transformation(tokens)
 def __init__(self, app):
     self.app = app
     self._cache = SimpleCache()
Пример #33
0
class BufferControl(UIControl):
    """
    Control for visualising the content of a `Buffer`.

    :param input_processors: list of :class:`~prompt_toolkit.layout.processors.Processor`.
    :param lexer: :class:`~prompt_toolkit.layout.lexers.Lexer` instance for syntax highlighting.
    :param preview_search: `bool` or `CLIFilter`: Show search while typing.
    :param get_search_state: Callable that takes a CommandLineInterface and
        returns the SearchState to be used. (If not CommandLineInterface.search_state.)
    :param buffer_name: String representing the name of the buffer to display.
    :param default_char: :class:`.Char` instance to use to fill the background. This is
        transparent by default.
    :param focus_on_click: Focus this buffer when it's click, but not yet focussed.
    """
    def __init__(self,
                 buffer_name=DEFAULT_BUFFER,
                 input_processors=None,
                 lexer=None,
                 preview_search=False,
                 search_buffer_name=SEARCH_BUFFER,
                 get_search_state=None,
                 menu_position=None,
                 default_char=None,
                 focus_on_click=False):
        assert input_processors is None or all(isinstance(i, Processor) for i in input_processors)
        assert menu_position is None or callable(menu_position)
        assert lexer is None or isinstance(lexer, Lexer)
        assert get_search_state is None or callable(get_search_state)
        assert default_char is None or isinstance(default_char, Char)

        self.preview_search = to_cli_filter(preview_search)
        self.get_search_state = get_search_state
        self.focus_on_click = to_cli_filter(focus_on_click)

        self.input_processors = input_processors or []
        self.buffer_name = buffer_name
        self.menu_position = menu_position
        self.lexer = lexer or SimpleLexer()
        self.default_char = default_char or Char(token=Token.Transparent)
        self.search_buffer_name = search_buffer_name

        #: 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 faily easy way to cache such an expensive operation.
        self._token_cache = SimpleCache(maxsize=8)
        self._processed_token_cache = SimpleCache(maxsize=8)

        self._xy_to_cursor_position = None
        self._last_click_timestamp = None
        self._last_get_processed_line = None

    def _buffer(self, cli):
        """
        The buffer object that contains the 'main' content.
        """
        return cli.buffers[self.buffer_name]

    def has_focus(self, cli):
        # This control gets the focussed if the actual `Buffer` instance has the
        # focus or when any of the `InputProcessor` classes tells us that it
        # wants the focus. (E.g. in case of a reverse-search, where the actual
        # search buffer may not be displayed, but the "reverse-i-search" text
        # should get the focus.)
        return cli.current_buffer_name == self.buffer_name or \
            any(i.has_focus(cli) for i in self.input_processors)

    def preferred_width(self, cli, max_available_width):
        """
        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 behaviour.
        """
        return None

    def preferred_height(self, cli, width, max_available_height, wrap_lines):
        # Calculate the content height, if it was drawn on a screen with the
        # given width.
        height = 0
        content = self.create_content(cli, width, None)

        # 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)

            if height >= max_available_height:
                return max_available_height

        return height

    def _get_tokens_for_line_func(self, cli, document):
        """
        Create a function that returns the tokens for a given line.
        """
        # Cache using `document.text`.
        def get_tokens_for_line():
            return self.lexer.lex_document(cli, document)

        return self._token_cache.get(document.text, get_tokens_for_line)

    def _create_get_processed_line_func(self, cli, document):
        """
        Create a function that takes a line number of the current document and
        returns a _ProcessedLine(processed_tokens, source_to_display, display_to_source)
        tuple.
        """
        def transform(lineno, tokens):
            " Transform the tokens for a given line number. "
            source_to_display_functions = []
            display_to_source_functions = []

            # Get cursor position at this line.
            if document.cursor_position_row == lineno:
                cursor_column = document.cursor_position_col
            else:
                cursor_column = None

            def source_to_display(i):
                """ Translate x position from the buffer to the x position in the
                processed token list. """
                for f in source_to_display_functions:
                    i = f(i)
                return i

            # Apply each processor.
            for p in self.input_processors:
                transformation = p.apply_transformation(
                    cli, document, lineno, source_to_display, tokens)
                tokens = transformation.tokens

                if cursor_column:
                    cursor_column = transformation.source_to_display(cursor_column)

                display_to_source_functions.append(transformation.display_to_source)
                source_to_display_functions.append(transformation.source_to_display)

            def display_to_source(i):
                for f in reversed(display_to_source_functions):
                    i = f(i)
                return i

            return _ProcessedLine(tokens, source_to_display, display_to_source)

        def create_func():
            get_line = self._get_tokens_for_line_func(cli, document)
            cache = {}

            def get_processed_line(i):
                try:
                    return cache[i]
                except KeyError:
                    processed_line = transform(i, get_line(i))
                    cache[i] = processed_line
                    return processed_line
            return get_processed_line

        return create_func()

    def create_content(self, cli, width, height):
        """
        Create a UIContent.
        """
        buffer = self._buffer(cli)

        # 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.)
        def preview_now():
            """ True when we should preview a search. """
            return bool(self.preview_search(cli) and
                        cli.buffers[self.search_buffer_name].text)

        if preview_now():
            if self.get_search_state:
                ss = self.get_search_state(cli)
            else:
                ss = cli.search_state

            document = buffer.document_for_search(SearchState(
                text=cli.current_buffer.text,
                direction=ss.direction,
                ignore_case=ss.ignore_case))
        else:
            document = buffer.document

        get_processed_line = self._create_get_processed_line_func(cli, document)
        self._last_get_processed_line = get_processed_line

        def translate_rowcol(row, col):
            " Return the content column for this coordinate. "
            return Point(y=row, x=get_processed_line(row).source_to_display(col))

        def get_line(i):
            " Return the tokens for a given line number. "
            tokens = get_processed_line(i).tokens

            # 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.)
            tokens = tokens + [(self.default_char.token, ' ')]
            return tokens

        content = UIContent(
            get_line=get_line,
            line_count=document.line_count,
            cursor_position=translate_rowcol(document.cursor_position_row,
                                             document.cursor_position_col),
            default_char=self.default_char)

        # 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 focussed buffer.)
        if cli.current_buffer_name == self.buffer_name:
            menu_position = self.menu_position(cli) 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, cli, mouse_event):
        """
        Mouse handler for this control.
        """
        buffer = self._buffer(cli)
        position = mouse_event.position

        # Focus buffer when clicked.
        if self.has_focus(cli):
            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 == MouseEventTypes.MOUSE_DOWN:
                    buffer.exit_selection()
                    buffer.cursor_position = index

                elif mouse_event.event_type == MouseEventTypes.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:
                        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 < .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 focussed, but focussing on click events.
        else:
            if self.focus_on_click(cli) and mouse_event.event_type == MouseEventTypes.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
                # focussed and be handled anyway.)
                cli.focus(self.buffer_name)
            else:
                return NotImplemented

    def move_cursor_down(self, cli):
        b = self._buffer(cli)
        b.cursor_position += b.document.get_cursor_down_position()

    def move_cursor_up(self, cli):
        b = self._buffer(cli)
        b.cursor_position += b.document.get_cursor_up_position()
Пример #34
0
class Window(Container):
    """
    Container that holds a control.

    :param content: :class:`~prompt_toolkit.layout.controls.UIControl` instance.
    :param width: :class:`~prompt_toolkit.layout.dimension.LayoutDimension` instance.
    :param height: :class:`~prompt_toolkit.layout.dimension.LayoutDimension` instance.
    :param get_width: callable which takes a `CommandLineInterface` and returns a `LayoutDimension`.
    :param get_height: callable which takes a `CommandLineInterface` and returns a `LayoutDimension`.
    :param dont_extend_width: When `True`, don't take up more width then the
                              preferred width reported by the control.
    :param dont_extend_height: When `True`, don't take up more width then the
                               preferred height reported by the control.
    :param left_margins: A list of :class:`~prompt_toolkit.layout.margins.Margin`
        instance to be displayed on the left. For instance:
        :class:`~prompt_toolkit.layout.margins.NumberredMargin` can be one of
        them in order to show line numbers.
    :param right_margins: Like `left_margins`, but on the other side.
    :param scroll_offsets: :class:`.ScrollOffsets` instance, representing the
        preferred amount of lines/columns to be always visible before/after the
        cursor. When both top and bottom are a very high number, the cursor
        will be centered vertically most of the time.
    :param allow_scroll_beyond_bottom: A `bool` or
        :class:`~prompt_toolkit.filters.CLIFilter` instance. When True, allow
        scrolling so far, that the top part of the content is not visible
        anymore, while there is still empty space available at the bottom of
        the window. In the Vi editor for instance, this is possible. You will
        see tildes while the top part of the body is hidden.
    :param get_vertical_scroll: Callable that takes this window
        instance as input and returns a preferred vertical scroll.
        (When this is `None`, the scroll is only determined by the last and
        current cursor position.)
    :param get_horizontal_scroll: Callable that takes this window
        instance as input and returns a preferred vertical scroll.
    :param always_hide_cursor: A `bool` or
        :class:`~prompt_toolkit.filters.CLIFilter` instance. When True, never
        display the cursor, even when the user control specifies a cursor
        position.
    """
    def __init__(self, content, width=None, height=None, get_width=None,
                 get_height=None, dont_extend_width=False, dont_extend_height=False,
                 left_margins=None, right_margins=None, scroll_offsets=None,
                 allow_scroll_beyond_bottom=False,
                 get_vertical_scroll=None, get_horizontal_scroll=None, always_hide_cursor=False):
        assert isinstance(content, UIControl)
        assert width is None or isinstance(width, LayoutDimension)
        assert height is None or isinstance(height, LayoutDimension)
        assert get_width is None or callable(get_width)
        assert get_height is None or callable(get_height)
        assert width is None or get_width is None
        assert height is None or get_height is None
        assert scroll_offsets is None or isinstance(scroll_offsets, ScrollOffsets)
        assert left_margins is None or all(isinstance(m, Margin) for m in left_margins)
        assert right_margins is None or all(isinstance(m, Margin) for m in right_margins)
        assert get_vertical_scroll is None or callable(get_vertical_scroll)
        assert get_horizontal_scroll is None or callable(get_horizontal_scroll)

        self.allow_scroll_beyond_bottom = to_cli_filter(allow_scroll_beyond_bottom)
        self.always_hide_cursor = to_cli_filter(always_hide_cursor)

        self.content = content
        self.dont_extend_width = dont_extend_width
        self.dont_extend_height = dont_extend_height
        self.left_margins = left_margins or []
        self.right_margins = right_margins or []
        self.scroll_offsets = scroll_offsets or ScrollOffsets()
        self.get_vertical_scroll = get_vertical_scroll
        self.get_horizontal_scroll = get_horizontal_scroll
        self._width = get_width or (lambda cli: width)
        self._height = get_height or (lambda cli: height)

        # Cache for the screens generated by the margin.
        self._margin_cache = SimpleCache(maxsize=8)

        self.reset()

    def __repr__(self):
        return 'Window(content=%r)' % self.content

    def reset(self):
        self.content.reset()

        #: Scrolling position of the main content.
        self.vertical_scroll = 0
        self.horizontal_scroll = 0

        #: Keep render information (mappings between buffer input and render
        #: output.)
        self.render_info = None

    def preferred_width(self, cli, max_available_width):
        # Width of the margins.
        total_margin_width = sum(m.get_width(cli) for m in
                                 self.left_margins + self.right_margins)

        # Window of the content.
        preferred_width = self.content.preferred_width(
            cli, max_available_width - total_margin_width)

        if preferred_width is not None:
            preferred_width += total_margin_width

        # Merge.
        return self._merge_dimensions(
            dimension=self._width(cli),
            preferred=preferred_width,
            dont_extend=self.dont_extend_width)

    def preferred_height(self, cli, width):
        return self._merge_dimensions(
            dimension=self._height(cli),
            preferred=self.content.preferred_height(cli, width),
            dont_extend=self.dont_extend_height)

    @staticmethod
    def _merge_dimensions(dimension, preferred=None, dont_extend=False):
        """
        Take the LayoutDimension from this `Window` class and the received
        preferred size from the `UIControl` and return a `LayoutDimension` to
        report to the parent container.
        """
        dimension = dimension or LayoutDimension()

        # When a preferred dimension was explicitly given to the Window,
        # ignore the UIControl.
        if dimension.preferred_specified:
            preferred = dimension.preferred

        # When a 'preferred' dimension is given by the UIControl, make sure
        # that it stays within the bounds of the Window.
        if preferred is not None:
            if dimension.max:
                preferred = min(preferred, dimension.max)

            if dimension.min:
                preferred = max(preferred, dimension.min)

        # When a `dont_extend` flag has been given, use the preferred dimension
        # also as the max dimension.
        if dont_extend and preferred is not None:
            max_ = min(dimension.max, preferred)
        else:
            max_ = dimension.max

        return LayoutDimension(min=dimension.min, max=max_, preferred=preferred)

    def write_to_screen(self, cli, screen, mouse_handlers, write_position):
        """
        Write window to screen. This renders the user control, the margins and
        copies everything over to the absolute position at the given screen.
        """
        # Calculate margin sizes.
        left_margin_widths = [m.get_width(cli) for m in self.left_margins]
        right_margin_widths = [m.get_width(cli) for m in self.right_margins]
        total_margin_width = sum(left_margin_widths + right_margin_widths)

        # Render UserControl.
        tpl = self.content.create_screen(
            cli, write_position.width - total_margin_width, write_position.height)
        if isinstance(tpl, tuple):
            temp_screen, highlighting = tpl
        else:
            # For backwards, compatibility.
            temp_screen, highlighting = tpl, defaultdict(lambda: defaultdict(lambda: None))

        # Scroll content.
        applied_scroll_offsets = self._scroll(
            temp_screen, write_position.width - total_margin_width, write_position.height, cli)

        # Write body to screen.
        self._copy_body(cli, temp_screen, highlighting, screen, write_position,
                        sum(left_margin_widths), write_position.width - total_margin_width,
                        applied_scroll_offsets)

        # Remember render info. (Set before generating the margins. They need this.)
        self.render_info = WindowRenderInfo(
            original_screen=temp_screen,
            horizontal_scroll=self.horizontal_scroll,
            vertical_scroll=self.vertical_scroll,
            window_width=write_position.width,
            window_height=write_position.height,
            cursor_position=Point(y=temp_screen.cursor_position.y - self.vertical_scroll,
                                  x=temp_screen.cursor_position.x - self.horizontal_scroll),
            configured_scroll_offsets=self.scroll_offsets,
            applied_scroll_offsets=applied_scroll_offsets)

        # Set mouse handlers.
        def mouse_handler(cli, mouse_event):
            """ Wrapper around the mouse_handler of the `UIControl` that turns
            absolute coordinates into relative coordinates. """
            position = mouse_event.position

            # Call the mouse handler of the UIControl first.
            result = self.content.mouse_handler(
                cli, MouseEvent(
                    position=Point(x=position.x - write_position.xpos - sum(left_margin_widths),
                                   y=position.y - write_position.ypos + self.vertical_scroll),
                    event_type=mouse_event.event_type))

            # If it returns NotImplemented, handle it here.
            if result == NotImplemented:
                return self._mouse_handler(cli, mouse_event)

            return result

        mouse_handlers.set_mouse_handler_for_range(
            x_min=write_position.xpos + sum(left_margin_widths),
            x_max=write_position.xpos + write_position.width - total_margin_width,
            y_min=write_position.ypos,
            y_max=write_position.ypos + write_position.height,
            handler=mouse_handler)

        # Render and copy margins.
        move_x = 0

        def render_margin(m, width):
            " Render margin. Return `Screen`. "
            # Retrieve margin tokens.
            tokens = m.create_margin(cli, self.render_info, width, write_position.height)

            # Turn it into a screen. (Take a screen from the cache if we
            # already rendered those tokens using this size.)
            def create_screen():
                return TokenListControl.static(tokens).create_screen(
                    cli, width + 1, write_position.height)

            key = (tuple(tokens), width, write_position.height)
            return self._margin_cache.get(key, create_screen)

        for m, width in zip(self.left_margins, left_margin_widths):
            # Create screen for margin.
            margin_screen = render_margin(m, width)

            # Copy and shift X.
            self._copy_margin(margin_screen, screen, write_position, move_x, width)
            move_x += width

        move_x = write_position.width - sum(right_margin_widths)

        for m, width in zip(self.right_margins, right_margin_widths):
            # Create screen for margin.
            margin_screen = render_margin(m, width)

            # Copy and shift X.
            self._copy_margin(margin_screen, screen, write_position, move_x, width)
            move_x += width

    def _copy_body(self, cli, temp_screen, highlighting, new_screen,
                   write_position, move_x, width, applied_scroll_offsets):
        """
        Copy characters from the temp screen that we got from the `UIControl`
        to the real screen.
        """
        xpos = write_position.xpos + move_x
        ypos = write_position.ypos
        height = write_position.height

        temp_buffer = temp_screen.data_buffer
        new_buffer = new_screen.data_buffer
        temp_screen_height = temp_screen.height

        vertical_scroll = self.vertical_scroll
        horizontal_scroll = self.horizontal_scroll
        y = 0

        # Now copy the region we need to the real screen.
        for y in range(0, height):
            # We keep local row variables. (Don't look up the row in the dict
            # for each iteration of the nested loop.)
            new_row = new_buffer[y + ypos]

            if y >= temp_screen_height and y >= write_position.height:
                # Break out of for loop when we pass after the last row of the
                # temp screen. (We use the 'y' position for calculation of new
                # screen's height.)
                break
            else:
                temp_row = temp_buffer[y + vertical_scroll]
                highlighting_row = highlighting[y + vertical_scroll]

                # Copy row content, except for transparent tokens.
                # (This is useful in case of floats.)
                # Also apply highlighting.
                for x in range(0, width):
                    cell = temp_row[x + horizontal_scroll]
                    highlighting_token = highlighting_row[x]

                    if highlighting_token:
                        new_row[x + xpos] = Char(cell.char, highlighting_token)
                    elif cell.token != Transparent:
                        new_row[x + xpos] = cell

        if self.content.has_focus(cli):
            new_screen.cursor_position = Point(y=temp_screen.cursor_position.y + ypos - vertical_scroll,
                                               x=temp_screen.cursor_position.x + xpos - horizontal_scroll)

            if not self.always_hide_cursor(cli):
                new_screen.show_cursor = temp_screen.show_cursor

        if not new_screen.menu_position and temp_screen.menu_position:
            new_screen.menu_position = Point(y=temp_screen.menu_position.y + ypos - vertical_scroll,
                                             x=temp_screen.menu_position.x + xpos - horizontal_scroll)

        # Update height of the output screen. (new_screen.write_data is not
        # called, so the screen is not aware of its height.)
        new_screen.height = max(new_screen.height, ypos + y + 1)

    def _copy_margin(self, temp_screen, new_screen, write_position, move_x, width):
        """
        Copy characters from the margin screen to the real screen.
        """
        xpos = write_position.xpos + move_x
        ypos = write_position.ypos

        temp_buffer = temp_screen.data_buffer
        new_buffer = new_screen.data_buffer

        # Now copy the region we need to the real screen.
        for y in range(0, write_position.height):
            new_row = new_buffer[y + ypos]
            temp_row = temp_buffer[y]

            # Copy row content, except for transparent tokens.
            # (This is useful in case of floats.)
            for x in range(0, width):
                cell = temp_row[x]
                if cell.token != Transparent:
                    new_row[x + xpos] = cell

    def _scroll(self, temp_screen, width, height, cli):
        """
        Scroll to make sure the cursor position is visible and that we maintain the
        requested scroll offset.
        Return the applied scroll offsets.
        """
        def do_scroll(current_scroll, scroll_offset_start, scroll_offset_end,
                      cursor_pos, window_size, content_size):
            " Scrolling algorithm. Used for both horizontal and vertical scrolling. "
            # Calculate the scroll offset to apply.
            # This can obviously never be more than have the screen size. Also, when the
            # cursor appears at the top or bottom, we don't apply the offset.
            scroll_offset_start = int(min(scroll_offset_start, window_size / 2, cursor_pos))
            scroll_offset_end = int(min(scroll_offset_end, window_size / 2,
                                        content_size - 1 - cursor_pos))

            # Prevent negative scroll offsets.
            if current_scroll < 0:
                current_scroll = 0

            # Scroll back if we scrolled to much and there's still space to show more of the document.
            if (not self.allow_scroll_beyond_bottom(cli) and
                    current_scroll > content_size - window_size):
                current_scroll = max(0, content_size - window_size)

            # Scroll up if cursor is before visible part.
            if current_scroll > cursor_pos - scroll_offset_start:
                current_scroll = max(0, cursor_pos - scroll_offset_start)

            # Scroll down if cursor is after visible part.
            if current_scroll < (cursor_pos + 1) - window_size + scroll_offset_end:
                current_scroll = (cursor_pos + 1) - window_size + scroll_offset_end

            # Calculate the applied scroll offset. This value can be lower than what we had.
            scroll_offset_start = max(0, min(current_scroll, scroll_offset_start))
            scroll_offset_end = max(0, min(content_size - current_scroll - window_size, scroll_offset_end))

            return current_scroll, scroll_offset_start, scroll_offset_end

        # When a preferred scroll is given, take that first into account.
        if self.get_vertical_scroll:
            self.vertical_scroll = self.get_vertical_scroll(self)
            assert isinstance(self.vertical_scroll, int)
        if self.get_horizontal_scroll:
            self.horizontal_scroll = self.get_horizontal_scroll(self)
            assert isinstance(self.horizontal_scroll, int)

        # Update horizontal/vertical scroll to make sure that the cursor
        # remains visible.
        offsets = self.scroll_offsets

        self.vertical_scroll, scroll_offset_top, scroll_offset_bottom = do_scroll(
            current_scroll=self.vertical_scroll,
            scroll_offset_start=offsets.top,
            scroll_offset_end=offsets.bottom,
            cursor_pos=temp_screen.cursor_position.y,
            window_size=height,
            content_size=temp_screen.height)

        self.horizontal_scroll, scroll_offset_left, scroll_offset_right = do_scroll(
            current_scroll=self.horizontal_scroll,
            scroll_offset_start=offsets.left,
            scroll_offset_end=offsets.right,
            cursor_pos=temp_screen.cursor_position.x,
            window_size=width,
            content_size=temp_screen.width)

        applied_scroll_offsets = ScrollOffsets(
            top=scroll_offset_top,
            bottom=scroll_offset_bottom,
            left=scroll_offset_left,
            right=scroll_offset_right)

        return applied_scroll_offsets

    def _mouse_handler(self, cli, mouse_event):
        """
        Mouse handler. Called when the UI control doesn't handle this
        particular event.
        """
        if mouse_event.event_type == MouseEventTypes.SCROLL_DOWN:
            self._scroll_down(cli)
        elif mouse_event.event_type == MouseEventTypes.SCROLL_UP:
            self._scroll_up(cli)

    def _scroll_down(self, cli):
        " Scroll window down. "
        info = self.render_info

        if self.vertical_scroll < info.content_height - info.window_height:
            if info.cursor_position.y <= info.configured_scroll_offsets.top:
                self.content.move_cursor_down(cli)

            self.vertical_scroll += 1

    def _scroll_up(self, cli):
        " Scroll window up. "
        info = self.render_info

        if info.vertical_scroll > 0:
            if info.cursor_position.y >= info.window_height - 1 - info.configured_scroll_offsets.bottom:
                self.content.move_cursor_up(cli)

            self.vertical_scroll -= 1

    def walk(self, cli):
        # Only yield self. A window doesn't have children.
        yield self
Пример #35
0
class BufferControl(UIControl):
    """
    Control for visualising 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=None,
                 input_processors=None,
                 include_default_input_processors=True,
                 lexer=None,
                 preview_search=False,
                 focusable=True,
                 search_buffer_control=None,
                 menu_position=None,
                 focus_on_click=False,
                 key_bindings=None):
        from prompt_toolkit.key_binding.key_bindings import KeyBindingsBase
        assert buffer is None or isinstance(buffer, Buffer)
        assert input_processors is None or isinstance(input_processors, list)
        assert isinstance(include_default_input_processors, bool)
        assert menu_position is None or callable(menu_position)
        assert lexer is None or isinstance(lexer, Lexer)
        assert (search_buffer_control is None
                or callable(search_buffer_control)
                or isinstance(search_buffer_control, SearchBufferControl))
        assert key_bindings is None or isinstance(key_bindings,
                                                  KeyBindingsBase)

        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(maxsize=8)

        self._xy_to_cursor_position = None
        self._last_click_timestamp = None
        self._last_get_processed_line = None

    def __repr__(self):
        return '<%s(buffer=%r at %r>' % (self.__class__.__name__, self.buffer,
                                         id(self))

    @property
    def search_buffer_control(self):
        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):
        control = self.search_buffer_control
        if control is not None:
            return control.buffer

    @property
    def search_state(self):
        """
        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):
        return self.focusable()

    def preferred_width(self, max_available_width):
        """
        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 behaviour.
        """
        return None

    def preferred_height(self, width, max_available_height, wrap_lines):
        # Calculate the content height, if it was drawn on a screen with the
        # given width.
        height = 0
        content = self.create_content(width, None)

        # 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)

            if height >= max_available_height:
                return max_available_height

        return height

    def _get_formatted_text_for_line_func(self, document):
        """
        Create a function that returns the fragments for a given line.
        """

        # Cache using `document.text`.
        def get_formatted_text_for_line():
            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, width, height):
        """
        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, fragments):
            " Transform the fragments for a given line number. "
            # Get cursor position at this line.
            if document.cursor_position_row == lineno:
                cursor_column = document.cursor_position_col
            else:
                cursor_column = None

            def source_to_display(i):
                """ 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))

            if cursor_column:
                cursor_column = transformation.source_to_display(cursor_column)

            return _ProcessedLine(transformation.fragments,
                                  transformation.source_to_display,
                                  transformation.display_to_source)

        def create_func():
            get_line = self._get_formatted_text_for_line_func(document)
            cache = {}

            def get_processed_line(i):
                try:
                    return cache[i]
                except KeyError:
                    processed_line = transform(i, get_line(i))
                    cache[i] = processed_line
                    return processed_line

            return get_processed_line

        return create_func()

    def create_content(self, width, height, preview_search=False):
        """
        Create a UIContent.
        """
        buffer = self.buffer

        # 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:
            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, col):
            " Return the content column for this coordinate. "
            return Point(x=get_processed_line(row).source_to_display(col),
                         y=row)

        def get_line(i):
            " 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):
        """
        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_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:
                        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 < .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

    def move_cursor_down(self):
        b = self.buffer
        b.cursor_position += b.document.get_cursor_down_position()

    def move_cursor_up(self):
        b = self.buffer
        b.cursor_position += b.document.get_cursor_up_position()

    def get_key_bindings(self):
        """
        When additional key bindings are given. Return these.
        """
        return self.key_bindings

    def get_invalidate_events(self):
        """
        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 TokenListControl(UIControl):
    """
    Control that displays a list of (Token, text) tuples.
    (It's mostly optimized for rather small widgets, like toolbars, menus, etc...)

    Mouse support:

        The list of tokens can also contain tuples of three items, looking like:
        (Token, text, handler). When mouse support is enabled and the user
        clicks on this token, then the given handler is called. That handler
        should accept two inputs: (CommandLineInterface, MouseEvent) and it
        should either handle the event or return `NotImplemented` in case we
        want the containing Window to handle this event.

    :param get_tokens: Callable that takes a `CommandLineInterface` instance
        and returns the list of (Token, text) tuples to be displayed right now.
    :param default_char: default :class:`.Char` (character and Token) to use
        for the background when there is more space available than `get_tokens`
        returns.
    :param get_default_char: Like `default_char`, but this is a callable that
        takes a :class:`prompt_toolkit.interface.CommandLineInterface` and
        returns a :class:`.Char` instance.
    :param has_focus: `bool` or `CLIFilter`, when this evaluates to `True`,
        this UI control will take the focus. The cursor will be shown in the
        upper left corner of this control, unless `get_token` returns a
        ``Token.SetCursorPosition`` token somewhere in the token list, then the
        cursor will be shown there.
    :param wrap_lines: `bool` or `CLIFilter`: Wrap long lines.
    """
    def __init__(self,
                 get_tokens,
                 default_char=None,
                 get_default_char=None,
                 align_right=False,
                 align_center=False,
                 has_focus=False,
                 wrap_lines=True):
        assert default_char is None or isinstance(default_char, Char)
        assert get_default_char is None or callable(get_default_char)
        assert not (default_char and get_default_char)

        self.align_right = to_cli_filter(align_right)
        self.align_center = to_cli_filter(align_center)
        self._has_focus_filter = to_cli_filter(has_focus)
        self.wrap_lines = to_cli_filter(wrap_lines)

        self.get_tokens = get_tokens

        # Construct `get_default_char` callable.
        if default_char:
            get_default_char = lambda _: default_char
        elif not get_default_char:
            get_default_char = lambda _: Char(' ', Token)

        self.get_default_char = get_default_char

        #: Cache for rendered screens.
        self._screen_cache = SimpleCache(maxsize=18)
        self._token_cache = SimpleCache(maxsize=1)
        # Only cache one token list. We don't need the previous item.

        # Render info for the mouse support.
        self._tokens = None  # The last rendered tokens.
        self._pos_to_indexes = None  # Mapping from mouse positions (x,y) to
        # positions in the token list.

    def __repr__(self):
        return '%s(%r)' % (self.__class__.__name__, self.get_tokens)

    def _get_tokens_cached(self, cli):
        """
        Get tokens, but only retrieve tokens 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._token_cache.get(cli.render_counter,
                                     lambda: self.get_tokens(cli))

    def has_focus(self, cli):
        return self._has_focus_filter(cli)

    def preferred_width(self, cli, max_available_width):
        """
        Return the preferred width for this control.
        That is the width of the longest line.
        """
        text = ''.join(t[1] for t in self._get_tokens_cached(cli))
        line_lengths = [get_cwidth(l) for l in text.split('\n')]
        return max(line_lengths)

    def preferred_height(self, cli, width):
        screen = self.create_screen(cli, width, None)
        return screen.height

    def create_screen(self, cli, width, height):
        # Get tokens
        tokens_with_mouse_handlers = self._get_tokens_cached(cli)

        default_char = self.get_default_char(cli)

        # Wrap/align right/center parameters.
        wrap_lines = self.wrap_lines(cli)
        right = self.align_right(cli)
        center = self.align_center(cli)

        def process_line(line):
            " Center or right align a single line. "
            used_width = token_list_width(line)
            padding = width - used_width
            if center:
                padding = int(padding / 2)
            return [(default_char.token, default_char.char * padding)
                    ] + line + [(Token, '\n')]

        if right or center:
            tokens2 = []
            for line in split_lines(tokens_with_mouse_handlers):
                tokens2.extend(process_line(line))
            tokens_with_mouse_handlers = tokens2

        # Strip mouse handlers from tokens.
        tokens = [tuple(item[:2]) for item in tokens_with_mouse_handlers]

        # Create screen, or take it from the cache.
        key = (default_char.char, default_char.token,
               tuple(tokens_with_mouse_handlers), width, wrap_lines, right,
               center)
        params = (default_char, tokens, width, wrap_lines, right, center)
        screen, self._pos_to_indexes = self._screen_cache.get(
            key, lambda: self._get_screen(*params))

        self._tokens = tokens_with_mouse_handlers
        return screen

    @classmethod
    def _get_screen(cls, default_char, tokens, width, wrap_lines, right,
                    center):
        screen = Screen(default_char, initial_width=width)

        # Only call write_data when we actually have tokens.
        # (Otherwise the screen height will go up from 0 to 1 while we don't
        # want that. -- An empty control should not take up any space.)
        if tokens:
            write_data_result = screen.write_data(
                tokens, width=(width if wrap_lines else None))

            indexes_to_pos = write_data_result.indexes_to_pos
            pos_to_indexes = _LazyReverseDict(indexes_to_pos)
        else:
            pos_to_indexes = {}

        return screen, pos_to_indexes

    @classmethod
    def static(cls, tokens):
        def get_static_tokens(cli):
            return tokens

        return cls(get_static_tokens)

    def mouse_handler(self, cli, mouse_event):
        """
        Handle mouse events.

        (When the token 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 `Window` to handle this
        particular event.)
        """
        if self._pos_to_indexes:
            # Find position in the token list.
            position = mouse_event.position
            index = self._pos_to_indexes.get((position.x, position.y))

            if index is not None:
                # Find mouse handler for this character.
                count = 0
                for item in self._tokens:
                    count += len(item[1])
                    if count >= index:
                        if len(item) >= 3:
                            # Handler found. Call it.
                            # (Handler can return NotImplemented, so return
                            # that result.)
                            handler = item[2]
                            return handler(cli, mouse_event)
                        else:
                            break

        # Otherwise, don't handle here.
        return NotImplemented
Пример #37
0
class Registry(object):
    """
    Key binding registry.

    ::

        r = Registry()

        @r.add_binding(Keys.ControlX, Keys.ControlC, filter=INSERT)
        def handler(event):
            # Handle ControlX-ControlC key sequence.
            pass
    """
    def __init__(self):
        self.key_bindings = []
        self._get_bindings_for_keys_cache = SimpleCache(maxsize=10000)
        self._get_bindings_starting_with_keys_cache = SimpleCache(maxsize=1000)

    def _clear_cache(self):
        self._get_bindings_for_keys_cache.clear()
        self._get_bindings_starting_with_keys_cache.clear()

    def add_binding(self, *keys, **kwargs):
        """
        Decorator for annotating key bindings.

        :param filter: :class:`~prompt_toolkit.filters.CLIFilter` to determine
            when this key binding is active.
        :param eager: :class:`~prompt_toolkit.filters.CLIFilter` or `bool`.
            When True, ignore potential longer matches when this key binding is
            hit. E.g. when there is an active eager key binding for Ctrl-X,
            execute the handler immediately and ignore the key binding for
            Ctrl-X Ctrl-E of which it is a prefix.
        :param save_before: Callable that takes an `Event` and returns True if
            we should save the current buffer, before handling the event.
            (That's the default.)
        """
        filter = to_cli_filter(kwargs.pop('filter', True))
        eager = to_cli_filter(kwargs.pop('eager', False))
        save_before = kwargs.pop('save_before', lambda e: True)
        to_cli_filter(kwargs.pop('invalidate_ui', True))  # Deprecated! (ignored.)

        assert not kwargs
        assert keys
        assert all(isinstance(k, (Key, text_type)) for k in keys), \
            'Key bindings should consist of Key and string (unicode) instances.'
        assert callable(save_before)

        if isinstance(filter, Never):
            # When a filter is Never, it will always stay disabled, so in that case
            # don't bother putting it in the registry. It will slow down every key
            # press otherwise.
            def decorator(func):
                return func
        else:
            def decorator(func):
                self.key_bindings.append(
                    _Binding(keys, func, filter=filter, eager=eager,
                             save_before=save_before))
                self._clear_cache()

                return func
        return decorator

    def remove_binding(self, function):
        """
        Remove a key binding.

        This expects a function that was given to `add_binding` method as
        parameter. Raises `ValueError` when the given function was not
        registered before.
        """
        assert callable(function)

        for b in self.key_bindings:
            if b.handler == function:
                self.key_bindings.remove(b)
                self._clear_cache()
                return

        # No key binding found for this function. Raise ValueError.
        raise ValueError('Binding not found: %r' % (function, ))

    def get_bindings_for_keys(self, keys):
        """
        Return a list of key bindings that can handle this key.
        (This return also inactive bindings, so the `filter` still has to be
        called, for checking it.)

        :param keys: tuple of keys.
        """
        def get():
            result = []
            for b in self.key_bindings:
                if len(keys) == len(b.keys):
                    match = True
                    any_count = 0

                    for i, j in zip(b.keys, keys):
                        if i != j and i != Keys.Any:
                            match = False
                            break

                        if i == Keys.Any:
                            any_count += 1

                    if match:
                        result.append((any_count, b))

            # Place bindings that have more 'Any' occurences in them at the end.
            result = sorted(result, key=lambda item: -item[0])

            return [item[1] for item in result]

        return self._get_bindings_for_keys_cache.get(keys, get)

    def get_bindings_starting_with_keys(self, keys):
        """
        Return a list of key bindings that handle a key sequence starting with
        `keys`. (It does only return bindings for which the sequences are
        longer than `keys`. And like `get_bindings_for_keys`, it also includes
        inactive bindings.)

        :param keys: tuple of keys.
        """
        def get():
            result = []
            for b in self.key_bindings:
                if len(keys) < len(b.keys):
                    match = True
                    for i, j in zip(b.keys, keys):
                        if i != j and i != Keys.Any:
                            match = False
                            break
                    if match:
                        result.append(b)
            return result

        return self._get_bindings_starting_with_keys_cache.get(keys, get)
class BufferControl(UIControl):
    """
    Control for visualising the content of a `Buffer`.

    :param input_processors: list of :class:`~prompt_toolkit.layout.processors.Processor`.
    :param lexer: :class:`~prompt_toolkit.layout.lexers.Lexer` instance for syntax highlighting.
    :param preview_search: `bool` or `CLIFilter`: Show search while typing.
    :param get_search_state: Callable that takes a CommandLineInterface and
        returns the SearchState to be used. (If not CommandLineInterface.search_state.)
    :param wrap_lines: `bool` or `CLIFilter`: Wrap long lines.
    :param buffer_name: String representing the name of the buffer to display.
    :param default_char: :class:`.Char` instance to use to fill the background. This is
        transparent by default.
    :param focus_on_click: Focus this buffer when it's click, but not yet focussed.
    """
    def __init__(self,
                 buffer_name=DEFAULT_BUFFER,
                 input_processors=None,
                 highlighters=None,
                 lexer=None,
                 preview_search=False,
                 search_buffer_name=SEARCH_BUFFER,
                 get_search_state=None,
                 wrap_lines=True,
                 menu_position=None,
                 default_char=None,
                 focus_on_click=False):
        assert input_processors is None or all(
            isinstance(i, Processor) for i in input_processors)
        assert highlighters is None or all(
            isinstance(i, Highlighter) for i in highlighters)
        assert menu_position is None or callable(menu_position)
        assert lexer is None or isinstance(lexer, Lexer)
        assert get_search_state is None or callable(get_search_state)

        self.preview_search = to_cli_filter(preview_search)
        self.get_search_state = get_search_state
        self.wrap_lines = to_cli_filter(wrap_lines)
        self.focus_on_click = to_cli_filter(focus_on_click)

        self.input_processors = input_processors or []
        self.highlighters = highlighters or []
        self.buffer_name = buffer_name
        self.menu_position = menu_position
        self.lexer = lexer or SimpleLexer()
        self.default_char = default_char or Char(token=Token.Transparent)
        self.search_buffer_name = search_buffer_name

        #: 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 faily easy way to cache such an expensive operation.
        self._token_cache = SimpleCache(maxsize=8)

        #: Keep a similar cache for rendered screens. (when we scroll up/down
        #: through the screen, or when we change another buffer, we don't want
        #: to recreate the same screen again.)
        self._screen_cache = SimpleCache(maxsize=8)

        #: Highlight Cache.
        #: When nothing of the buffer content or processors has changed, but
        #: the highlighting of the selection/search changes,
        self._highlight_cache = SimpleCache(maxsize=8)

        self._xy_to_cursor_position = None
        self._last_click_timestamp = None

    def _buffer(self, cli):
        """
        The buffer object that contains the 'main' content.
        """
        return cli.buffers[self.buffer_name]

    def has_focus(self, cli):
        # This control gets the focussed if the actual `Buffer` instance has the
        # focus or when any of the `InputProcessor` classes tells us that it
        # wants the focus. (E.g. in case of a reverse-search, where the actual
        # search buffer may not be displayed, but the "reverse-i-search" text
        # should get the focus.)
        return cli.current_buffer_name == self.buffer_name or \
            any(i.has_focus(cli) for i in self.input_processors)

    def preferred_width(self, cli, max_available_width):
        # Return the length of the longest line.
        return max(map(len, self._buffer(cli).document.lines))

    def preferred_height(self, cli, width):
        # Draw content on a screen using this width. Measure the height of the
        # result.
        screen, highlighters = self.create_screen(cli, width, None)
        return screen.height

    def _get_input_tokens(self, cli, document):
        """
        Tokenize input text for highlighting.
        Return (tokens, source_to_display, display_to_source) tuple.

        :param document: The document to be shown. This can be `buffer.document`
                         but could as well be a different one, in case we are
                         searching through the history. (Buffer.document_for_search)
        """
        def get():
            # Call lexer.
            tokens = self.lexer.get_tokens(cli, document.text)

            # 'Explode' tokens in characters. (And turn generator into a list.)
            # (Some input processors -- like search/selection highlighter --
            # rely on that each item in the tokens array only contains one
            # character.)
            tokens = [(token, c) for token, text in tokens for c in text]

            # Run all processors over the input.
            # (They can transform both the tokens and the cursor position.)
            source_to_display_functions = []
            display_to_source_functions = []

            d_ = document  # Each processor receives the document of the previous one.

            for p in self.input_processors:
                transformation = p.apply_transformation(cli, d_, tokens)
                d_ = transformation.document
                assert isinstance(transformation, Transformation)

                tokens = transformation.tokens
                source_to_display_functions.append(
                    transformation.source_to_display)
                display_to_source_functions.append(
                    transformation.display_to_source)

            # Chain cursor transformation (movement) functions.

            def source_to_display(cursor_position):
                " Chained source_to_display. "
                for f in source_to_display_functions:
                    cursor_position = f(cursor_position)
                return cursor_position

            def display_to_source(cursor_position):
                " Chained display_to_source. "
                for f in reversed(display_to_source_functions):
                    cursor_position = f(cursor_position)
                return cursor_position

            return tokens, source_to_display, display_to_source

        key = (
            document.text,

            # Include invalidation_hashes from all processors.
            tuple(
                p.invalidation_hash(cli, document)
                for p in self.input_processors),
        )

        return self._token_cache.get(key, get)

    def create_screen(self, cli, width, height):
        buffer = self._buffer(cli)

        # 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.)
        def preview_now():
            """ True when we should preview a search. """
            return bool(
                self.preview_search(cli)
                and cli.buffers[self.search_buffer_name].text)

        if preview_now():
            if self.get_search_state:
                ss = self.get_search_state(cli)
            else:
                ss = cli.search_state

            document = buffer.document_for_search(
                SearchState(text=cli.current_buffer.text,
                            direction=ss.direction,
                            ignore_case=ss.ignore_case))
        else:
            document = buffer.document

        # Wrap.
        wrap_width = width if self.wrap_lines(cli) else None

        def _create_screen():
            screen = Screen(self.default_char, initial_width=width)

            # Get tokens
            # Note: we add the space character at the end, because that's where
            #       the cursor can also be.
            input_tokens, source_to_display, display_to_source = self._get_input_tokens(
                cli, document)
            input_tokens += [(self.default_char.token, ' ')]

            write_data_result = screen.write_data(input_tokens,
                                                  width=wrap_width)
            indexes_to_pos = write_data_result.indexes_to_pos
            line_lengths = write_data_result.line_lengths

            pos_to_indexes = _LazyReverseDict(indexes_to_pos)

            def cursor_position_to_xy(cursor_position):
                """ Turn a cursor position in the buffer into x/y coordinates
                on the screen. """
                cursor_position = min(len(document.text), cursor_position)

                # First get the real token position by applying all transformations.
                cursor_position = source_to_display(cursor_position)

                # Then look up into the table.
                try:
                    return indexes_to_pos[cursor_position]
                except KeyError:
                    # This can fail with KeyError, but only if one of the
                    # processors is returning invalid key locations.
                    raise
                    # return 0, 0

            def xy_to_cursor_position(x, y):
                """ Turn x/y screen coordinates back to the original cursor
                position in the buffer. """
                # Look up reverse in table.
                while x > 0 or y > 0:
                    try:
                        index = pos_to_indexes[x, y]
                        break
                    except KeyError:
                        # No match found -> mouse click outside of region
                        # containing text. Look to the left or up.
                        if x: x -= 1
                        elif y: y -= 1
                else:
                    # Nobreak.
                    index = 0

                # Transform.
                return display_to_source(index)

            return screen, cursor_position_to_xy, xy_to_cursor_position, line_lengths

        # Build a key for the caching. If any of these parameters changes, we
        # have to recreate a new screen.
        key = (
            # When the text changes, we obviously have to recreate a new screen.
            document.text,

            # When the width changes, line wrapping will be different.
            # (None when disabled.)
            wrap_width,

            # Include invalidation_hashes from all processors.
            tuple(
                p.invalidation_hash(cli, document)
                for p in self.input_processors),
        )

        # Get from cache, or create if this doesn't exist yet.
        screen, cursor_position_to_xy, self._xy_to_cursor_position, line_lengths = \
            self._screen_cache.get(key, _create_screen)

        x, y = cursor_position_to_xy(document.cursor_position)
        screen.cursor_position = Point(y=y, x=x)

        # 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 focussed buffer.)
        if cli.current_buffer_name == self.buffer_name:
            menu_position = self.menu_position(
                cli) if self.menu_position else None
            if menu_position is not None:
                assert isinstance(menu_position, int)
                x, y = cursor_position_to_xy(menu_position)
                screen.menu_position = Point(y=y, x=x)
            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.)
                x, y = cursor_position_to_xy(
                    min(
                        buffer.cursor_position, buffer.complete_state.
                        original_document.cursor_position))
                screen.menu_position = Point(y=y, x=x)
            else:
                screen.menu_position = None

        # Add highlighting.
        highlight_key = (
            key,  # Includes everything from the 'key' above. (E.g. when the
            # document changes, we have to recalculate highlighting.)

            # Include invalidation_hashes from all highlighters.
            tuple(
                h.invalidation_hash(cli, document) for h in self.highlighters))

        highlighting = self._highlight_cache.get(
            highlight_key, lambda: self._get_highlighting(
                cli, document, cursor_position_to_xy, line_lengths))

        return screen, highlighting

    def _get_highlighting(self, cli, document, cursor_position_to_xy,
                          line_lengths):
        """
        Return a _HighlightDict for the highlighting. (This is a lazy dict of dicts.)

        The Window class will apply this for the visible regions. - That way,
        we don't have to recalculate the screen again for each selection/search
        change.

        :param line_lengths: Maps line numbers to the length of these lines.
        """
        def get_row_size(y):
            " Return the max 'x' value for a given row in the screen. "
            return max(1, line_lengths.get(y, 0))

        # Get list of fragments.
        row_to_fragments = defaultdict(list)

        for h in self.highlighters:
            for fragment in h.get_fragments(cli, document):
                # Expand fragments.
                start_column, start_row = cursor_position_to_xy(fragment.start)
                end_column, end_row = cursor_position_to_xy(fragment.end)
                token = fragment.token

                if start_row == end_row:
                    # Single line highlighting.
                    row_to_fragments[start_row].append(
                        _HighlightFragment(start_column, end_column, token))
                else:
                    # Multi line highlighting.
                    # (First line.)
                    row_to_fragments[start_row].append(
                        _HighlightFragment(start_column,
                                           get_row_size(start_row), token))

                    # (Middle lines.)
                    for y in range(start_row + 1, end_row):
                        row_to_fragments[y].append(
                            _HighlightFragment(0, get_row_size(y), token))

                    # (Last line.)
                    row_to_fragments[end_row].append(
                        _HighlightFragment(0, end_column, token))

        # Create dict to return.
        return _HighlightDict(row_to_fragments)

    def mouse_handler(self, cli, mouse_event):
        """
        Mouse handler for this control.
        """
        buffer = self._buffer(cli)
        position = mouse_event.position

        # Focus buffer when clicked.
        if self.has_focus(cli):
            if self._xy_to_cursor_position:
                # Translate coordinates back to the cursor position of the
                # original input.
                pos = self._xy_to_cursor_position(position.x, position.y)

                # Set the cursor position.
                if pos <= len(buffer.text):
                    if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN:
                        buffer.exit_selection()
                        buffer.cursor_position = pos

                    elif mouse_event.event_type == MouseEventTypes.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 - pos) > 1:
                            buffer.start_selection(
                                selection_type=SelectionType.CHARACTERS)
                            buffer.cursor_position = pos

                        # 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 < .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 focussed, but focussing on click events.
        else:
            if self.focus_on_click(
                    cli
            ) and mouse_event.event_type == MouseEventTypes.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
                # focussed and be handled anyway.)
                cli.focus(self.buffer_name)
            else:
                return NotImplemented

    def move_cursor_down(self, cli):
        b = self._buffer(cli)
        b.cursor_position += b.document.get_cursor_down_position()

    def move_cursor_up(self, cli):
        b = self._buffer(cli)
        b.cursor_position += b.document.get_cursor_up_position()
Пример #39
0
class TokenListControl(UIControl):
    """
    Control that displays a list of (Token, text) tuples.
    (It's mostly optimized for rather small widgets, like toolbars, menus, etc...)

    Mouse support:

        The list of tokens can also contain tuples of three items, looking like:
        (Token, text, handler). When mouse support is enabled and the user
        clicks on this token, then the given handler is called. That handler
        should accept two inputs: (CommandLineInterface, MouseEvent) and it
        should either handle the event or return `NotImplemented` in case we
        want the containing Window to handle this event.

    :param get_tokens: Callable that takes a `CommandLineInterface` instance
        and returns the list of (Token, text) tuples to be displayed right now.
    :param default_char: default :class:`.Char` (character and Token) to use
        for the background when there is more space available than `get_tokens`
        returns.
    :param get_default_char: Like `default_char`, but this is a callable that
        takes a :class:`prompt_toolkit.interface.CommandLineInterface` and
        returns a :class:`.Char` instance.
    :param has_focus: `bool` or `CLIFilter`, when this evaluates to `True`,
        this UI control will take the focus. The cursor will be shown in the
        upper left corner of this control, unless `get_token` returns a
        ``Token.SetCursorPosition`` token somewhere in the token list, then the
        cursor will be shown there.
    """
    def __init__(self,
                 get_tokens,
                 default_char=None,
                 get_default_char=None,
                 align_right=False,
                 align_center=False,
                 has_focus=False):
        assert callable(get_tokens)
        assert default_char is None or isinstance(default_char, Char)
        assert get_default_char is None or callable(get_default_char)
        assert not (default_char and get_default_char)

        self.align_right = to_cli_filter(align_right)
        self.align_center = to_cli_filter(align_center)
        self._has_focus_filter = to_cli_filter(has_focus)

        self.get_tokens = get_tokens

        # Construct `get_default_char` callable.
        if default_char:
            get_default_char = lambda _: default_char
        elif not get_default_char:
            get_default_char = lambda _: Char(' ', Token.Transparent)

        self.get_default_char = get_default_char

        #: Cache for the content.
        self._content_cache = SimpleCache(maxsize=18)
        self._token_cache = SimpleCache(maxsize=1)
        # Only cache one token list. We don't need the previous item.

        # Render info for the mouse support.
        self._tokens = None

    def reset(self):
        self._tokens = None

    def __repr__(self):
        return '%s(%r)' % (self.__class__.__name__, self.get_tokens)

    def _get_tokens_cached(self, cli):
        """
        Get tokens, but only retrieve tokens 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._token_cache.get(cli.render_counter,
                                     lambda: self.get_tokens(cli))

    def has_focus(self, cli):
        return self._has_focus_filter(cli)

    def preferred_width(self, cli, max_available_width):
        """
        Return the preferred width for this control.
        That is the width of the longest line.
        """
        text = token_list_to_text(self._get_tokens_cached(cli))
        line_lengths = [get_cwidth(l) for l in text.split('\n')]
        return max(line_lengths)

    def preferred_height(self, cli, width, max_available_height, wrap_lines):
        content = self.create_content(cli, width, None)
        return content.line_count

    def create_content(self, cli, width, height):
        # Get tokens
        tokens_with_mouse_handlers = self._get_tokens_cached(cli)

        default_char = self.get_default_char(cli)

        # Wrap/align right/center parameters.
        right = self.align_right(cli)
        center = self.align_center(cli)

        def process_line(line):
            " Center or right align a single line. "
            used_width = token_list_width(line)
            padding = width - used_width
            if center:
                padding = int(padding / 2)
            return [(default_char.token, default_char.char * padding)] + line

        if right or center:
            token_lines_with_mouse_handlers = []

            for line in split_lines(tokens_with_mouse_handlers):
                token_lines_with_mouse_handlers.append(process_line(line))
        else:
            token_lines_with_mouse_handlers = list(
                split_lines(tokens_with_mouse_handlers))

        # Strip mouse handlers from tokens.
        token_lines = [[tuple(item[:2]) for item in line]
                       for line in token_lines_with_mouse_handlers]

        # Keep track of the tokens with mouse handler, for later use in
        # `mouse_handler`.
        self._tokens = tokens_with_mouse_handlers

        # If there is a `Token.SetCursorPosition` in the token list, set the
        # cursor position here.
        def get_cursor_position():
            SetCursorPosition = Token.SetCursorPosition

            for y, line in enumerate(token_lines):
                x = 0
                for token, text in line:
                    if token == SetCursorPosition:
                        return Point(x=x, y=y)
                    x += len(text)
            return None

        # Create content, or take it from the cache.
        key = (default_char.char, default_char.token,
               tuple(tokens_with_mouse_handlers), width, right, center)

        def get_content():
            return UIContent(get_line=lambda i: token_lines[i],
                             line_count=len(token_lines),
                             default_char=default_char,
                             cursor_position=get_cursor_position())

        return self._content_cache.get(key, get_content)

    @classmethod
    def static(cls, tokens):
        def get_static_tokens(cli):
            return tokens

        return cls(get_static_tokens)

    def mouse_handler(self, cli, mouse_event):
        """
        Handle mouse events.

        (When the token 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 `Window` to handle this
        particular event.)
        """
        if self._tokens:
            # Read the generator.
            tokens_for_line = list(split_lines(self._tokens))

            try:
                tokens = tokens_for_line[mouse_event.position.y]
            except IndexError:
                return NotImplemented
            else:
                # Find position in the token list.
                xpos = mouse_event.position.x

                # Find mouse handler for this character.
                count = 0
                for item in tokens:
                    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(cli, mouse_event)
                        else:
                            break

        # Otherwise, don't handle here.
        return NotImplemented
Пример #40
0
class BufferControl(UIControl):
    """
    Control for visualising the content of a `Buffer`.

    :param input_processors: list of :class:`~prompt_toolkit.layout.processors.Processor`.
    :param lexer: :class:`~prompt_toolkit.layout.lexers.Lexer` instance for syntax highlighting.
    :param preview_search: `bool` or `CLIFilter`: Show search while typing.
    :param get_search_state: Callable that takes a CommandLineInterface and
        returns the SearchState to be used. (If not CommandLineInterface.search_state.)
    :param wrap_lines: `bool` or `CLIFilter`: Wrap long lines.
    :param buffer_name: String representing the name of the buffer to display.
    :param default_char: :class:`.Char` instance to use to fill the background. This is
        transparent by default.
    :param focus_on_click: Focus this buffer when it's click, but not yet focussed.
    """
    def __init__(self,
                 buffer_name=DEFAULT_BUFFER,
                 input_processors=None,
                 highlighters=None,
                 lexer=None,
                 preview_search=False,
                 search_buffer_name=SEARCH_BUFFER,
                 get_search_state=None,
                 wrap_lines=True,
                 menu_position=None,
                 default_char=None,
                 focus_on_click=False):
        assert input_processors is None or all(isinstance(i, Processor) for i in input_processors)
        assert highlighters is None or all(isinstance(i, Highlighter) for i in highlighters)
        assert menu_position is None or callable(menu_position)
        assert lexer is None or isinstance(lexer, Lexer)
        assert get_search_state is None or callable(get_search_state)

        self.preview_search = to_cli_filter(preview_search)
        self.get_search_state = get_search_state
        self.wrap_lines = to_cli_filter(wrap_lines)
        self.focus_on_click = to_cli_filter(focus_on_click)

        self.input_processors = input_processors or []
        self.highlighters = highlighters or []
        self.buffer_name = buffer_name
        self.menu_position = menu_position
        self.lexer = lexer or SimpleLexer()
        self.default_char = default_char or Char(token=Token.Transparent)
        self.search_buffer_name = search_buffer_name

        #: 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 faily easy way to cache such an expensive operation.
        self._token_cache = SimpleCache(maxsize=8)

        #: Keep a similar cache for rendered screens. (when we scroll up/down
        #: through the screen, or when we change another buffer, we don't want
        #: to recreate the same screen again.)
        self._screen_cache = SimpleCache(maxsize=8)

        #: Highlight Cache.
        #: When nothing of the buffer content or processors has changed, but
        #: the highlighting of the selection/search changes,
        self._highlight_cache = SimpleCache(maxsize=8)

        self._xy_to_cursor_position = None
        self._last_click_timestamp = None

    def _buffer(self, cli):
        """
        The buffer object that contains the 'main' content.
        """
        return cli.buffers[self.buffer_name]

    def has_focus(self, cli):
        # This control gets the focussed if the actual `Buffer` instance has the
        # focus or when any of the `InputProcessor` classes tells us that it
        # wants the focus. (E.g. in case of a reverse-search, where the actual
        # search buffer may not be displayed, but the "reverse-i-search" text
        # should get the focus.)
        return cli.current_buffer_name == self.buffer_name or \
            any(i.has_focus(cli) for i in self.input_processors)

    def preferred_width(self, cli, max_available_width):
        # Return the length of the longest line.
        return max(map(len, self._buffer(cli).document.lines))

    def preferred_height(self, cli, width):
        # Draw content on a screen using this width. Measure the height of the
        # result.
        screen, highlighters = self.create_screen(cli, width, None)
        return screen.height

    def _get_input_tokens(self, cli, document):
        """
        Tokenize input text for highlighting.
        Return (tokens, source_to_display, display_to_source) tuple.

        :param document: The document to be shown. This can be `buffer.document`
                         but could as well be a different one, in case we are
                         searching through the history. (Buffer.document_for_search)
        """
        def get():
            # Call lexer.
            tokens = self.lexer.get_tokens(cli, document.text)

            # 'Explode' tokens in characters. (And turn generator into a list.)
            # (Some input processors -- like search/selection highlighter --
            # rely on that each item in the tokens array only contains one
            # character.)
            tokens = [(token, c) for token, text in tokens for c in text]

            # Run all processors over the input.
            # (They can transform both the tokens and the cursor position.)
            source_to_display_functions = []
            display_to_source_functions = []

            d_ = document  # Each processor receives the document of the previous one.

            for p in self.input_processors:
                transformation  = p.apply_transformation(cli, d_, tokens)
                d_ = transformation.document
                assert isinstance(transformation, Transformation)

                tokens = transformation.tokens
                source_to_display_functions.append(transformation.source_to_display)
                display_to_source_functions.append(transformation.display_to_source)

            # Chain cursor transformation (movement) functions.

            def source_to_display(cursor_position):
                " Chained source_to_display. "
                for f in source_to_display_functions:
                    cursor_position = f(cursor_position)
                return cursor_position

            def display_to_source(cursor_position):
                " Chained display_to_source. "
                for f in reversed(display_to_source_functions):
                    cursor_position = f(cursor_position)
                return cursor_position

            return tokens, source_to_display, display_to_source

        key = (
            document.text,

            # Include invalidation_hashes from all processors.
            tuple(p.invalidation_hash(cli, document) for p in self.input_processors),
        )

        return self._token_cache.get(key, get)

    def create_screen(self, cli, width, height):
        buffer = self._buffer(cli)

        # 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.)
        def preview_now():
            """ True when we should preview a search. """
            return bool(self.preview_search(cli) and
                        cli.buffers[self.search_buffer_name].text)

        if preview_now():
            if self.get_search_state:
                ss = self.get_search_state(cli)
            else:
                ss = cli.search_state

            document = buffer.document_for_search(SearchState(
                text=cli.current_buffer.text,
                direction=ss.direction,
                ignore_case=ss.ignore_case))
        else:
            document = buffer.document

        # Wrap.
        wrap_width = width if self.wrap_lines(cli) else None

        def _create_screen():
            screen = Screen(self.default_char, initial_width=width)

            # Get tokens
            # Note: we add the space character at the end, because that's where
            #       the cursor can also be.
            input_tokens, source_to_display, display_to_source = self._get_input_tokens(cli, document)
            input_tokens += [(self.default_char.token, ' ')]

            write_data_result = screen.write_data(input_tokens, width=wrap_width)
            indexes_to_pos = write_data_result.indexes_to_pos
            line_lengths = write_data_result.line_lengths

            pos_to_indexes = _LazyReverseDict(indexes_to_pos)

            def cursor_position_to_xy(cursor_position):
                """ Turn a cursor position in the buffer into x/y coordinates
                on the screen. """
                cursor_position = min(len(document.text), cursor_position)

                # First get the real token position by applying all transformations.
                cursor_position = source_to_display(cursor_position)

                # Then look up into the table.
                try:
                    return indexes_to_pos[cursor_position]
                except KeyError:
                    # This can fail with KeyError, but only if one of the
                    # processors is returning invalid key locations.
                    raise
                    # return 0, 0

            def xy_to_cursor_position(x, y):
                """ Turn x/y screen coordinates back to the original cursor
                position in the buffer. """
                # Look up reverse in table.
                while x > 0 or y > 0:
                    try:
                        index = pos_to_indexes[x, y]
                        break
                    except KeyError:
                        # No match found -> mouse click outside of region
                        # containing text. Look to the left or up.
                        if x: x -= 1
                        elif y: y -=1
                else:
                    # Nobreak.
                    index = 0

                # Transform.
                return display_to_source(index)

            return screen, cursor_position_to_xy, xy_to_cursor_position, line_lengths

        # Build a key for the caching. If any of these parameters changes, we
        # have to recreate a new screen.
        key = (
            # When the text changes, we obviously have to recreate a new screen.
            document.text,

            # When the width changes, line wrapping will be different.
            # (None when disabled.)
            wrap_width,

            # Include invalidation_hashes from all processors.
            tuple(p.invalidation_hash(cli, document) for p in self.input_processors),
        )

        # Get from cache, or create if this doesn't exist yet.
        screen, cursor_position_to_xy, self._xy_to_cursor_position, line_lengths = \
            self._screen_cache.get(key, _create_screen)

        x, y = cursor_position_to_xy(document.cursor_position)
        screen.cursor_position = Point(y=y, x=x)

        # 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 focussed buffer.)
        if cli.current_buffer_name == self.buffer_name:
            menu_position = self.menu_position(cli) if self.menu_position else None
            if menu_position is not None:
                assert isinstance(menu_position, int)
                x, y = cursor_position_to_xy(menu_position)
                screen.menu_position = Point(y=y, x=x)
            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.)
                x, y = cursor_position_to_xy(
                    min(buffer.cursor_position,
                        buffer.complete_state.original_document.cursor_position))
                screen.menu_position = Point(y=y, x=x)
            else:
                screen.menu_position = None

        # Add highlighting.
        highlight_key = (
            key,  # Includes everything from the 'key' above. (E.g. when the
                     # document changes, we have to recalculate highlighting.)

            # Include invalidation_hashes from all highlighters.
            tuple(h.invalidation_hash(cli, document) for h in self.highlighters)
        )

        highlighting = self._highlight_cache.get(highlight_key, lambda:
            self._get_highlighting(cli, document, cursor_position_to_xy, line_lengths))

        return screen, highlighting

    def _get_highlighting(self, cli, document, cursor_position_to_xy, line_lengths):
        """
        Return a _HighlightDict for the highlighting. (This is a lazy dict of dicts.)

        The Window class will apply this for the visible regions. - That way,
        we don't have to recalculate the screen again for each selection/search
        change.

        :param line_lengths: Maps line numbers to the length of these lines.
        """
        def get_row_size(y):
            " Return the max 'x' value for a given row in the screen. "
            return max(1, line_lengths.get(y, 0))

        # Get list of fragments.
        row_to_fragments = defaultdict(list)

        for h in self.highlighters:
            for fragment in h.get_fragments(cli, document):
                # Expand fragments.
                start_column, start_row = cursor_position_to_xy(fragment.start)
                end_column, end_row = cursor_position_to_xy(fragment.end)
                token = fragment.token

                if start_row == end_row:
                    # Single line highlighting.
                    row_to_fragments[start_row].append(
                        _HighlightFragment(start_column, end_column, token))
                else:
                    # Multi line highlighting.
                    # (First line.)
                    row_to_fragments[start_row].append(
                        _HighlightFragment(start_column, get_row_size(start_row), token))

                    # (Middle lines.)
                    for y in range(start_row + 1, end_row):
                        row_to_fragments[y].append(_HighlightFragment(0, get_row_size(y), token))

                    # (Last line.)
                    row_to_fragments[end_row].append(_HighlightFragment(0, end_column, token))

        # Create dict to return.
        return _HighlightDict(row_to_fragments)

    def mouse_handler(self, cli, mouse_event):
        """
        Mouse handler for this control.
        """
        buffer = self._buffer(cli)
        position = mouse_event.position

        # Focus buffer when clicked.
        if self.has_focus(cli):
            if self._xy_to_cursor_position:
                # Translate coordinates back to the cursor position of the
                # original input.
                pos = self._xy_to_cursor_position(position.x, position.y)

                # Set the cursor position.
                if pos <= len(buffer.text):
                    if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN:
                        buffer.exit_selection()
                        buffer.cursor_position = pos

                    elif mouse_event.event_type == MouseEventTypes.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 - pos) > 1:
                            buffer.start_selection(selection_type=SelectionType.CHARACTERS)
                            buffer.cursor_position = pos

                        # 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 < .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 focussed, but focussing on click events.
        else:
            if self.focus_on_click(cli) and mouse_event.event_type == MouseEventTypes.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
                # focussed and be handled anyway.)
                cli.focus(self.buffer_name)
            else:
                return NotImplemented

    def move_cursor_down(self, cli):
        b = self._buffer(cli)
        b.cursor_position += b.document.get_cursor_down_position()

    def move_cursor_up(self, cli):
        b = self._buffer(cli)
        b.cursor_position += b.document.get_cursor_up_position()
Пример #41
0
class BufferControl(UIControl):
    """
    Control for visualising the content of a `Buffer`.

    :param input_processors: list of :class:`~prompt_toolkit.layout.processors.Processor`.
    :param lexer: :class:`~prompt_toolkit.layout.lexers.Lexer` instance for syntax highlighting.
    :param preview_search: `bool` or `CLIFilter`: Show search while typing.
    :param get_search_state: Callable that takes a CommandLineInterface and
        returns the SearchState to be used. (If not CommandLineInterface.search_state.)
    :param buffer_name: String representing the name of the buffer to display.
    :param default_char: :class:`.Char` instance to use to fill the background. This is
        transparent by default.
    :param focus_on_click: Focus this buffer when it's click, but not yet focussed.
    """
    def __init__(self,
                 buffer_name=DEFAULT_BUFFER,
                 input_processors=None,
                 lexer=None,
                 preview_search=False,
                 search_buffer_name=SEARCH_BUFFER,
                 get_search_state=None,
                 menu_position=None,
                 default_char=None,
                 focus_on_click=False):
        assert input_processors is None or all(
            isinstance(i, Processor) for i in input_processors)
        assert menu_position is None or callable(menu_position)
        assert lexer is None or isinstance(lexer, Lexer)
        assert get_search_state is None or callable(get_search_state)
        assert default_char is None or isinstance(default_char, Char)

        self.preview_search = to_cli_filter(preview_search)
        self.get_search_state = get_search_state
        self.focus_on_click = to_cli_filter(focus_on_click)

        self.input_processors = input_processors or []
        self.buffer_name = buffer_name
        self.menu_position = menu_position
        self.lexer = lexer or SimpleLexer()
        self.default_char = default_char or Char(token=Token.Transparent)
        self.search_buffer_name = search_buffer_name

        #: 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 faily easy way to cache such an expensive operation.
        self._token_cache = SimpleCache(maxsize=8)

        self._xy_to_cursor_position = None
        self._last_click_timestamp = None
        self._last_get_processed_line = None

    def _buffer(self, cli):
        """
        The buffer object that contains the 'main' content.
        """
        return cli.buffers[self.buffer_name]

    def has_focus(self, cli):
        # This control gets the focussed if the actual `Buffer` instance has the
        # focus or when any of the `InputProcessor` classes tells us that it
        # wants the focus. (E.g. in case of a reverse-search, where the actual
        # search buffer may not be displayed, but the "reverse-i-search" text
        # should get the focus.)
        return cli.current_buffer_name == self.buffer_name or \
            any(i.has_focus(cli) for i in self.input_processors)

    def preferred_width(self, cli, max_available_width):
        """
        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 behaviour.
        """
        return None

    def preferred_height(self, cli, width, max_available_height, wrap_lines):
        # Calculate the content height, if it was drawn on a screen with the
        # given width.
        height = 0
        content = self.create_content(cli, width, None)

        # 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)

            if height >= max_available_height:
                return max_available_height

        return height

    def _get_tokens_for_line_func(self, cli, document):
        """
        Create a function that returns the tokens for a given line.
        """

        # Cache using `document.text`.
        def get_tokens_for_line():
            return self.lexer.lex_document(cli, document)

        return self._token_cache.get(document.text, get_tokens_for_line)

    def _create_get_processed_line_func(self, cli, document):
        """
        Create a function that takes a line number of the current document and
        returns a _ProcessedLine(processed_tokens, source_to_display, display_to_source)
        tuple.
        """
        def transform(lineno, tokens):
            " Transform the tokens for a given line number. "
            source_to_display_functions = []
            display_to_source_functions = []

            # Get cursor position at this line.
            if document.cursor_position_row == lineno:
                cursor_column = document.cursor_position_col
            else:
                cursor_column = None

            def source_to_display(i):
                """ Translate x position from the buffer to the x position in the
                processed token list. """
                for f in source_to_display_functions:
                    i = f(i)
                return i

            # Apply each processor.
            for p in self.input_processors:
                transformation = p.apply_transformation(
                    cli, document, lineno, source_to_display, tokens)
                tokens = transformation.tokens

                if cursor_column:
                    cursor_column = transformation.source_to_display(
                        cursor_column)

                display_to_source_functions.append(
                    transformation.display_to_source)
                source_to_display_functions.append(
                    transformation.source_to_display)

            def display_to_source(i):
                for f in reversed(display_to_source_functions):
                    i = f(i)
                return i

            return _ProcessedLine(tokens, source_to_display, display_to_source)

        def create_func():
            get_line = self._get_tokens_for_line_func(cli, document)
            cache = {}

            def get_processed_line(i):
                try:
                    return cache[i]
                except KeyError:
                    processed_line = transform(i, get_line(i))
                    cache[i] = processed_line
                    return processed_line

            return get_processed_line

        return create_func()

    def create_content(self, cli, width, height):
        """
        Create a UIContent.
        """
        buffer = self._buffer(cli)

        # 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.)
        def preview_now():
            """ True when we should preview a search. """
            return bool(
                self.preview_search(cli)
                and cli.buffers[self.search_buffer_name].text)

        if preview_now():
            if self.get_search_state:
                ss = self.get_search_state(cli)
            else:
                ss = cli.search_state

            document = buffer.document_for_search(
                SearchState(text=cli.current_buffer.text,
                            direction=ss.direction,
                            ignore_case=ss.ignore_case))
        else:
            document = buffer.document

        get_processed_line = self._create_get_processed_line_func(
            cli, document)
        self._last_get_processed_line = get_processed_line

        def translate_rowcol(row, col):
            " Return the content column for this coordinate. "
            return Point(y=row,
                         x=get_processed_line(row).source_to_display(col))

        def get_line(i):
            " Return the tokens for a given line number. "
            tokens = get_processed_line(i).tokens

            # 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.)
            tokens = tokens + [(self.default_char.token, ' ')]
            return tokens

        content = UIContent(get_line=get_line,
                            line_count=document.line_count,
                            cursor_position=translate_rowcol(
                                document.cursor_position_row,
                                document.cursor_position_col),
                            default_char=self.default_char)

        # 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 focussed buffer.)
        if cli.current_buffer_name == self.buffer_name:
            menu_position = self.menu_position(
                cli) 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, cli, mouse_event):
        """
        Mouse handler for this control.
        """
        buffer = self._buffer(cli)
        position = mouse_event.position

        # Focus buffer when clicked.
        if self.has_focus(cli):
            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_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:
                        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 < .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 focussed, but focussing on click events.
        else:
            if self.focus_on_click(
                    cli) 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
                # focussed and be handled anyway.)
                cli.focus(self.buffer_name)
            else:
                return NotImplemented

    def move_cursor_down(self, cli):
        b = self._buffer(cli)
        b.cursor_position += b.document.get_cursor_down_position()

    def move_cursor_up(self, cli):
        b = self._buffer(cli)
        b.cursor_position += b.document.get_cursor_up_position()
Пример #42
0
 def __init__(self):
     self.bindings = []
     self._get_bindings_for_keys_cache = SimpleCache(maxsize=10000)
     self._get_bindings_starting_with_keys_cache = SimpleCache(maxsize=1000)
     self.__version = 0  # For cache invalidation.
Пример #43
0
 def __init__(self, styles: List[BaseStyle]) -> None:
     self.styles = styles
     self._style: SimpleCache[Hashable, Style] = SimpleCache(maxsize=1)
Пример #44
0
class Registry(object):
    """
    Key binding registry.

    ::

        r = Registry()

        @r.add_binding(Keys.ControlX, Keys.ControlC, filter=INSERT)
        def handler(event):
            # Handle ControlX-ControlC key sequence.
            pass
    """
    def __init__(self):
        self.key_bindings = []
        self._get_bindings_for_keys_cache = SimpleCache(maxsize=10000)
        self._get_bindings_starting_with_keys_cache = SimpleCache(maxsize=1000)

    def _clear_cache(self):
        self._get_bindings_for_keys_cache.clear()
        self._get_bindings_starting_with_keys_cache.clear()

    def add_binding(self, *keys, **kwargs):
        """
        Decorator for annotating key bindings.

        :param filter: :class:`~prompt_toolkit.filters.CLIFilter` to determine
            when this key binding is active.
        :param eager: :class:`~prompt_toolkit.filters.CLIFilter` or `bool`.
            When True, ignore potential longer matches when this key binding is
            hit. E.g. when there is an active eager key binding for Ctrl-X,
            execute the handler immediately and ignore the key binding for
            Ctrl-X Ctrl-E of which it is a prefix.
        :param save_before: Callable that takes an `Event` and returns True if
            we should save the current buffer, before handling the event.
            (That's the default.)
        """
        filter = to_cli_filter(kwargs.pop('filter', True))
        eager = to_cli_filter(kwargs.pop('eager', False))
        save_before = kwargs.pop('save_before', lambda e: True)
        to_cli_filter(kwargs.pop('invalidate_ui',
                                 True))  # Deprecated! (ignored.)

        assert not kwargs
        assert keys
        assert all(isinstance(k, (Key, text_type)) for k in keys), \
            'Key bindings should consist of Key and string (unicode) instances.'
        assert callable(save_before)

        def decorator(func):
            # When a filter is Never, it will always stay disabled, so in that case
            # don't bother putting it in the registry. It will slow down every key
            # press otherwise.
            if not isinstance(filter, Never):
                self.key_bindings.append(
                    _Binding(keys,
                             func,
                             filter=filter,
                             eager=eager,
                             save_before=save_before))
                self._clear_cache()

            return func

        return decorator

    def remove_binding(self, function):
        """
        Remove a key binding.

        This expects a function that was given to `add_binding` method as
        parameter. Raises `ValueError` when the given function was not
        registered before.
        """
        assert callable(function)

        for b in self.key_bindings:
            if b.handler == function:
                self.key_bindings.remove(b)
                self._clear_cache()
                return

        # No key binding found for this function. Raise ValueError.
        raise ValueError('Binding not found: %r' % (function, ))

    def get_bindings_for_keys(self, keys):
        """
        Return a list of key bindings that can handle this key.
        (This return also inactive bindings, so the `filter` still has to be
        called, for checking it.)

        :param keys: tuple of keys.
        """
        def get():
            result = []
            for b in self.key_bindings:
                if len(keys) == len(b.keys):
                    match = True
                    any_count = 0

                    for i, j in zip(b.keys, keys):
                        if i != j and i != Keys.Any:
                            match = False
                            break

                        if i == Keys.Any:
                            any_count += 1

                    if match:
                        result.append((any_count, b))

            # Place bindings that have more 'Any' occurences in them at the end.
            result = sorted(result, key=lambda item: -item[0])

            return [item[1] for item in result]

        return self._get_bindings_for_keys_cache.get(keys, get)

    def get_bindings_starting_with_keys(self, keys):
        """
        Return a list of key bindings that handle a key sequence starting with
        `keys`. (It does only return bindings for which the sequences are
        longer than `keys`. And like `get_bindings_for_keys`, it also includes
        inactive bindings.)

        :param keys: tuple of keys.
        """
        def get():
            result = []
            for b in self.key_bindings:
                if len(keys) < len(b.keys):
                    match = True
                    for i, j in zip(b.keys, keys):
                        if i != j and i != Keys.Any:
                            match = False
                            break
                    if match:
                        result.append(b)
            return result

        return self._get_bindings_starting_with_keys_cache.get(keys, get)
Пример #45
0
 def __init__(self):
     self.key_bindings = []
     self._get_bindings_for_keys_cache = SimpleCache(maxsize=10000)
     self._get_bindings_starting_with_keys_cache = SimpleCache(maxsize=1000)
Пример #46
0
class TokenListControl(UIControl):
    """
    Control that displays a list of (Token, text) tuples.
    (It's mostly optimized for rather small widgets, like toolbars, menus, etc...)

    Mouse support:

        The list of tokens can also contain tuples of three items, looking like:
        (Token, text, handler). When mouse support is enabled and the user
        clicks on this token, then the given handler is called. That handler
        should accept two inputs: (CommandLineInterface, MouseEvent) and it
        should either handle the event or return `NotImplemented` in case we
        want the containing Window to handle this event.

    :param get_tokens: Callable that takes a `CommandLineInterface` instance
        and returns the list of (Token, text) tuples to be displayed right now.
    :param default_char: default :class:`.Char` (character and Token) to use
        for the background when there is more space available than `get_tokens`
        returns.
    :param get_default_char: Like `default_char`, but this is a callable that
        takes a :class:`prompt_toolkit.interface.CommandLineInterface` and
        returns a :class:`.Char` instance.
    :param has_focus: `bool` or `CLIFilter`, when this evaluates to `True`,
        this UI control will take the focus. The cursor will be shown in the
        upper left corner of this control, unless `get_token` returns a
        ``Token.SetCursorPosition`` token somewhere in the token list, then the
        cursor will be shown there.
    :param wrap_lines: `bool` or `CLIFilter`: Wrap long lines.
    """
    def __init__(self, get_tokens, default_char=None, get_default_char=None,
                 align_right=False, align_center=False,
                 has_focus=False, wrap_lines=True):
        assert default_char is None or isinstance(default_char, Char)
        assert get_default_char is None or callable(get_default_char)
        assert not (default_char and get_default_char)

        self.align_right = to_cli_filter(align_right)
        self.align_center = to_cli_filter(align_center)
        self._has_focus_filter = to_cli_filter(has_focus)
        self.wrap_lines = to_cli_filter(wrap_lines)

        self.get_tokens = get_tokens

        # Construct `get_default_char` callable.
        if default_char:
            get_default_char = lambda _: default_char
        elif not get_default_char:
            get_default_char = lambda _: Char(' ', Token)

        self.get_default_char = get_default_char

        #: Cache for rendered screens.
        self._screen_cache = SimpleCache(maxsize=18)
        self._token_cache = SimpleCache(maxsize=1)
            # Only cache one token list. We don't need the previous item.

        # Render info for the mouse support.
        self._tokens = None  # The last rendered tokens.
        self._pos_to_indexes = None  # Mapping from mouse positions (x,y) to
                                     # positions in the token list.

    def __repr__(self):
        return '%s(%r)' % (self.__class__.__name__, self.get_tokens)

    def _get_tokens_cached(self, cli):
        """
        Get tokens, but only retrieve tokens 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._token_cache.get(
            cli.render_counter, lambda: self.get_tokens(cli))

    def has_focus(self, cli):
        return self._has_focus_filter(cli)

    def preferred_width(self, cli, max_available_width):
        """
        Return the preferred width for this control.
        That is the width of the longest line.
        """
        text = ''.join(t[1] for t in self._get_tokens_cached(cli))
        line_lengths = [get_cwidth(l) for l in text.split('\n')]
        return max(line_lengths)

    def preferred_height(self, cli, width):
        screen = self.create_screen(cli, width, None)
        return screen.height

    def create_screen(self, cli, width, height):
        # Get tokens
        tokens_with_mouse_handlers = self._get_tokens_cached(cli)

        default_char = self.get_default_char(cli)

        # Wrap/align right/center parameters.
        wrap_lines = self.wrap_lines(cli)
        right = self.align_right(cli)
        center = self.align_center(cli)

        def process_line(line):
            " Center or right align a single line. "
            used_width = token_list_width(line)
            padding = width - used_width
            if center:
                padding = int(padding / 2)
            return [(default_char.token, default_char.char * padding)] + line + [(Token, '\n')]

        if right or center:
            tokens2 = []
            for line in split_lines(tokens_with_mouse_handlers):
                tokens2.extend(process_line(line))
            tokens_with_mouse_handlers = tokens2

        # Strip mouse handlers from tokens.
        tokens = [tuple(item[:2]) for item in tokens_with_mouse_handlers]

        # Create screen, or take it from the cache.
        key = (default_char.char, default_char.token,
                tuple(tokens_with_mouse_handlers), width, wrap_lines, right, center)
        params = (default_char, tokens, width, wrap_lines, right, center)
        screen, self._pos_to_indexes = self._screen_cache.get(key, lambda: self._get_screen(*params))

        self._tokens = tokens_with_mouse_handlers
        return screen

    @classmethod
    def _get_screen(cls, default_char, tokens, width, wrap_lines, right, center):
        screen = Screen(default_char, initial_width=width)

        # Only call write_data when we actually have tokens.
        # (Otherwise the screen height will go up from 0 to 1 while we don't
        # want that. -- An empty control should not take up any space.)
        if tokens:
            write_data_result = screen.write_data(tokens, width=(width if wrap_lines else None))

            indexes_to_pos = write_data_result.indexes_to_pos
            pos_to_indexes = _LazyReverseDict(indexes_to_pos)
        else:
            pos_to_indexes = {}

        return screen, pos_to_indexes

    @classmethod
    def static(cls, tokens):
        def get_static_tokens(cli):
            return tokens
        return cls(get_static_tokens)

    def mouse_handler(self, cli, mouse_event):
        """
        Handle mouse events.

        (When the token 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 `Window` to handle this
        particular event.)
        """
        if self._pos_to_indexes:
            # Find position in the token list.
            position = mouse_event.position
            index = self._pos_to_indexes.get((position.x, position.y))

            if index is not None:
                # Find mouse handler for this character.
                count = 0
                for item in self._tokens:
                    count += len(item[1])
                    if count >= index:
                        if len(item) >= 3:
                            # Handler found. Call it.
                            # (Handler can return NotImplemented, so return
                            # that result.)
                            handler = item[2]
                            return handler(cli, mouse_event)
                        else:
                            break

        # Otherwise, don't handle here.
        return NotImplemented
Пример #47
0
 def __init__(self, app):
     self.app = app
     self._cache = SimpleCache()
Пример #48
0
    def __init__(self, styles):
        assert all(isinstance(style, BaseStyle) for style in styles)

        self.styles = styles
        self._style = SimpleCache(maxsize=1)
    def __init__(self, chars='[](){}<>', max_cursor_distance=1000):
        self.chars = chars
        self.max_cursor_distance = max_cursor_distance

        self._positions_cache = SimpleCache(maxsize=8)
class KeyBindings(KeyBindingsBase):
    """
    A container for a set of key bindings.

    Example usage::

        kb = KeyBindings()

        @kb.add('c-t')
        def _(event):
            print('Control-T pressed')

        @kb.add('c-a', 'c-b')
        def _(event):
            print('Control-A pressed, followed by Control-B')

        @kb.add('c-x', filter=is_searching)
        def _(event):
            print('Control-X pressed')  # Works only if we are searching.

    """
    def __init__(self):
        self.bindings = []
        self._get_bindings_for_keys_cache = SimpleCache(maxsize=10000)
        self._get_bindings_starting_with_keys_cache = SimpleCache(maxsize=1000)
        self.__version = 0  # For cache invalidation.

    def _clear_cache(self):
        self.__version += 1
        self._get_bindings_for_keys_cache.clear()
        self._get_bindings_starting_with_keys_cache.clear()

    @property
    def _version(self):
        return self.__version

    def add(self, *keys, **kwargs):
        """
        Decorator for adding a key bindings.

        :param filter: :class:`~prompt_toolkit.filters.Filter` to determine
            when this key binding is active.
        :param eager: :class:`~prompt_toolkit.filters.Filter` or `bool`.
            When True, ignore potential longer matches when this key binding is
            hit. E.g. when there is an active eager key binding for Ctrl-X,
            execute the handler immediately and ignore the key binding for
            Ctrl-X Ctrl-E of which it is a prefix.
        :param is_global: When this key bindings is added to a `Container` or
            `Control`, make it a global (always active) binding.
        :param save_before: Callable that takes an `Event` and returns True if
            we should save the current buffer, before handling the event.
            (That's the default.)
        :param record_in_macro: Record these key bindings when a macro is
            being recorded. (True by default.)
        """
        filter = to_filter(kwargs.pop('filter', True))
        eager = to_filter(kwargs.pop('eager', False))
        is_global = to_filter(kwargs.pop('is_global', False))
        save_before = kwargs.pop('save_before', lambda e: True)
        record_in_macro = to_filter(kwargs.pop('record_in_macro', True))

        assert not kwargs
        assert keys
        assert callable(save_before)

        keys = tuple(_check_and_expand_key(k) for k in keys)

        if isinstance(filter, Never):
            # When a filter is Never, it will always stay disabled, so in that
            # case don't bother putting it in the key bindings. It will slow
            # down every key press otherwise.
            def decorator(func):
                return func
        else:
            def decorator(func):
                if isinstance(func, _Binding):
                    # We're adding an existing _Binding object.
                    self.bindings.append(
                        _Binding(
                            keys, func.handler,
                            filter=func.filter & filter,
                            eager=eager | func.eager,
                            is_global = is_global | func.is_global,
                            save_before=func.save_before,
                            record_in_macro=func.record_in_macro))
                else:
                    self.bindings.append(
                        _Binding(keys, func, filter=filter, eager=eager,
                                 is_global=is_global, save_before=save_before,
                                 record_in_macro=record_in_macro))
                self._clear_cache()

                return func
        return decorator

    def remove(self, *args):
        """
        Remove a key binding.

        This expects either a function that was given to `add` method as
        parameter or a sequence of key bindings.

        Raises `ValueError` when no bindings was found.

        Usage::

            remove(handler)  # Pass handler.
            remove('c-x', 'c-a')  # Or pass the key bindings.
        """
        found = False

        if callable(args[0]):
            assert len(args) == 1
            function = args[0]

            # Remove the given function.
            for b in self.bindings:
                if b.handler == function:
                    self.bindings.remove(b)
                    found = True

        else:
            assert len(args) > 0

            # Remove this sequence of key bindings.
            keys = tuple(_check_and_expand_key(k) for k in args)

            for b in self.bindings:
                if b.keys == keys:
                    self.bindings.remove(b)
                    found = True

        if found:
            self._clear_cache()
        else:
            # No key binding found for this function. Raise ValueError.
            raise ValueError('Binding not found: %r' % (function, ))

    # For backwards-compatibility.
    add_binding = add
    remove_binding = remove

    def get_bindings_for_keys(self, keys):
        """
        Return a list of key bindings that can handle this key.
        (This return also inactive bindings, so the `filter` still has to be
        called, for checking it.)

        :param keys: tuple of keys.
        """
        def get():
            result = []
            for b in self.bindings:
                if len(keys) == len(b.keys):
                    match = True
                    any_count = 0

                    for i, j in zip(b.keys, keys):
                        if i != j and i != Keys.Any:
                            match = False
                            break

                        if i == Keys.Any:
                            any_count += 1

                    if match:
                        result.append((any_count, b))

            # Place bindings that have more 'Any' occurrences in them at the end.
            result = sorted(result, key=lambda item: -item[0])

            return [item[1] for item in result]

        return self._get_bindings_for_keys_cache.get(keys, get)

    def get_bindings_starting_with_keys(self, keys):
        """
        Return a list of key bindings that handle a key sequence starting with
        `keys`. (It does only return bindings for which the sequences are
        longer than `keys`. And like `get_bindings_for_keys`, it also includes
        inactive bindings.)

        :param keys: tuple of keys.
        """
        def get():
            result = []
            for b in self.bindings:
                if len(keys) < len(b.keys):
                    match = True
                    for i, j in zip(b.keys, keys):
                        if i != j and i != Keys.Any:
                            match = False
                            break
                    if match:
                        result.append(b)
            return result

        return self._get_bindings_starting_with_keys_cache.get(keys, get)