mirror of
https://github.com/aykhans/AzSuicideDataVisualization.git
synced 2025-07-01 22:13:01 +00:00
first commit
This commit is contained in:
144
.venv/Lib/site-packages/prompt_toolkit/layout/__init__.py
Normal file
144
.venv/Lib/site-packages/prompt_toolkit/layout/__init__.py
Normal file
@ -0,0 +1,144 @@
|
||||
"""
|
||||
Command line layout definitions
|
||||
-------------------------------
|
||||
|
||||
The layout of a command line interface is defined by a Container instance.
|
||||
There are two main groups of classes here. Containers and controls:
|
||||
|
||||
- A container can contain other containers or controls, it can have multiple
|
||||
children and it decides about the dimensions.
|
||||
- A control is responsible for rendering the actual content to a screen.
|
||||
A control can propose some dimensions, but it's the container who decides
|
||||
about the dimensions -- or when the control consumes more space -- which part
|
||||
of the control will be visible.
|
||||
|
||||
|
||||
Container classes::
|
||||
|
||||
- Container (Abstract base class)
|
||||
|- HSplit (Horizontal split)
|
||||
|- VSplit (Vertical split)
|
||||
|- FloatContainer (Container which can also contain menus and other floats)
|
||||
`- Window (Container which contains one actual control
|
||||
|
||||
Control classes::
|
||||
|
||||
- UIControl (Abstract base class)
|
||||
|- FormattedTextControl (Renders formatted text, or a simple list of text fragments)
|
||||
`- BufferControl (Renders an input buffer.)
|
||||
|
||||
|
||||
Usually, you end up wrapping every control inside a `Window` object, because
|
||||
that's the only way to render it in a layout.
|
||||
|
||||
There are some prepared toolbars which are ready to use::
|
||||
|
||||
- SystemToolbar (Shows the 'system' input buffer, for entering system commands.)
|
||||
- ArgToolbar (Shows the input 'arg', for repetition of input commands.)
|
||||
- SearchToolbar (Shows the 'search' input buffer, for incremental search.)
|
||||
- CompletionsToolbar (Shows the completions of the current buffer.)
|
||||
- ValidationToolbar (Shows validation errors of the current buffer.)
|
||||
|
||||
And one prepared menu:
|
||||
|
||||
- CompletionsMenu
|
||||
|
||||
"""
|
||||
from .containers import (
|
||||
AnyContainer,
|
||||
ColorColumn,
|
||||
ConditionalContainer,
|
||||
Container,
|
||||
DynamicContainer,
|
||||
Float,
|
||||
FloatContainer,
|
||||
HorizontalAlign,
|
||||
HSplit,
|
||||
ScrollOffsets,
|
||||
VerticalAlign,
|
||||
VSplit,
|
||||
Window,
|
||||
WindowAlign,
|
||||
WindowRenderInfo,
|
||||
is_container,
|
||||
to_container,
|
||||
to_window,
|
||||
)
|
||||
from .controls import (
|
||||
BufferControl,
|
||||
DummyControl,
|
||||
FormattedTextControl,
|
||||
SearchBufferControl,
|
||||
UIContent,
|
||||
UIControl,
|
||||
)
|
||||
from .dimension import (
|
||||
AnyDimension,
|
||||
D,
|
||||
Dimension,
|
||||
is_dimension,
|
||||
max_layout_dimensions,
|
||||
sum_layout_dimensions,
|
||||
to_dimension,
|
||||
)
|
||||
from .layout import InvalidLayoutError, Layout, walk
|
||||
from .margins import (
|
||||
ConditionalMargin,
|
||||
Margin,
|
||||
NumberedMargin,
|
||||
PromptMargin,
|
||||
ScrollbarMargin,
|
||||
)
|
||||
from .menus import CompletionsMenu, MultiColumnCompletionsMenu
|
||||
from .scrollable_pane import ScrollablePane
|
||||
|
||||
__all__ = [
|
||||
# Layout.
|
||||
"Layout",
|
||||
"InvalidLayoutError",
|
||||
"walk",
|
||||
# Dimensions.
|
||||
"AnyDimension",
|
||||
"Dimension",
|
||||
"D",
|
||||
"sum_layout_dimensions",
|
||||
"max_layout_dimensions",
|
||||
"to_dimension",
|
||||
"is_dimension",
|
||||
# Containers.
|
||||
"AnyContainer",
|
||||
"Container",
|
||||
"HorizontalAlign",
|
||||
"VerticalAlign",
|
||||
"HSplit",
|
||||
"VSplit",
|
||||
"FloatContainer",
|
||||
"Float",
|
||||
"WindowAlign",
|
||||
"Window",
|
||||
"WindowRenderInfo",
|
||||
"ConditionalContainer",
|
||||
"ScrollOffsets",
|
||||
"ColorColumn",
|
||||
"to_container",
|
||||
"to_window",
|
||||
"is_container",
|
||||
"DynamicContainer",
|
||||
"ScrollablePane",
|
||||
# Controls.
|
||||
"BufferControl",
|
||||
"SearchBufferControl",
|
||||
"DummyControl",
|
||||
"FormattedTextControl",
|
||||
"UIControl",
|
||||
"UIContent",
|
||||
# Margins.
|
||||
"Margin",
|
||||
"NumberedMargin",
|
||||
"ScrollbarMargin",
|
||||
"ConditionalMargin",
|
||||
"PromptMargin",
|
||||
# Menus.
|
||||
"CompletionsMenu",
|
||||
"MultiColumnCompletionsMenu",
|
||||
]
|
2757
.venv/Lib/site-packages/prompt_toolkit/layout/containers.py
Normal file
2757
.venv/Lib/site-packages/prompt_toolkit/layout/containers.py
Normal file
File diff suppressed because it is too large
Load Diff
957
.venv/Lib/site-packages/prompt_toolkit/layout/controls.py
Normal file
957
.venv/Lib/site-packages/prompt_toolkit/layout/controls.py
Normal file
@ -0,0 +1,957 @@
|
||||
"""
|
||||
User interface Controls for the layout.
|
||||
"""
|
||||
import time
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Callable,
|
||||
Dict,
|
||||
Hashable,
|
||||
Iterable,
|
||||
List,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Union,
|
||||
)
|
||||
|
||||
from prompt_toolkit.application.current import get_app
|
||||
from prompt_toolkit.buffer import Buffer
|
||||
from prompt_toolkit.cache import SimpleCache
|
||||
from prompt_toolkit.data_structures import Point
|
||||
from prompt_toolkit.document import Document
|
||||
from prompt_toolkit.filters import FilterOrBool, to_filter
|
||||
from prompt_toolkit.formatted_text import (
|
||||
AnyFormattedText,
|
||||
StyleAndTextTuples,
|
||||
to_formatted_text,
|
||||
)
|
||||
from prompt_toolkit.formatted_text.utils import (
|
||||
fragment_list_to_text,
|
||||
fragment_list_width,
|
||||
split_lines,
|
||||
)
|
||||
from prompt_toolkit.lexers import Lexer, SimpleLexer
|
||||
from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType
|
||||
from prompt_toolkit.search import SearchState
|
||||
from prompt_toolkit.selection import SelectionType
|
||||
from prompt_toolkit.utils import get_cwidth
|
||||
|
||||
from .processors import (
|
||||
DisplayMultipleCursors,
|
||||
HighlightIncrementalSearchProcessor,
|
||||
HighlightSearchProcessor,
|
||||
HighlightSelectionProcessor,
|
||||
Processor,
|
||||
TransformationInput,
|
||||
merge_processors,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from prompt_toolkit.key_binding.key_bindings import (
|
||||
KeyBindingsBase,
|
||||
NotImplementedOrNone,
|
||||
)
|
||||
from prompt_toolkit.utils import Event
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BufferControl",
|
||||
"SearchBufferControl",
|
||||
"DummyControl",
|
||||
"FormattedTextControl",
|
||||
"UIControl",
|
||||
"UIContent",
|
||||
]
|
||||
|
||||
GetLinePrefixCallable = Callable[[int, int], AnyFormattedText]
|
||||
|
||||
|
||||
class UIControl(metaclass=ABCMeta):
|
||||
"""
|
||||
Base class for all user interface controls.
|
||||
"""
|
||||
|
||||
def reset(self) -> None:
|
||||
# Default reset. (Doesn't have to be implemented.)
|
||||
pass
|
||||
|
||||
def preferred_width(self, max_available_width: int) -> Optional[int]:
|
||||
return None
|
||||
|
||||
def preferred_height(
|
||||
self,
|
||||
width: int,
|
||||
max_available_height: int,
|
||||
wrap_lines: bool,
|
||||
get_line_prefix: Optional[GetLinePrefixCallable],
|
||||
) -> Optional[int]:
|
||||
return None
|
||||
|
||||
def is_focusable(self) -> bool:
|
||||
"""
|
||||
Tell whether this user control is focusable.
|
||||
"""
|
||||
return False
|
||||
|
||||
@abstractmethod
|
||||
def create_content(self, width: int, height: int) -> "UIContent":
|
||||
"""
|
||||
Generate the content for this user control.
|
||||
|
||||
Returns a :class:`.UIContent` instance.
|
||||
"""
|
||||
|
||||
def mouse_handler(self, mouse_event: MouseEvent) -> "NotImplementedOrNone":
|
||||
"""
|
||||
Handle mouse events.
|
||||
|
||||
When `NotImplemented` is returned, it means that the given event is not
|
||||
handled by the `UIControl` itself. The `Window` or key bindings can
|
||||
decide to handle this event as scrolling or changing focus.
|
||||
|
||||
:param mouse_event: `MouseEvent` instance.
|
||||
"""
|
||||
return NotImplemented
|
||||
|
||||
def move_cursor_down(self) -> None:
|
||||
"""
|
||||
Request to move the cursor down.
|
||||
This happens when scrolling down and the cursor is completely at the
|
||||
top.
|
||||
"""
|
||||
|
||||
def move_cursor_up(self) -> None:
|
||||
"""
|
||||
Request to move the cursor up.
|
||||
"""
|
||||
|
||||
def get_key_bindings(self) -> Optional["KeyBindingsBase"]:
|
||||
"""
|
||||
The key bindings that are specific for this user control.
|
||||
|
||||
Return a :class:`.KeyBindings` object if some key bindings are
|
||||
specified, or `None` otherwise.
|
||||
"""
|
||||
|
||||
def get_invalidate_events(self) -> Iterable["Event[object]"]:
|
||||
"""
|
||||
Return a list of `Event` objects. This can be a generator.
|
||||
(The application collects all these events, in order to bind redraw
|
||||
handlers to these events.)
|
||||
"""
|
||||
return []
|
||||
|
||||
|
||||
class UIContent:
|
||||
"""
|
||||
Content generated by a user control. This content consists of a list of
|
||||
lines.
|
||||
|
||||
:param get_line: Callable that takes a line number and returns the current
|
||||
line. This is a list of (style_str, text) tuples.
|
||||
:param line_count: The number of lines.
|
||||
:param cursor_position: a :class:`.Point` for the cursor position.
|
||||
:param menu_position: a :class:`.Point` for the menu position.
|
||||
:param show_cursor: Make the cursor visible.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
get_line: Callable[[int], StyleAndTextTuples] = (lambda i: []),
|
||||
line_count: int = 0,
|
||||
cursor_position: Optional[Point] = None,
|
||||
menu_position: Optional[Point] = None,
|
||||
show_cursor: bool = True,
|
||||
):
|
||||
|
||||
self.get_line = get_line
|
||||
self.line_count = line_count
|
||||
self.cursor_position = cursor_position or Point(x=0, y=0)
|
||||
self.menu_position = menu_position
|
||||
self.show_cursor = show_cursor
|
||||
|
||||
# Cache for line heights. Maps cache key -> height
|
||||
self._line_heights_cache: Dict[Hashable, int] = {}
|
||||
|
||||
def __getitem__(self, lineno: int) -> StyleAndTextTuples:
|
||||
"Make it iterable (iterate line by line)."
|
||||
if lineno < self.line_count:
|
||||
return self.get_line(lineno)
|
||||
else:
|
||||
raise IndexError
|
||||
|
||||
def get_height_for_line(
|
||||
self,
|
||||
lineno: int,
|
||||
width: int,
|
||||
get_line_prefix: Optional[GetLinePrefixCallable],
|
||||
slice_stop: Optional[int] = None,
|
||||
) -> int:
|
||||
"""
|
||||
Return the height that a given line would need if it is rendered in a
|
||||
space with the given width (using line wrapping).
|
||||
|
||||
:param get_line_prefix: None or a `Window.get_line_prefix` callable
|
||||
that returns the prefix to be inserted before this line.
|
||||
:param slice_stop: Wrap only "line[:slice_stop]" and return that
|
||||
partial result. This is needed for scrolling the window correctly
|
||||
when line wrapping.
|
||||
:returns: The computed height.
|
||||
"""
|
||||
# Instead of using `get_line_prefix` as key, we use render_counter
|
||||
# instead. This is more reliable, because this function could still be
|
||||
# the same, while the content would change over time.
|
||||
key = get_app().render_counter, lineno, width, slice_stop
|
||||
|
||||
try:
|
||||
return self._line_heights_cache[key]
|
||||
except KeyError:
|
||||
if width == 0:
|
||||
height = 10**8
|
||||
else:
|
||||
# Calculate line width first.
|
||||
line = fragment_list_to_text(self.get_line(lineno))[:slice_stop]
|
||||
text_width = get_cwidth(line)
|
||||
|
||||
if get_line_prefix:
|
||||
# Add prefix width.
|
||||
text_width += fragment_list_width(
|
||||
to_formatted_text(get_line_prefix(lineno, 0))
|
||||
)
|
||||
|
||||
# Slower path: compute path when there's a line prefix.
|
||||
height = 1
|
||||
|
||||
# Keep wrapping as long as the line doesn't fit.
|
||||
# Keep adding new prefixes for every wrapped line.
|
||||
while text_width > width:
|
||||
height += 1
|
||||
text_width -= width
|
||||
|
||||
fragments2 = to_formatted_text(
|
||||
get_line_prefix(lineno, height - 1)
|
||||
)
|
||||
prefix_width = get_cwidth(fragment_list_to_text(fragments2))
|
||||
|
||||
if prefix_width >= width: # Prefix doesn't fit.
|
||||
height = 10**8
|
||||
break
|
||||
|
||||
text_width += prefix_width
|
||||
else:
|
||||
# Fast path: compute height when there's no line prefix.
|
||||
try:
|
||||
quotient, remainder = divmod(text_width, width)
|
||||
except ZeroDivisionError:
|
||||
height = 10**8
|
||||
else:
|
||||
if remainder:
|
||||
quotient += 1 # Like math.ceil.
|
||||
height = max(1, quotient)
|
||||
|
||||
# Cache and return
|
||||
self._line_heights_cache[key] = height
|
||||
return height
|
||||
|
||||
|
||||
class FormattedTextControl(UIControl):
|
||||
"""
|
||||
Control that displays formatted text. This can be either plain text, an
|
||||
:class:`~prompt_toolkit.formatted_text.HTML` object an
|
||||
:class:`~prompt_toolkit.formatted_text.ANSI` object, a list of ``(style_str,
|
||||
text)`` tuples or a callable that takes no argument and returns one of
|
||||
those, depending on how you prefer to do the formatting. See
|
||||
``prompt_toolkit.layout.formatted_text`` for more information.
|
||||
|
||||
(It's mostly optimized for rather small widgets, like toolbars, menus, etc...)
|
||||
|
||||
When this UI control has the focus, the cursor will be shown in the upper
|
||||
left corner of this control by default. There are two ways for specifying
|
||||
the cursor position:
|
||||
|
||||
- Pass a `get_cursor_position` function which returns a `Point` instance
|
||||
with the current cursor position.
|
||||
|
||||
- If the (formatted) text is passed as a list of ``(style, text)`` tuples
|
||||
and there is one that looks like ``('[SetCursorPosition]', '')``, then
|
||||
this will specify the cursor position.
|
||||
|
||||
Mouse support:
|
||||
|
||||
The list of fragments can also contain tuples of three items, looking like:
|
||||
(style_str, text, handler). When mouse support is enabled and the user
|
||||
clicks on this fragment, then the given handler is called. That handler
|
||||
should accept two inputs: (Application, MouseEvent) and it should
|
||||
either handle the event or return `NotImplemented` in case we want the
|
||||
containing Window to handle this event.
|
||||
|
||||
:param focusable: `bool` or :class:`.Filter`: Tell whether this control is
|
||||
focusable.
|
||||
|
||||
:param text: Text or formatted text to be displayed.
|
||||
:param style: Style string applied to the content. (If you want to style
|
||||
the whole :class:`~prompt_toolkit.layout.Window`, pass the style to the
|
||||
:class:`~prompt_toolkit.layout.Window` instead.)
|
||||
:param key_bindings: a :class:`.KeyBindings` object.
|
||||
:param get_cursor_position: A callable that returns the cursor position as
|
||||
a `Point` instance.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
text: AnyFormattedText = "",
|
||||
style: str = "",
|
||||
focusable: FilterOrBool = False,
|
||||
key_bindings: 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 reset(self) -> None:
|
||||
self._fragments = None
|
||||
|
||||
def is_focusable(self) -> bool:
|
||||
return self.focusable()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.text!r})"
|
||||
|
||||
def _get_formatted_text_cached(self) -> StyleAndTextTuples:
|
||||
"""
|
||||
Get fragments, but only retrieve fragments once during one render run.
|
||||
(This function is called several times during one rendering, because
|
||||
we also need those for calculating the dimensions.)
|
||||
"""
|
||||
return self._fragment_cache.get(
|
||||
get_app().render_counter, lambda: to_formatted_text(self.text, self.style)
|
||||
)
|
||||
|
||||
def preferred_width(self, max_available_width: int) -> int:
|
||||
"""
|
||||
Return the preferred width for this control.
|
||||
That is the width of the longest line.
|
||||
"""
|
||||
text = fragment_list_to_text(self._get_formatted_text_cached())
|
||||
line_lengths = [get_cwidth(l) for l in text.split("\n")]
|
||||
return max(line_lengths)
|
||||
|
||||
def preferred_height(
|
||||
self,
|
||||
width: int,
|
||||
max_available_height: int,
|
||||
wrap_lines: bool,
|
||||
get_line_prefix: Optional[GetLinePrefixCallable],
|
||||
) -> Optional[int]:
|
||||
"""
|
||||
Return the preferred height for this control.
|
||||
"""
|
||||
content = self.create_content(width, None)
|
||||
if wrap_lines:
|
||||
height = 0
|
||||
for i in range(content.line_count):
|
||||
height += content.get_height_for_line(i, width, get_line_prefix)
|
||||
if height >= max_available_height:
|
||||
return max_available_height
|
||||
return height
|
||||
else:
|
||||
return content.line_count
|
||||
|
||||
def create_content(self, width: int, height: Optional[int]) -> UIContent:
|
||||
# Get fragments
|
||||
fragments_with_mouse_handlers = self._get_formatted_text_cached()
|
||||
fragment_lines_with_mouse_handlers = list(
|
||||
split_lines(fragments_with_mouse_handlers)
|
||||
)
|
||||
|
||||
# Strip mouse handlers from fragments.
|
||||
fragment_lines: List[StyleAndTextTuples] = [
|
||||
[(item[0], item[1]) for item in line]
|
||||
for line in fragment_lines_with_mouse_handlers
|
||||
]
|
||||
|
||||
# Keep track of the fragments with mouse handler, for later use in
|
||||
# `mouse_handler`.
|
||||
self._fragments = fragments_with_mouse_handlers
|
||||
|
||||
# If there is a `[SetCursorPosition]` in the fragment list, set the
|
||||
# cursor position here.
|
||||
def get_cursor_position(
|
||||
fragment: str = "[SetCursorPosition]",
|
||||
) -> Optional[Point]:
|
||||
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() -> Optional[Point]:
|
||||
return get_cursor_position("[SetMenuPosition]")
|
||||
|
||||
cursor_position = (self.get_cursor_position or get_cursor_position)()
|
||||
|
||||
# Create content, or take it from the cache.
|
||||
key = (tuple(fragments_with_mouse_handlers), width, cursor_position)
|
||||
|
||||
def get_content() -> UIContent:
|
||||
return UIContent(
|
||||
get_line=lambda i: fragment_lines[i],
|
||||
line_count=len(fragment_lines),
|
||||
show_cursor=self.show_cursor,
|
||||
cursor_position=cursor_position,
|
||||
menu_position=get_menu_position(),
|
||||
)
|
||||
|
||||
return self._content_cache.get(key, get_content)
|
||||
|
||||
def mouse_handler(self, mouse_event: MouseEvent) -> "NotImplementedOrNone":
|
||||
"""
|
||||
Handle mouse events.
|
||||
|
||||
(When the fragment list contained mouse handlers and the user clicked on
|
||||
on any of these, the matching handler is called. This handler can still
|
||||
return `NotImplemented` in case we want the
|
||||
:class:`~prompt_toolkit.layout.Window` to handle this particular
|
||||
event.)
|
||||
"""
|
||||
if self._fragments:
|
||||
# Read the generator.
|
||||
fragments_for_line = list(split_lines(self._fragments))
|
||||
|
||||
try:
|
||||
fragments = fragments_for_line[mouse_event.position.y]
|
||||
except IndexError:
|
||||
return NotImplemented
|
||||
else:
|
||||
# Find position in the fragment list.
|
||||
xpos = mouse_event.position.x
|
||||
|
||||
# Find mouse handler for this character.
|
||||
count = 0
|
||||
for item in fragments:
|
||||
count += len(item[1])
|
||||
if count > xpos:
|
||||
if len(item) >= 3:
|
||||
# Handler found. Call it.
|
||||
# (Handler can return NotImplemented, so return
|
||||
# that result.)
|
||||
handler = item[2] # type: ignore
|
||||
return handler(mouse_event)
|
||||
else:
|
||||
break
|
||||
|
||||
# Otherwise, don't handle here.
|
||||
return NotImplemented
|
||||
|
||||
def is_modal(self) -> bool:
|
||||
return self.modal
|
||||
|
||||
def get_key_bindings(self) -> Optional["KeyBindingsBase"]:
|
||||
return self.key_bindings
|
||||
|
||||
|
||||
class DummyControl(UIControl):
|
||||
"""
|
||||
A dummy control object that doesn't paint any content.
|
||||
|
||||
Useful for filling a :class:`~prompt_toolkit.layout.Window`. (The
|
||||
`fragment` and `char` attributes of the `Window` class can be used to
|
||||
define the filling.)
|
||||
"""
|
||||
|
||||
def create_content(self, width: int, height: int) -> UIContent:
|
||||
def get_line(i: int) -> StyleAndTextTuples:
|
||||
return []
|
||||
|
||||
return UIContent(
|
||||
get_line=get_line, line_count=100**100
|
||||
) # Something very big.
|
||||
|
||||
def is_focusable(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class _ProcessedLine(NamedTuple):
|
||||
fragments: StyleAndTextTuples
|
||||
source_to_display: Callable[[int], int]
|
||||
display_to_source: Callable[[int], int]
|
||||
|
||||
|
||||
class BufferControl(UIControl):
|
||||
"""
|
||||
Control for 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: 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[[], Optional[int]]] = 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 __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__} buffer={self.buffer!r} at {id(self)!r}>"
|
||||
|
||||
@property
|
||||
def search_buffer_control(self) -> Optional["SearchBufferControl"]:
|
||||
result: Optional[SearchBufferControl]
|
||||
|
||||
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) -> Optional[Buffer]:
|
||||
control = self.search_buffer_control
|
||||
if control is not None:
|
||||
return control.buffer
|
||||
return None
|
||||
|
||||
@property
|
||||
def search_state(self) -> SearchState:
|
||||
"""
|
||||
Return the `SearchState` for searching this `BufferControl`. This is
|
||||
always associated with the search control. If one search bar is used
|
||||
for searching multiple `BufferControls`, then they share the same
|
||||
`SearchState`.
|
||||
"""
|
||||
search_buffer_control = self.search_buffer_control
|
||||
if search_buffer_control:
|
||||
return search_buffer_control.searcher_search_state
|
||||
else:
|
||||
return SearchState()
|
||||
|
||||
def is_focusable(self) -> bool:
|
||||
return self.focusable()
|
||||
|
||||
def preferred_width(self, max_available_width: int) -> Optional[int]:
|
||||
"""
|
||||
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: int,
|
||||
max_available_height: int,
|
||||
wrap_lines: bool,
|
||||
get_line_prefix: Optional[GetLinePrefixCallable],
|
||||
) -> Optional[int]:
|
||||
|
||||
# Calculate the content height, if it was drawn on a screen with the
|
||||
# given width.
|
||||
height = 0
|
||||
content = self.create_content(width, height=1) # Pass a dummy '1' as height.
|
||||
|
||||
# When line wrapping is off, the height should be equal to the amount
|
||||
# of lines.
|
||||
if not wrap_lines:
|
||||
return content.line_count
|
||||
|
||||
# When the number of lines exceeds the max_available_height, just
|
||||
# return max_available_height. No need to calculate anything.
|
||||
if content.line_count >= max_available_height:
|
||||
return max_available_height
|
||||
|
||||
for i in range(content.line_count):
|
||||
height += content.get_height_for_line(i, width, get_line_prefix)
|
||||
|
||||
if height >= max_available_height:
|
||||
return max_available_height
|
||||
|
||||
return height
|
||||
|
||||
def _get_formatted_text_for_line_func(
|
||||
self, document: Document
|
||||
) -> Callable[[int], StyleAndTextTuples]:
|
||||
"""
|
||||
Create a function that returns the fragments for a given line.
|
||||
"""
|
||||
# Cache using `document.text`.
|
||||
def get_formatted_text_for_line() -> Callable[[int], StyleAndTextTuples]:
|
||||
return self.lexer.lex_document(document)
|
||||
|
||||
key = (document.text, self.lexer.invalidation_hash())
|
||||
return self._fragment_cache.get(key, get_formatted_text_for_line)
|
||||
|
||||
def _create_get_processed_line_func(
|
||||
self, document: Document, width: int, height: int
|
||||
) -> Callable[[int], _ProcessedLine]:
|
||||
"""
|
||||
Create a function that takes a line number of the current document and
|
||||
returns a _ProcessedLine(processed_fragments, source_to_display, display_to_source)
|
||||
tuple.
|
||||
"""
|
||||
# Merge all input processors together.
|
||||
input_processors = self.input_processors or []
|
||||
if self.include_default_input_processors:
|
||||
input_processors = self.default_input_processors + input_processors
|
||||
|
||||
merged_processor = merge_processors(input_processors)
|
||||
|
||||
def transform(lineno: int, fragments: StyleAndTextTuples) -> _ProcessedLine:
|
||||
"Transform the fragments for a given line number."
|
||||
# Get cursor position at this line.
|
||||
def source_to_display(i: int) -> int:
|
||||
"""X position from the buffer to the x position in the
|
||||
processed fragment list. By default, we start from the 'identity'
|
||||
operation."""
|
||||
return i
|
||||
|
||||
transformation = merged_processor.apply_transformation(
|
||||
TransformationInput(
|
||||
self, document, lineno, source_to_display, fragments, width, height
|
||||
)
|
||||
)
|
||||
|
||||
return _ProcessedLine(
|
||||
transformation.fragments,
|
||||
transformation.source_to_display,
|
||||
transformation.display_to_source,
|
||||
)
|
||||
|
||||
def create_func() -> Callable[[int], _ProcessedLine]:
|
||||
get_line = self._get_formatted_text_for_line_func(document)
|
||||
cache: Dict[int, _ProcessedLine] = {}
|
||||
|
||||
def get_processed_line(i: int) -> _ProcessedLine:
|
||||
try:
|
||||
return cache[i]
|
||||
except KeyError:
|
||||
processed_line = transform(i, get_line(i))
|
||||
cache[i] = processed_line
|
||||
return processed_line
|
||||
|
||||
return get_processed_line
|
||||
|
||||
return create_func()
|
||||
|
||||
def create_content(
|
||||
self, width: int, height: int, preview_search: bool = False
|
||||
) -> UIContent:
|
||||
"""
|
||||
Create a UIContent.
|
||||
"""
|
||||
buffer = self.buffer
|
||||
|
||||
# Trigger history loading of the buffer. We do this during the
|
||||
# rendering of the UI here, because it needs to happen when an
|
||||
# `Application` with its event loop is running. During the rendering of
|
||||
# the buffer control is the earliest place we can achieve this, where
|
||||
# we're sure the right event loop is active, and don't require user
|
||||
# interaction (like in a key binding).
|
||||
buffer.load_history_if_not_yet_loaded()
|
||||
|
||||
# Get the document to be shown. If we are currently searching (the
|
||||
# search buffer has focus, and the preview_search filter is enabled),
|
||||
# then use the search document, which has possibly a different
|
||||
# text/cursor position.)
|
||||
search_control = self.search_buffer_control
|
||||
preview_now = preview_search or bool(
|
||||
# Only if this feature is enabled.
|
||||
self.preview_search()
|
||||
and
|
||||
# And something was typed in the associated search field.
|
||||
search_control
|
||||
and search_control.buffer.text
|
||||
and
|
||||
# And we are searching in this control. (Many controls can point to
|
||||
# the same search field, like in Pyvim.)
|
||||
get_app().layout.search_target_buffer_control == self
|
||||
)
|
||||
|
||||
if preview_now and search_control is not None:
|
||||
ss = self.search_state
|
||||
|
||||
document = buffer.document_for_search(
|
||||
SearchState(
|
||||
text=search_control.buffer.text,
|
||||
direction=ss.direction,
|
||||
ignore_case=ss.ignore_case,
|
||||
)
|
||||
)
|
||||
else:
|
||||
document = buffer.document
|
||||
|
||||
get_processed_line = self._create_get_processed_line_func(
|
||||
document, width, height
|
||||
)
|
||||
self._last_get_processed_line = get_processed_line
|
||||
|
||||
def translate_rowcol(row: int, col: int) -> Point:
|
||||
"Return the content column for this coordinate."
|
||||
return Point(x=get_processed_line(row).source_to_display(col), y=row)
|
||||
|
||||
def get_line(i: int) -> StyleAndTextTuples:
|
||||
"Return the fragments for a given line number."
|
||||
fragments = get_processed_line(i).fragments
|
||||
|
||||
# Add a space at the end, because that is a possible cursor
|
||||
# position. (When inserting after the input.) We should do this on
|
||||
# all the lines, not just the line containing the cursor. (Because
|
||||
# otherwise, line wrapping/scrolling could change when moving the
|
||||
# cursor around.)
|
||||
fragments = fragments + [("", " ")]
|
||||
return fragments
|
||||
|
||||
content = UIContent(
|
||||
get_line=get_line,
|
||||
line_count=document.line_count,
|
||||
cursor_position=translate_rowcol(
|
||||
document.cursor_position_row, document.cursor_position_col
|
||||
),
|
||||
)
|
||||
|
||||
# If there is an auto completion going on, use that start point for a
|
||||
# pop-up menu position. (But only when this buffer has the focus --
|
||||
# there is only one place for a menu, determined by the focused buffer.)
|
||||
if get_app().layout.current_control == self:
|
||||
menu_position = self.menu_position() if self.menu_position else None
|
||||
if menu_position is not None:
|
||||
assert isinstance(menu_position, int)
|
||||
menu_row, menu_col = buffer.document.translate_index_to_position(
|
||||
menu_position
|
||||
)
|
||||
content.menu_position = translate_rowcol(menu_row, menu_col)
|
||||
elif buffer.complete_state:
|
||||
# Position for completion menu.
|
||||
# Note: We use 'min', because the original cursor position could be
|
||||
# behind the input string when the actual completion is for
|
||||
# some reason shorter than the text we had before. (A completion
|
||||
# can change and shorten the input.)
|
||||
menu_row, menu_col = buffer.document.translate_index_to_position(
|
||||
min(
|
||||
buffer.cursor_position,
|
||||
buffer.complete_state.original_document.cursor_position,
|
||||
)
|
||||
)
|
||||
content.menu_position = translate_rowcol(menu_row, menu_col)
|
||||
else:
|
||||
content.menu_position = None
|
||||
|
||||
return content
|
||||
|
||||
def mouse_handler(self, mouse_event: MouseEvent) -> "NotImplementedOrNone":
|
||||
"""
|
||||
Mouse handler for this control.
|
||||
"""
|
||||
buffer = self.buffer
|
||||
position = mouse_event.position
|
||||
|
||||
# Focus buffer when clicked.
|
||||
if get_app().layout.current_control == self:
|
||||
if self._last_get_processed_line:
|
||||
processed_line = self._last_get_processed_line(position.y)
|
||||
|
||||
# Translate coordinates back to the cursor position of the
|
||||
# original input.
|
||||
xpos = processed_line.display_to_source(position.x)
|
||||
index = buffer.document.translate_row_col_to_index(position.y, xpos)
|
||||
|
||||
# Set the cursor position.
|
||||
if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
|
||||
buffer.exit_selection()
|
||||
buffer.cursor_position = index
|
||||
|
||||
elif (
|
||||
mouse_event.event_type == MouseEventType.MOUSE_MOVE
|
||||
and mouse_event.button != MouseButton.NONE
|
||||
):
|
||||
# Click and drag to highlight a selection
|
||||
if (
|
||||
buffer.selection_state is None
|
||||
and abs(buffer.cursor_position - index) > 0
|
||||
):
|
||||
buffer.start_selection(selection_type=SelectionType.CHARACTERS)
|
||||
buffer.cursor_position = index
|
||||
|
||||
elif mouse_event.event_type == MouseEventType.MOUSE_UP:
|
||||
# When the cursor was moved to another place, select the text.
|
||||
# (The >1 is actually a small but acceptable workaround for
|
||||
# selecting text in Vi navigation mode. In navigation mode,
|
||||
# the cursor can never be after the text, so the cursor
|
||||
# will be repositioned automatically.)
|
||||
if abs(buffer.cursor_position - index) > 1:
|
||||
if buffer.selection_state is None:
|
||||
buffer.start_selection(
|
||||
selection_type=SelectionType.CHARACTERS
|
||||
)
|
||||
buffer.cursor_position = index
|
||||
|
||||
# Select word around cursor on double click.
|
||||
# Two MOUSE_UP events in a short timespan are considered a double click.
|
||||
double_click = (
|
||||
self._last_click_timestamp
|
||||
and time.time() - self._last_click_timestamp < 0.3
|
||||
)
|
||||
self._last_click_timestamp = time.time()
|
||||
|
||||
if double_click:
|
||||
start, end = buffer.document.find_boundaries_of_current_word()
|
||||
buffer.cursor_position += start
|
||||
buffer.start_selection(selection_type=SelectionType.CHARACTERS)
|
||||
buffer.cursor_position += end - start
|
||||
else:
|
||||
# Don't handle scroll events here.
|
||||
return NotImplemented
|
||||
|
||||
# Not focused, but focusing on click events.
|
||||
else:
|
||||
if (
|
||||
self.focus_on_click()
|
||||
and mouse_event.event_type == MouseEventType.MOUSE_UP
|
||||
):
|
||||
# Focus happens on mouseup. (If we did this on mousedown, the
|
||||
# up event will be received at the point where this widget is
|
||||
# focused and be handled anyway.)
|
||||
get_app().layout.current_control = self
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
return None
|
||||
|
||||
def move_cursor_down(self) -> None:
|
||||
b = self.buffer
|
||||
b.cursor_position += b.document.get_cursor_down_position()
|
||||
|
||||
def move_cursor_up(self) -> None:
|
||||
b = self.buffer
|
||||
b.cursor_position += b.document.get_cursor_up_position()
|
||||
|
||||
def get_key_bindings(self) -> Optional["KeyBindingsBase"]:
|
||||
"""
|
||||
When additional key bindings are given. Return these.
|
||||
"""
|
||||
return self.key_bindings
|
||||
|
||||
def get_invalidate_events(self) -> Iterable["Event[object]"]:
|
||||
"""
|
||||
Return the Window invalidate events.
|
||||
"""
|
||||
# Whenever the buffer changes, the UI has to be updated.
|
||||
yield self.buffer.on_text_changed
|
||||
yield self.buffer.on_cursor_position_changed
|
||||
|
||||
yield self.buffer.on_completions_changed
|
||||
yield self.buffer.on_suggestion_set
|
||||
|
||||
|
||||
class SearchBufferControl(BufferControl):
|
||||
"""
|
||||
:class:`.BufferControl` which is used for searching another
|
||||
:class:`.BufferControl`.
|
||||
|
||||
:param ignore_case: Search case insensitive.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
buffer: Optional[Buffer] = None,
|
||||
input_processors: Optional[List[Processor]] = None,
|
||||
lexer: Optional[Lexer] = None,
|
||||
focus_on_click: FilterOrBool = False,
|
||||
key_bindings: Optional["KeyBindingsBase"] = None,
|
||||
ignore_case: FilterOrBool = False,
|
||||
):
|
||||
|
||||
super().__init__(
|
||||
buffer=buffer,
|
||||
input_processors=input_processors,
|
||||
lexer=lexer,
|
||||
focus_on_click=focus_on_click,
|
||||
key_bindings=key_bindings,
|
||||
)
|
||||
|
||||
# If this BufferControl is used as a search field for one or more other
|
||||
# BufferControls, then represents the search state.
|
||||
self.searcher_search_state = SearchState(ignore_case=ignore_case)
|
217
.venv/Lib/site-packages/prompt_toolkit/layout/dimension.py
Normal file
217
.venv/Lib/site-packages/prompt_toolkit/layout/dimension.py
Normal file
@ -0,0 +1,217 @@
|
||||
"""
|
||||
Layout dimensions are used to give the minimum, maximum and preferred
|
||||
dimensions for containers and controls.
|
||||
"""
|
||||
from typing import TYPE_CHECKING, Any, Callable, List, Optional, Union
|
||||
|
||||
__all__ = [
|
||||
"Dimension",
|
||||
"D",
|
||||
"sum_layout_dimensions",
|
||||
"max_layout_dimensions",
|
||||
"AnyDimension",
|
||||
"to_dimension",
|
||||
"is_dimension",
|
||||
]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import TypeGuard
|
||||
|
||||
|
||||
class Dimension:
|
||||
"""
|
||||
Specified dimension (width/height) of a user control or window.
|
||||
|
||||
The layout engine tries to honor the preferred size. If that is not
|
||||
possible, because the terminal is larger or smaller, it tries to keep in
|
||||
between min and max.
|
||||
|
||||
:param min: Minimum size.
|
||||
:param max: Maximum size.
|
||||
:param weight: For a VSplit/HSplit, the actual size will be determined
|
||||
by taking the proportion of weights from all the children.
|
||||
E.g. When there are two children, one with a weight of 1,
|
||||
and the other with a weight of 2, the second will always be
|
||||
twice as big as the first, if the min/max values allow it.
|
||||
:param preferred: Preferred size.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
min: Optional[int] = None,
|
||||
max: Optional[int] = None,
|
||||
weight: Optional[int] = None,
|
||||
preferred: Optional[int] = None,
|
||||
) -> None:
|
||||
if weight is not None:
|
||||
assert weight >= 0 # Also cannot be a float.
|
||||
|
||||
assert min is None or min >= 0
|
||||
assert max is None or max >= 0
|
||||
assert preferred is None or preferred >= 0
|
||||
|
||||
self.min_specified = min is not None
|
||||
self.max_specified = max is not None
|
||||
self.preferred_specified = preferred is not None
|
||||
self.weight_specified = weight is not None
|
||||
|
||||
if min is None:
|
||||
min = 0 # Smallest possible value.
|
||||
if max is None: # 0-values are allowed, so use "is None"
|
||||
max = 1000**10 # Something huge.
|
||||
if preferred is None:
|
||||
preferred = min
|
||||
if weight is None:
|
||||
weight = 1
|
||||
|
||||
self.min = min
|
||||
self.max = max
|
||||
self.preferred = preferred
|
||||
self.weight = weight
|
||||
|
||||
# Don't allow situations where max < min. (This would be a bug.)
|
||||
if max < min:
|
||||
raise ValueError("Invalid Dimension: max < min.")
|
||||
|
||||
# Make sure that the 'preferred' size is always in the min..max range.
|
||||
if self.preferred < self.min:
|
||||
self.preferred = self.min
|
||||
|
||||
if self.preferred > self.max:
|
||||
self.preferred = self.max
|
||||
|
||||
@classmethod
|
||||
def exact(cls, amount: int) -> "Dimension":
|
||||
"""
|
||||
Return a :class:`.Dimension` with an exact size. (min, max and
|
||||
preferred set to ``amount``).
|
||||
"""
|
||||
return cls(min=amount, max=amount, preferred=amount)
|
||||
|
||||
@classmethod
|
||||
def zero(cls) -> "Dimension":
|
||||
"""
|
||||
Create a dimension that represents a zero size. (Used for 'invisible'
|
||||
controls.)
|
||||
"""
|
||||
return cls.exact(amount=0)
|
||||
|
||||
def is_zero(self) -> bool:
|
||||
"True if this `Dimension` represents a zero size."
|
||||
return self.preferred == 0 or self.max == 0
|
||||
|
||||
def __repr__(self) -> str:
|
||||
fields = []
|
||||
if self.min_specified:
|
||||
fields.append("min=%r" % self.min)
|
||||
if self.max_specified:
|
||||
fields.append("max=%r" % self.max)
|
||||
if self.preferred_specified:
|
||||
fields.append("preferred=%r" % self.preferred)
|
||||
if self.weight_specified:
|
||||
fields.append("weight=%r" % self.weight)
|
||||
|
||||
return "Dimension(%s)" % ", ".join(fields)
|
||||
|
||||
|
||||
def sum_layout_dimensions(dimensions: List[Dimension]) -> Dimension:
|
||||
"""
|
||||
Sum a list of :class:`.Dimension` instances.
|
||||
"""
|
||||
min = sum(d.min for d in dimensions)
|
||||
max = sum(d.max for d in dimensions)
|
||||
preferred = sum(d.preferred for d in dimensions)
|
||||
|
||||
return Dimension(min=min, max=max, preferred=preferred)
|
||||
|
||||
|
||||
def max_layout_dimensions(dimensions: List[Dimension]) -> Dimension:
|
||||
"""
|
||||
Take the maximum of a list of :class:`.Dimension` instances.
|
||||
Used when we have a HSplit/VSplit, and we want to get the best width/height.)
|
||||
"""
|
||||
if not len(dimensions):
|
||||
return Dimension.zero()
|
||||
|
||||
# If all dimensions are size zero. Return zero.
|
||||
# (This is important for HSplit/VSplit, to report the right values to their
|
||||
# parent when all children are invisible.)
|
||||
if all(d.is_zero() for d in dimensions):
|
||||
return dimensions[0]
|
||||
|
||||
# Ignore empty dimensions. (They should not reduce the size of others.)
|
||||
dimensions = [d for d in dimensions if not d.is_zero()]
|
||||
|
||||
if dimensions:
|
||||
# Take the highest minimum dimension.
|
||||
min_ = max(d.min for d in dimensions)
|
||||
|
||||
# For the maximum, we would prefer not to go larger than then smallest
|
||||
# 'max' value, unless other dimensions have a bigger preferred value.
|
||||
# This seems to work best:
|
||||
# - We don't want that a widget with a small height in a VSplit would
|
||||
# shrink other widgets in the split.
|
||||
# If it doesn't work well enough, then it's up to the UI designer to
|
||||
# explicitly pass dimensions.
|
||||
max_ = min(d.max for d in dimensions)
|
||||
max_ = max(max_, max(d.preferred for d in dimensions))
|
||||
|
||||
# Make sure that min>=max. In some scenarios, when certain min..max
|
||||
# ranges don't have any overlap, we can end up in such an impossible
|
||||
# situation. In that case, give priority to the max value.
|
||||
# E.g. taking (1..5) and (8..9) would return (8..5). Instead take (8..8).
|
||||
if min_ > max_:
|
||||
max_ = min_
|
||||
|
||||
preferred = max(d.preferred for d in dimensions)
|
||||
|
||||
return Dimension(min=min_, max=max_, preferred=preferred)
|
||||
else:
|
||||
return Dimension()
|
||||
|
||||
|
||||
# Anything that can be converted to a dimension.
|
||||
AnyDimension = Union[
|
||||
None, # None is a valid dimension that will fit anything.
|
||||
int,
|
||||
Dimension,
|
||||
# Callable[[], 'AnyDimension'] # Recursive definition not supported by mypy.
|
||||
Callable[[], Any],
|
||||
]
|
||||
|
||||
|
||||
def to_dimension(value: AnyDimension) -> Dimension:
|
||||
"""
|
||||
Turn the given object into a `Dimension` object.
|
||||
"""
|
||||
if value is None:
|
||||
return Dimension()
|
||||
if isinstance(value, int):
|
||||
return Dimension.exact(value)
|
||||
if isinstance(value, Dimension):
|
||||
return value
|
||||
if callable(value):
|
||||
return to_dimension(value())
|
||||
|
||||
raise ValueError("Not an integer or Dimension object.")
|
||||
|
||||
|
||||
def is_dimension(value: object) -> "TypeGuard[AnyDimension]":
|
||||
"""
|
||||
Test whether the given value could be a valid dimension.
|
||||
(For usage in an assertion. It's not guaranteed in case of a callable.)
|
||||
"""
|
||||
if value is None:
|
||||
return True
|
||||
if callable(value):
|
||||
return True # Assume it's a callable that doesn't take arguments.
|
||||
if isinstance(value, (int, Dimension)):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# Common alias.
|
||||
D = Dimension
|
||||
|
||||
# For backward-compatibility.
|
||||
LayoutDimension = Dimension
|
37
.venv/Lib/site-packages/prompt_toolkit/layout/dummy.py
Normal file
37
.venv/Lib/site-packages/prompt_toolkit/layout/dummy.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""
|
||||
Dummy layout. Used when somebody creates an `Application` without specifying a
|
||||
`Layout`.
|
||||
"""
|
||||
from prompt_toolkit.formatted_text import HTML
|
||||
from prompt_toolkit.key_binding import KeyBindings
|
||||
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
|
||||
|
||||
from .containers import Window
|
||||
from .controls import FormattedTextControl
|
||||
from .dimension import D
|
||||
from .layout import Layout
|
||||
|
||||
__all__ = [
|
||||
"create_dummy_layout",
|
||||
]
|
||||
|
||||
E = KeyPressEvent
|
||||
|
||||
|
||||
def create_dummy_layout() -> Layout:
|
||||
"""
|
||||
Create a dummy layout for use in an 'Application' that doesn't have a
|
||||
layout specified. When ENTER is pressed, the application quits.
|
||||
"""
|
||||
kb = KeyBindings()
|
||||
|
||||
@kb.add("enter")
|
||||
def enter(event: E) -> None:
|
||||
event.app.exit()
|
||||
|
||||
control = FormattedTextControl(
|
||||
HTML("No layout specified. Press <reverse>ENTER</reverse> to quit."),
|
||||
key_bindings=kb,
|
||||
)
|
||||
window = Window(content=control, height=D(min=1))
|
||||
return Layout(container=window, focused_element=window)
|
411
.venv/Lib/site-packages/prompt_toolkit/layout/layout.py
Normal file
411
.venv/Lib/site-packages/prompt_toolkit/layout/layout.py
Normal file
@ -0,0 +1,411 @@
|
||||
"""
|
||||
Wrapper for the layout.
|
||||
"""
|
||||
from typing import Dict, Generator, Iterable, List, Optional, Union
|
||||
|
||||
from prompt_toolkit.buffer import Buffer
|
||||
|
||||
from .containers import (
|
||||
AnyContainer,
|
||||
ConditionalContainer,
|
||||
Container,
|
||||
Window,
|
||||
to_container,
|
||||
)
|
||||
from .controls import BufferControl, SearchBufferControl, UIControl
|
||||
|
||||
__all__ = [
|
||||
"Layout",
|
||||
"InvalidLayoutError",
|
||||
"walk",
|
||||
]
|
||||
|
||||
FocusableElement = Union[str, Buffer, UIControl, AnyContainer]
|
||||
|
||||
|
||||
class Layout:
|
||||
"""
|
||||
The layout for a prompt_toolkit
|
||||
:class:`~prompt_toolkit.application.Application`.
|
||||
This also keeps track of which user control is focused.
|
||||
|
||||
:param container: The "root" container for the layout.
|
||||
:param focused_element: element to be focused initially. (Can be anything
|
||||
the `focus` function accepts.)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
container: AnyContainer,
|
||||
focused_element: Optional[FocusableElement] = None,
|
||||
) -> None:
|
||||
|
||||
self.container = to_container(container)
|
||||
self._stack: List[Window] = []
|
||||
|
||||
# Map search BufferControl back to the original BufferControl.
|
||||
# This is used to keep track of when exactly we are searching, and for
|
||||
# applying the search.
|
||||
# When a link exists in this dictionary, that means the search is
|
||||
# currently active.
|
||||
# Map: search_buffer_control -> original buffer control.
|
||||
self.search_links: Dict[SearchBufferControl, BufferControl] = {}
|
||||
|
||||
# Mapping that maps the children in the layout to their parent.
|
||||
# This relationship is calculated dynamically, each time when the UI
|
||||
# is rendered. (UI elements have only references to their children.)
|
||||
self._child_to_parent: Dict[Container, Container] = {}
|
||||
|
||||
if focused_element is None:
|
||||
try:
|
||||
self._stack.append(next(self.find_all_windows()))
|
||||
except StopIteration as e:
|
||||
raise InvalidLayoutError(
|
||||
"Invalid layout. The layout does not contain any Window object."
|
||||
) from e
|
||||
else:
|
||||
self.focus(focused_element)
|
||||
|
||||
# List of visible windows.
|
||||
self.visible_windows: List[Window] = [] # List of `Window` objects.
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Layout({self.container!r}, current_window={self.current_window!r})"
|
||||
|
||||
def find_all_windows(self) -> Generator[Window, None, None]:
|
||||
"""
|
||||
Find all the :class:`.UIControl` objects in this layout.
|
||||
"""
|
||||
for item in self.walk():
|
||||
if isinstance(item, Window):
|
||||
yield item
|
||||
|
||||
def find_all_controls(self) -> Iterable[UIControl]:
|
||||
for container in self.find_all_windows():
|
||||
yield container.content
|
||||
|
||||
def focus(self, value: FocusableElement) -> None:
|
||||
"""
|
||||
Focus the given UI element.
|
||||
|
||||
`value` can be either:
|
||||
|
||||
- a :class:`.UIControl`
|
||||
- a :class:`.Buffer` instance or the name of a :class:`.Buffer`
|
||||
- a :class:`.Window`
|
||||
- Any container object. In this case we will focus the :class:`.Window`
|
||||
from this container that was focused most recent, or the very first
|
||||
focusable :class:`.Window` of the container.
|
||||
"""
|
||||
# BufferControl by buffer name.
|
||||
if isinstance(value, str):
|
||||
for control in self.find_all_controls():
|
||||
if isinstance(control, BufferControl) and control.buffer.name == value:
|
||||
self.focus(control)
|
||||
return
|
||||
raise ValueError(f"Couldn't find Buffer in the current layout: {value!r}.")
|
||||
|
||||
# BufferControl by buffer object.
|
||||
elif isinstance(value, Buffer):
|
||||
for control in self.find_all_controls():
|
||||
if isinstance(control, BufferControl) and control.buffer == value:
|
||||
self.focus(control)
|
||||
return
|
||||
raise ValueError(f"Couldn't find Buffer in the current layout: {value!r}.")
|
||||
|
||||
# Focus UIControl.
|
||||
elif isinstance(value, UIControl):
|
||||
if value not in self.find_all_controls():
|
||||
raise ValueError(
|
||||
"Invalid value. Container does not appear in the layout."
|
||||
)
|
||||
if not value.is_focusable():
|
||||
raise ValueError("Invalid value. UIControl is not focusable.")
|
||||
|
||||
self.current_control = value
|
||||
|
||||
# Otherwise, expecting any Container object.
|
||||
else:
|
||||
value = to_container(value)
|
||||
|
||||
if isinstance(value, Window):
|
||||
# This is a `Window`: focus that.
|
||||
if value not in self.find_all_windows():
|
||||
raise ValueError(
|
||||
"Invalid value. Window does not appear in the layout: %r"
|
||||
% (value,)
|
||||
)
|
||||
|
||||
self.current_window = value
|
||||
else:
|
||||
# Focus a window in this container.
|
||||
# If we have many windows as part of this container, and some
|
||||
# of them have been focused before, take the last focused
|
||||
# item. (This is very useful when the UI is composed of more
|
||||
# complex sub components.)
|
||||
windows = []
|
||||
for c in walk(value, skip_hidden=True):
|
||||
if isinstance(c, Window) and c.content.is_focusable():
|
||||
windows.append(c)
|
||||
|
||||
# Take the first one that was focused before.
|
||||
for w in reversed(self._stack):
|
||||
if w in windows:
|
||||
self.current_window = w
|
||||
return
|
||||
|
||||
# None was focused before: take the very first focusable window.
|
||||
if windows:
|
||||
self.current_window = windows[0]
|
||||
return
|
||||
|
||||
raise ValueError(
|
||||
f"Invalid value. Container cannot be focused: {value!r}"
|
||||
)
|
||||
|
||||
def has_focus(self, value: FocusableElement) -> bool:
|
||||
"""
|
||||
Check whether the given control has the focus.
|
||||
:param value: :class:`.UIControl` or :class:`.Window` instance.
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
if self.current_buffer is None:
|
||||
return False
|
||||
return self.current_buffer.name == value
|
||||
if isinstance(value, Buffer):
|
||||
return self.current_buffer == value
|
||||
if isinstance(value, UIControl):
|
||||
return self.current_control == value
|
||||
else:
|
||||
value = to_container(value)
|
||||
if isinstance(value, Window):
|
||||
return self.current_window == value
|
||||
else:
|
||||
# Check whether this "container" is focused. This is true if
|
||||
# one of the elements inside is focused.
|
||||
for element in walk(value):
|
||||
if element == self.current_window:
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def current_control(self) -> UIControl:
|
||||
"""
|
||||
Get the :class:`.UIControl` to currently has the focus.
|
||||
"""
|
||||
return self._stack[-1].content
|
||||
|
||||
@current_control.setter
|
||||
def current_control(self, control: UIControl) -> None:
|
||||
"""
|
||||
Set the :class:`.UIControl` to receive the focus.
|
||||
"""
|
||||
for window in self.find_all_windows():
|
||||
if window.content == control:
|
||||
self.current_window = window
|
||||
return
|
||||
|
||||
raise ValueError("Control not found in the user interface.")
|
||||
|
||||
@property
|
||||
def current_window(self) -> Window:
|
||||
"Return the :class:`.Window` object that is currently focused."
|
||||
return self._stack[-1]
|
||||
|
||||
@current_window.setter
|
||||
def current_window(self, value: Window) -> None:
|
||||
"Set the :class:`.Window` object to be currently focused."
|
||||
self._stack.append(value)
|
||||
|
||||
@property
|
||||
def is_searching(self) -> bool:
|
||||
"True if we are searching right now."
|
||||
return self.current_control in self.search_links
|
||||
|
||||
@property
|
||||
def search_target_buffer_control(self) -> Optional[BufferControl]:
|
||||
"""
|
||||
Return the :class:`.BufferControl` in which we are searching or `None`.
|
||||
"""
|
||||
# Not every `UIControl` is a `BufferControl`. This only applies to
|
||||
# `BufferControl`.
|
||||
control = self.current_control
|
||||
|
||||
if isinstance(control, SearchBufferControl):
|
||||
return self.search_links.get(control)
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_focusable_windows(self) -> Iterable[Window]:
|
||||
"""
|
||||
Return all the :class:`.Window` objects which are focusable (in the
|
||||
'modal' area).
|
||||
"""
|
||||
for w in self.walk_through_modal_area():
|
||||
if isinstance(w, Window) and w.content.is_focusable():
|
||||
yield w
|
||||
|
||||
def get_visible_focusable_windows(self) -> List[Window]:
|
||||
"""
|
||||
Return a list of :class:`.Window` objects that are focusable.
|
||||
"""
|
||||
# focusable windows are windows that are visible, but also part of the
|
||||
# modal container. Make sure to keep the ordering.
|
||||
visible_windows = self.visible_windows
|
||||
return [w for w in self.get_focusable_windows() if w in visible_windows]
|
||||
|
||||
@property
|
||||
def current_buffer(self) -> Optional[Buffer]:
|
||||
"""
|
||||
The currently focused :class:`~.Buffer` or `None`.
|
||||
"""
|
||||
ui_control = self.current_control
|
||||
if isinstance(ui_control, BufferControl):
|
||||
return ui_control.buffer
|
||||
return None
|
||||
|
||||
def get_buffer_by_name(self, buffer_name: str) -> Optional[Buffer]:
|
||||
"""
|
||||
Look in the layout for a buffer with the given name.
|
||||
Return `None` when nothing was found.
|
||||
"""
|
||||
for w in self.walk():
|
||||
if isinstance(w, Window) and isinstance(w.content, BufferControl):
|
||||
if w.content.buffer.name == buffer_name:
|
||||
return w.content.buffer
|
||||
return None
|
||||
|
||||
@property
|
||||
def buffer_has_focus(self) -> bool:
|
||||
"""
|
||||
Return `True` if the currently focused control is a
|
||||
:class:`.BufferControl`. (For instance, used to determine whether the
|
||||
default key bindings should be active or not.)
|
||||
"""
|
||||
ui_control = self.current_control
|
||||
return isinstance(ui_control, BufferControl)
|
||||
|
||||
@property
|
||||
def previous_control(self) -> UIControl:
|
||||
"""
|
||||
Get the :class:`.UIControl` to previously had the focus.
|
||||
"""
|
||||
try:
|
||||
return self._stack[-2].content
|
||||
except IndexError:
|
||||
return self._stack[-1].content
|
||||
|
||||
def focus_last(self) -> None:
|
||||
"""
|
||||
Give the focus to the last focused control.
|
||||
"""
|
||||
if len(self._stack) > 1:
|
||||
self._stack = self._stack[:-1]
|
||||
|
||||
def focus_next(self) -> None:
|
||||
"""
|
||||
Focus the next visible/focusable Window.
|
||||
"""
|
||||
windows = self.get_visible_focusable_windows()
|
||||
|
||||
if len(windows) > 0:
|
||||
try:
|
||||
index = windows.index(self.current_window)
|
||||
except ValueError:
|
||||
index = 0
|
||||
else:
|
||||
index = (index + 1) % len(windows)
|
||||
|
||||
self.focus(windows[index])
|
||||
|
||||
def focus_previous(self) -> None:
|
||||
"""
|
||||
Focus the previous visible/focusable Window.
|
||||
"""
|
||||
windows = self.get_visible_focusable_windows()
|
||||
|
||||
if len(windows) > 0:
|
||||
try:
|
||||
index = windows.index(self.current_window)
|
||||
except ValueError:
|
||||
index = 0
|
||||
else:
|
||||
index = (index - 1) % len(windows)
|
||||
|
||||
self.focus(windows[index])
|
||||
|
||||
def walk(self) -> Iterable[Container]:
|
||||
"""
|
||||
Walk through all the layout nodes (and their children) and yield them.
|
||||
"""
|
||||
yield from walk(self.container)
|
||||
|
||||
def walk_through_modal_area(self) -> Iterable[Container]:
|
||||
"""
|
||||
Walk through all the containers which are in the current 'modal' part
|
||||
of the layout.
|
||||
"""
|
||||
# Go up in the tree, and find the root. (it will be a part of the
|
||||
# layout, if the focus is in a modal part.)
|
||||
root: Container = self.current_window
|
||||
while not root.is_modal() and root in self._child_to_parent:
|
||||
root = self._child_to_parent[root]
|
||||
|
||||
yield from walk(root)
|
||||
|
||||
def update_parents_relations(self) -> None:
|
||||
"""
|
||||
Update child->parent relationships mapping.
|
||||
"""
|
||||
parents = {}
|
||||
|
||||
def walk(e: Container) -> None:
|
||||
for c in e.get_children():
|
||||
parents[c] = e
|
||||
walk(c)
|
||||
|
||||
walk(self.container)
|
||||
|
||||
self._child_to_parent = parents
|
||||
|
||||
def reset(self) -> None:
|
||||
# Remove all search links when the UI starts.
|
||||
# (Important, for instance when control-c is been pressed while
|
||||
# searching. The prompt cancels, but next `run()` call the search
|
||||
# links are still there.)
|
||||
self.search_links.clear()
|
||||
|
||||
self.container.reset()
|
||||
|
||||
def get_parent(self, container: Container) -> Optional[Container]:
|
||||
"""
|
||||
Return the parent container for the given container, or ``None``, if it
|
||||
wasn't found.
|
||||
"""
|
||||
try:
|
||||
return self._child_to_parent[container]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
class InvalidLayoutError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def walk(container: Container, skip_hidden: bool = False) -> Iterable[Container]:
|
||||
"""
|
||||
Walk through layout, starting at this container.
|
||||
"""
|
||||
# When `skip_hidden` is set, don't go into disabled ConditionalContainer containers.
|
||||
if (
|
||||
skip_hidden
|
||||
and isinstance(container, ConditionalContainer)
|
||||
and not container.filter()
|
||||
):
|
||||
return
|
||||
|
||||
yield container
|
||||
|
||||
for c in container.get_children():
|
||||
# yield from walk(c)
|
||||
yield from walk(c, skip_hidden=skip_hidden)
|
305
.venv/Lib/site-packages/prompt_toolkit/layout/margins.py
Normal file
305
.venv/Lib/site-packages/prompt_toolkit/layout/margins.py
Normal file
@ -0,0 +1,305 @@
|
||||
"""
|
||||
Margin implementations for a :class:`~prompt_toolkit.layout.containers.Window`.
|
||||
"""
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from typing import TYPE_CHECKING, Callable, Optional
|
||||
|
||||
from prompt_toolkit.filters import FilterOrBool, to_filter
|
||||
from prompt_toolkit.formatted_text import (
|
||||
StyleAndTextTuples,
|
||||
fragment_list_to_text,
|
||||
to_formatted_text,
|
||||
)
|
||||
from prompt_toolkit.utils import get_cwidth
|
||||
|
||||
from .controls import UIContent
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .containers import WindowRenderInfo
|
||||
|
||||
__all__ = [
|
||||
"Margin",
|
||||
"NumberedMargin",
|
||||
"ScrollbarMargin",
|
||||
"ConditionalMargin",
|
||||
"PromptMargin",
|
||||
]
|
||||
|
||||
|
||||
class Margin(metaclass=ABCMeta):
|
||||
"""
|
||||
Base interface for a margin.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_width(self, get_ui_content: Callable[[], UIContent]) -> int:
|
||||
"""
|
||||
Return the width that this margin is going to consume.
|
||||
|
||||
:param get_ui_content: Callable that asks the user control to create
|
||||
a :class:`.UIContent` instance. This can be used for instance to
|
||||
obtain the number of lines.
|
||||
"""
|
||||
return 0
|
||||
|
||||
@abstractmethod
|
||||
def create_margin(
|
||||
self, window_render_info: "WindowRenderInfo", width: int, height: int
|
||||
) -> StyleAndTextTuples:
|
||||
"""
|
||||
Creates a margin.
|
||||
This should return a list of (style_str, text) tuples.
|
||||
|
||||
:param window_render_info:
|
||||
:class:`~prompt_toolkit.layout.containers.WindowRenderInfo`
|
||||
instance, generated after rendering and copying the visible part of
|
||||
the :class:`~prompt_toolkit.layout.controls.UIControl` into the
|
||||
:class:`~prompt_toolkit.layout.containers.Window`.
|
||||
:param width: The width that's available for this margin. (As reported
|
||||
by :meth:`.get_width`.)
|
||||
:param height: The height that's available for this margin. (The height
|
||||
of the :class:`~prompt_toolkit.layout.containers.Window`.)
|
||||
"""
|
||||
return []
|
||||
|
||||
|
||||
class NumberedMargin(Margin):
|
||||
"""
|
||||
Margin that displays the line numbers.
|
||||
|
||||
:param relative: Number relative to the cursor position. Similar to the Vi
|
||||
'relativenumber' option.
|
||||
:param display_tildes: Display tildes after the end of the document, just
|
||||
like Vi does.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, relative: FilterOrBool = False, display_tildes: FilterOrBool = False
|
||||
) -> None:
|
||||
|
||||
self.relative = to_filter(relative)
|
||||
self.display_tildes = to_filter(display_tildes)
|
||||
|
||||
def get_width(self, get_ui_content: Callable[[], UIContent]) -> int:
|
||||
line_count = get_ui_content().line_count
|
||||
return max(3, len("%s" % line_count) + 1)
|
||||
|
||||
def create_margin(
|
||||
self, window_render_info: "WindowRenderInfo", width: int, height: int
|
||||
) -> StyleAndTextTuples:
|
||||
relative = self.relative()
|
||||
|
||||
style = "class:line-number"
|
||||
style_current = "class:line-number.current"
|
||||
|
||||
# Get current line number.
|
||||
current_lineno = window_render_info.ui_content.cursor_position.y
|
||||
|
||||
# Construct margin.
|
||||
result: StyleAndTextTuples = []
|
||||
last_lineno = None
|
||||
|
||||
for y, lineno in enumerate(window_render_info.displayed_lines):
|
||||
# Only display line number if this line is not a continuation of the previous line.
|
||||
if lineno != last_lineno:
|
||||
if lineno is None:
|
||||
pass
|
||||
elif lineno == current_lineno:
|
||||
# Current line.
|
||||
if relative:
|
||||
# Left align current number in relative mode.
|
||||
result.append((style_current, "%i" % (lineno + 1)))
|
||||
else:
|
||||
result.append(
|
||||
(style_current, ("%i " % (lineno + 1)).rjust(width))
|
||||
)
|
||||
else:
|
||||
# Other lines.
|
||||
if relative:
|
||||
lineno = abs(lineno - current_lineno) - 1
|
||||
|
||||
result.append((style, ("%i " % (lineno + 1)).rjust(width)))
|
||||
|
||||
last_lineno = lineno
|
||||
result.append(("", "\n"))
|
||||
|
||||
# Fill with tildes.
|
||||
if self.display_tildes():
|
||||
while y < window_render_info.window_height:
|
||||
result.append(("class:tilde", "~\n"))
|
||||
y += 1
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class ConditionalMargin(Margin):
|
||||
"""
|
||||
Wrapper around other :class:`.Margin` classes to show/hide them.
|
||||
"""
|
||||
|
||||
def __init__(self, margin: Margin, filter: FilterOrBool) -> None:
|
||||
self.margin = margin
|
||||
self.filter = to_filter(filter)
|
||||
|
||||
def get_width(self, get_ui_content: Callable[[], UIContent]) -> int:
|
||||
if self.filter():
|
||||
return self.margin.get_width(get_ui_content)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def create_margin(
|
||||
self, window_render_info: "WindowRenderInfo", width: int, height: int
|
||||
) -> StyleAndTextTuples:
|
||||
if width and self.filter():
|
||||
return self.margin.create_margin(window_render_info, width, height)
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
class ScrollbarMargin(Margin):
|
||||
"""
|
||||
Margin displaying a scrollbar.
|
||||
|
||||
:param display_arrows: Display scroll up/down arrows.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
display_arrows: FilterOrBool = False,
|
||||
up_arrow_symbol: str = "^",
|
||||
down_arrow_symbol: str = "v",
|
||||
) -> None:
|
||||
|
||||
self.display_arrows = to_filter(display_arrows)
|
||||
self.up_arrow_symbol = up_arrow_symbol
|
||||
self.down_arrow_symbol = down_arrow_symbol
|
||||
|
||||
def get_width(self, get_ui_content: Callable[[], UIContent]) -> int:
|
||||
return 1
|
||||
|
||||
def create_margin(
|
||||
self, window_render_info: "WindowRenderInfo", width: int, height: int
|
||||
) -> StyleAndTextTuples:
|
||||
content_height = window_render_info.content_height
|
||||
window_height = window_render_info.window_height
|
||||
display_arrows = self.display_arrows()
|
||||
|
||||
if display_arrows:
|
||||
window_height -= 2
|
||||
|
||||
try:
|
||||
fraction_visible = len(window_render_info.displayed_lines) / float(
|
||||
content_height
|
||||
)
|
||||
fraction_above = window_render_info.vertical_scroll / float(content_height)
|
||||
|
||||
scrollbar_height = int(
|
||||
min(window_height, max(1, window_height * fraction_visible))
|
||||
)
|
||||
scrollbar_top = int(window_height * fraction_above)
|
||||
except ZeroDivisionError:
|
||||
return []
|
||||
else:
|
||||
|
||||
def is_scroll_button(row: int) -> bool:
|
||||
"True if we should display a button on this row."
|
||||
return scrollbar_top <= row <= scrollbar_top + scrollbar_height
|
||||
|
||||
# Up arrow.
|
||||
result: StyleAndTextTuples = []
|
||||
if display_arrows:
|
||||
result.extend(
|
||||
[
|
||||
("class:scrollbar.arrow", self.up_arrow_symbol),
|
||||
("class:scrollbar", "\n"),
|
||||
]
|
||||
)
|
||||
|
||||
# Scrollbar body.
|
||||
scrollbar_background = "class:scrollbar.background"
|
||||
scrollbar_background_start = "class:scrollbar.background,scrollbar.start"
|
||||
scrollbar_button = "class:scrollbar.button"
|
||||
scrollbar_button_end = "class:scrollbar.button,scrollbar.end"
|
||||
|
||||
for i in range(window_height):
|
||||
if is_scroll_button(i):
|
||||
if not is_scroll_button(i + 1):
|
||||
# Give the last cell a different style, because we
|
||||
# want to underline this.
|
||||
result.append((scrollbar_button_end, " "))
|
||||
else:
|
||||
result.append((scrollbar_button, " "))
|
||||
else:
|
||||
if is_scroll_button(i + 1):
|
||||
result.append((scrollbar_background_start, " "))
|
||||
else:
|
||||
result.append((scrollbar_background, " "))
|
||||
result.append(("", "\n"))
|
||||
|
||||
# Down arrow
|
||||
if display_arrows:
|
||||
result.append(("class:scrollbar.arrow", self.down_arrow_symbol))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class PromptMargin(Margin):
|
||||
"""
|
||||
[Deprecated]
|
||||
|
||||
Create margin that displays a prompt.
|
||||
This can display one prompt at the first line, and a continuation prompt
|
||||
(e.g, just dots) on all the following lines.
|
||||
|
||||
This `PromptMargin` implementation has been largely superseded in favor of
|
||||
the `get_line_prefix` attribute of `Window`. The reason is that a margin is
|
||||
always a fixed width, while `get_line_prefix` can return a variable width
|
||||
prefix in front of every line, making it more powerful, especially for line
|
||||
continuations.
|
||||
|
||||
:param get_prompt: Callable returns formatted text or a list of
|
||||
`(style_str, type)` tuples to be shown as the prompt at the first line.
|
||||
:param get_continuation: Callable that takes three inputs. The width (int),
|
||||
line_number (int), and is_soft_wrap (bool). It should return formatted
|
||||
text or a list of `(style_str, type)` tuples for the next lines of the
|
||||
input.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
get_prompt: Callable[[], StyleAndTextTuples],
|
||||
get_continuation: Optional[
|
||||
Callable[[int, int, bool], StyleAndTextTuples]
|
||||
] = None,
|
||||
) -> None:
|
||||
|
||||
self.get_prompt = get_prompt
|
||||
self.get_continuation = get_continuation
|
||||
|
||||
def get_width(self, get_ui_content: Callable[[], UIContent]) -> int:
|
||||
"Width to report to the `Window`."
|
||||
# Take the width from the first line.
|
||||
text = fragment_list_to_text(self.get_prompt())
|
||||
return get_cwidth(text)
|
||||
|
||||
def create_margin(
|
||||
self, window_render_info: "WindowRenderInfo", width: int, height: int
|
||||
) -> StyleAndTextTuples:
|
||||
get_continuation = self.get_continuation
|
||||
result: StyleAndTextTuples = []
|
||||
|
||||
# First line.
|
||||
result.extend(to_formatted_text(self.get_prompt()))
|
||||
|
||||
# Next lines.
|
||||
if get_continuation:
|
||||
last_y = None
|
||||
|
||||
for y in window_render_info.displayed_lines[1:]:
|
||||
result.append(("", "\n"))
|
||||
result.extend(
|
||||
to_formatted_text(get_continuation(width, y, y == last_y))
|
||||
)
|
||||
last_y = y
|
||||
|
||||
return result
|
722
.venv/Lib/site-packages/prompt_toolkit/layout/menus.py
Normal file
722
.venv/Lib/site-packages/prompt_toolkit/layout/menus.py
Normal file
@ -0,0 +1,722 @@
|
||||
import math
|
||||
from itertools import zip_longest
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
from prompt_toolkit.application.current import get_app
|
||||
from prompt_toolkit.buffer import CompletionState
|
||||
from prompt_toolkit.completion import Completion
|
||||
from prompt_toolkit.data_structures import Point
|
||||
from prompt_toolkit.filters import (
|
||||
Condition,
|
||||
FilterOrBool,
|
||||
has_completions,
|
||||
is_done,
|
||||
to_filter,
|
||||
)
|
||||
from prompt_toolkit.formatted_text import (
|
||||
StyleAndTextTuples,
|
||||
fragment_list_width,
|
||||
to_formatted_text,
|
||||
)
|
||||
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
|
||||
from prompt_toolkit.layout.utils import explode_text_fragments
|
||||
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
|
||||
from prompt_toolkit.utils import get_cwidth
|
||||
|
||||
from .containers import ConditionalContainer, HSplit, ScrollOffsets, Window
|
||||
from .controls import GetLinePrefixCallable, UIContent, UIControl
|
||||
from .dimension import Dimension
|
||||
from .margins import ScrollbarMargin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from prompt_toolkit.key_binding.key_bindings import (
|
||||
KeyBindings,
|
||||
NotImplementedOrNone,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CompletionsMenu",
|
||||
"MultiColumnCompletionsMenu",
|
||||
]
|
||||
|
||||
E = KeyPressEvent
|
||||
|
||||
|
||||
class CompletionsMenuControl(UIControl):
|
||||
"""
|
||||
Helper for drawing the complete menu to the screen.
|
||||
|
||||
:param scroll_offset: Number (integer) representing the preferred amount of
|
||||
completions to be displayed before and after the current one. When this
|
||||
is a very high number, the current completion will be shown in the
|
||||
middle most of the time.
|
||||
"""
|
||||
|
||||
# Preferred minimum size of the menu control.
|
||||
# The CompletionsMenu class defines a width of 8, and there is a scrollbar
|
||||
# of 1.)
|
||||
MIN_WIDTH = 7
|
||||
|
||||
def has_focus(self) -> bool:
|
||||
return False
|
||||
|
||||
def preferred_width(self, max_available_width: int) -> Optional[int]:
|
||||
complete_state = get_app().current_buffer.complete_state
|
||||
if complete_state:
|
||||
menu_width = self._get_menu_width(500, complete_state)
|
||||
menu_meta_width = self._get_menu_meta_width(500, complete_state)
|
||||
|
||||
return menu_width + menu_meta_width
|
||||
else:
|
||||
return 0
|
||||
|
||||
def preferred_height(
|
||||
self,
|
||||
width: int,
|
||||
max_available_height: int,
|
||||
wrap_lines: bool,
|
||||
get_line_prefix: Optional[GetLinePrefixCallable],
|
||||
) -> Optional[int]:
|
||||
|
||||
complete_state = get_app().current_buffer.complete_state
|
||||
if complete_state:
|
||||
return len(complete_state.completions)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def create_content(self, width: int, height: int) -> UIContent:
|
||||
"""
|
||||
Create a UIContent object for this control.
|
||||
"""
|
||||
complete_state = get_app().current_buffer.complete_state
|
||||
if complete_state:
|
||||
completions = complete_state.completions
|
||||
index = complete_state.complete_index # Can be None!
|
||||
|
||||
# Calculate width of completions menu.
|
||||
menu_width = self._get_menu_width(width, complete_state)
|
||||
menu_meta_width = self._get_menu_meta_width(
|
||||
width - menu_width, complete_state
|
||||
)
|
||||
show_meta = self._show_meta(complete_state)
|
||||
|
||||
def get_line(i: int) -> StyleAndTextTuples:
|
||||
c = completions[i]
|
||||
is_current_completion = i == index
|
||||
result = _get_menu_item_fragments(
|
||||
c, is_current_completion, menu_width, space_after=True
|
||||
)
|
||||
|
||||
if show_meta:
|
||||
result += self._get_menu_item_meta_fragments(
|
||||
c, is_current_completion, menu_meta_width
|
||||
)
|
||||
return result
|
||||
|
||||
return UIContent(
|
||||
get_line=get_line,
|
||||
cursor_position=Point(x=0, y=index or 0),
|
||||
line_count=len(completions),
|
||||
)
|
||||
|
||||
return UIContent()
|
||||
|
||||
def _show_meta(self, complete_state: CompletionState) -> bool:
|
||||
"""
|
||||
Return ``True`` if we need to show a column with meta information.
|
||||
"""
|
||||
return any(c.display_meta_text for c in complete_state.completions)
|
||||
|
||||
def _get_menu_width(self, max_width: int, complete_state: CompletionState) -> int:
|
||||
"""
|
||||
Return the width of the main column.
|
||||
"""
|
||||
return min(
|
||||
max_width,
|
||||
max(
|
||||
self.MIN_WIDTH,
|
||||
max(get_cwidth(c.display_text) for c in complete_state.completions) + 2,
|
||||
),
|
||||
)
|
||||
|
||||
def _get_menu_meta_width(
|
||||
self, max_width: int, complete_state: CompletionState
|
||||
) -> int:
|
||||
"""
|
||||
Return the width of the meta column.
|
||||
"""
|
||||
|
||||
def meta_width(completion: Completion) -> int:
|
||||
return get_cwidth(completion.display_meta_text)
|
||||
|
||||
if self._show_meta(complete_state):
|
||||
return min(
|
||||
max_width, max(meta_width(c) for c in complete_state.completions) + 2
|
||||
)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def _get_menu_item_meta_fragments(
|
||||
self, completion: Completion, is_current_completion: bool, width: int
|
||||
) -> StyleAndTextTuples:
|
||||
|
||||
if is_current_completion:
|
||||
style_str = "class:completion-menu.meta.completion.current"
|
||||
else:
|
||||
style_str = "class:completion-menu.meta.completion"
|
||||
|
||||
text, tw = _trim_formatted_text(completion.display_meta, width - 2)
|
||||
padding = " " * (width - 1 - tw)
|
||||
|
||||
return to_formatted_text(
|
||||
cast(StyleAndTextTuples, []) + [("", " ")] + text + [("", padding)],
|
||||
style=style_str,
|
||||
)
|
||||
|
||||
def mouse_handler(self, mouse_event: MouseEvent) -> "NotImplementedOrNone":
|
||||
"""
|
||||
Handle mouse events: clicking and scrolling.
|
||||
"""
|
||||
b = get_app().current_buffer
|
||||
|
||||
if mouse_event.event_type == MouseEventType.MOUSE_UP:
|
||||
# Select completion.
|
||||
b.go_to_completion(mouse_event.position.y)
|
||||
b.complete_state = None
|
||||
|
||||
elif mouse_event.event_type == MouseEventType.SCROLL_DOWN:
|
||||
# Scroll up.
|
||||
b.complete_next(count=3, disable_wrap_around=True)
|
||||
|
||||
elif mouse_event.event_type == MouseEventType.SCROLL_UP:
|
||||
# Scroll down.
|
||||
b.complete_previous(count=3, disable_wrap_around=True)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_menu_item_fragments(
|
||||
completion: Completion,
|
||||
is_current_completion: bool,
|
||||
width: int,
|
||||
space_after: bool = False,
|
||||
) -> StyleAndTextTuples:
|
||||
"""
|
||||
Get the style/text tuples for a menu item, styled and trimmed to the given
|
||||
width.
|
||||
"""
|
||||
if is_current_completion:
|
||||
style_str = "class:completion-menu.completion.current {} {}".format(
|
||||
completion.style,
|
||||
completion.selected_style,
|
||||
)
|
||||
else:
|
||||
style_str = "class:completion-menu.completion " + completion.style
|
||||
|
||||
text, tw = _trim_formatted_text(
|
||||
completion.display, (width - 2 if space_after else width - 1)
|
||||
)
|
||||
|
||||
padding = " " * (width - 1 - tw)
|
||||
|
||||
return to_formatted_text(
|
||||
cast(StyleAndTextTuples, []) + [("", " ")] + text + [("", padding)],
|
||||
style=style_str,
|
||||
)
|
||||
|
||||
|
||||
def _trim_formatted_text(
|
||||
formatted_text: StyleAndTextTuples, max_width: int
|
||||
) -> Tuple[StyleAndTextTuples, int]:
|
||||
"""
|
||||
Trim the text to `max_width`, append dots when the text is too long.
|
||||
Returns (text, width) tuple.
|
||||
"""
|
||||
width = fragment_list_width(formatted_text)
|
||||
|
||||
# When the text is too wide, trim it.
|
||||
if width > max_width:
|
||||
result = [] # Text fragments.
|
||||
remaining_width = max_width - 3
|
||||
|
||||
for style_and_ch in explode_text_fragments(formatted_text):
|
||||
ch_width = get_cwidth(style_and_ch[1])
|
||||
|
||||
if ch_width <= remaining_width:
|
||||
result.append(style_and_ch)
|
||||
remaining_width -= ch_width
|
||||
else:
|
||||
break
|
||||
|
||||
result.append(("", "..."))
|
||||
|
||||
return result, max_width - remaining_width
|
||||
else:
|
||||
return formatted_text, width
|
||||
|
||||
|
||||
class CompletionsMenu(ConditionalContainer):
|
||||
# NOTE: We use a pretty big z_index by default. Menus are supposed to be
|
||||
# above anything else. We also want to make sure that the content is
|
||||
# visible at the point where we draw this menu.
|
||||
def __init__(
|
||||
self,
|
||||
max_height: Optional[int] = None,
|
||||
scroll_offset: Union[int, Callable[[], int]] = 0,
|
||||
extra_filter: FilterOrBool = True,
|
||||
display_arrows: FilterOrBool = False,
|
||||
z_index: int = 10**8,
|
||||
) -> None:
|
||||
|
||||
extra_filter = to_filter(extra_filter)
|
||||
display_arrows = to_filter(display_arrows)
|
||||
|
||||
super().__init__(
|
||||
content=Window(
|
||||
content=CompletionsMenuControl(),
|
||||
width=Dimension(min=8),
|
||||
height=Dimension(min=1, max=max_height),
|
||||
scroll_offsets=ScrollOffsets(top=scroll_offset, bottom=scroll_offset),
|
||||
right_margins=[ScrollbarMargin(display_arrows=display_arrows)],
|
||||
dont_extend_width=True,
|
||||
style="class:completion-menu",
|
||||
z_index=z_index,
|
||||
),
|
||||
# Show when there are completions but not at the point we are
|
||||
# returning the input.
|
||||
filter=has_completions & ~is_done & extra_filter,
|
||||
)
|
||||
|
||||
|
||||
class MultiColumnCompletionMenuControl(UIControl):
|
||||
"""
|
||||
Completion menu that displays all the completions in several columns.
|
||||
When there are more completions than space for them to be displayed, an
|
||||
arrow is shown on the left or right side.
|
||||
|
||||
`min_rows` indicates how many rows will be available in any possible case.
|
||||
When this is larger than one, it will try to use less columns and more
|
||||
rows until this value is reached.
|
||||
Be careful passing in a too big value, if less than the given amount of
|
||||
rows are available, more columns would have been required, but
|
||||
`preferred_width` doesn't know about that and reports a too small value.
|
||||
This results in less completions displayed and additional scrolling.
|
||||
(It's a limitation of how the layout engine currently works: first the
|
||||
widths are calculated, then the heights.)
|
||||
|
||||
:param suggested_max_column_width: The suggested max width of a column.
|
||||
The column can still be bigger than this, but if there is place for two
|
||||
columns of this width, we will display two columns. This to avoid that
|
||||
if there is one very wide completion, that it doesn't significantly
|
||||
reduce the amount of columns.
|
||||
"""
|
||||
|
||||
_required_margin = 3 # One extra padding on the right + space for arrows.
|
||||
|
||||
def __init__(self, min_rows: int = 3, suggested_max_column_width: int = 30) -> None:
|
||||
assert min_rows >= 1
|
||||
|
||||
self.min_rows = min_rows
|
||||
self.suggested_max_column_width = suggested_max_column_width
|
||||
self.scroll = 0
|
||||
|
||||
# Info of last rendering.
|
||||
self._rendered_rows = 0
|
||||
self._rendered_columns = 0
|
||||
self._total_columns = 0
|
||||
self._render_pos_to_completion: Dict[Tuple[int, int], Completion] = {}
|
||||
self._render_left_arrow = False
|
||||
self._render_right_arrow = False
|
||||
self._render_width = 0
|
||||
|
||||
def reset(self) -> None:
|
||||
self.scroll = 0
|
||||
|
||||
def has_focus(self) -> bool:
|
||||
return False
|
||||
|
||||
def preferred_width(self, max_available_width: int) -> Optional[int]:
|
||||
"""
|
||||
Preferred width: prefer to use at least min_rows, but otherwise as much
|
||||
as possible horizontally.
|
||||
"""
|
||||
complete_state = get_app().current_buffer.complete_state
|
||||
if complete_state is None:
|
||||
return 0
|
||||
|
||||
column_width = self._get_column_width(complete_state)
|
||||
result = int(
|
||||
column_width
|
||||
* math.ceil(len(complete_state.completions) / float(self.min_rows))
|
||||
)
|
||||
|
||||
# When the desired width is still more than the maximum available,
|
||||
# reduce by removing columns until we are less than the available
|
||||
# width.
|
||||
while (
|
||||
result > column_width
|
||||
and result > max_available_width - self._required_margin
|
||||
):
|
||||
result -= column_width
|
||||
return result + self._required_margin
|
||||
|
||||
def preferred_height(
|
||||
self,
|
||||
width: int,
|
||||
max_available_height: int,
|
||||
wrap_lines: bool,
|
||||
get_line_prefix: Optional[GetLinePrefixCallable],
|
||||
) -> Optional[int]:
|
||||
"""
|
||||
Preferred height: as much as needed in order to display all the completions.
|
||||
"""
|
||||
complete_state = get_app().current_buffer.complete_state
|
||||
if complete_state is None:
|
||||
return 0
|
||||
|
||||
column_width = self._get_column_width(complete_state)
|
||||
column_count = max(1, (width - self._required_margin) // column_width)
|
||||
|
||||
return int(math.ceil(len(complete_state.completions) / float(column_count)))
|
||||
|
||||
def create_content(self, width: int, height: int) -> UIContent:
|
||||
"""
|
||||
Create a UIContent object for this menu.
|
||||
"""
|
||||
complete_state = get_app().current_buffer.complete_state
|
||||
if complete_state is None:
|
||||
return UIContent()
|
||||
|
||||
column_width = self._get_column_width(complete_state)
|
||||
self._render_pos_to_completion = {}
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
def grouper(
|
||||
n: int, iterable: Iterable[_T], fillvalue: Optional[_T] = None
|
||||
) -> Iterable[List[_T]]:
|
||||
"grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
|
||||
args = [iter(iterable)] * n
|
||||
return zip_longest(fillvalue=fillvalue, *args)
|
||||
|
||||
def is_current_completion(completion: Completion) -> bool:
|
||||
"Returns True when this completion is the currently selected one."
|
||||
return (
|
||||
complete_state is not None
|
||||
and complete_state.complete_index is not None
|
||||
and c == complete_state.current_completion
|
||||
)
|
||||
|
||||
# Space required outside of the regular columns, for displaying the
|
||||
# left and right arrow.
|
||||
HORIZONTAL_MARGIN_REQUIRED = 3
|
||||
|
||||
# There should be at least one column, but it cannot be wider than
|
||||
# the available width.
|
||||
column_width = min(width - HORIZONTAL_MARGIN_REQUIRED, column_width)
|
||||
|
||||
# However, when the columns tend to be very wide, because there are
|
||||
# some very wide entries, shrink it anyway.
|
||||
if column_width > self.suggested_max_column_width:
|
||||
# `column_width` can still be bigger that `suggested_max_column_width`,
|
||||
# but if there is place for two columns, we divide by two.
|
||||
column_width //= column_width // self.suggested_max_column_width
|
||||
|
||||
visible_columns = max(1, (width - self._required_margin) // column_width)
|
||||
|
||||
columns_ = list(grouper(height, complete_state.completions))
|
||||
rows_ = list(zip(*columns_))
|
||||
|
||||
# Make sure the current completion is always visible: update scroll offset.
|
||||
selected_column = (complete_state.complete_index or 0) // height
|
||||
self.scroll = min(
|
||||
selected_column, max(self.scroll, selected_column - visible_columns + 1)
|
||||
)
|
||||
|
||||
render_left_arrow = self.scroll > 0
|
||||
render_right_arrow = self.scroll < len(rows_[0]) - visible_columns
|
||||
|
||||
# Write completions to screen.
|
||||
fragments_for_line = []
|
||||
|
||||
for row_index, row in enumerate(rows_):
|
||||
fragments: StyleAndTextTuples = []
|
||||
middle_row = row_index == len(rows_) // 2
|
||||
|
||||
# Draw left arrow if we have hidden completions on the left.
|
||||
if render_left_arrow:
|
||||
fragments.append(("class:scrollbar", "<" if middle_row else " "))
|
||||
elif render_right_arrow:
|
||||
# Reserve one column empty space. (If there is a right
|
||||
# arrow right now, there can be a left arrow as well.)
|
||||
fragments.append(("", " "))
|
||||
|
||||
# Draw row content.
|
||||
for column_index, c in enumerate(row[self.scroll :][:visible_columns]):
|
||||
if c is not None:
|
||||
fragments += _get_menu_item_fragments(
|
||||
c, is_current_completion(c), column_width, space_after=False
|
||||
)
|
||||
|
||||
# Remember render position for mouse click handler.
|
||||
for x in range(column_width):
|
||||
self._render_pos_to_completion[
|
||||
(column_index * column_width + x, row_index)
|
||||
] = c
|
||||
else:
|
||||
fragments.append(("class:completion", " " * column_width))
|
||||
|
||||
# Draw trailing padding for this row.
|
||||
# (_get_menu_item_fragments only returns padding on the left.)
|
||||
if render_left_arrow or render_right_arrow:
|
||||
fragments.append(("class:completion", " "))
|
||||
|
||||
# Draw right arrow if we have hidden completions on the right.
|
||||
if render_right_arrow:
|
||||
fragments.append(("class:scrollbar", ">" if middle_row else " "))
|
||||
elif render_left_arrow:
|
||||
fragments.append(("class:completion", " "))
|
||||
|
||||
# Add line.
|
||||
fragments_for_line.append(
|
||||
to_formatted_text(fragments, style="class:completion-menu")
|
||||
)
|
||||
|
||||
self._rendered_rows = height
|
||||
self._rendered_columns = visible_columns
|
||||
self._total_columns = len(columns_)
|
||||
self._render_left_arrow = render_left_arrow
|
||||
self._render_right_arrow = render_right_arrow
|
||||
self._render_width = (
|
||||
column_width * visible_columns + render_left_arrow + render_right_arrow + 1
|
||||
)
|
||||
|
||||
def get_line(i: int) -> StyleAndTextTuples:
|
||||
return fragments_for_line[i]
|
||||
|
||||
return UIContent(get_line=get_line, line_count=len(rows_))
|
||||
|
||||
def _get_column_width(self, complete_state: CompletionState) -> int:
|
||||
"""
|
||||
Return the width of each column.
|
||||
"""
|
||||
return max(get_cwidth(c.display_text) for c in complete_state.completions) + 1
|
||||
|
||||
def mouse_handler(self, mouse_event: MouseEvent) -> "NotImplementedOrNone":
|
||||
"""
|
||||
Handle scroll and click events.
|
||||
"""
|
||||
b = get_app().current_buffer
|
||||
|
||||
def scroll_left() -> None:
|
||||
b.complete_previous(count=self._rendered_rows, disable_wrap_around=True)
|
||||
self.scroll = max(0, self.scroll - 1)
|
||||
|
||||
def scroll_right() -> None:
|
||||
b.complete_next(count=self._rendered_rows, disable_wrap_around=True)
|
||||
self.scroll = min(
|
||||
self._total_columns - self._rendered_columns, self.scroll + 1
|
||||
)
|
||||
|
||||
if mouse_event.event_type == MouseEventType.SCROLL_DOWN:
|
||||
scroll_right()
|
||||
|
||||
elif mouse_event.event_type == MouseEventType.SCROLL_UP:
|
||||
scroll_left()
|
||||
|
||||
elif mouse_event.event_type == MouseEventType.MOUSE_UP:
|
||||
x = mouse_event.position.x
|
||||
y = mouse_event.position.y
|
||||
|
||||
# Mouse click on left arrow.
|
||||
if x == 0:
|
||||
if self._render_left_arrow:
|
||||
scroll_left()
|
||||
|
||||
# Mouse click on right arrow.
|
||||
elif x == self._render_width - 1:
|
||||
if self._render_right_arrow:
|
||||
scroll_right()
|
||||
|
||||
# Mouse click on completion.
|
||||
else:
|
||||
completion = self._render_pos_to_completion.get((x, y))
|
||||
if completion:
|
||||
b.apply_completion(completion)
|
||||
|
||||
return None
|
||||
|
||||
def get_key_bindings(self) -> "KeyBindings":
|
||||
"""
|
||||
Expose key bindings that handle the left/right arrow keys when the menu
|
||||
is displayed.
|
||||
"""
|
||||
from prompt_toolkit.key_binding.key_bindings import KeyBindings
|
||||
|
||||
kb = KeyBindings()
|
||||
|
||||
@Condition
|
||||
def filter() -> bool:
|
||||
"Only handle key bindings if this menu is visible."
|
||||
app = get_app()
|
||||
complete_state = app.current_buffer.complete_state
|
||||
|
||||
# There need to be completions, and one needs to be selected.
|
||||
if complete_state is None or complete_state.complete_index is None:
|
||||
return False
|
||||
|
||||
# This menu needs to be visible.
|
||||
return any(window.content == self for window in app.layout.visible_windows)
|
||||
|
||||
def move(right: bool = False) -> None:
|
||||
buff = get_app().current_buffer
|
||||
complete_state = buff.complete_state
|
||||
|
||||
if complete_state is not None and complete_state.complete_index is not None:
|
||||
# Calculate new complete index.
|
||||
new_index = complete_state.complete_index
|
||||
if right:
|
||||
new_index += self._rendered_rows
|
||||
else:
|
||||
new_index -= self._rendered_rows
|
||||
|
||||
if 0 <= new_index < len(complete_state.completions):
|
||||
buff.go_to_completion(new_index)
|
||||
|
||||
# NOTE: the is_global is required because the completion menu will
|
||||
# never be focussed.
|
||||
|
||||
@kb.add("left", is_global=True, filter=filter)
|
||||
def _left(event: E) -> None:
|
||||
move()
|
||||
|
||||
@kb.add("right", is_global=True, filter=filter)
|
||||
def _right(event: E) -> None:
|
||||
move(True)
|
||||
|
||||
return kb
|
||||
|
||||
|
||||
class MultiColumnCompletionsMenu(HSplit):
|
||||
"""
|
||||
Container that displays the completions in several columns.
|
||||
When `show_meta` (a :class:`~prompt_toolkit.filters.Filter`) evaluates
|
||||
to True, it shows the meta information at the bottom.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
min_rows: int = 3,
|
||||
suggested_max_column_width: int = 30,
|
||||
show_meta: FilterOrBool = True,
|
||||
extra_filter: FilterOrBool = True,
|
||||
z_index: int = 10**8,
|
||||
) -> None:
|
||||
|
||||
show_meta = to_filter(show_meta)
|
||||
extra_filter = to_filter(extra_filter)
|
||||
|
||||
# Display filter: show when there are completions but not at the point
|
||||
# we are returning the input.
|
||||
full_filter = has_completions & ~is_done & extra_filter
|
||||
|
||||
@Condition
|
||||
def any_completion_has_meta() -> bool:
|
||||
complete_state = get_app().current_buffer.complete_state
|
||||
return complete_state is not None and any(
|
||||
c.display_meta for c in complete_state.completions
|
||||
)
|
||||
|
||||
# Create child windows.
|
||||
# NOTE: We don't set style='class:completion-menu' to the
|
||||
# `MultiColumnCompletionMenuControl`, because this is used in a
|
||||
# Float that is made transparent, and the size of the control
|
||||
# doesn't always correspond exactly with the size of the
|
||||
# generated content.
|
||||
completions_window = ConditionalContainer(
|
||||
content=Window(
|
||||
content=MultiColumnCompletionMenuControl(
|
||||
min_rows=min_rows,
|
||||
suggested_max_column_width=suggested_max_column_width,
|
||||
),
|
||||
width=Dimension(min=8),
|
||||
height=Dimension(min=1),
|
||||
),
|
||||
filter=full_filter,
|
||||
)
|
||||
|
||||
meta_window = ConditionalContainer(
|
||||
content=Window(content=_SelectedCompletionMetaControl()),
|
||||
filter=show_meta & full_filter & any_completion_has_meta,
|
||||
)
|
||||
|
||||
# Initialise split.
|
||||
super().__init__([completions_window, meta_window], z_index=z_index)
|
||||
|
||||
|
||||
class _SelectedCompletionMetaControl(UIControl):
|
||||
"""
|
||||
Control that shows the meta information of the selected completion.
|
||||
"""
|
||||
|
||||
def preferred_width(self, max_available_width: int) -> Optional[int]:
|
||||
"""
|
||||
Report the width of the longest meta text as the preferred width of this control.
|
||||
|
||||
It could be that we use less width, but this way, we're sure that the
|
||||
layout doesn't change when we select another completion (E.g. that
|
||||
completions are suddenly shown in more or fewer columns.)
|
||||
"""
|
||||
app = get_app()
|
||||
if app.current_buffer.complete_state:
|
||||
state = app.current_buffer.complete_state
|
||||
return 2 + max(get_cwidth(c.display_meta_text) for c in state.completions)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def preferred_height(
|
||||
self,
|
||||
width: int,
|
||||
max_available_height: int,
|
||||
wrap_lines: bool,
|
||||
get_line_prefix: Optional[GetLinePrefixCallable],
|
||||
) -> Optional[int]:
|
||||
return 1
|
||||
|
||||
def create_content(self, width: int, height: int) -> UIContent:
|
||||
fragments = self._get_text_fragments()
|
||||
|
||||
def get_line(i: int) -> StyleAndTextTuples:
|
||||
return fragments
|
||||
|
||||
return UIContent(get_line=get_line, line_count=1 if fragments else 0)
|
||||
|
||||
def _get_text_fragments(self) -> StyleAndTextTuples:
|
||||
style = "class:completion-menu.multi-column-meta"
|
||||
state = get_app().current_buffer.complete_state
|
||||
|
||||
if (
|
||||
state
|
||||
and state.current_completion
|
||||
and state.current_completion.display_meta_text
|
||||
):
|
||||
return to_formatted_text(
|
||||
cast(StyleAndTextTuples, [("", " ")])
|
||||
+ state.current_completion.display_meta
|
||||
+ [("", " ")],
|
||||
style=style,
|
||||
)
|
||||
|
||||
return []
|
@ -0,0 +1,54 @@
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Callable, DefaultDict
|
||||
|
||||
from prompt_toolkit.mouse_events import MouseEvent
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
|
||||
|
||||
__all__ = [
|
||||
"MouseHandler",
|
||||
"MouseHandlers",
|
||||
]
|
||||
|
||||
|
||||
MouseHandler = Callable[[MouseEvent], "NotImplementedOrNone"]
|
||||
|
||||
|
||||
class MouseHandlers:
|
||||
"""
|
||||
Two dimensional raster of callbacks for mouse events.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
def dummy_callback(mouse_event: MouseEvent) -> "NotImplementedOrNone":
|
||||
"""
|
||||
:param mouse_event: `MouseEvent` instance.
|
||||
"""
|
||||
return NotImplemented
|
||||
|
||||
# NOTE: Previously, the data structure was a dictionary mapping (x,y)
|
||||
# to the handlers. This however would be more inefficient when copying
|
||||
# over the mouse handlers of the visible region in the scrollable pane.
|
||||
|
||||
# Map y (row) to x (column) to handlers.
|
||||
self.mouse_handlers: DefaultDict[
|
||||
int, DefaultDict[int, MouseHandler]
|
||||
] = defaultdict(lambda: defaultdict(lambda: dummy_callback))
|
||||
|
||||
def set_mouse_handler_for_range(
|
||||
self,
|
||||
x_min: int,
|
||||
x_max: int,
|
||||
y_min: int,
|
||||
y_max: int,
|
||||
handler: Callable[[MouseEvent], "NotImplementedOrNone"],
|
||||
) -> None:
|
||||
"""
|
||||
Set mouse handler for a region.
|
||||
"""
|
||||
for y in range(y_min, y_max):
|
||||
row = self.mouse_handlers[y]
|
||||
|
||||
for x in range(x_min, x_max):
|
||||
row[x] = handler
|
1029
.venv/Lib/site-packages/prompt_toolkit/layout/processors.py
Normal file
1029
.venv/Lib/site-packages/prompt_toolkit/layout/processors.py
Normal file
File diff suppressed because it is too large
Load Diff
327
.venv/Lib/site-packages/prompt_toolkit/layout/screen.py
Normal file
327
.venv/Lib/site-packages/prompt_toolkit/layout/screen.py
Normal file
@ -0,0 +1,327 @@
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Callable, DefaultDict, Dict, List, Optional, Tuple
|
||||
|
||||
from prompt_toolkit.cache import FastDictCache
|
||||
from prompt_toolkit.data_structures import Point
|
||||
from prompt_toolkit.utils import get_cwidth
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .containers import Window
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Screen",
|
||||
"Char",
|
||||
]
|
||||
|
||||
|
||||
class Char:
|
||||
"""
|
||||
Represent a single character in a :class:`.Screen`.
|
||||
|
||||
This should be considered immutable.
|
||||
|
||||
:param char: A single character (can be a double-width character).
|
||||
:param style: A style string. (Can contain classnames.)
|
||||
"""
|
||||
|
||||
__slots__ = ("char", "style", "width")
|
||||
|
||||
# If we end up having one of these special control sequences in the input string,
|
||||
# we should display them as follows:
|
||||
# Usually this happens after a "quoted insert".
|
||||
display_mappings: Dict[str, str] = {
|
||||
"\x00": "^@", # Control space
|
||||
"\x01": "^A",
|
||||
"\x02": "^B",
|
||||
"\x03": "^C",
|
||||
"\x04": "^D",
|
||||
"\x05": "^E",
|
||||
"\x06": "^F",
|
||||
"\x07": "^G",
|
||||
"\x08": "^H",
|
||||
"\x09": "^I",
|
||||
"\x0a": "^J",
|
||||
"\x0b": "^K",
|
||||
"\x0c": "^L",
|
||||
"\x0d": "^M",
|
||||
"\x0e": "^N",
|
||||
"\x0f": "^O",
|
||||
"\x10": "^P",
|
||||
"\x11": "^Q",
|
||||
"\x12": "^R",
|
||||
"\x13": "^S",
|
||||
"\x14": "^T",
|
||||
"\x15": "^U",
|
||||
"\x16": "^V",
|
||||
"\x17": "^W",
|
||||
"\x18": "^X",
|
||||
"\x19": "^Y",
|
||||
"\x1a": "^Z",
|
||||
"\x1b": "^[", # Escape
|
||||
"\x1c": "^\\",
|
||||
"\x1d": "^]",
|
||||
"\x1f": "^_",
|
||||
"\x7f": "^?", # ASCII Delete (backspace).
|
||||
# Special characters. All visualized like Vim does.
|
||||
"\x80": "<80>",
|
||||
"\x81": "<81>",
|
||||
"\x82": "<82>",
|
||||
"\x83": "<83>",
|
||||
"\x84": "<84>",
|
||||
"\x85": "<85>",
|
||||
"\x86": "<86>",
|
||||
"\x87": "<87>",
|
||||
"\x88": "<88>",
|
||||
"\x89": "<89>",
|
||||
"\x8a": "<8a>",
|
||||
"\x8b": "<8b>",
|
||||
"\x8c": "<8c>",
|
||||
"\x8d": "<8d>",
|
||||
"\x8e": "<8e>",
|
||||
"\x8f": "<8f>",
|
||||
"\x90": "<90>",
|
||||
"\x91": "<91>",
|
||||
"\x92": "<92>",
|
||||
"\x93": "<93>",
|
||||
"\x94": "<94>",
|
||||
"\x95": "<95>",
|
||||
"\x96": "<96>",
|
||||
"\x97": "<97>",
|
||||
"\x98": "<98>",
|
||||
"\x99": "<99>",
|
||||
"\x9a": "<9a>",
|
||||
"\x9b": "<9b>",
|
||||
"\x9c": "<9c>",
|
||||
"\x9d": "<9d>",
|
||||
"\x9e": "<9e>",
|
||||
"\x9f": "<9f>",
|
||||
# For the non-breaking space: visualize like Emacs does by default.
|
||||
# (Print a space, but attach the 'nbsp' class that applies the
|
||||
# underline style.)
|
||||
"\xa0": " ",
|
||||
}
|
||||
|
||||
def __init__(self, char: str = " ", style: str = "") -> None:
|
||||
# If this character has to be displayed otherwise, take that one.
|
||||
if char in self.display_mappings:
|
||||
if char == "\xa0":
|
||||
style += " class:nbsp " # Will be underlined.
|
||||
else:
|
||||
style += " class:control-character "
|
||||
|
||||
char = self.display_mappings[char]
|
||||
|
||||
self.char = char
|
||||
self.style = style
|
||||
|
||||
# Calculate width. (We always need this, so better to store it directly
|
||||
# as a member for performance.)
|
||||
self.width = get_cwidth(char)
|
||||
|
||||
# In theory, `other` can be any type of object, but because of performance
|
||||
# we don't want to do an `isinstance` check every time. We assume "other"
|
||||
# is always a "Char".
|
||||
def _equal(self, other: "Char") -> bool:
|
||||
return self.char == other.char and self.style == other.style
|
||||
|
||||
def _not_equal(self, other: "Char") -> bool:
|
||||
# Not equal: We don't do `not char.__eq__` here, because of the
|
||||
# performance of calling yet another function.
|
||||
return self.char != other.char or self.style != other.style
|
||||
|
||||
if not TYPE_CHECKING:
|
||||
__eq__ = _equal
|
||||
__ne__ = _not_equal
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.char!r}, {self.style!r})"
|
||||
|
||||
|
||||
_CHAR_CACHE: FastDictCache[Tuple[str, str], Char] = FastDictCache(
|
||||
Char, size=1000 * 1000
|
||||
)
|
||||
Transparent = "[transparent]"
|
||||
|
||||
|
||||
class Screen:
|
||||
"""
|
||||
Two dimensional buffer of :class:`.Char` instances.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
default_char: Optional[Char] = None,
|
||||
initial_width: int = 0,
|
||||
initial_height: int = 0,
|
||||
) -> None:
|
||||
|
||||
if default_char is None:
|
||||
default_char2 = _CHAR_CACHE[" ", Transparent]
|
||||
else:
|
||||
default_char2 = default_char
|
||||
|
||||
self.data_buffer: DefaultDict[int, DefaultDict[int, Char]] = defaultdict(
|
||||
lambda: defaultdict(lambda: default_char2)
|
||||
)
|
||||
|
||||
#: Escape sequences to be injected.
|
||||
self.zero_width_escapes: DefaultDict[int, DefaultDict[int, str]] = defaultdict(
|
||||
lambda: defaultdict(lambda: "")
|
||||
)
|
||||
|
||||
#: Position of the cursor.
|
||||
self.cursor_positions: Dict[
|
||||
"Window", Point
|
||||
] = {} # Map `Window` objects to `Point` objects.
|
||||
|
||||
#: Visibility of the cursor.
|
||||
self.show_cursor = True
|
||||
|
||||
#: (Optional) Where to position the menu. E.g. at the start of a completion.
|
||||
#: (We can't use the cursor position, because we don't want the
|
||||
#: completion menu to change its position when we browse through all the
|
||||
#: completions.)
|
||||
self.menu_positions: Dict[
|
||||
"Window", Point
|
||||
] = {} # Map `Window` objects to `Point` objects.
|
||||
|
||||
#: Currently used width/height of the screen. This will increase when
|
||||
#: data is written to the screen.
|
||||
self.width = initial_width or 0
|
||||
self.height = initial_height or 0
|
||||
|
||||
# Windows that have been drawn. (Each `Window` class will add itself to
|
||||
# this list.)
|
||||
self.visible_windows_to_write_positions: Dict["Window", "WritePosition"] = {}
|
||||
|
||||
# List of (z_index, draw_func)
|
||||
self._draw_float_functions: List[Tuple[int, Callable[[], None]]] = []
|
||||
|
||||
@property
|
||||
def visible_windows(self) -> List["Window"]:
|
||||
return list(self.visible_windows_to_write_positions.keys())
|
||||
|
||||
def set_cursor_position(self, window: "Window", position: Point) -> None:
|
||||
"""
|
||||
Set the cursor position for a given window.
|
||||
"""
|
||||
self.cursor_positions[window] = position
|
||||
|
||||
def set_menu_position(self, window: "Window", position: Point) -> None:
|
||||
"""
|
||||
Set the cursor position for a given window.
|
||||
"""
|
||||
self.menu_positions[window] = position
|
||||
|
||||
def get_cursor_position(self, window: "Window") -> Point:
|
||||
"""
|
||||
Get the cursor position for a given window.
|
||||
Returns a `Point`.
|
||||
"""
|
||||
try:
|
||||
return self.cursor_positions[window]
|
||||
except KeyError:
|
||||
return Point(x=0, y=0)
|
||||
|
||||
def get_menu_position(self, window: "Window") -> Point:
|
||||
"""
|
||||
Get the menu position for a given window.
|
||||
(This falls back to the cursor position if no menu position was set.)
|
||||
"""
|
||||
try:
|
||||
return self.menu_positions[window]
|
||||
except KeyError:
|
||||
try:
|
||||
return self.cursor_positions[window]
|
||||
except KeyError:
|
||||
return Point(x=0, y=0)
|
||||
|
||||
def draw_with_z_index(self, z_index: int, draw_func: Callable[[], None]) -> None:
|
||||
"""
|
||||
Add a draw-function for a `Window` which has a >= 0 z_index.
|
||||
This will be postponed until `draw_all_floats` is called.
|
||||
"""
|
||||
self._draw_float_functions.append((z_index, draw_func))
|
||||
|
||||
def draw_all_floats(self) -> None:
|
||||
"""
|
||||
Draw all float functions in order of z-index.
|
||||
"""
|
||||
# We keep looping because some draw functions could add new functions
|
||||
# to this list. See `FloatContainer`.
|
||||
while self._draw_float_functions:
|
||||
# Sort the floats that we have so far by z_index.
|
||||
functions = sorted(self._draw_float_functions, key=lambda item: item[0])
|
||||
|
||||
# Draw only one at a time, then sort everything again. Now floats
|
||||
# might have been added.
|
||||
self._draw_float_functions = functions[1:]
|
||||
functions[0][1]()
|
||||
|
||||
def append_style_to_content(self, style_str: str) -> None:
|
||||
"""
|
||||
For all the characters in the screen.
|
||||
Set the style string to the given `style_str`.
|
||||
"""
|
||||
b = self.data_buffer
|
||||
char_cache = _CHAR_CACHE
|
||||
|
||||
append_style = " " + style_str
|
||||
|
||||
for y, row in b.items():
|
||||
for x, char in row.items():
|
||||
b[y][x] = char_cache[char.char, char.style + append_style]
|
||||
|
||||
def fill_area(
|
||||
self, write_position: "WritePosition", style: str = "", after: bool = False
|
||||
) -> None:
|
||||
"""
|
||||
Fill the content of this area, using the given `style`.
|
||||
The style is prepended before whatever was here before.
|
||||
"""
|
||||
if not style.strip():
|
||||
return
|
||||
|
||||
xmin = write_position.xpos
|
||||
xmax = write_position.xpos + write_position.width
|
||||
char_cache = _CHAR_CACHE
|
||||
data_buffer = self.data_buffer
|
||||
|
||||
if after:
|
||||
append_style = " " + style
|
||||
prepend_style = ""
|
||||
else:
|
||||
append_style = ""
|
||||
prepend_style = style + " "
|
||||
|
||||
for y in range(
|
||||
write_position.ypos, write_position.ypos + write_position.height
|
||||
):
|
||||
row = data_buffer[y]
|
||||
for x in range(xmin, xmax):
|
||||
cell = row[x]
|
||||
row[x] = char_cache[
|
||||
cell.char, prepend_style + cell.style + append_style
|
||||
]
|
||||
|
||||
|
||||
class WritePosition:
|
||||
def __init__(self, xpos: int, ypos: int, width: int, height: int) -> None:
|
||||
assert height >= 0
|
||||
assert width >= 0
|
||||
# xpos and ypos can be negative. (A float can be partially visible.)
|
||||
|
||||
self.xpos = xpos
|
||||
self.ypos = ypos
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "{}(x={!r}, y={!r}, width={!r}, height={!r})".format(
|
||||
self.__class__.__name__,
|
||||
self.xpos,
|
||||
self.ypos,
|
||||
self.width,
|
||||
self.height,
|
||||
)
|
493
.venv/Lib/site-packages/prompt_toolkit/layout/scrollable_pane.py
Normal file
493
.venv/Lib/site-packages/prompt_toolkit/layout/scrollable_pane.py
Normal file
@ -0,0 +1,493 @@
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from prompt_toolkit.data_structures import Point
|
||||
from prompt_toolkit.filters import FilterOrBool, to_filter
|
||||
from prompt_toolkit.key_binding import KeyBindingsBase
|
||||
from prompt_toolkit.mouse_events import MouseEvent
|
||||
|
||||
from .containers import Container, ScrollOffsets
|
||||
from .dimension import AnyDimension, Dimension, sum_layout_dimensions, to_dimension
|
||||
from .mouse_handlers import MouseHandler, MouseHandlers
|
||||
from .screen import Char, Screen, WritePosition
|
||||
|
||||
__all__ = ["ScrollablePane"]
|
||||
|
||||
# Never go beyond this height, because performance will degrade.
|
||||
MAX_AVAILABLE_HEIGHT = 10_000
|
||||
|
||||
|
||||
class ScrollablePane(Container):
|
||||
"""
|
||||
Container widget that exposes a larger virtual screen to its content and
|
||||
displays it in a vertical scrollbale region.
|
||||
|
||||
Typically this is wrapped in a large `HSplit` container. Make sure in that
|
||||
case to not specify a `height` dimension of the `HSplit`, so that it will
|
||||
scale according to the content.
|
||||
|
||||
.. note::
|
||||
|
||||
If you want to display a completion menu for widgets in this
|
||||
`ScrollablePane`, then it's still a good practice to use a
|
||||
`FloatContainer` with a `CompletionsMenu` in a `Float` at the top-level
|
||||
of the layout hierarchy, rather then nesting a `FloatContainer` in this
|
||||
`ScrollablePane`. (Otherwise, it's possible that the completion menu
|
||||
is clipped.)
|
||||
|
||||
:param content: The content container.
|
||||
:param scrolloffset: Try to keep the cursor within this distance from the
|
||||
top/bottom (left/right offset is not used).
|
||||
:param keep_cursor_visible: When `True`, automatically scroll the pane so
|
||||
that the cursor (of the focused window) is always visible.
|
||||
:param keep_focused_window_visible: When `True`, automatically scroll th e
|
||||
pane so that the focused window is visible, or as much visible as
|
||||
possible if it doen't completely fit the screen.
|
||||
:param max_available_height: Always constraint the height to this amount
|
||||
for performance reasons.
|
||||
:param width: When given, use this width instead of looking at the children.
|
||||
:param height: When given, use this height instead of looking at the children.
|
||||
:param show_scrollbar: When `True` display a scrollbar on the right.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
content: Container,
|
||||
scroll_offsets: Optional[ScrollOffsets] = None,
|
||||
keep_cursor_visible: FilterOrBool = True,
|
||||
keep_focused_window_visible: FilterOrBool = True,
|
||||
max_available_height: int = MAX_AVAILABLE_HEIGHT,
|
||||
width: AnyDimension = None,
|
||||
height: AnyDimension = None,
|
||||
show_scrollbar: FilterOrBool = True,
|
||||
display_arrows: FilterOrBool = True,
|
||||
up_arrow_symbol: str = "^",
|
||||
down_arrow_symbol: str = "v",
|
||||
) -> None:
|
||||
self.content = content
|
||||
self.scroll_offsets = scroll_offsets or ScrollOffsets(top=1, bottom=1)
|
||||
self.keep_cursor_visible = to_filter(keep_cursor_visible)
|
||||
self.keep_focused_window_visible = to_filter(keep_focused_window_visible)
|
||||
self.max_available_height = max_available_height
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.show_scrollbar = to_filter(show_scrollbar)
|
||||
self.display_arrows = to_filter(display_arrows)
|
||||
self.up_arrow_symbol = up_arrow_symbol
|
||||
self.down_arrow_symbol = down_arrow_symbol
|
||||
|
||||
self.vertical_scroll = 0
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ScrollablePane({self.content!r})"
|
||||
|
||||
def reset(self) -> None:
|
||||
self.content.reset()
|
||||
|
||||
def preferred_width(self, max_available_width: int) -> Dimension:
|
||||
if self.width is not None:
|
||||
return to_dimension(self.width)
|
||||
|
||||
# We're only scrolling vertical. So the preferred width is equal to
|
||||
# that of the content.
|
||||
content_width = self.content.preferred_width(max_available_width)
|
||||
|
||||
# If a scrollbar needs to be displayed, add +1 to the content width.
|
||||
if self.show_scrollbar():
|
||||
return sum_layout_dimensions([Dimension.exact(1), content_width])
|
||||
|
||||
return content_width
|
||||
|
||||
def preferred_height(self, width: int, max_available_height: int) -> Dimension:
|
||||
if self.height is not None:
|
||||
return to_dimension(self.height)
|
||||
|
||||
# Prefer a height large enough so that it fits all the content. If not,
|
||||
# we'll make the pane scrollable.
|
||||
if self.show_scrollbar():
|
||||
# If `show_scrollbar` is set. Always reserve space for the scrollbar.
|
||||
width -= 1
|
||||
|
||||
dimension = self.content.preferred_height(width, self.max_available_height)
|
||||
|
||||
# Only take 'preferred' into account. Min/max can be anything.
|
||||
return Dimension(min=0, preferred=dimension.preferred)
|
||||
|
||||
def write_to_screen(
|
||||
self,
|
||||
screen: Screen,
|
||||
mouse_handlers: MouseHandlers,
|
||||
write_position: WritePosition,
|
||||
parent_style: str,
|
||||
erase_bg: bool,
|
||||
z_index: Optional[int],
|
||||
) -> None:
|
||||
"""
|
||||
Render scrollable pane content.
|
||||
|
||||
This works by rendering on an off-screen canvas, and copying over the
|
||||
visible region.
|
||||
"""
|
||||
show_scrollbar = self.show_scrollbar()
|
||||
|
||||
if show_scrollbar:
|
||||
virtual_width = write_position.width - 1
|
||||
else:
|
||||
virtual_width = write_position.width
|
||||
|
||||
# Compute preferred height again.
|
||||
virtual_height = self.content.preferred_height(
|
||||
virtual_width, self.max_available_height
|
||||
).preferred
|
||||
|
||||
# Ensure virtual height is at least the available height.
|
||||
virtual_height = max(virtual_height, write_position.height)
|
||||
virtual_height = min(virtual_height, self.max_available_height)
|
||||
|
||||
# First, write the content to a virtual screen, then copy over the
|
||||
# visible part to the real screen.
|
||||
temp_screen = Screen(default_char=Char(char=" ", style=parent_style))
|
||||
temp_write_position = WritePosition(
|
||||
xpos=0, ypos=0, width=virtual_width, height=virtual_height
|
||||
)
|
||||
|
||||
temp_mouse_handlers = MouseHandlers()
|
||||
|
||||
self.content.write_to_screen(
|
||||
temp_screen,
|
||||
temp_mouse_handlers,
|
||||
temp_write_position,
|
||||
parent_style,
|
||||
erase_bg,
|
||||
z_index,
|
||||
)
|
||||
temp_screen.draw_all_floats()
|
||||
|
||||
# If anything in the virtual screen is focused, move vertical scroll to
|
||||
from prompt_toolkit.application import get_app
|
||||
|
||||
focused_window = get_app().layout.current_window
|
||||
|
||||
try:
|
||||
visible_win_write_pos = temp_screen.visible_windows_to_write_positions[
|
||||
focused_window
|
||||
]
|
||||
except KeyError:
|
||||
pass # No window focused here. Don't scroll.
|
||||
else:
|
||||
# Make sure this window is visible.
|
||||
self._make_window_visible(
|
||||
write_position.height,
|
||||
virtual_height,
|
||||
visible_win_write_pos,
|
||||
temp_screen.cursor_positions.get(focused_window),
|
||||
)
|
||||
|
||||
# Copy over virtual screen and zero width escapes to real screen.
|
||||
self._copy_over_screen(screen, temp_screen, write_position, virtual_width)
|
||||
|
||||
# Copy over mouse handlers.
|
||||
self._copy_over_mouse_handlers(
|
||||
mouse_handlers, temp_mouse_handlers, write_position, virtual_width
|
||||
)
|
||||
|
||||
# Set screen.width/height.
|
||||
ypos = write_position.ypos
|
||||
xpos = write_position.xpos
|
||||
|
||||
screen.width = max(screen.width, xpos + virtual_width)
|
||||
screen.height = max(screen.height, ypos + write_position.height)
|
||||
|
||||
# Copy over window write positions.
|
||||
self._copy_over_write_positions(screen, temp_screen, write_position)
|
||||
|
||||
if temp_screen.show_cursor:
|
||||
screen.show_cursor = True
|
||||
|
||||
# Copy over cursor positions, if they are visible.
|
||||
for window, point in temp_screen.cursor_positions.items():
|
||||
if (
|
||||
0 <= point.x < write_position.width
|
||||
and self.vertical_scroll
|
||||
<= point.y
|
||||
< write_position.height + self.vertical_scroll
|
||||
):
|
||||
screen.cursor_positions[window] = Point(
|
||||
x=point.x + xpos, y=point.y + ypos - self.vertical_scroll
|
||||
)
|
||||
|
||||
# Copy over menu positions, but clip them to the visible area.
|
||||
for window, point in temp_screen.menu_positions.items():
|
||||
screen.menu_positions[window] = self._clip_point_to_visible_area(
|
||||
Point(x=point.x + xpos, y=point.y + ypos - self.vertical_scroll),
|
||||
write_position,
|
||||
)
|
||||
|
||||
# Draw scrollbar.
|
||||
if show_scrollbar:
|
||||
self._draw_scrollbar(
|
||||
write_position,
|
||||
virtual_height,
|
||||
screen,
|
||||
)
|
||||
|
||||
def _clip_point_to_visible_area(
|
||||
self, point: Point, write_position: WritePosition
|
||||
) -> Point:
|
||||
"""
|
||||
Ensure that the cursor and menu positions always are always reported
|
||||
"""
|
||||
if point.x < write_position.xpos:
|
||||
point = point._replace(x=write_position.xpos)
|
||||
if point.y < write_position.ypos:
|
||||
point = point._replace(y=write_position.ypos)
|
||||
if point.x >= write_position.xpos + write_position.width:
|
||||
point = point._replace(x=write_position.xpos + write_position.width - 1)
|
||||
if point.y >= write_position.ypos + write_position.height:
|
||||
point = point._replace(y=write_position.ypos + write_position.height - 1)
|
||||
|
||||
return point
|
||||
|
||||
def _copy_over_screen(
|
||||
self,
|
||||
screen: Screen,
|
||||
temp_screen: Screen,
|
||||
write_position: WritePosition,
|
||||
virtual_width: int,
|
||||
) -> None:
|
||||
"""
|
||||
Copy over visible screen content and "zero width escape sequences".
|
||||
"""
|
||||
ypos = write_position.ypos
|
||||
xpos = write_position.xpos
|
||||
|
||||
for y in range(write_position.height):
|
||||
temp_row = temp_screen.data_buffer[y + self.vertical_scroll]
|
||||
row = screen.data_buffer[y + ypos]
|
||||
temp_zero_width_escapes = temp_screen.zero_width_escapes[
|
||||
y + self.vertical_scroll
|
||||
]
|
||||
zero_width_escapes = screen.zero_width_escapes[y + ypos]
|
||||
|
||||
for x in range(virtual_width):
|
||||
row[x + xpos] = temp_row[x]
|
||||
|
||||
if x in temp_zero_width_escapes:
|
||||
zero_width_escapes[x + xpos] = temp_zero_width_escapes[x]
|
||||
|
||||
def _copy_over_mouse_handlers(
|
||||
self,
|
||||
mouse_handlers: MouseHandlers,
|
||||
temp_mouse_handlers: MouseHandlers,
|
||||
write_position: WritePosition,
|
||||
virtual_width: int,
|
||||
) -> None:
|
||||
"""
|
||||
Copy over mouse handlers from virtual screen to real screen.
|
||||
|
||||
Note: we take `virtual_width` because we don't want to copy over mouse
|
||||
handlers that we possibly have behind the scrollbar.
|
||||
"""
|
||||
ypos = write_position.ypos
|
||||
xpos = write_position.xpos
|
||||
|
||||
# Cache mouse handlers when wrapping them. Very often the same mouse
|
||||
# handler is registered for many positions.
|
||||
mouse_handler_wrappers: Dict[MouseHandler, MouseHandler] = {}
|
||||
|
||||
def wrap_mouse_handler(handler: MouseHandler) -> MouseHandler:
|
||||
"Wrap mouse handler. Translate coordinates in `MouseEvent`."
|
||||
if handler not in mouse_handler_wrappers:
|
||||
|
||||
def new_handler(event: MouseEvent) -> None:
|
||||
new_event = MouseEvent(
|
||||
position=Point(
|
||||
x=event.position.x - xpos,
|
||||
y=event.position.y + self.vertical_scroll - ypos,
|
||||
),
|
||||
event_type=event.event_type,
|
||||
button=event.button,
|
||||
modifiers=event.modifiers,
|
||||
)
|
||||
handler(new_event)
|
||||
|
||||
mouse_handler_wrappers[handler] = new_handler
|
||||
return mouse_handler_wrappers[handler]
|
||||
|
||||
# Copy handlers.
|
||||
mouse_handlers_dict = mouse_handlers.mouse_handlers
|
||||
temp_mouse_handlers_dict = temp_mouse_handlers.mouse_handlers
|
||||
|
||||
for y in range(write_position.height):
|
||||
if y in temp_mouse_handlers_dict:
|
||||
temp_mouse_row = temp_mouse_handlers_dict[y + self.vertical_scroll]
|
||||
mouse_row = mouse_handlers_dict[y + ypos]
|
||||
for x in range(virtual_width):
|
||||
if x in temp_mouse_row:
|
||||
mouse_row[x + xpos] = wrap_mouse_handler(temp_mouse_row[x])
|
||||
|
||||
def _copy_over_write_positions(
|
||||
self, screen: Screen, temp_screen: Screen, write_position: WritePosition
|
||||
) -> None:
|
||||
"""
|
||||
Copy over window write positions.
|
||||
"""
|
||||
ypos = write_position.ypos
|
||||
xpos = write_position.xpos
|
||||
|
||||
for win, write_pos in temp_screen.visible_windows_to_write_positions.items():
|
||||
screen.visible_windows_to_write_positions[win] = WritePosition(
|
||||
xpos=write_pos.xpos + xpos,
|
||||
ypos=write_pos.ypos + ypos - self.vertical_scroll,
|
||||
# TODO: if the window is only partly visible, then truncate width/height.
|
||||
# This could be important if we have nested ScrollablePanes.
|
||||
height=write_pos.height,
|
||||
width=write_pos.width,
|
||||
)
|
||||
|
||||
def is_modal(self) -> bool:
|
||||
return self.content.is_modal()
|
||||
|
||||
def get_key_bindings(self) -> Optional[KeyBindingsBase]:
|
||||
return self.content.get_key_bindings()
|
||||
|
||||
def get_children(self) -> List["Container"]:
|
||||
return [self.content]
|
||||
|
||||
def _make_window_visible(
|
||||
self,
|
||||
visible_height: int,
|
||||
virtual_height: int,
|
||||
visible_win_write_pos: WritePosition,
|
||||
cursor_position: Optional[Point],
|
||||
) -> None:
|
||||
"""
|
||||
Scroll the scrollable pane, so that this window becomes visible.
|
||||
|
||||
:param visible_height: Height of this `ScrollablePane` that is rendered.
|
||||
:param virtual_height: Height of the virtual, temp screen.
|
||||
:param visible_win_write_pos: `WritePosition` of the nested window on the
|
||||
temp screen.
|
||||
:param cursor_position: The location of the cursor position of this
|
||||
window on the temp screen.
|
||||
"""
|
||||
# Start with maximum allowed scroll range, and then reduce according to
|
||||
# the focused window and cursor position.
|
||||
min_scroll = 0
|
||||
max_scroll = virtual_height - visible_height
|
||||
|
||||
if self.keep_cursor_visible():
|
||||
# Reduce min/max scroll according to the cursor in the focused window.
|
||||
if cursor_position is not None:
|
||||
offsets = self.scroll_offsets
|
||||
cpos_min_scroll = (
|
||||
cursor_position.y - visible_height + 1 + offsets.bottom
|
||||
)
|
||||
cpos_max_scroll = cursor_position.y - offsets.top
|
||||
min_scroll = max(min_scroll, cpos_min_scroll)
|
||||
max_scroll = max(0, min(max_scroll, cpos_max_scroll))
|
||||
|
||||
if self.keep_focused_window_visible():
|
||||
# Reduce min/max scroll according to focused window position.
|
||||
# If the window is small enough, bot the top and bottom of the window
|
||||
# should be visible.
|
||||
if visible_win_write_pos.height <= visible_height:
|
||||
window_min_scroll = (
|
||||
visible_win_write_pos.ypos
|
||||
+ visible_win_write_pos.height
|
||||
- visible_height
|
||||
)
|
||||
window_max_scroll = visible_win_write_pos.ypos
|
||||
else:
|
||||
# Window does not fit on the screen. Make sure at least the whole
|
||||
# screen is occupied with this window, and nothing else is shown.
|
||||
window_min_scroll = visible_win_write_pos.ypos
|
||||
window_max_scroll = (
|
||||
visible_win_write_pos.ypos
|
||||
+ visible_win_write_pos.height
|
||||
- visible_height
|
||||
)
|
||||
|
||||
min_scroll = max(min_scroll, window_min_scroll)
|
||||
max_scroll = min(max_scroll, window_max_scroll)
|
||||
|
||||
if min_scroll > max_scroll:
|
||||
min_scroll = max_scroll # Should not happen.
|
||||
|
||||
# Finally, properly clip the vertical scroll.
|
||||
if self.vertical_scroll > max_scroll:
|
||||
self.vertical_scroll = max_scroll
|
||||
if self.vertical_scroll < min_scroll:
|
||||
self.vertical_scroll = min_scroll
|
||||
|
||||
def _draw_scrollbar(
|
||||
self, write_position: WritePosition, content_height: int, screen: Screen
|
||||
) -> None:
|
||||
"""
|
||||
Draw the scrollbar on the screen.
|
||||
|
||||
Note: There is some code duplication with the `ScrollbarMargin`
|
||||
implementation.
|
||||
"""
|
||||
|
||||
window_height = write_position.height
|
||||
display_arrows = self.display_arrows()
|
||||
|
||||
if display_arrows:
|
||||
window_height -= 2
|
||||
|
||||
try:
|
||||
fraction_visible = write_position.height / float(content_height)
|
||||
fraction_above = self.vertical_scroll / float(content_height)
|
||||
|
||||
scrollbar_height = int(
|
||||
min(window_height, max(1, window_height * fraction_visible))
|
||||
)
|
||||
scrollbar_top = int(window_height * fraction_above)
|
||||
except ZeroDivisionError:
|
||||
return
|
||||
else:
|
||||
|
||||
def is_scroll_button(row: int) -> bool:
|
||||
"True if we should display a button on this row."
|
||||
return scrollbar_top <= row <= scrollbar_top + scrollbar_height
|
||||
|
||||
xpos = write_position.xpos + write_position.width - 1
|
||||
ypos = write_position.ypos
|
||||
data_buffer = screen.data_buffer
|
||||
|
||||
# Up arrow.
|
||||
if display_arrows:
|
||||
data_buffer[ypos][xpos] = Char(
|
||||
self.up_arrow_symbol, "class:scrollbar.arrow"
|
||||
)
|
||||
ypos += 1
|
||||
|
||||
# Scrollbar body.
|
||||
scrollbar_background = "class:scrollbar.background"
|
||||
scrollbar_background_start = "class:scrollbar.background,scrollbar.start"
|
||||
scrollbar_button = "class:scrollbar.button"
|
||||
scrollbar_button_end = "class:scrollbar.button,scrollbar.end"
|
||||
|
||||
for i in range(window_height):
|
||||
style = ""
|
||||
if is_scroll_button(i):
|
||||
if not is_scroll_button(i + 1):
|
||||
# Give the last cell a different style, because we want
|
||||
# to underline this.
|
||||
style = scrollbar_button_end
|
||||
else:
|
||||
style = scrollbar_button
|
||||
else:
|
||||
if is_scroll_button(i + 1):
|
||||
style = scrollbar_background_start
|
||||
else:
|
||||
style = scrollbar_background
|
||||
|
||||
data_buffer[ypos][xpos] = Char(" ", style)
|
||||
ypos += 1
|
||||
|
||||
# Down arrow
|
||||
if display_arrows:
|
||||
data_buffer[ypos][xpos] = Char(
|
||||
self.down_arrow_symbol, "class:scrollbar.arrow"
|
||||
)
|
80
.venv/Lib/site-packages/prompt_toolkit/layout/utils.py
Normal file
80
.venv/Lib/site-packages/prompt_toolkit/layout/utils.py
Normal file
@ -0,0 +1,80 @@
|
||||
from typing import TYPE_CHECKING, Iterable, List, TypeVar, Union, cast, overload
|
||||
|
||||
from prompt_toolkit.formatted_text.base import OneStyleAndTextTuple
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import SupportsIndex
|
||||
|
||||
__all__ = [
|
||||
"explode_text_fragments",
|
||||
]
|
||||
|
||||
_T = TypeVar("_T", bound=OneStyleAndTextTuple)
|
||||
|
||||
|
||||
class _ExplodedList(List[_T]):
|
||||
"""
|
||||
Wrapper around a list, that marks it as 'exploded'.
|
||||
|
||||
As soon as items are added or the list is extended, the new items are
|
||||
automatically exploded as well.
|
||||
"""
|
||||
|
||||
exploded = True
|
||||
|
||||
def append(self, item: _T) -> None:
|
||||
self.extend([item])
|
||||
|
||||
def extend(self, lst: Iterable[_T]) -> None:
|
||||
super().extend(explode_text_fragments(lst))
|
||||
|
||||
def insert(self, index: "SupportsIndex", item: _T) -> None:
|
||||
raise NotImplementedError # TODO
|
||||
|
||||
# TODO: When creating a copy() or [:], return also an _ExplodedList.
|
||||
|
||||
@overload
|
||||
def __setitem__(self, index: "SupportsIndex", value: _T) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __setitem__(self, index: slice, value: Iterable[_T]) -> None:
|
||||
...
|
||||
|
||||
def __setitem__(
|
||||
self, index: Union["SupportsIndex", slice], value: Union[_T, Iterable[_T]]
|
||||
) -> None:
|
||||
"""
|
||||
Ensure that when `(style_str, 'long string')` is set, the string will be
|
||||
exploded.
|
||||
"""
|
||||
if not isinstance(index, slice):
|
||||
int_index = index.__index__()
|
||||
index = slice(int_index, int_index + 1)
|
||||
if isinstance(value, tuple): # In case of `OneStyleAndTextTuple`.
|
||||
value = cast("List[_T]", [value])
|
||||
|
||||
super().__setitem__(index, explode_text_fragments(cast("Iterable[_T]", value)))
|
||||
|
||||
|
||||
def explode_text_fragments(fragments: Iterable[_T]) -> _ExplodedList[_T]:
|
||||
"""
|
||||
Turn a list of (style_str, text) tuples into another list where each string is
|
||||
exactly one character.
|
||||
|
||||
It should be fine to call this function several times. Calling this on a
|
||||
list that is already exploded, is a null operation.
|
||||
|
||||
:param fragments: List of (style, text) tuples.
|
||||
"""
|
||||
# When the fragments is already exploded, don't explode again.
|
||||
if isinstance(fragments, _ExplodedList):
|
||||
return fragments
|
||||
|
||||
result: List[_T] = []
|
||||
|
||||
for style, string, *rest in fragments: # type: ignore
|
||||
for c in string: # type: ignore
|
||||
result.append((style, c, *rest)) # type: ignore
|
||||
|
||||
return _ExplodedList(result)
|
Reference in New Issue
Block a user