Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

### Fixed

- Fixed `TextArea` scrollbar position not updated after paste https://github.com/Textualize/textual/issues/4852
- Fixed `TextArea` cursor display on wrapped lines https://github.com/Textualize/textual/pull/6196
- Fixed `remove_children` not refreshing layout https://github.com/Textualize/textual/pull/6206
- Fixed flicker with :hover pseudo class https://github.com/Textualize/textual/pull/6214
Expand Down
2 changes: 2 additions & 0 deletions src/textual/widgets/_text_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -1850,6 +1850,7 @@ async def _on_paste(self, event: events.Paste) -> None:
return
if result := self._replace_via_keyboard(event.text, *self.selection):
self.move_cursor(result.end_location)
self.scroll_cursor_visible()
self.focus()

def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int:
Expand Down Expand Up @@ -2530,6 +2531,7 @@ def action_paste(self) -> None:
clipboard = self.app.clipboard
if result := self._replace_via_keyboard(clipboard, *self.selection):
self.move_cursor(result.end_location)
self.scroll_cursor_visible()

def action_delete_to_start_of_line(self) -> None:
"""Deletes from the cursor location to the start of the line."""
Expand Down
113 changes: 113 additions & 0 deletions tests/text_area/test_textarea_cut_copy_paste.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,116 @@ async def test_paste():
assert text_area.text == "Hello, World"
await pilot.press("ctrl+v")
assert text_area.text == "Hello, WorldWorld"


async def test_paste_scrollbar_position_updated():
"""Regression test for issue #4852: scrollbar position should be updated after paste.

When pasting text that extends beyond the visible area, the scrollbar should
automatically scroll to show the cursor position after the pasted text.
"""
app = TextAreaApp()
async with app.run_test(size=(40, 10)) as pilot:
text_area = app.query_one(TextArea)
await pilot.click(text_area)

# Create long text that will require scrolling
long_text = "\n".join(f"Line {i}" for i in range(50))
app.copy_to_clipboard(long_text)

# Get initial scroll position
initial_scroll_x, initial_scroll_y = text_area.scroll_offset

# Paste the text
await pilot.press("ctrl+v")

# Wait for the paste to complete and scrollbar to update
await pilot.pause()

# Get scroll position after paste
final_scroll_x, final_scroll_y = text_area.scroll_offset

# The scrollbar should have scrolled down to show the cursor
# (which should be at the end of the pasted text)
assert final_scroll_y > initial_scroll_y, (
f"Scrollbar should have scrolled down after paste. "
f"Initial: {initial_scroll_y}, Final: {final_scroll_y}"
)

# Verify the text was pasted
assert text_area.text == long_text


async def test_paste_event_scrollbar_position_updated():
"""Regression test for issue #4852: scrollbar position should be updated after paste event.

Tests the _on_paste method specifically (paste via event, not action).
"""
from textual.events import Paste

app = TextAreaApp()
async with app.run_test(size=(40, 10)) as pilot:
text_area = app.query_one(TextArea)
await pilot.click(text_area)

# Create long text that will require scrolling
long_text = "\n".join(f"Line {i}" for i in range(50))

# Get initial scroll position
initial_scroll_x, initial_scroll_y = text_area.scroll_offset

# Post a paste event
app.post_message(Paste(long_text))
await pilot.pause()

# Get scroll position after paste
final_scroll_x, final_scroll_y = text_area.scroll_offset

# The scrollbar should have scrolled down to show the cursor
assert final_scroll_y > initial_scroll_y, (
f"Scrollbar should have scrolled down after paste event. "
f"Initial: {initial_scroll_y}, Final: {final_scroll_y}"
)

# Verify the text was pasted
assert text_area.text == long_text


async def test_paste_scrollbar_with_wide_text():
"""Regression test for issue #4852: scrollbar position updated with wide text.

Based on comment from TomJGooding: vertical scrollbar updates correctly
when horizontal scrollbar is also triggered. This test verifies both scrollbars work.
"""
app = TextAreaApp()
async with app.run_test(size=(40, 10)) as pilot:
text_area = app.query_one(TextArea)
await pilot.click(text_area)

# Create text that requires both horizontal and vertical scrolling
# Wide lines + many lines
wide_text = "\n".join(f"Line {i}: {'x' * 100}" for i in range(50))
app.copy_to_clipboard(wide_text)

# Get initial scroll position
initial_scroll_x, initial_scroll_y = text_area.scroll_offset

# Paste the text
await pilot.press("ctrl+v")
await pilot.pause()

# Get scroll position after paste
final_scroll_x, final_scroll_y = text_area.scroll_offset

# Both scrollbars should have updated
# Vertical scrollbar should have scrolled down
assert final_scroll_y > initial_scroll_y, (
f"Vertical scrollbar should have scrolled down after paste. "
f"Initial: {initial_scroll_y}, Final: {final_scroll_y}"
)

# Horizontal scrollbar may or may not scroll depending on cursor position
# But the important thing is that vertical scrollbar updated correctly

# Verify the text was pasted
assert text_area.text == wide_text
Loading