Uploaded Test files
This commit is contained in:
parent
f584ad9d97
commit
2e81cb7d99
16627 changed files with 2065359 additions and 102444 deletions
142
venv/Lib/site-packages/prompt_toolkit/layout/__init__.py
Normal file
142
venv/Lib/site-packages/prompt_toolkit/layout/__init__.py
Normal file
|
@ -0,0 +1,142 @@
|
|||
"""
|
||||
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
|
||||
|
||||
__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",
|
||||
# Controls.
|
||||
"BufferControl",
|
||||
"SearchBufferControl",
|
||||
"DummyControl",
|
||||
"FormattedTextControl",
|
||||
"UIControl",
|
||||
"UIContent",
|
||||
# Margins.
|
||||
"Margin",
|
||||
"NumberedMargin",
|
||||
"ScrollbarMargin",
|
||||
"ConditionalMargin",
|
||||
"PromptMargin",
|
||||
# Menus.
|
||||
"CompletionsMenu",
|
||||
"MultiColumnCompletionsMenu",
|
||||
]
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2702
venv/Lib/site-packages/prompt_toolkit/layout/containers.py
Normal file
2702
venv/Lib/site-packages/prompt_toolkit/layout/containers.py
Normal file
File diff suppressed because it is too large
Load diff
947
venv/Lib/site-packages/prompt_toolkit/layout/controls.py
Normal file
947
venv/Lib/site-packages/prompt_toolkit/layout/controls.py
Normal file
|
@ -0,0 +1,947 @@
|
|||
"""
|
||||
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 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
|
||||
from prompt_toolkit.utils import Event
|
||||
|
||||
# The only two return values for a mouse hander are `None` and
|
||||
# `NotImplemented`. For the type checker it's best to annotate this as
|
||||
# `object`. (The consumer never expects a more specific instance: checking
|
||||
# for NotImplemented can be done using `is NotImplemented`.)
|
||||
NotImplementedOrNone = object
|
||||
# Other non-working options are:
|
||||
# * Optional[Literal[NotImplemented]]
|
||||
# --> Doesn't work, Literal can't take an Any.
|
||||
# * None
|
||||
# --> Doesn't work. We can't assign the result of a function that
|
||||
# returns `None` to a variable.
|
||||
# * Any
|
||||
# --> Works, but too broad.
|
||||
|
||||
|
||||
__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 "%s(%r)" % (self.__class__.__name__, self.text)
|
||||
|
||||
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]:
|
||||
|
||||
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
|
||||
|
||||
|
||||
_ProcessedLine = NamedTuple(
|
||||
"_ProcessedLine",
|
||||
[
|
||||
("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] = 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 "<%s buffer=%r at %r>" % (self.__class__.__name__, self.buffer, id(self))
|
||||
|
||||
@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
|
||||
|
||||
# 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_UP:
|
||||
# When the cursor was moved to another place, select the text.
|
||||
# (The >1 is actually a small but acceptable workaround for
|
||||
# selecting text in Vi navigation mode. In navigation mode,
|
||||
# the cursor can never be after the text, so the cursor
|
||||
# will be repositioned automatically.)
|
||||
if abs(buffer.cursor_position - index) > 1:
|
||||
buffer.start_selection(selection_type=SelectionType.CHARACTERS)
|
||||
buffer.cursor_position = index
|
||||
|
||||
# Select word around cursor on double click.
|
||||
# Two MOUSE_UP events in a short timespan are considered a double click.
|
||||
double_click = (
|
||||
self._last_click_timestamp
|
||||
and time.time() - self._last_click_timestamp < 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)
|
214
venv/Lib/site-packages/prompt_toolkit/layout/dimension.py
Normal file
214
venv/Lib/site-packages/prompt_toolkit/layout/dimension.py
Normal file
|
@ -0,0 +1,214 @@
|
|||
"""
|
||||
Layout dimensions are used to give the minimum, maximum and preferred
|
||||
dimensions for containers and controls.
|
||||
"""
|
||||
from typing import Any, Callable, List, Optional, Union
|
||||
|
||||
__all__ = [
|
||||
"Dimension",
|
||||
"D",
|
||||
"sum_layout_dimensions",
|
||||
"max_layout_dimensions",
|
||||
"AnyDimension",
|
||||
"to_dimension",
|
||||
"is_dimension",
|
||||
]
|
||||
|
||||
|
||||
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) -> bool:
|
||||
"""
|
||||
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)
|
417
venv/Lib/site-packages/prompt_toolkit/layout/layout.py
Normal file
417
venv/Lib/site-packages/prompt_toolkit/layout/layout.py
Normal file
|
@ -0,0 +1,417 @@
|
|||
"""
|
||||
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 "Layout(%r, current_window=%r)" % (self.container, self.current_window)
|
||||
|
||||
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(
|
||||
"Couldn't find Buffer in the current layout: %r." % (value,)
|
||||
)
|
||||
|
||||
# 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(
|
||||
"Couldn't find Buffer in the current layout: %r." % (value,)
|
||||
)
|
||||
|
||||
# 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(
|
||||
"Invalid value. Container cannot be focused: %r" % (value,)
|
||||
)
|
||||
|
||||
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):
|
||||
" 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.
|
||||
"""
|
||||
for i in walk(self.container):
|
||||
yield i
|
||||
|
||||
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]
|
||||
|
||||
for container in walk(root):
|
||||
yield container
|
||||
|
||||
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
|
720
venv/Lib/site-packages/prompt_toolkit/layout/menus.py
Normal file
720
venv/Lib/site-packages/prompt_toolkit/layout/menus.py
Normal file
|
@ -0,0 +1,720 @@
|
|||
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 = object
|
||||
|
||||
__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 %s %s" % (
|
||||
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,40 @@
|
|||
from collections import defaultdict
|
||||
from itertools import product
|
||||
from typing import Callable, DefaultDict, Tuple
|
||||
|
||||
from prompt_toolkit.mouse_events import MouseEvent
|
||||
|
||||
__all__ = [
|
||||
"MouseHandlers",
|
||||
]
|
||||
|
||||
|
||||
class MouseHandlers:
|
||||
"""
|
||||
Two dimensional raster of callbacks for mouse events.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
def dummy_callback(mouse_event: MouseEvent) -> None:
|
||||
"""
|
||||
:param mouse_event: `MouseEvent` instance.
|
||||
"""
|
||||
|
||||
# Map (x,y) tuples to handlers.
|
||||
self.mouse_handlers: DefaultDict[
|
||||
Tuple[int, int], Callable[[MouseEvent], None]
|
||||
] = 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], None],
|
||||
) -> None:
|
||||
"""
|
||||
Set mouse handler for a region.
|
||||
"""
|
||||
for x, y in product(range(x_min, x_max), range(y_min, y_max)):
|
||||
self.mouse_handlers[x, y] = 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
316
venv/Lib/site-packages/prompt_toolkit/layout/screen.py
Normal file
316
venv/Lib/site-packages/prompt_toolkit/layout/screen.py
Normal file
|
@ -0,0 +1,316 @@
|
|||
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 = ""):
|
||||
# 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)
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
return self.char == other.char and self.style == other.style
|
||||
|
||||
def __ne__(self, other) -> 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
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "%s(%r, %r)" % (self.__class__.__name__, self.char, self.style)
|
||||
|
||||
|
||||
_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: List["Window"] = []
|
||||
|
||||
# List of (z_index, draw_func)
|
||||
self._draw_float_functions: List[Tuple[int, Callable[[], None]]] = []
|
||||
|
||||
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 "%s(x=%r, y=%r, width=%r, height=%r)" % (
|
||||
self.__class__.__name__,
|
||||
self.xpos,
|
||||
self.ypos,
|
||||
self.width,
|
||||
self.height,
|
||||
)
|
76
venv/Lib/site-packages/prompt_toolkit/layout/utils.py
Normal file
76
venv/Lib/site-packages/prompt_toolkit/layout/utils.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
from typing import Iterable, List, TypeVar, Union, cast, overload
|
||||
|
||||
from prompt_toolkit.formatted_text.base import OneStyleAndTextTuple
|
||||
|
||||
__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: int, item: _T) -> None:
|
||||
raise NotImplementedError # TODO
|
||||
|
||||
# TODO: When creating a copy() or [:], return also an _ExplodedList.
|
||||
|
||||
@overload
|
||||
def __setitem__(self, index: int, value: _T) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __setitem__(self, index: slice, value: Iterable[_T]) -> None:
|
||||
...
|
||||
|
||||
def __setitem__(
|
||||
self, index: Union[int, 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):
|
||||
index = slice(index, 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)
|
Loading…
Add table
Add a link
Reference in a new issue