721 lines
25 KiB
Python
721 lines
25 KiB
Python
|
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 []
|