@@ -1789,6 +1789,7 @@ def _write_to_screen_at_index(
1789
1789
has_focus = get_app ().layout .current_control == self .content ,
1790
1790
align = align ,
1791
1791
get_line_prefix = self .get_line_prefix ,
1792
+ wrap_finder = self ._whitespace_wrap_finder (ui_content ),
1792
1793
)
1793
1794
1794
1795
# Remember render info. (Set before generating the margins. They need this.)
@@ -1920,6 +1921,47 @@ def render_margin(m: Margin, width: int) -> UIContent:
1920
1921
# position.
1921
1922
screen .visible_windows_to_write_positions [self ] = write_position
1922
1923
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
+
1923
1965
def _copy_body (
1924
1966
self ,
1925
1967
ui_content : UIContent ,
@@ -1936,6 +1978,7 @@ def _copy_body(
1936
1978
has_focus : bool = False ,
1937
1979
align : WindowAlign = WindowAlign .LEFT ,
1938
1980
get_line_prefix : Callable [[int , int ], AnyFormattedText ] | None = None ,
1981
+ wrap_finder : Callable [[int , int , int ], tuple [int , int , AnyFormattedText ] | None ] | None = None ,
1939
1982
) -> tuple [dict [int , tuple [int , int ]], dict [tuple [int , int ], tuple [int , int ]]]:
1940
1983
"""
1941
1984
Copy the UIContent into the output screen.
@@ -1957,6 +2000,42 @@ def _copy_body(
1957
2000
# Maps (row, col) from the input to (y, x) screen coordinates.
1958
2001
rowcol_to_yx : dict [tuple [int , int ], tuple [int , int ]] = {}
1959
2002
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
+
1960
2039
def copy_line (
1961
2040
line : StyleAndTextTuples ,
1962
2041
lineno : int ,
@@ -2003,27 +2082,46 @@ def copy_line(
2003
2082
if line_width < width :
2004
2083
x += width - line_width
2005
2084
2085
+ new_buffer_row = new_buffer [y + ypos ]
2086
+ wrap_start , wrap_replaced , continuation = find_next_wrap (width - x , is_input , lineno )
2087
+
2006
2088
col = 0
2007
2089
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 ):
2011
2093
# Remember raw VT escape sequences. (E.g. FinalTerm's
2012
2094
# escape sequences.)
2013
2095
if "[ZeroWidthEscape]" in style :
2014
2096
new_screen .zero_width_escapes [y + ypos ][x + xpos ] += text
2015
2097
continue
2016
2098
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 ):
2018
2102
char = _CHAR_CACHE [c , style ]
2019
2103
char_width = char .width
2020
2104
2021
2105
# 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
2023
2112
visible_line_to_row_col [y + 1 ] = (
2024
2113
lineno ,
2025
- visible_line_to_row_col [y ][1 ] + x ,
2114
+ visible_line_to_row_col [y ][1 ] + x + skipped_width ,
2026
2115
)
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
+
2027
2125
y += 1
2028
2126
wrap_count += 1
2029
2127
x = 0
@@ -2036,10 +2134,18 @@ def copy_line(
2036
2134
x , y = copy_line (prompt , lineno , x , y , is_input = False )
2037
2135
2038
2136
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
+ )
2039
2140
2040
2141
if y >= write_position .height :
2041
2142
return x , y # Break out of all for loops.
2042
2143
2144
+ # Chars skipped by wrapping (e.g. whitespace)
2145
+ if wrap_lines and wrap_skip > 0 :
2146
+ wrap_skip -= 1
2147
+ continue
2148
+
2043
2149
# Set character in screen and shift 'x'.
2044
2150
if x >= 0 and y >= 0 and x < width :
2045
2151
new_buffer_row [x + xpos ] = char
0 commit comments