From 467804dc2e0cd6e2ae5009656866ef713542207e Mon Sep 17 00:00:00 2001 From: fan wu <336798967+FanWu-ai@users.noreply.github.com> Date: Fri, 16 Oct 2026 06:27:05 +0900 Subject: [PATCH] Honor multiline filters in TextArea layout --- src/prompt_toolkit/widgets/base.py | 24 ++++---- tests/test_widgets.py | 89 +++++++++++++++++++++++++++++- 2 files changed, 98 insertions(+), 15 deletions(-) diff --git a/src/prompt_toolkit/widgets/base.py b/src/prompt_toolkit/widgets/base.py index 6c59fa8ea..8e5138fc8 100644 --- a/src/prompt_toolkit/widgets/base.py +++ b/src/prompt_toolkit/widgets/base.py @@ -127,7 +127,8 @@ class TextArea: Buffer attributes: :param text: The initial text. - :param multiline: If True, allow multiline input. + :param multiline: A bool or :class:`~prompt_toolkit.filters.Filter` + controlling whether multiline input is allowed. :param completer: :class:`~prompt_toolkit.completion.Completer` instance for auto completion. :param complete_while_typing: Boolean. @@ -252,19 +253,16 @@ def __init__( focus_on_click=focus_on_click, ) - if multiline: - if scrollbar: - right_margins = [ScrollbarMargin(display_arrows=True)] - else: - right_margins = [] - if line_numbers: - left_margins = [NumberedMargin()] - else: - left_margins = [] + if scrollbar: + right_margins = [ + ConditionalMargin(ScrollbarMargin(display_arrows=True), multiline) + ] else: - height = D.exact(1) - left_margins = [] right_margins = [] + if line_numbers: + left_margins = [ConditionalMargin(NumberedMargin(), multiline)] + else: + left_margins = [] style = "class:text-area " + style @@ -273,7 +271,7 @@ def __init__( height = D(min=1) self.window = Window( - height=height, + height=lambda: height if is_true(multiline) else D.exact(1), width=width, dont_extend_height=dont_extend_height, dont_extend_width=dont_extend_width, diff --git a/tests/test_widgets.py b/tests/test_widgets.py index ee7745a2d..09f2fee73 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -1,8 +1,18 @@ from __future__ import annotations +from asyncio import run, wait_for + +import pytest + +from prompt_toolkit.application import Application, get_app +from prompt_toolkit.filters import Always, Condition, Never from prompt_toolkit.formatted_text import fragment_list_to_text -from prompt_toolkit.layout import to_window -from prompt_toolkit.widgets import Button +from prompt_toolkit.input import create_pipe_input +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.layout import Layout, UIContent, to_window +from prompt_toolkit.layout.dimension import to_dimension +from prompt_toolkit.output import DummyOutput +from prompt_toolkit.widgets import Button, TextArea def _to_text(button: Button) -> str: @@ -18,3 +28,78 @@ def test_default_button(): def test_custom_button(): button = Button("Exit", left_symbol="[", right_symbol="]") assert _to_text(button) == "[ Exit ]" + + +@pytest.mark.parametrize( + "multiline, enabled", + [ + (True, True), + (False, False), + (Always(), True), + (Never(), False), + (Condition(lambda: True), True), + (Condition(lambda: False), False), + ], +) +def test_text_area_multiline_filter(multiline, enabled): + area = TextArea(multiline=multiline, height=5, line_numbers=True, scrollbar=True) + assert area.buffer.multiline() is enabled + dimension = to_dimension(area.window.height) + assert dimension.min == dimension.max == (5 if enabled else 1) + get_content = lambda: UIContent(line_count=2) + assert sum(m.get_width(get_content) for m in area.window.left_margins) == ( + 3 if enabled else 0 + ) + assert sum(m.get_width(get_content) for m in area.window.right_margins) == ( + 1 if enabled else 0 + ) + + +def test_text_area_multiline_filter_updates_layout(): + enabled = False + height = 5 + area = TextArea( + multiline=Condition(lambda: enabled), + height=lambda: height, + line_numbers=True, + scrollbar=True, + ) + get_content = lambda: UIContent(line_count=2) + + for enabled, height in [(False, 5), (True, 5), (True, 7), (False, 7), (True, 7)]: + assert area.buffer.multiline() is enabled + dimension = to_dimension(area.window.height) + assert dimension.min == dimension.max == (height if enabled else 1) + assert sum(m.get_width(get_content) for m in area.window.left_margins) == ( + 3 if enabled else 0 + ) + assert sum(m.get_width(get_content) for m in area.window.right_margins) == ( + 1 if enabled else 0 + ) + + +def test_text_area_multiline_filter_changes_enter_behavior(): + async def check(): + enabled = True + bindings = KeyBindings() + + @bindings.add("c-t") + def toggle(event): + nonlocal enabled + enabled = not enabled + + area = TextArea( + multiline=Condition(lambda: enabled), + accept_handler=lambda buffer: get_app().exit(result=buffer.text), + ) + with create_pipe_input() as pipe_input: + app = Application( + layout=Layout(area), + key_bindings=bindings, + input=pipe_input, + output=DummyOutput(), + ) + pipe_input.send_text("first\r\x14second\r") + assert await wait_for(app.run_async(), timeout=3) == "first\nsecond" + + run(check())