55
66from __future__ import annotations
77
8+ import re
9+ import sys
810from abc import ABCMeta , abstractmethod
911from enum import Enum
1012from 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
1315from prompt_toolkit .application .current import get_app
1416from prompt_toolkit .cache import SimpleCache
2325 AnyFormattedText ,
2426 StyleAndTextTuples ,
2527 to_formatted_text ,
28+ to_plain_text ,
2629)
2730from prompt_toolkit .formatted_text .utils import (
31+ fragment_list_len ,
2832 fragment_list_to_text ,
2933 fragment_list_width ,
3034)
3842 GetLinePrefixCallable ,
3943 UIContent ,
4044 UIControl ,
45+ WrapFinderCallable ,
4146)
4247from .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