Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ docs/_build

# pycharm metadata
.idea

# vscode
.vscode/
2 changes: 1 addition & 1 deletion prompt_toolkit/completion/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def display_meta(self) -> StyleAndTextTuples:
" Return meta-text. (This is lazy when using a callable). "
from prompt_toolkit.formatted_text import to_formatted_text

return to_formatted_text(self._display_meta or "")
return to_formatted_text(self._display_meta)

@property
def display_meta_text(self) -> str:
Expand Down
50 changes: 40 additions & 10 deletions prompt_toolkit/layout/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -789,10 +789,30 @@ def preferred_width(self, max_available_width: int) -> Dimension:
def preferred_height(self, width: int, max_available_height: int) -> Dimension:
"""
Return the preferred height of the float container.
(We don't care about the height of the floats, they should always fit
into the dimensions provided by the container.)
"""
return self.content.preferred_height(width, max_available_height)
This includes the height of the child floats flagged for dont_shrink_height.
"""
content_ph = self.content.preferred_height(width, max_available_height)

def get_float_ph():
for f in self.floats:
if f.dont_shrink_height:
h = f.content.preferred_height(
width, max_available_height
).preferred
if h > 0 and getattr(f, "ycursor", False):
h += (
content_ph.preferred
) # Assumes float is tied to current input point in background
else:
h = 0
yield h # FIXME: should yield just selected floats, handle yield none case.

floats_ph = max(get_float_ph())
return Dimension(
min=content_ph.min,
max=content_ph.max,
preferred=max(content_ph.preferred, floats_ph),
)

def write_to_screen(
self,
Expand Down Expand Up @@ -896,8 +916,12 @@ def _draw_float(
# Near x position of cursor.
elif fl.xcursor:
if fl_width is None:
width = fl.content.preferred_width(write_position.width).preferred
width = min(write_position.width, width)
width = min(
fl.content.preferred_width(
write_position.width - cursor_position.x
).preferred,
write_position.width,
)
else:
width = fl_width

Expand Down Expand Up @@ -945,9 +969,12 @@ def _draw_float(
else:
height = fl_height

# Reduce height if not enough space. (We can use the height
# when the content requires it.)
if height > write_position.height - ypos:
# Reduce height if not enough space.
# Dan't do this for dont_shrink_height floats, even if they would fit *above* cursor,
# because render has already determined height of FloatContainer and moving the
# float above cursor would still leave a blank line at bottom.
# For "regular" floats, either fit above cursor or trim to fit below.
if (not fl.dont_shrink_height) and height > write_position.height - ypos:
if write_position.height - ypos + 1 >= ypos:
# When the space below the cursor is more than
# the space above, just reduce the height.
Expand Down Expand Up @@ -1034,7 +1061,8 @@ class Float:

:param width: :class:`.Dimension` or callable which returns a :class:`.Dimension`.
:param height: :class:`.Dimension` or callable which returns a :class:`.Dimension`.

:param dont_shrink_height" When `True`, ensure all rows of content is displayed.
Default `False`.
:param left: Distance to the left edge of the :class:`.FloatContainer`.
:param right: Distance to the right edge of the :class:`.FloatContainer`.
:param top: Distance to the top of the :class:`.FloatContainer`.
Expand All @@ -1060,6 +1088,7 @@ def __init__(
left: Optional[int] = None,
width: Optional[Union[int, Callable[[], int]]] = None,
height: Optional[Union[int, Callable[[], int]]] = None,
dont_shrink_height: bool = False,
xcursor: bool = False,
ycursor: bool = False,
attach_to_window: Optional[AnyContainer] = None,
Expand All @@ -1078,6 +1107,7 @@ def __init__(

self.width = width
self.height = height
self.dont_shrink_height = dont_shrink_height

self.xcursor = xcursor
self.ycursor = ycursor
Expand Down
160 changes: 100 additions & 60 deletions prompt_toolkit/layout/menus.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,33 +304,46 @@ class MultiColumnCompletionMenuControl(UIControl):
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 min_rows: indicates how many rows will be available.
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 max_rows: maximum height of the completion menu. Must be > 0.
: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.
_arrow_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:
def __init__(
self,
min_rows: int = 1,
max_rows: int = 3, # 3 is for backward comapt.
suggested_max_column_width: int = 30,
) -> None:
assert min_rows >= 1

self.min_rows = min_rows
self.max_rows = max_rows
self.suggested_max_column_width = suggested_max_column_width
self.scroll = 0

# results cached during current render cycle
self._cur_num_cols = 0
self._cur_col_width = 0
self._cur_preferred_width = 0
self._cur_preferred_height = 0

# Info of last rendering.
# FIXME: where is this used?
self._rendered_rows = 0
self._rendered_columns = 0
self._total_columns = 0
Expand All @@ -348,27 +361,23 @@ def has_focus(self) -> bool:
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.
as will fit horizontally.
Hueristics used when scanning completions to prevent a few really wide
ones from uglifying the menu.
"""
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))
self._cur_col_width = self._get_column_width(complete_state)
self._cur_num_cols = (
max_available_width - self._arrow_margin
) // self._cur_col_width
# FIXME: doesn't obey min_rows
self._cur_preferred_width = (
self._arrow_margin + self._cur_num_cols * self._cur_col_width
)

