Skip to content

Commit b419b95

Browse files
committed
Add custom wrapping to get_height_for_line
1 parent 4f59928 commit b419b95

File tree

3 files changed

+112
-46
lines changed

3 files changed

+112
-46
lines changed

examples/prompts/auto-completion/fuzzy-custom-completer.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,28 @@ def get_completions(self, document, complete_event):
3636
def main():
3737
# Simple completion menu.
3838
print("(The completion menu displays colors.)")
39-
prompt("Type a color: ", completer=FuzzyCompleter(ColorCompleter()))
39+
r = prompt(
40+
"Type a color: ",
41+
completer=FuzzyCompleter(ColorCompleter()),
42+
complete_style=CompleteStyle.MULTI_COLUMN,
43+
)
44+
print(r)
4045

4146
# Multi-column menu.
42-
prompt(
47+
r = prompt(
4348
"Type a color: ",
4449
completer=FuzzyCompleter(ColorCompleter()),
4550
complete_style=CompleteStyle.MULTI_COLUMN,
4651
)
52+
print(r)
4753

4854
# Readline-like
49-
prompt(
55+
r = prompt(
5056
"Type a color: ",
5157
completer=FuzzyCompleter(ColorCompleter()),
5258
complete_style=CompleteStyle.READLINE_LIKE,
5359
)
60+
print(r)
5461

5562

5663
if __name__ == "__main__":

src/prompt_toolkit/layout/containers.py

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55

66
from __future__ import annotations
77

8+
import re
9+
import sys
810
from abc import ABCMeta, abstractmethod
911
from enum import Enum
1012
from functools import partial
11-
from typing import TYPE_CHECKING, Callable, Sequence, Union, cast
13+
from typing import TYPE_CHECKING, Callable, Sequence, Tuple, Union, cast
1214

1315
from prompt_toolkit.application.current import get_app
1416
from prompt_toolkit.cache import SimpleCache
@@ -23,8 +25,10 @@
2325
AnyFormattedText,
2426
StyleAndTextTuples,
2527
to_formatted_text,
28+
to_plain_text,
2629
)
2730
from prompt_toolkit.formatted_text.utils import (
31+
fragment_list_len,
2832
fragment_list_to_text,
2933
fragment_list_width,
3034
)
@@ -38,6 +42,7 @@
3842
GetLinePrefixCallable,
3943
UIContent,
4044
UIControl,
45+
WrapFinderCallable,
4146
)
4247
from .dimension import (
4348
AnyDimension,
@@ -1310,7 +1315,10 @@ def get_height_for_line(self, lineno: int) -> int:
13101315
"""
13111316
if self.wrap_lines:
13121317
return self.ui_content.get_height_for_line(
1313-
lineno, self.window_width, self.window.get_line_prefix
1318+
lineno,
1319+
self.window_width,
1320+
self.window.get_line_prefix,
1321+
self.window.wrap_finder,
13141322
)
13151323
else:
13161324
return 1
@@ -1442,6 +1450,10 @@ class Window(Container):
14421450
wrap_count and returns formatted text. This can be used for
14431451
implementation of line continuations, things like Vim "breakindent" and
14441452
so on.
1453+
:param wrap_finder: None or a callable that returns how to wrap a line.
1454+
It takes a line number, a start and an end position (ints) and returns
1455+
the the wrap position, a number of characters to be skipped (if any),
1456+
and formatted text for the continuation marker.
14451457
"""
14461458

14471459
def __init__(
@@ -1459,6 +1471,7 @@ def __init__(
14591471
scroll_offsets: ScrollOffsets | None = None,
14601472
allow_scroll_beyond_bottom: FilterOrBool = False,
14611473
wrap_lines: FilterOrBool = False,
1474+
word_wrap: FilterOrBool = False,
14621475
get_vertical_scroll: Callable[[Window], int] | None = None,
14631476
get_horizontal_scroll: Callable[[Window], int] | None = None,
14641477
always_hide_cursor: FilterOrBool = False,
@@ -1471,10 +1484,12 @@ def __init__(
14711484
style: str | Callable[[], str] = "",
14721485
char: None | str | Callable[[], str] = None,
14731486
get_line_prefix: GetLinePrefixCallable | None = None,
1487+
wrap_finder: WrapFinderCallable | None = None,
14741488
) -> None:
14751489
self.allow_scroll_beyond_bottom = to_filter(allow_scroll_beyond_bottom)
14761490
self.always_hide_cursor = to_filter(always_hide_cursor)
14771491
self.wrap_lines = to_filter(wrap_lines)
1492+
self.word_wrap = to_filter(word_wrap)
14781493
self.cursorline = to_filter(cursorline)
14791494
self.cursorcolumn = to_filter(cursorcolumn)
14801495

@@ -1493,6 +1508,7 @@ def __init__(
14931508
self.style = style
14941509
self.char = char
14951510
self.get_line_prefix = get_line_prefix
1511+
self.wrap_finder = wrap_finder
14961512

14971513
self.width = width
14981514
self.height = height
@@ -1601,6 +1617,7 @@ def preferred_content_height() -> int | None:
16011617
max_available_height,
16021618
wrap_lines,
16031619
self.get_line_prefix,
1620+
self.wrap_finder,
16041621
)
16051622

16061623
return self._merge_dimensions(
@@ -1766,6 +1783,9 @@ def _write_to_screen_at_index(
17661783
self._scroll(
17671784
ui_content, write_position.width - total_margin_width, write_position.height
17681785
)
1786+
wrap_finder = self.wrap_finder or (
1787+
self._whitespace_wrap_finder(ui_content) if self.word_wrap() else None
1788+
)
17691789

17701790
# Erase background and fill with `char`.
17711791
self._fill_bg(screen, write_position, erase_bg)
@@ -1789,7 +1809,7 @@ def _write_to_screen_at_index(
17891809
has_focus=get_app().layout.current_control == self.content,
17901810
align=align,
17911811
get_line_prefix=self.get_line_prefix,
1792-
wrap_finder=self._whitespace_wrap_finder(ui_content),
1812+
wrap_finder=wrap_finder,
17931813
)
17941814

17951815
# Remember render info. (Set before generating the margins. They need this.)
@@ -1924,26 +1944,30 @@ def render_margin(m: Margin, width: int) -> UIContent:
19241944
def _whitespace_wrap_finder(
19251945
self,
19261946
ui_content: UIContent,
1927-
sep: str | re.Pattern = r'\s',
1928-
split: str = 'remove',
1947+
sep: str | re.Pattern = r"\s",
1948+
split: str = "remove",
19291949
continuation: StyleAndTextTuples = [],
1930-
) -> Callable[[int, int, int], tuple[int, int, StyleAndTextTuples]]:
1931-
""" Returns a function that defines where to break """
1950+
) -> WrapFinderCallable:
1951+
"""Returns a function that defines where to break"""
19321952
sep_re = sep if isinstance(sep, re.Pattern) else re.compile(sep)
19331953
if sep_re.groups:
1934-
raise ValueError(f'Pattern {sep_re.pattern!r} has capture group – use non-capturing groups instead')
1935-
elif split == 'after':
1936-
sep_re = re.compile('(?<={sep_re.pattern})()')
1937-
elif split == 'before':
1938-
sep_re = re.compile('(?={sep_re.pattern})()')
1939-
elif split == 'remove':
1940-
sep_re = re.compile(f'({sep_re.pattern})')
1954+
raise ValueError(
1955+
f"Pattern {sep_re.pattern!r} has capture group – use non-capturing groups instead"
1956+
)
1957+
elif split == "after":
1958+
sep_re = re.compile("(?<={sep_re.pattern})()")
1959+
elif split == "before":
1960+
sep_re = re.compile("(?={sep_re.pattern})()")
1961+
elif split == "remove":
1962+
sep_re = re.compile(f"({sep_re.pattern})")
19411963
else:
1942-
raise ValueError(f'Unrecognized value of split paramter: {split!r}')
1964+
raise ValueError(f"Unrecognized value of split paramter: {split!r}")
19431965

1944-
cont_width = fragment_list_width(text)
1966+
cont_width = fragment_list_width(continuation)
19451967

1946-
def wrap_finder(lineno: int, start: int, end: int) -> tuple[int, int, StyleAndTextTuples]:
1968+
def wrap_finder(
1969+
lineno: int, start: int, end: int
1970+
) -> Tuple[int, int, AnyFormattedText]:
19471971
line = explode_text_fragments(ui_content.get_line(lineno))
19481972
cont_reserved = 0
19491973
while cont_reserved < cont_width:
@@ -1961,7 +1985,6 @@ def wrap_finder(lineno: int, start: int, end: int) -> tuple[int, int, StyleAndTe
19611985

19621986
return wrap_finder
19631987

1964-
19651988
def _copy_body(
19661989
self,
19671990
ui_content: UIContent,
@@ -1978,7 +2001,8 @@ def _copy_body(
19782001
has_focus: bool = False,
19792002
align: WindowAlign = WindowAlign.LEFT,
19802003
get_line_prefix: Callable[[int, int], AnyFormattedText] | None = None,
1981-
wrap_finder: Callable[[int, int, int], tuple[int, int, AnyFormattedText] | None] | None = None,
2004+
wrap_finder: Callable[[int, int, int], Tuple[int, int, AnyFormattedText] | None]
2005+
| None = None,
19822006
) -> tuple[dict[int, tuple[int, int]], dict[tuple[int, int], tuple[int, int]]]:
19832007
"""
19842008
Copy the UIContent into the output screen.
@@ -2007,10 +2031,9 @@ def find_next_wrap(remaining_width, is_input, lineno, fragment=0, char_pos=0):
20072031
line = ui_content.get_line(lineno)
20082032
style0, text0, *more = line[fragment]
20092033
fragment_pos = char_pos - fragment_list_len(line[:fragment])
2010-
line_part = [(style0, text0[char_pos:], *more), *line[fragment + 1:]]
2034+
line_part = [(style0, text0[char_pos:], *more), *line[fragment + 1 :]]
20112035
line_width = [fragment_list_width([fragment]) for fragment in line_part]
20122036

2013-
orig_remaining_width = remaining_width
20142037
if sum(line_width) <= remaining_width:
20152038
return sys.maxsize, 0, []
20162039

@@ -2083,7 +2106,10 @@ def copy_line(
20832106
x += width - line_width
20842107

20852108
new_buffer_row = new_buffer[y + ypos]
2086-
wrap_start, wrap_replaced, continuation = find_next_wrap(width - x, is_input, lineno)
2109+
wrap_start, wrap_replaced, continuation = find_next_wrap(
2110+
width - x, is_input, lineno
2111+
)
2112+
continuation = to_formatted_text(continuation)
20872113

20882114
col = 0
20892115
wrap_count = 0
@@ -2106,7 +2132,7 @@ def copy_line(
21062132
if wrap_lines and char_count == wrap_start:
21072133
skipped_width = sum(
21082134
_CHAR_CACHE[char, style].width
2109-
for char in text[wrap_start - text_start:][:wrap_replaced]
2135+
for char in text[wrap_start - text_start :][:wrap_replaced]
21102136
)
21112137
col += wrap_replaced
21122138
visible_line_to_row_col[y + 1] = (
@@ -2135,8 +2161,13 @@ def copy_line(
21352161

21362162
new_buffer_row = new_buffer[y + ypos]
21372163
wrap_start, wrap_replaced, continuation = find_next_wrap(
2138-
width - x, is_input, lineno, fragment_count, wrap_start + wrap_replaced
2164+
width - x,
2165+
is_input,
2166+
lineno,
2167+
fragment_count,
2168+
wrap_start + wrap_replaced,
21392169
)
2170+
continuation = to_formatted_text(continuation)
21402171

21412172
if y >= write_position.height:
21422173
return x, y # Break out of all for loops.
@@ -2435,7 +2466,9 @@ def _scroll_when_linewrapping(
24352466
self.horizontal_scroll = 0
24362467

24372468
def get_line_height(lineno: int) -> int:
2438-
return ui_content.get_height_for_line(lineno, width, self.get_line_prefix)
2469+
return ui_content.get_height_for_line(
2470+
lineno, width, self.get_line_prefix, self.wrap_finder
2471+
)
24392472

24402473
# When there is no space, reset `vertical_scroll_2` to zero and abort.
24412474
# This can happen if the margin is bigger than the window width.
@@ -2460,6 +2493,7 @@ def get_line_height(lineno: int) -> int:
24602493
ui_content.cursor_position.y,
24612494
width,
24622495
self.get_line_prefix,
2496+
self.wrap_finder,
24632497
slice_stop=ui_content.cursor_position.x,
24642498
)
24652499

0 commit comments

Comments
 (0)