Skip to content

Commit 4f59928

Browse files
committed
Add whitespace-matching wrapper
1 parent 6695411 commit 4f59928

File tree

1 file changed

+112
-6
lines changed

1 file changed

+112
-6
lines changed

src/prompt_toolkit/layout/containers.py

Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1789,6 +1789,7 @@ def _write_to_screen_at_index(
17891789
has_focus=get_app().layout.current_control == self.content,
17901790
align=align,
17911791
get_line_prefix=self.get_line_prefix,
1792+
wrap_finder=self._whitespace_wrap_finder(ui_content),
17921793
)
17931794

17941795
# Remember render info. (Set before generating the margins. They need this.)
@@ -1920,6 +1921,47 @@ def render_margin(m: Margin, width: int) -> UIContent:
19201921
# position.
19211922
screen.visible_windows_to_write_positions[self] = write_position
19221923

1924+
def _whitespace_wrap_finder(
1925+
self,
1926+
ui_content: UIContent,
1927+
sep: str | re.Pattern = r'\s',
1928+
split: str = 'remove',
1929+
continuation: StyleAndTextTuples = [],
1930+
) -> Callable[[int, int, int], tuple[int, int, StyleAndTextTuples]]:
1931+
""" Returns a function that defines where to break """
1932+
sep_re = sep if isinstance(sep, re.Pattern) else re.compile(sep)
1933+
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})')
1941+
else:
1942+
raise ValueError(f'Unrecognized value of split paramter: {split!r}')
1943+
1944+
cont_width = fragment_list_width(text)
1945+
1946+
def wrap_finder(lineno: int, start: int, end: int) -> tuple[int, int, StyleAndTextTuples]:
1947+
line = explode_text_fragments(ui_content.get_line(lineno))
1948+
cont_reserved = 0
1949+
while cont_reserved < cont_width:
1950+
style, char, *_ = line[end - 1]
1951+
cont_reserved += _CHAR_CACHE[style, char].width
1952+
end -= 1
1953+
1954+
segment = to_plain_text(line[start:end])
1955+
try:
1956+
after, sep, before = sep_re.split(segment[::-1], maxsplit=1)
1957+
except ValueError:
1958+
return (end, 0, continuation)
1959+
else:
1960+
return (start + len(before), len(sep), continuation)
1961+
1962+
return wrap_finder
1963+
1964+
19231965
def _copy_body(
19241966
self,
19251967
ui_content: UIContent,
@@ -1936,6 +1978,7 @@ def _copy_body(
19361978
has_focus: bool = False,
19371979
align: WindowAlign = WindowAlign.LEFT,
19381980
get_line_prefix: Callable[[int, int], AnyFormattedText] | None = None,
1981+
wrap_finder: Callable[[int, int, int], tuple[int, int, AnyFormattedText] | None] | None = None,
19391982
) -> tuple[dict[int, tuple[int, int]], dict[tuple[int, int], tuple[int, int]]]:
19401983
"""
19411984
Copy the UIContent into the output screen.
@@ -1957,6 +2000,42 @@ def _copy_body(
19572000
# Maps (row, col) from the input to (y, x) screen coordinates.
19582001
rowcol_to_yx: dict[tuple[int, int], tuple[int, int]] = {}
19592002

2003+
def find_next_wrap(remaining_width, is_input, lineno, fragment=0, char_pos=0):
2004+
if not wrap_lines:
2005+
return sys.maxsize, 0, []
2006+
2007+
line = ui_content.get_line(lineno)
2008+
style0, text0, *more = line[fragment]
2009+
fragment_pos = char_pos - fragment_list_len(line[:fragment])
2010+
line_part = [(style0, text0[char_pos:], *more), *line[fragment + 1:]]
2011+
line_width = [fragment_list_width([fragment]) for fragment in line_part]
2012+
2013+
orig_remaining_width = remaining_width
2014+
if sum(line_width) <= remaining_width:
2015+
return sys.maxsize, 0, []
2016+
2017+
min_wrap_pos = max_wrap_pos = char_pos
2018+
for next_fragment, fragment_width in zip(line_part, line_width):
2019+
if remaining_width < fragment_width:
2020+
break
2021+
remaining_width -= fragment_width
2022+
max_wrap_pos += fragment_list_len([next_fragment])
2023+
else:
2024+
# Should never happen
2025+
return sys.maxsize, 0, []
2026+
2027+
style, text, *_ = next_fragment
2028+
for char_width in (_CHAR_CACHE[char, style].width for char in text):
2029+
if remaining_width < char_width:
2030+
break
2031+
remaining_width -= char_width
2032+
max_wrap_pos += 1
2033+
2034+
if is_input and wrap_finder:
2035+
return wrap_finder(lineno, min_wrap_pos, max_wrap_pos)
2036+
else:
2037+
return max_wrap_pos, 0, []
2038+
19602039
def copy_line(
19612040
line: StyleAndTextTuples,
19622041
lineno: int,
@@ -2003,27 +2082,46 @@ def copy_line(
20032082
if line_width < width:
20042083
x += width - line_width
20052084

2085+
new_buffer_row = new_buffer[y + ypos]
2086+
wrap_start, wrap_replaced, continuation = find_next_wrap(width - x, is_input, lineno)
2087+
20062088
col = 0
20072089
wrap_count = 0
2008-
for style, text, *_ in line:
2009-
new_buffer_row = new_buffer[y + ypos]
2010-
2090+
wrap_skip = 0
2091+
text_end = 0
2092+
for fragment_count, (style, text, *_) in enumerate(line):
20112093
# Remember raw VT escape sequences. (E.g. FinalTerm's
20122094
# escape sequences.)
20132095
if "[ZeroWidthEscape]" in style:
20142096
new_screen.zero_width_escapes[y + ypos][x + xpos] += text
20152097
continue
20162098

2017-
for c in text:
2099+
text_start, text_end = text_end, text_end + len(text)
2100+
2101+
for char_count, c in enumerate(text, text_start):
20182102
char = _CHAR_CACHE[c, style]
20192103
char_width = char.width
20202104

20212105
# Wrap when the line width is exceeded.
2022-
if wrap_lines and x + char_width > width:
2106+
if wrap_lines and char_count == wrap_start:
2107+
skipped_width = sum(
2108+
_CHAR_CACHE[char, style].width
2109+
for char in text[wrap_start - text_start:][:wrap_replaced]
2110+
)
2111+
col += wrap_replaced
20232112
visible_line_to_row_col[y + 1] = (
20242113
lineno,
2025-
visible_line_to_row_col[y][1] + x,
2114+
visible_line_to_row_col[y][1] + x + skipped_width,
20262115
)
2116+
2117+
# Append continuation (e.g. hyphen)
2118+
if continuation:
2119+
x, y = copy_line(continuation, lineno, x, y, is_input=False)
2120+
# Make sure to erase rest of the line
2121+
for i in range(x, width):
2122+
new_buffer_row[i + xpos] = empty_char
2123+
wrap_skip = wrap_replaced
2124+
20272125
y += 1
20282126
wrap_count += 1
20292127
x = 0
@@ -2036,10 +2134,18 @@ def copy_line(
20362134
x, y = copy_line(prompt, lineno, x, y, is_input=False)
20372135

20382136
new_buffer_row = new_buffer[y + ypos]
2137+
wrap_start, wrap_replaced, continuation = find_next_wrap(
2138+
width - x, is_input, lineno, fragment_count, wrap_start + wrap_replaced
2139+
)
20392140

20402141
if y >= write_position.height:
20412142
return x, y # Break out of all for loops.
20422143

2144+
# Chars skipped by wrapping (e.g. whitespace)
2145+
if wrap_lines and wrap_skip > 0:
2146+
wrap_skip -= 1
2147+
continue
2148+
20432149
# Set character in screen and shift 'x'.
20442150
if x >= 0 and y >= 0 and x < width:
20452151
new_buffer_row[x + xpos] = char

0 commit comments

Comments
 (0)