# 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
return self._cur_preferred_width

def preferred_height(
self,
Expand All @@ -384,10 +393,16 @@ def preferred_height(
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)
if width != self._cur_preferred_width:
# does happen often
self.preferred_width(width)

return int(math.ceil(len(complete_state.completions) / float(column_count)))
self._cur_preferred_height = min(
math.ceil(len(complete_state.completions) / float(self._cur_num_cols)),
self.max_rows,
max_available_height,
)
return self._cur_preferred_height

def create_content(self, width: int, height: int) -> UIContent:
"""
Expand All @@ -397,9 +412,6 @@ def create_content(self, width: int, height: int) -> UIContent:
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(
Expand All @@ -417,36 +429,29 @@ def is_current_completion(completion: Completion) -> bool:
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)
if width != self._cur_preferred_width:
self.preferred_width(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)
assert self._cur_num_cols >= 1
assert self._cur_col_width < width
assert (
height <= self._cur_preferred_height
), "rendered height > last preferred height?"

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)
selected_column, max(self.scroll, selected_column - self._cur_num_cols + 1)
)

render_left_arrow = self.scroll > 0
render_right_arrow = self.scroll < len(rows_[0]) - visible_columns
render_right_arrow = self.scroll < len(rows_[0]) - self._cur_num_cols

# Write completions to screen.
self._render_pos_to_completion = {}
fragments_for_line = []

for row_index, row in enumerate(rows_):
Expand All @@ -462,19 +467,22 @@ def is_current_completion(completion: Completion) -> bool:
fragments.append(("", " "))

# Draw row content.
for column_index, c in enumerate(row[self.scroll :][:visible_columns]):
for column_index, c in enumerate(row[self.scroll :][: self._cur_num_cols]):
if c is not None:
fragments += _get_menu_item_fragments(
c, is_current_completion(c), column_width, space_after=False
c,
is_current_completion(c),
self._cur_col_width,
space_after=False,
)

# Remember render position for mouse click handler.
for x in range(column_width):
for x in range(self._cur_col_width):
self._render_pos_to_completion[
(column_index * column_width + x, row_index)
(column_index * self._cur_col_width + x, row_index)
] = c
else:
fragments.append(("class:completion", " " * column_width))
fragments.append(("class:completion", " " * self._cur_col_width))

# Draw trailing padding for this row.
# (_get_menu_item_fragments only returns padding on the left.)
Expand All @@ -493,12 +501,15 @@ def is_current_completion(completion: Completion) -> bool:
)

self._rendered_rows = height
self._rendered_columns = visible_columns
self._rendered_columns = self._cur_num_cols
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
self._cur_col_width * self._cur_num_cols
+ render_left_arrow
+ render_right_arrow
+ 1
)

def get_line(i: int) -> StyleAndTextTuples:
Expand All @@ -509,10 +520,32 @@ def get_line(i: int) -> StyleAndTextTuples:
def _get_column_width(self, complete_state: CompletionState) -> int:
"""
Return the width of each column.
Prevent a few very long outliers from forcing too few columns.
Return not max but 90th percentile largest (e.g max of 9, but 2nd largest of 10)
"""
return max(get_cwidth(c.display_text) for c in complete_state.completions) + 1

def mouse_handler(self, mouse_event: MouseEvent) -> "NotImplementedOrNone":
top_n = (
1 + len(complete_state.completions) // 10
) # number of top values to learn

if top_n <= 1: # max of 1-10 completions
ret_val = (
max(get_cwidth(c.display_text) for c in complete_state.completions) + 1
)
else: # 90th percentile longest length of more.
top_n_list = list([0 for i in range(top_n)])
for c in complete_state.completions:
cur_len = get_cwidth(c.display_text)
if cur_len > top_n_list[0]:
top_n_list[0] = cur_len
top_n_list.sort()
ret_val = top_n_list[0] + 1
# FIXME: should consider self.suggested_max_column_width, too.
return ret_val

def mouse_handler(
self, mouse_event: MouseEvent
) -> Optional["NotImplementedOrNone"]:
"""
Handle scroll and click events.
"""
Expand Down Expand Up @@ -612,11 +645,17 @@ 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.
:param min_rows: is minimum number of rows in the completions window.
meta window will add 1 additional row if it is visible.
:param max_rows: is maximum number of rows in the completions window.
Must be > 0.

"""

def __init__(
self,
min_rows: int = 3,
min_rows: int = 1,
max_rows: int = 3,
suggested_max_column_width: int = 30,
show_meta: FilterOrBool = True,
extra_filter: FilterOrBool = True,
Expand Down Expand Up @@ -647,6 +686,7 @@ def any_completion_has_meta() -> bool:
content=Window(
content=MultiColumnCompletionMenuControl(
min_rows=min_rows,
max_rows=max_rows,
suggested_max_column_width=suggested_max_column_width,
),
width=Dimension(min=8),
Expand Down
Loading