From 4f59928f3c323dc29be22123a99755d49171dbb2 Mon Sep 17 00:00:00 2001 From: Cimbali Date: Tue, 11 Jun 2024 18:36:54 +0100 Subject: [PATCH 01/13] Add whitespace-matching wrapper --- src/prompt_toolkit/layout/containers.py | 118 ++++++++++++++++++++++-- 1 file changed, 112 insertions(+), 6 deletions(-) diff --git a/src/prompt_toolkit/layout/containers.py b/src/prompt_toolkit/layout/containers.py index 99b453477..4ae6f382f 100644 --- a/src/prompt_toolkit/layout/containers.py +++ b/src/prompt_toolkit/layout/containers.py @@ -1789,6 +1789,7 @@ def _write_to_screen_at_index( has_focus=get_app().layout.current_control == self.content, align=align, get_line_prefix=self.get_line_prefix, + wrap_finder=self._whitespace_wrap_finder(ui_content), ) # Remember render info. (Set before generating the margins. They need this.) @@ -1920,6 +1921,47 @@ def render_margin(m: Margin, width: int) -> UIContent: # position. screen.visible_windows_to_write_positions[self] = write_position + def _whitespace_wrap_finder( + self, + ui_content: UIContent, + sep: str | re.Pattern = r'\s', + split: str = 'remove', + continuation: StyleAndTextTuples = [], + ) -> Callable[[int, int, int], tuple[int, int, StyleAndTextTuples]]: + """ Returns a function that defines where to break """ + sep_re = sep if isinstance(sep, re.Pattern) else re.compile(sep) + if sep_re.groups: + raise ValueError(f'Pattern {sep_re.pattern!r} has capture group – use non-capturing groups instead') + elif split == 'after': + sep_re = re.compile('(?<={sep_re.pattern})()') + elif split == 'before': + sep_re = re.compile('(?={sep_re.pattern})()') + elif split == 'remove': + sep_re = re.compile(f'({sep_re.pattern})') + else: + raise ValueError(f'Unrecognized value of split paramter: {split!r}') + + cont_width = fragment_list_width(text) + + def wrap_finder(lineno: int, start: int, end: int) -> tuple[int, int, StyleAndTextTuples]: + line = explode_text_fragments(ui_content.get_line(lineno)) + cont_reserved = 0 + while cont_reserved < cont_width: + style, char, *_ = line[end - 1] + cont_reserved += _CHAR_CACHE[style, char].width + end -= 1 + + segment = to_plain_text(line[start:end]) + try: + after, sep, before = sep_re.split(segment[::-1], maxsplit=1) + except ValueError: + return (end, 0, continuation) + else: + return (start + len(before), len(sep), continuation) + + return wrap_finder + + def _copy_body( self, ui_content: UIContent, @@ -1936,6 +1978,7 @@ def _copy_body( has_focus: bool = False, align: WindowAlign = WindowAlign.LEFT, get_line_prefix: Callable[[int, int], AnyFormattedText] | None = None, + wrap_finder: Callable[[int, int, int], tuple[int, int, AnyFormattedText] | None] | None = None, ) -> tuple[dict[int, tuple[int, int]], dict[tuple[int, int], tuple[int, int]]]: """ Copy the UIContent into the output screen. @@ -1957,6 +2000,42 @@ def _copy_body( # Maps (row, col) from the input to (y, x) screen coordinates. rowcol_to_yx: dict[tuple[int, int], tuple[int, int]] = {} + def find_next_wrap(remaining_width, is_input, lineno, fragment=0, char_pos=0): + if not wrap_lines: + return sys.maxsize, 0, [] + + line = ui_content.get_line(lineno) + style0, text0, *more = line[fragment] + fragment_pos = char_pos - fragment_list_len(line[:fragment]) + line_part = [(style0, text0[char_pos:], *more), *line[fragment + 1:]] + line_width = [fragment_list_width([fragment]) for fragment in line_part] + + orig_remaining_width = remaining_width + if sum(line_width) <= remaining_width: + return sys.maxsize, 0, [] + + min_wrap_pos = max_wrap_pos = char_pos + for next_fragment, fragment_width in zip(line_part, line_width): + if remaining_width < fragment_width: + break + remaining_width -= fragment_width + max_wrap_pos += fragment_list_len([next_fragment]) + else: + # Should never happen + return sys.maxsize, 0, [] + + style, text, *_ = next_fragment + for char_width in (_CHAR_CACHE[char, style].width for char in text): + if remaining_width < char_width: + break + remaining_width -= char_width + max_wrap_pos += 1 + + if is_input and wrap_finder: + return wrap_finder(lineno, min_wrap_pos, max_wrap_pos) + else: + return max_wrap_pos, 0, [] + def copy_line( line: StyleAndTextTuples, lineno: int, @@ -2003,27 +2082,46 @@ def copy_line( if line_width < width: x += width - line_width + new_buffer_row = new_buffer[y + ypos] + wrap_start, wrap_replaced, continuation = find_next_wrap(width - x, is_input, lineno) + col = 0 wrap_count = 0 - for style, text, *_ in line: - new_buffer_row = new_buffer[y + ypos] - + wrap_skip = 0 + text_end = 0 + for fragment_count, (style, text, *_) in enumerate(line): # Remember raw VT escape sequences. (E.g. FinalTerm's # escape sequences.) if "[ZeroWidthEscape]" in style: new_screen.zero_width_escapes[y + ypos][x + xpos] += text continue - for c in text: + text_start, text_end = text_end, text_end + len(text) + + for char_count, c in enumerate(text, text_start): char = _CHAR_CACHE[c, style] char_width = char.width # Wrap when the line width is exceeded. - if wrap_lines and x + char_width > width: + if wrap_lines and char_count == wrap_start: + skipped_width = sum( + _CHAR_CACHE[char, style].width + for char in text[wrap_start - text_start:][:wrap_replaced] + ) + col += wrap_replaced visible_line_to_row_col[y + 1] = ( lineno, - visible_line_to_row_col[y][1] + x, + visible_line_to_row_col[y][1] + x + skipped_width, ) + + # Append continuation (e.g. hyphen) + if continuation: + x, y = copy_line(continuation, lineno, x, y, is_input=False) + # Make sure to erase rest of the line + for i in range(x, width): + new_buffer_row[i + xpos] = empty_char + wrap_skip = wrap_replaced + y += 1 wrap_count += 1 x = 0 @@ -2036,10 +2134,18 @@ def copy_line( x, y = copy_line(prompt, lineno, x, y, is_input=False) new_buffer_row = new_buffer[y + ypos] + wrap_start, wrap_replaced, continuation = find_next_wrap( + width - x, is_input, lineno, fragment_count, wrap_start + wrap_replaced + ) if y >= write_position.height: return x, y # Break out of all for loops. + # Chars skipped by wrapping (e.g. whitespace) + if wrap_lines and wrap_skip > 0: + wrap_skip -= 1 + continue + # Set character in screen and shift 'x'. if x >= 0 and y >= 0 and x < width: new_buffer_row[x + xpos] = char From b419b954bd9de7dedb5a59624f9e408d5d0f2467 Mon Sep 17 00:00:00 2001 From: Cimbali Date: Tue, 11 Jun 2024 19:45:51 +0100 Subject: [PATCH 02/13] Add custom wrapping to get_height_for_line --- .../auto-completion/fuzzy-custom-completer.py | 13 ++- src/prompt_toolkit/layout/containers.py | 84 +++++++++++++------ src/prompt_toolkit/layout/controls.py | 61 ++++++++++---- 3 files changed, 112 insertions(+), 46 deletions(-) diff --git a/examples/prompts/auto-completion/fuzzy-custom-completer.py b/examples/prompts/auto-completion/fuzzy-custom-completer.py index ca763c7d1..2e5b0324b 100755 --- a/examples/prompts/auto-completion/fuzzy-custom-completer.py +++ b/examples/prompts/auto-completion/fuzzy-custom-completer.py @@ -36,21 +36,28 @@ def get_completions(self, document, complete_event): def main(): # Simple completion menu. print("(The completion menu displays colors.)") - prompt("Type a color: ", completer=FuzzyCompleter(ColorCompleter())) + r = prompt( + "Type a color: ", + completer=FuzzyCompleter(ColorCompleter()), + complete_style=CompleteStyle.MULTI_COLUMN, + ) + print(r) # Multi-column menu. - prompt( + r = prompt( "Type a color: ", completer=FuzzyCompleter(ColorCompleter()), complete_style=CompleteStyle.MULTI_COLUMN, ) + print(r) # Readline-like - prompt( + r = prompt( "Type a color: ", completer=FuzzyCompleter(ColorCompleter()), complete_style=CompleteStyle.READLINE_LIKE, ) + print(r) if __name__ == "__main__": diff --git a/src/prompt_toolkit/layout/containers.py b/src/prompt_toolkit/layout/containers.py index 4ae6f382f..93967367d 100644 --- a/src/prompt_toolkit/layout/containers.py +++ b/src/prompt_toolkit/layout/containers.py @@ -5,10 +5,12 @@ from __future__ import annotations +import re +import sys from abc import ABCMeta, abstractmethod from enum import Enum from functools import partial -from typing import TYPE_CHECKING, Callable, Sequence, Union, cast +from typing import TYPE_CHECKING, Callable, Sequence, Tuple, Union, cast from prompt_toolkit.application.current import get_app from prompt_toolkit.cache import SimpleCache @@ -23,8 +25,10 @@ AnyFormattedText, StyleAndTextTuples, to_formatted_text, + to_plain_text, ) from prompt_toolkit.formatted_text.utils import ( + fragment_list_len, fragment_list_to_text, fragment_list_width, ) @@ -38,6 +42,7 @@ GetLinePrefixCallable, UIContent, UIControl, + WrapFinderCallable, ) from .dimension import ( AnyDimension, @@ -1310,7 +1315,10 @@ def get_height_for_line(self, lineno: int) -> int: """ if self.wrap_lines: return self.ui_content.get_height_for_line( - lineno, self.window_width, self.window.get_line_prefix + lineno, + self.window_width, + self.window.get_line_prefix, + self.window.wrap_finder, ) else: return 1 @@ -1442,6 +1450,10 @@ class Window(Container): wrap_count and returns formatted text. This can be used for implementation of line continuations, things like Vim "breakindent" and so on. + :param wrap_finder: None or a callable that returns how to wrap a line. + It takes a line number, a start and an end position (ints) and returns + the the wrap position, a number of characters to be skipped (if any), + and formatted text for the continuation marker. """ def __init__( @@ -1459,6 +1471,7 @@ def __init__( scroll_offsets: ScrollOffsets | None = None, allow_scroll_beyond_bottom: FilterOrBool = False, wrap_lines: FilterOrBool = False, + word_wrap: FilterOrBool = False, get_vertical_scroll: Callable[[Window], int] | None = None, get_horizontal_scroll: Callable[[Window], int] | None = None, always_hide_cursor: FilterOrBool = False, @@ -1471,10 +1484,12 @@ def __init__( style: str | Callable[[], str] = "", char: None | str | Callable[[], str] = None, get_line_prefix: GetLinePrefixCallable | None = None, + wrap_finder: WrapFinderCallable | None = None, ) -> None: self.allow_scroll_beyond_bottom = to_filter(allow_scroll_beyond_bottom) self.always_hide_cursor = to_filter(always_hide_cursor) self.wrap_lines = to_filter(wrap_lines) + self.word_wrap = to_filter(word_wrap) self.cursorline = to_filter(cursorline) self.cursorcolumn = to_filter(cursorcolumn) @@ -1493,6 +1508,7 @@ def __init__( self.style = style self.char = char self.get_line_prefix = get_line_prefix + self.wrap_finder = wrap_finder self.width = width self.height = height @@ -1601,6 +1617,7 @@ def preferred_content_height() -> int | None: max_available_height, wrap_lines, self.get_line_prefix, + self.wrap_finder, ) return self._merge_dimensions( @@ -1766,6 +1783,9 @@ def _write_to_screen_at_index( self._scroll( ui_content, write_position.width - total_margin_width, write_position.height ) + wrap_finder = self.wrap_finder or ( + self._whitespace_wrap_finder(ui_content) if self.word_wrap() else None + ) # Erase background and fill with `char`. self._fill_bg(screen, write_position, erase_bg) @@ -1789,7 +1809,7 @@ def _write_to_screen_at_index( has_focus=get_app().layout.current_control == self.content, align=align, get_line_prefix=self.get_line_prefix, - wrap_finder=self._whitespace_wrap_finder(ui_content), + wrap_finder=wrap_finder, ) # Remember render info. (Set before generating the margins. They need this.) @@ -1924,26 +1944,30 @@ def render_margin(m: Margin, width: int) -> UIContent: def _whitespace_wrap_finder( self, ui_content: UIContent, - sep: str | re.Pattern = r'\s', - split: str = 'remove', + sep: str | re.Pattern = r"\s", + split: str = "remove", continuation: StyleAndTextTuples = [], - ) -> Callable[[int, int, int], tuple[int, int, StyleAndTextTuples]]: - """ Returns a function that defines where to break """ + ) -> WrapFinderCallable: + """Returns a function that defines where to break""" sep_re = sep if isinstance(sep, re.Pattern) else re.compile(sep) if sep_re.groups: - raise ValueError(f'Pattern {sep_re.pattern!r} has capture group – use non-capturing groups instead') - elif split == 'after': - sep_re = re.compile('(?<={sep_re.pattern})()') - elif split == 'before': - sep_re = re.compile('(?={sep_re.pattern})()') - elif split == 'remove': - sep_re = re.compile(f'({sep_re.pattern})') + raise ValueError( + f"Pattern {sep_re.pattern!r} has capture group – use non-capturing groups instead" + ) + elif split == "after": + sep_re = re.compile("(?<={sep_re.pattern})()") + elif split == "before": + sep_re = re.compile("(?={sep_re.pattern})()") + elif split == "remove": + sep_re = re.compile(f"({sep_re.pattern})") else: - raise ValueError(f'Unrecognized value of split paramter: {split!r}') + raise ValueError(f"Unrecognized value of split paramter: {split!r}") - cont_width = fragment_list_width(text) + cont_width = fragment_list_width(continuation) - def wrap_finder(lineno: int, start: int, end: int) -> tuple[int, int, StyleAndTextTuples]: + def wrap_finder( + lineno: int, start: int, end: int + ) -> Tuple[int, int, AnyFormattedText]: line = explode_text_fragments(ui_content.get_line(lineno)) cont_reserved = 0 while cont_reserved < cont_width: @@ -1961,7 +1985,6 @@ def wrap_finder(lineno: int, start: int, end: int) -> tuple[int, int, StyleAndTe return wrap_finder - def _copy_body( self, ui_content: UIContent, @@ -1978,7 +2001,8 @@ def _copy_body( has_focus: bool = False, align: WindowAlign = WindowAlign.LEFT, get_line_prefix: Callable[[int, int], AnyFormattedText] | None = None, - wrap_finder: Callable[[int, int, int], tuple[int, int, AnyFormattedText] | None] | None = None, + wrap_finder: Callable[[int, int, int], Tuple[int, int, AnyFormattedText] | None] + | None = None, ) -> tuple[dict[int, tuple[int, int]], dict[tuple[int, int], tuple[int, int]]]: """ 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): line = ui_content.get_line(lineno) style0, text0, *more = line[fragment] fragment_pos = char_pos - fragment_list_len(line[:fragment]) - line_part = [(style0, text0[char_pos:], *more), *line[fragment + 1:]] + line_part = [(style0, text0[char_pos:], *more), *line[fragment + 1 :]] line_width = [fragment_list_width([fragment]) for fragment in line_part] - orig_remaining_width = remaining_width if sum(line_width) <= remaining_width: return sys.maxsize, 0, [] @@ -2083,7 +2106,10 @@ def copy_line( x += width - line_width new_buffer_row = new_buffer[y + ypos] - wrap_start, wrap_replaced, continuation = find_next_wrap(width - x, is_input, lineno) + wrap_start, wrap_replaced, continuation = find_next_wrap( + width - x, is_input, lineno + ) + continuation = to_formatted_text(continuation) col = 0 wrap_count = 0 @@ -2106,7 +2132,7 @@ def copy_line( if wrap_lines and char_count == wrap_start: skipped_width = sum( _CHAR_CACHE[char, style].width - for char in text[wrap_start - text_start:][:wrap_replaced] + for char in text[wrap_start - text_start :][:wrap_replaced] ) col += wrap_replaced visible_line_to_row_col[y + 1] = ( @@ -2135,8 +2161,13 @@ def copy_line( new_buffer_row = new_buffer[y + ypos] wrap_start, wrap_replaced, continuation = find_next_wrap( - width - x, is_input, lineno, fragment_count, wrap_start + wrap_replaced + width - x, + is_input, + lineno, + fragment_count, + wrap_start + wrap_replaced, ) + continuation = to_formatted_text(continuation) if y >= write_position.height: return x, y # Break out of all for loops. @@ -2435,7 +2466,9 @@ def _scroll_when_linewrapping( self.horizontal_scroll = 0 def get_line_height(lineno: int) -> int: - return ui_content.get_height_for_line(lineno, width, self.get_line_prefix) + return ui_content.get_height_for_line( + lineno, width, self.get_line_prefix, self.wrap_finder + ) # When there is no space, reset `vertical_scroll_2` to zero and abort. # This can happen if the margin is bigger than the window width. @@ -2460,6 +2493,7 @@ def get_line_height(lineno: int) -> int: ui_content.cursor_position.y, width, self.get_line_prefix, + self.wrap_finder, slice_stop=ui_content.cursor_position.x, ) diff --git a/src/prompt_toolkit/layout/controls.py b/src/prompt_toolkit/layout/controls.py index 222e471c5..44a71df0e 100644 --- a/src/prompt_toolkit/layout/controls.py +++ b/src/prompt_toolkit/layout/controls.py @@ -40,6 +40,8 @@ merge_processors, ) +from typing import Tuple + if TYPE_CHECKING: from prompt_toolkit.key_binding.key_bindings import ( KeyBindingsBase, @@ -58,6 +60,7 @@ ] GetLinePrefixCallable = Callable[[int, int], AnyFormattedText] +WrapFinderCallable = Callable[[int, int, int], Tuple[int, int, AnyFormattedText]] class UIControl(metaclass=ABCMeta): @@ -78,6 +81,7 @@ def preferred_height( max_available_height: int, wrap_lines: bool, get_line_prefix: GetLinePrefixCallable | None, + wrap_finder: WrapFinderCallable | None, ) -> int | None: return None @@ -178,6 +182,7 @@ def get_height_for_line( lineno: int, width: int, get_line_prefix: GetLinePrefixCallable | None, + wrap_finder: WrapFinderCallable | None, slice_stop: int | None = None, ) -> int: """ @@ -204,33 +209,47 @@ def get_height_for_line( else: # Calculate line width first. line = fragment_list_to_text(self.get_line(lineno))[:slice_stop] - text_width = get_cwidth(line) + start = 0 + text_width = get_cwidth(line[start:]) - if get_line_prefix: + if get_line_prefix or wrap_finder: # Add prefix width. - text_width += fragment_list_width( - to_formatted_text(get_line_prefix(lineno, 0)) - ) + if get_line_prefix: + prefix_width = fragment_list_width( + to_formatted_text(get_line_prefix(lineno, 0)) + ) + else: + prefix_width = 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: + while prefix_width + 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 wrap_finder: + # Decent guess for max breakpoint place? + end = start + width - prefix_width + start_end_width = get_cwidth(line[start:end]) + while start_end_width >= width - prefix_width: + start_end_width -= get_cwidth(line[end - 1]) + end -= 1 + wrap, skip, cont = wrap_finder(lineno, start, end) + start = wrap + skip + text_width = get_cwidth(line[start:]) + else: + text_width -= width - if prefix_width >= width: # Prefix doesn't fit. - height = 10**8 - break + if get_line_prefix: + fragments2 = to_formatted_text( + get_line_prefix(lineno, height - 1) + ) + prefix_width = get_cwidth(fragment_list_to_text(fragments2)) - text_width += prefix_width + if prefix_width >= width: # Prefix doesn't fit. + height = 10**8 + break else: # Fast path: compute height when there's no line prefix. try: @@ -354,6 +373,7 @@ def preferred_height( max_available_height: int, wrap_lines: bool, get_line_prefix: GetLinePrefixCallable | None, + wrap_finder: WrapFinderCallable | None, ) -> int | None: """ Return the preferred height for this control. @@ -362,7 +382,9 @@ def preferred_height( if wrap_lines: height = 0 for i in range(content.line_count): - height += content.get_height_for_line(i, width, get_line_prefix) + height += content.get_height_for_line( + i, width, get_line_prefix, wrap_finder + ) if height >= max_available_height: return max_available_height return height @@ -614,6 +636,7 @@ def preferred_height( max_available_height: int, wrap_lines: bool, get_line_prefix: GetLinePrefixCallable | None, + wrap_finder: WrapFinderCallable | None, ) -> int | None: # Calculate the content height, if it was drawn on a screen with the # given width. @@ -631,7 +654,9 @@ def preferred_height( return max_available_height for i in range(content.line_count): - height += content.get_height_for_line(i, width, get_line_prefix) + height += content.get_height_for_line( + i, width, get_line_prefix, wrap_finder + ) if height >= max_available_height: return max_available_height From ec2784d14a5ec96346061d0b16cc13f1f12fdbab Mon Sep 17 00:00:00 2001 From: Cimbali Date: Fri, 14 Jun 2024 02:12:21 +0100 Subject: [PATCH 03/13] Add styling fixes --- src/prompt_toolkit/layout/containers.py | 8 ++++---- src/prompt_toolkit/layout/controls.py | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/prompt_toolkit/layout/containers.py b/src/prompt_toolkit/layout/containers.py index 93967367d..7e8ad3527 100644 --- a/src/prompt_toolkit/layout/containers.py +++ b/src/prompt_toolkit/layout/containers.py @@ -10,7 +10,7 @@ from abc import ABCMeta, abstractmethod from enum import Enum from functools import partial -from typing import TYPE_CHECKING, Callable, Sequence, Tuple, Union, cast +from typing import TYPE_CHECKING, Callable, Sequence, Union, cast from prompt_toolkit.application.current import get_app from prompt_toolkit.cache import SimpleCache @@ -1967,7 +1967,7 @@ def _whitespace_wrap_finder( def wrap_finder( lineno: int, start: int, end: int - ) -> Tuple[int, int, AnyFormattedText]: + ) -> tuple[int, int, AnyFormattedText]: line = explode_text_fragments(ui_content.get_line(lineno)) cont_reserved = 0 while cont_reserved < cont_width: @@ -2001,7 +2001,7 @@ def _copy_body( has_focus: bool = False, align: WindowAlign = WindowAlign.LEFT, get_line_prefix: Callable[[int, int], AnyFormattedText] | None = None, - wrap_finder: Callable[[int, int, int], Tuple[int, int, AnyFormattedText] | None] + wrap_finder: Callable[[int, int, int], tuple[int, int, AnyFormattedText] | None] | None = None, ) -> tuple[dict[int, tuple[int, int]], dict[tuple[int, int], tuple[int, int]]]: """ @@ -2030,7 +2030,7 @@ def find_next_wrap(remaining_width, is_input, lineno, fragment=0, char_pos=0): line = ui_content.get_line(lineno) style0, text0, *more = line[fragment] - fragment_pos = char_pos - fragment_list_len(line[:fragment]) + char_pos - fragment_list_len(line[:fragment]) line_part = [(style0, text0[char_pos:], *more), *line[fragment + 1 :]] line_width = [fragment_list_width([fragment]) for fragment in line_part] diff --git a/src/prompt_toolkit/layout/controls.py b/src/prompt_toolkit/layout/controls.py index 44a71df0e..8bb8a5ac9 100644 --- a/src/prompt_toolkit/layout/controls.py +++ b/src/prompt_toolkit/layout/controls.py @@ -6,7 +6,7 @@ import time from abc import ABCMeta, abstractmethod -from typing import TYPE_CHECKING, Callable, Hashable, Iterable, NamedTuple +from typing import TYPE_CHECKING, Callable, Hashable, Iterable, NamedTuple, Tuple from prompt_toolkit.application.current import get_app from prompt_toolkit.buffer import Buffer @@ -40,8 +40,6 @@ merge_processors, ) -from typing import Tuple - if TYPE_CHECKING: from prompt_toolkit.key_binding.key_bindings import ( KeyBindingsBase, From 2339c02037197331c0189983713ed523c96ea151 Mon Sep 17 00:00:00 2001 From: Cimbali Date: Fri, 14 Jun 2024 02:15:56 +0100 Subject: [PATCH 04/13] Fix typo --- src/prompt_toolkit/layout/containers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/prompt_toolkit/layout/containers.py b/src/prompt_toolkit/layout/containers.py index 7e8ad3527..bb8afa391 100644 --- a/src/prompt_toolkit/layout/containers.py +++ b/src/prompt_toolkit/layout/containers.py @@ -1961,7 +1961,7 @@ def _whitespace_wrap_finder( elif split == "remove": sep_re = re.compile(f"({sep_re.pattern})") else: - raise ValueError(f"Unrecognized value of split paramter: {split!r}") + raise ValueError(f"Unrecognized value of split parameter: {split!r}") cont_width = fragment_list_width(continuation) From a26c6f5c604c6e85ac6592c42034fc450eab7065 Mon Sep 17 00:00:00 2001 From: Cimbali Date: Fri, 14 Jun 2024 16:24:33 +0100 Subject: [PATCH 05/13] Fix typechecking --- src/prompt_toolkit/layout/containers.py | 30 +++++++++++++++++-------- src/prompt_toolkit/layout/menus.py | 5 ++++- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/prompt_toolkit/layout/containers.py b/src/prompt_toolkit/layout/containers.py index bb8afa391..97a09747d 100644 --- a/src/prompt_toolkit/layout/containers.py +++ b/src/prompt_toolkit/layout/containers.py @@ -1944,7 +1944,7 @@ def render_margin(m: Margin, width: int) -> UIContent: def _whitespace_wrap_finder( self, ui_content: UIContent, - sep: str | re.Pattern = r"\s", + sep: str | re.Pattern[str] = r"\s", split: str = "remove", continuation: StyleAndTextTuples = [], ) -> WrapFinderCallable: @@ -2024,15 +2024,22 @@ def _copy_body( # Maps (row, col) from the input to (y, x) screen coordinates. rowcol_to_yx: dict[tuple[int, int], tuple[int, int]] = {} - def find_next_wrap(remaining_width, is_input, lineno, fragment=0, char_pos=0): + def find_next_wrap( + remaining_width: int, + is_input: bool, + lineno: int, + fragment: int = 0, + char_pos: int = 0, + ) -> tuple[int, int, AnyFormattedText]: if not wrap_lines: return sys.maxsize, 0, [] line = ui_content.get_line(lineno) style0, text0, *more = line[fragment] - char_pos - fragment_list_len(line[:fragment]) - line_part = [(style0, text0[char_pos:], *more), *line[fragment + 1 :]] - line_width = [fragment_list_width([fragment]) for fragment in line_part] + char_pos -= fragment_list_len(line[:fragment]) + line_part = [(style0, text0[char_pos:]), *line[fragment + 1 :]] + line_width = [fragment_list_width([frag]) for frag in line_part] + line_width = [fragment_list_width([frag]) for frag in line_part] if sum(line_width) <= remaining_width: return sys.maxsize, 0, [] @@ -2054,10 +2061,15 @@ def find_next_wrap(remaining_width, is_input, lineno, fragment=0, char_pos=0): remaining_width -= char_width max_wrap_pos += 1 - if is_input and wrap_finder: - return wrap_finder(lineno, min_wrap_pos, max_wrap_pos) - else: - return max_wrap_pos, 0, [] + return ( + wrap_finder(lineno, min_wrap_pos, max_wrap_pos) + if is_input and wrap_finder + else None + ) or ( + max_wrap_pos, + 0, + [], + ) def copy_line( line: StyleAndTextTuples, diff --git a/src/prompt_toolkit/layout/menus.py b/src/prompt_toolkit/layout/menus.py index 612e8ab6a..4e92bdb26 100644 --- a/src/prompt_toolkit/layout/menus.py +++ b/src/prompt_toolkit/layout/menus.py @@ -27,7 +27,7 @@ from prompt_toolkit.utils import get_cwidth from .containers import ConditionalContainer, HSplit, ScrollOffsets, Window -from .controls import GetLinePrefixCallable, UIContent, UIControl +from .controls import GetLinePrefixCallable, UIContent, UIControl, WrapFinderCallable from .dimension import Dimension from .margins import ScrollbarMargin @@ -80,6 +80,7 @@ def preferred_height( max_available_height: int, wrap_lines: bool, get_line_prefix: GetLinePrefixCallable | None, + wrap_finder: WrapFinderCallable | None, ) -> int | None: complete_state = get_app().current_buffer.complete_state if complete_state: @@ -378,6 +379,7 @@ def preferred_height( max_available_height: int, wrap_lines: bool, get_line_prefix: GetLinePrefixCallable | None, + wrap_finder: WrapFinderCallable | None, ) -> int | None: """ Preferred height: as much as needed in order to display all the completions. @@ -718,6 +720,7 @@ def preferred_height( max_available_height: int, wrap_lines: bool, get_line_prefix: GetLinePrefixCallable | None, + wrap_finder: WrapFinderCallable | None, ) -> int | None: return 1 From 283408153c229db09529f735a5d00353b88f4315 Mon Sep 17 00:00:00 2001 From: Cimbali Date: Mon, 17 Jun 2024 14:18:54 +0100 Subject: [PATCH 06/13] Edge case --- src/prompt_toolkit/layout/containers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/prompt_toolkit/layout/containers.py b/src/prompt_toolkit/layout/containers.py index 97a09747d..da6899802 100644 --- a/src/prompt_toolkit/layout/containers.py +++ b/src/prompt_toolkit/layout/containers.py @@ -2035,7 +2035,11 @@ def find_next_wrap( return sys.maxsize, 0, [] line = ui_content.get_line(lineno) - style0, text0, *more = line[fragment] + try: + style0, text0, *more = line[fragment] + except IndexError: + return sys.maxsize, 0, [] + char_pos -= fragment_list_len(line[:fragment]) line_part = [(style0, text0[char_pos:]), *line[fragment + 1 :]] line_width = [fragment_list_width([frag]) for frag in line_part] From 75a1128799c26506b923d470460c041f0a1946eb Mon Sep 17 00:00:00 2001 From: Cimbali Date: Mon, 17 Jun 2024 14:35:15 +0100 Subject: [PATCH 07/13] Make wrap_finder somewhat easier to reuse --- src/prompt_toolkit/layout/containers.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/prompt_toolkit/layout/containers.py b/src/prompt_toolkit/layout/containers.py index da6899802..d3986447c 100644 --- a/src/prompt_toolkit/layout/containers.py +++ b/src/prompt_toolkit/layout/containers.py @@ -1784,7 +1784,7 @@ def _write_to_screen_at_index( ui_content, write_position.width - total_margin_width, write_position.height ) wrap_finder = self.wrap_finder or ( - self._whitespace_wrap_finder(ui_content) if self.word_wrap() else None + self._whitespace_wrap_finder(ui_content.get_line) if self.word_wrap() else None ) # Erase background and fill with `char`. @@ -1941,10 +1941,11 @@ def render_margin(m: Margin, width: int) -> UIContent: # position. screen.visible_windows_to_write_positions[self] = write_position + @classmethod def _whitespace_wrap_finder( - self, - ui_content: UIContent, - sep: str | re.Pattern[str] = r"\s", + cls, + get_line: Callable[[int], StyleAndTextTuples], + sep: str | re.Pattern[str] = r"[ \t]", # Don’t include \xA0 by default (in \s) split: str = "remove", continuation: StyleAndTextTuples = [], ) -> WrapFinderCallable: @@ -1968,7 +1969,7 @@ def _whitespace_wrap_finder( def wrap_finder( lineno: int, start: int, end: int ) -> tuple[int, int, AnyFormattedText]: - line = explode_text_fragments(ui_content.get_line(lineno)) + line = explode_text_fragments(get_line(lineno)) cont_reserved = 0 while cont_reserved < cont_width: style, char, *_ = line[end - 1] From 925cb428136cd6937a77dc6859fda5e57dce644c Mon Sep 17 00:00:00 2001 From: Cimbali Date: Mon, 17 Jun 2024 15:13:45 +0100 Subject: [PATCH 08/13] Actually implement truncation mentioned in PR Also small fixes to character width computation --- src/prompt_toolkit/layout/containers.py | 26 +++++++++++++++---------- src/prompt_toolkit/layout/controls.py | 8 ++++++-- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/prompt_toolkit/layout/containers.py b/src/prompt_toolkit/layout/containers.py index d3986447c..049849324 100644 --- a/src/prompt_toolkit/layout/containers.py +++ b/src/prompt_toolkit/layout/containers.py @@ -1784,7 +1784,9 @@ def _write_to_screen_at_index( ui_content, write_position.width - total_margin_width, write_position.height ) wrap_finder = self.wrap_finder or ( - self._whitespace_wrap_finder(ui_content.get_line) if self.word_wrap() else None + self._whitespace_wrap_finder(ui_content.get_line) + if self.word_wrap() + else None ) # Erase background and fill with `char`. @@ -1967,13 +1969,13 @@ def _whitespace_wrap_finder( cont_width = fragment_list_width(continuation) def wrap_finder( - lineno: int, start: int, end: int + lineno: int, wrap_count: int, start: int, end: int ) -> tuple[int, int, AnyFormattedText]: line = explode_text_fragments(get_line(lineno)) cont_reserved = 0 while cont_reserved < cont_width: style, char, *_ = line[end - 1] - cont_reserved += _CHAR_CACHE[style, char].width + cont_reserved += get_cwidth(char) end -= 1 segment = to_plain_text(line[start:end]) @@ -2002,8 +2004,7 @@ def _copy_body( has_focus: bool = False, align: WindowAlign = WindowAlign.LEFT, get_line_prefix: Callable[[int, int], AnyFormattedText] | None = None, - wrap_finder: Callable[[int, int, int], tuple[int, int, AnyFormattedText] | None] - | None = None, + wrap_finder: WrapFinderCallable | None = None, ) -> tuple[dict[int, tuple[int, int]], dict[tuple[int, int], tuple[int, int]]]: """ Copy the UIContent into the output screen. @@ -2029,6 +2030,7 @@ def find_next_wrap( remaining_width: int, is_input: bool, lineno: int, + wrap_count: int, fragment: int = 0, char_pos: int = 0, ) -> tuple[int, int, AnyFormattedText]: @@ -2060,14 +2062,14 @@ def find_next_wrap( return sys.maxsize, 0, [] style, text, *_ = next_fragment - for char_width in (_CHAR_CACHE[char, style].width for char in text): + for char_width in (get_cwidth(char) for char in text): if remaining_width < char_width: break remaining_width -= char_width max_wrap_pos += 1 return ( - wrap_finder(lineno, min_wrap_pos, max_wrap_pos) + wrap_finder(lineno, wrap_count, min_wrap_pos, max_wrap_pos) if is_input and wrap_finder else None ) or ( @@ -2124,7 +2126,7 @@ def copy_line( new_buffer_row = new_buffer[y + ypos] wrap_start, wrap_replaced, continuation = find_next_wrap( - width - x, is_input, lineno + width - x, is_input, lineno, 0 ) continuation = to_formatted_text(continuation) @@ -2148,7 +2150,7 @@ def copy_line( # Wrap when the line width is exceeded. if wrap_lines and char_count == wrap_start: skipped_width = sum( - _CHAR_CACHE[char, style].width + get_cwidth(char) for char in text[wrap_start - text_start :][:wrap_replaced] ) col += wrap_replaced @@ -2163,8 +2165,11 @@ def copy_line( # Make sure to erase rest of the line for i in range(x, width): new_buffer_row[i + xpos] = empty_char - wrap_skip = wrap_replaced + if wrap_replaced < 0: + return x, y + + wrap_skip = wrap_replaced y += 1 wrap_count += 1 x = 0 @@ -2181,6 +2186,7 @@ def copy_line( width - x, is_input, lineno, + wrap_count, fragment_count, wrap_start + wrap_replaced, ) diff --git a/src/prompt_toolkit/layout/controls.py b/src/prompt_toolkit/layout/controls.py index 8bb8a5ac9..56877a14e 100644 --- a/src/prompt_toolkit/layout/controls.py +++ b/src/prompt_toolkit/layout/controls.py @@ -58,7 +58,7 @@ ] GetLinePrefixCallable = Callable[[int, int], AnyFormattedText] -WrapFinderCallable = Callable[[int, int, int], Tuple[int, int, AnyFormattedText]] +WrapFinderCallable = Callable[[int, int, int, int], Tuple[int, int, AnyFormattedText]] class UIControl(metaclass=ABCMeta): @@ -233,7 +233,11 @@ def get_height_for_line( while start_end_width >= width - prefix_width: start_end_width -= get_cwidth(line[end - 1]) end -= 1 - wrap, skip, cont = wrap_finder(lineno, start, end) + wrap, skip, cont = wrap_finder( + lineno, height - 1, start, end + ) + if skip < 0: + break # Truncate line start = wrap + skip text_width = get_cwidth(line[start:]) else: From 42b48d1034c2576d2a55d109fc6071f44944e938 Mon Sep 17 00:00:00 2001 From: Cimbali Date: Mon, 17 Jun 2024 15:17:27 +0100 Subject: [PATCH 09/13] Add an example --- .../full-screen/simple-demos/word-wrapping.py | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100755 examples/full-screen/simple-demos/word-wrapping.py diff --git a/examples/full-screen/simple-demos/word-wrapping.py b/examples/full-screen/simple-demos/word-wrapping.py new file mode 100755 index 000000000..44f15c922 --- /dev/null +++ b/examples/full-screen/simple-demos/word-wrapping.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python +""" +An example of a BufferControl in a full screen layout that offers auto +completion. + +Important is to make sure that there is a `CompletionsMenu` in the layout, +otherwise the completions won't be visible. +""" + +from prompt_toolkit.application import Application +from prompt_toolkit.buffer import Buffer +from prompt_toolkit.filters import Condition +from prompt_toolkit.formatted_text import HTML, to_formatted_text +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.layout.containers import Float, FloatContainer, HSplit, Window +from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl +from prompt_toolkit.layout.layout import Layout +from prompt_toolkit.layout.menus import CompletionsMenu + +LIPSUM = " ".join( + """\ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas +quis interdum enim. Nam viverra, mauris et blandit malesuada, ante est bibendum +mauris, ac dignissim dui tellus quis ligula. Aenean condimentum leo at +dignissim placerat. In vel dictum ex, vulputate accumsan mi. Donec ut quam +placerat massa tempor elementum. Sed tristique mauris ac suscipit euismod. Ut +tempus vehicula augue non venenatis. Mauris aliquam velit turpis, nec congue +risus aliquam sit amet. Pellentesque blandit scelerisque felis, faucibus +consequat ante. Curabitur tempor tortor a imperdiet tincidunt. Nam sed justo +sit amet odio bibendum congue. Quisque varius ligula nec ligula gravida, sed +convallis augue faucibus. Nunc ornare pharetra bibendum. Praesent blandit ex +quis sodales maximus.""".split("\n") +) + + +def get_line_prefix(lineno, wrap_count): + if wrap_count == 0: + return HTML('[%s] ') % lineno + + text = str(lineno) + "-" + "*" * (lineno // 2) + ": " + return HTML('[%s.%s] ') % ( + lineno, + wrap_count, + text, + ) + + +# Global wrap lines flag. +wrap_lines = True + + +# The layout +buff = Buffer(complete_while_typing=True) +buff.text = LIPSUM + + +body = FloatContainer( + content=HSplit( + [ + Window( + FormattedTextControl( + 'Press "q" to quit. Press "w" to enable/disable wrapping.' + ), + height=1, + style="reverse", + ), + # Break words + Window( + BufferControl(buffer=buff), + wrap_lines=Condition(lambda: wrap_lines), + word_wrap=False, + ), + # Default word wrapping + Window( + BufferControl(buffer=buff), + wrap_lines=Condition(lambda: wrap_lines), + word_wrap=True, + ), + # Add a marker to signify continuation + Window( + BufferControl(buffer=buff), + wrap_lines=True, + wrap_finder=Window._whitespace_wrap_finder( + lambda n: [] if n else to_formatted_text(LIPSUM), + continuation=to_formatted_text(" ⮠"), + ), + get_line_prefix=lambda lineno, wrap_count: to_formatted_text(" ⭢ ") + if wrap_count + else [], + ), + # Truncating (only wrap the first timle around) + Window( + BufferControl(buffer=buff), + wrap_lines=True, + wrap_finder=lambda lineno, + wrap_count, + start, + end, + fallback=Window._whitespace_wrap_finder( + lambda n: [] if n else to_formatted_text(LIPSUM), + ): (end - 3, -1, "...") + if wrap_count > 0 + else fallback(lineno, wrap_count, start, end), + ), + ] + ), + floats=[ + Float( + xcursor=True, + ycursor=True, + content=CompletionsMenu(max_height=16, scroll_offset=1), + ) + ], +) + + +# Key bindings +kb = KeyBindings() + + +@kb.add("q") +@kb.add("c-c") +def _(event): + "Quit application." + event.app.exit() + + +@kb.add("w") +def _(event): + "Disable/enable wrapping." + global wrap_lines + wrap_lines = not wrap_lines + + +# The `Application` +application = Application( + layout=Layout(body), key_bindings=kb, full_screen=True, mouse_support=True +) + + +def run(): + application.run() + + +if __name__ == "__main__": + run() From aa25eb9779acf3b9ef3a3dc6b41d2bfd71a432d6 Mon Sep 17 00:00:00 2001 From: Cimbali Date: Fri, 12 Jul 2024 13:52:58 +0100 Subject: [PATCH 10/13] =?UTF-8?q?Don=E2=80=99t=20assume=20we=E2=80=99re=20?= =?UTF-8?q?wrapping=20a=20content=20line?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Though it should be the most common case, maybe we may have to wrap some is_input=False content. --- .../full-screen/simple-demos/word-wrapping.py | 12 +++++------ src/prompt_toolkit/layout/containers.py | 20 ++++++++++--------- src/prompt_toolkit/layout/controls.py | 17 +++++++++------- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/examples/full-screen/simple-demos/word-wrapping.py b/examples/full-screen/simple-demos/word-wrapping.py index 44f15c922..cdc6e1a8a 100755 --- a/examples/full-screen/simple-demos/word-wrapping.py +++ b/examples/full-screen/simple-demos/word-wrapping.py @@ -81,8 +81,7 @@ def get_line_prefix(lineno, wrap_count): BufferControl(buffer=buff), wrap_lines=True, wrap_finder=Window._whitespace_wrap_finder( - lambda n: [] if n else to_formatted_text(LIPSUM), - continuation=to_formatted_text(" ⮠"), + continuation=to_formatted_text(" ⮠") ), get_line_prefix=lambda lineno, wrap_count: to_formatted_text(" ⭢ ") if wrap_count @@ -92,15 +91,14 @@ def get_line_prefix(lineno, wrap_count): Window( BufferControl(buffer=buff), wrap_lines=True, - wrap_finder=lambda lineno, + wrap_finder=lambda line, + lineno, wrap_count, start, end, - fallback=Window._whitespace_wrap_finder( - lambda n: [] if n else to_formatted_text(LIPSUM), - ): (end - 3, -1, "...") + fallback=Window._whitespace_wrap_finder(): (end - 3, -1, "...") if wrap_count > 0 - else fallback(lineno, wrap_count, start, end), + else fallback(line, lineno, wrap_count, start, end), ), ] ), diff --git a/src/prompt_toolkit/layout/containers.py b/src/prompt_toolkit/layout/containers.py index 049849324..f347373ba 100644 --- a/src/prompt_toolkit/layout/containers.py +++ b/src/prompt_toolkit/layout/containers.py @@ -1784,9 +1784,7 @@ def _write_to_screen_at_index( ui_content, write_position.width - total_margin_width, write_position.height ) wrap_finder = self.wrap_finder or ( - self._whitespace_wrap_finder(ui_content.get_line) - if self.word_wrap() - else None + self._whitespace_wrap_finder() if self.word_wrap() else None ) # Erase background and fill with `char`. @@ -1946,7 +1944,6 @@ def render_margin(m: Margin, width: int) -> UIContent: @classmethod def _whitespace_wrap_finder( cls, - get_line: Callable[[int], StyleAndTextTuples], sep: str | re.Pattern[str] = r"[ \t]", # Don’t include \xA0 by default (in \s) split: str = "remove", continuation: StyleAndTextTuples = [], @@ -1969,9 +1966,9 @@ def _whitespace_wrap_finder( cont_width = fragment_list_width(continuation) def wrap_finder( - lineno: int, wrap_count: int, start: int, end: int + line: AnyFormattedText, lineno: int, wrap_count: int, start: int, end: int ) -> tuple[int, int, AnyFormattedText]: - line = explode_text_fragments(get_line(lineno)) + line = explode_text_fragments(to_formatted_text(line)) cont_reserved = 0 while cont_reserved < cont_width: style, char, *_ = line[end - 1] @@ -2027,6 +2024,7 @@ def _copy_body( rowcol_to_yx: dict[tuple[int, int], tuple[int, int]] = {} def find_next_wrap( + line: StyleAndTextTuples, remaining_width: int, is_input: bool, lineno: int, @@ -2037,7 +2035,6 @@ def find_next_wrap( if not wrap_lines: return sys.maxsize, 0, [] - line = ui_content.get_line(lineno) try: style0, text0, *more = line[fragment] except IndexError: @@ -2069,7 +2066,7 @@ def find_next_wrap( max_wrap_pos += 1 return ( - wrap_finder(lineno, wrap_count, min_wrap_pos, max_wrap_pos) + wrap_finder(line, lineno, wrap_count, min_wrap_pos, max_wrap_pos) if is_input and wrap_finder else None ) or ( @@ -2126,7 +2123,11 @@ def copy_line( new_buffer_row = new_buffer[y + ypos] wrap_start, wrap_replaced, continuation = find_next_wrap( - width - x, is_input, lineno, 0 + line, + width - x, + is_input, + lineno, + 0, ) continuation = to_formatted_text(continuation) @@ -2183,6 +2184,7 @@ def copy_line( new_buffer_row = new_buffer[y + ypos] wrap_start, wrap_replaced, continuation = find_next_wrap( + line, width - x, is_input, lineno, diff --git a/src/prompt_toolkit/layout/controls.py b/src/prompt_toolkit/layout/controls.py index 56877a14e..d9f5c68c3 100644 --- a/src/prompt_toolkit/layout/controls.py +++ b/src/prompt_toolkit/layout/controls.py @@ -58,7 +58,9 @@ ] GetLinePrefixCallable = Callable[[int, int], AnyFormattedText] -WrapFinderCallable = Callable[[int, int, int, int], Tuple[int, int, AnyFormattedText]] +WrapFinderCallable = Callable[ + [AnyFormattedText, int, int, int, int], Tuple[int, int, AnyFormattedText] +] class UIControl(metaclass=ABCMeta): @@ -206,9 +208,10 @@ def get_height_for_line( height = 10**8 else: # Calculate line width first. - line = fragment_list_to_text(self.get_line(lineno))[:slice_stop] + line = self.get_line(lineno) + line_text = fragment_list_to_text(line)[:slice_stop] start = 0 - text_width = get_cwidth(line[start:]) + text_width = get_cwidth(line_text[start:]) if get_line_prefix or wrap_finder: # Add prefix width. @@ -229,17 +232,17 @@ def get_height_for_line( if wrap_finder: # Decent guess for max breakpoint place? end = start + width - prefix_width - start_end_width = get_cwidth(line[start:end]) + start_end_width = get_cwidth(line_text[start:end]) while start_end_width >= width - prefix_width: - start_end_width -= get_cwidth(line[end - 1]) + start_end_width -= get_cwidth(line_text[end - 1]) end -= 1 wrap, skip, cont = wrap_finder( - lineno, height - 1, start, end + line, lineno, height - 1, start, end ) if skip < 0: break # Truncate line start = wrap + skip - text_width = get_cwidth(line[start:]) + text_width = get_cwidth(line_text[start:]) else: text_width -= width From 58c8acf23cffa448f058f571cc858a8423826338 Mon Sep 17 00:00:00 2001 From: Cimbali Date: Fri, 12 Jul 2024 14:28:31 +0100 Subject: [PATCH 11/13] Fix some regex handling and add another example --- examples/full-screen/simple-demos/word-wrapping.py | 12 +++++++++++- src/prompt_toolkit/layout/containers.py | 4 ++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/examples/full-screen/simple-demos/word-wrapping.py b/examples/full-screen/simple-demos/word-wrapping.py index cdc6e1a8a..e6a160eb5 100755 --- a/examples/full-screen/simple-demos/word-wrapping.py +++ b/examples/full-screen/simple-demos/word-wrapping.py @@ -87,7 +87,7 @@ def get_line_prefix(lineno, wrap_count): if wrap_count else [], ), - # Truncating (only wrap the first timle around) + # Truncating (only wrap the first time around) Window( BufferControl(buffer=buff), wrap_lines=True, @@ -100,6 +100,16 @@ def get_line_prefix(lineno, wrap_count): if wrap_count > 0 else fallback(line, lineno, wrap_count, start, end), ), + # Split only after vowels + Window( + BufferControl(buffer=buff), + wrap_lines=True, + wrap_finder=Window._whitespace_wrap_finder( + sep="[aeiouyAEIOUY]", + split="after", + continuation=to_formatted_text("-"), + ), + ), ] ), floats=[ diff --git a/src/prompt_toolkit/layout/containers.py b/src/prompt_toolkit/layout/containers.py index f347373ba..19c134a42 100644 --- a/src/prompt_toolkit/layout/containers.py +++ b/src/prompt_toolkit/layout/containers.py @@ -1955,9 +1955,9 @@ def _whitespace_wrap_finder( f"Pattern {sep_re.pattern!r} has capture group – use non-capturing groups instead" ) elif split == "after": - sep_re = re.compile("(?<={sep_re.pattern})()") + sep_re = re.compile(f"(?={sep_re.pattern})()") elif split == "before": - sep_re = re.compile("(?={sep_re.pattern})()") + sep_re = re.compile(f"(?<={sep_re.pattern})()") elif split == "remove": sep_re = re.compile(f"({sep_re.pattern})") else: From 8d907f99f3d43cf7e4b1b4338f6347812be4c643 Mon Sep 17 00:00:00 2001 From: Cimbali Date: Fri, 12 Jul 2024 15:04:27 +0100 Subject: [PATCH 12/13] Fix handling of fragmented lines --- src/prompt_toolkit/layout/containers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/prompt_toolkit/layout/containers.py b/src/prompt_toolkit/layout/containers.py index 19c134a42..8f2b9896a 100644 --- a/src/prompt_toolkit/layout/containers.py +++ b/src/prompt_toolkit/layout/containers.py @@ -2040,8 +2040,8 @@ def find_next_wrap( except IndexError: return sys.maxsize, 0, [] - char_pos -= fragment_list_len(line[:fragment]) - line_part = [(style0, text0[char_pos:]), *line[fragment + 1 :]] + frag_char_pos = char_pos - fragment_list_len(line[:fragment]) + line_part = [(style0, text0[frag_char_pos:]), *line[fragment + 1 :]] line_width = [fragment_list_width([frag]) for frag in line_part] line_width = [fragment_list_width([frag]) for frag in line_part] From f0fad1bab54a098554150e3b219c1d9f7ed1ef51 Mon Sep 17 00:00:00 2001 From: Cimbali Date: Fri, 12 Jul 2024 15:25:13 +0100 Subject: [PATCH 13/13] Fix overcorrection --- src/prompt_toolkit/layout/containers.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/prompt_toolkit/layout/containers.py b/src/prompt_toolkit/layout/containers.py index 8f2b9896a..e618514ad 100644 --- a/src/prompt_toolkit/layout/containers.py +++ b/src/prompt_toolkit/layout/containers.py @@ -2163,9 +2163,6 @@ def copy_line( # Append continuation (e.g. hyphen) if continuation: x, y = copy_line(continuation, lineno, x, y, is_input=False) - # Make sure to erase rest of the line - for i in range(x, width): - new_buffer_row[i + xpos] = empty_char if wrap_replaced < 0: return x, y