-
Notifications
You must be signed in to change notification settings - Fork 788
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
TextArea swallows lines after the Undo event #4301
Comments
I've tried to follow what you've reported above, but I can't seem to reproduce what you're seeing. Just to be clear:
As you'll see below, nothing untoward seems to happen: Screen.Recording.2024-03-18.at.08.37.49.movDo you think you could give slightly more detailed directions so we can exactly reproduce what you're doing? |
Hi, thanks for your comment. Your video shows the exact behavior that I expected to see. Please, see also mine which shows what I have with the same steps (selection, typing, undoing). 18.03.2024_14.05.58_REC.mp4 |
Okay, I think I can recreate now: Screen.Recording.2024-03-18.at.11.01.17.movThe issue seems to be to do with selecting "backwards" and/or from part way through a line. As well as now managing to drop a line on undo, I can also cause the application to crash by attempting to cursor past the end of the document. |
Reducing this to a test: from textual.app import App, ComposeResult
from textual.widgets import TextArea
from textual.widgets.text_area import Selection
TEST_TEXT = "\n".join(f"01234567890 - {n}" for n in range(10))
TEST_SELECTION = Selection((2, 5), (0, 5))
TEST_SELECTED_TEXT = "567890 - 0\n01234567890 - 1\n01234"
TEST_REPLACEMENT = "A"
TEST_RESULT_TEXT = TEST_TEXT.replace(TEST_SELECTED_TEXT, TEST_REPLACEMENT)
class TextAreaApp(App[None]):
def compose(self) -> ComposeResult:
yield TextArea(TEST_TEXT)
async def test_issue_4301_reproduction() -> None:
async with (app := TextAreaApp()).run_test() as pilot:
assert app.query_one(TextArea).text == TEST_TEXT
app.query_one(TextArea).selection = TEST_SELECTION
assert app.query_one(TextArea).selected_text == TEST_SELECTED_TEXT
await pilot.press("A")
assert app.query_one(TextArea).text == TEST_RESULT_TEXT
await pilot.press("ctrl+z")
assert app.query_one(TextArea).text == TEST_TEXT this is the automated equivalent of the above and it passes without an issue. This then suggests that the issue isn't with the underlying document -- which remains intact -- but is instead the display of the document. |
Furthermore, if I turn the above into a quick test application, like this: from textual.app import App, ComposeResult
from textual.widgets import TextArea
from textual.widgets.text_area import Selection
TEST_TEXT = "\n".join(f"01234567890 - {n}" for n in range(10))
TEST_SELECTION = Selection((2, 5), (0, 5))
class TextAreaApp(App[None]):
def compose(self) -> ComposeResult:
yield TextArea(TEST_TEXT)
def on_mount(self) -> None:
self.query_one(TextArea).selection = TEST_SELECTION
if __name__ == "__main__":
TextAreaApp().run() type TEST_SELECTION = Selection((2, 5), (0, 5)) for this: TEST_SELECTION = Selection((0, 5), (2, 5)) everything works as expected. The issue does seem to be down to a backward selection, replaced, and then undone. |
So I don't forget: a failing test can be made of this by adding the following to the end of the test: await pilot.press(*(["down"] * 10)) |
@TomJGooding Thanks, I'll take a look. |
To be decided if this is the fix, but this fixes the above crashing unit test: diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py
index c4e9a8bbf..fa66d8a53 100644
--- a/src/textual/widgets/_text_area.py
+++ b/src/textual/widgets/_text_area.py
@@ -1396,7 +1396,7 @@ TextArea {
# `insert` is not None because event.character cannot be
# None because we've checked that it's printable.
assert insert is not None
- start, end = self.selection
+ start, end = sorted(self.selection)
self._replace_via_keyboard(insert, start, end)
def _find_columns_to_next_tab_stop(self) -> int: |
The above diff also pans out in use, and all other |
Sorry just to add some detail now I'm back at my desk... Looking at the My thought was that these could be replaced with Of course this isn't to say that this simpler fix from @davep isn't correct, I just wonder why this wasn't the approach before (perhaps retaining the edit from/to order matters) or there might be other types of edits this wouldn't account for? |
Yup, my wondering too (hence still in draft for now). |
Just running a quick test using your example app above, I think if you delete rather than press |
Good to know, thanks. |
Co-authored-by: TomJGooding <101601846+TomJGooding@users.noreply.github.com>
Confirmed Del being an issue with davep@7d06a12 |
Co-authored-by: TomJGooding <101601846+TomJGooding@users.noreply.github.com>
Don't forget to star the repository! Follow @textualizeio for Textual updates. |
Hi, I've just updated to 0.54.0 to check the fix, it does not work :( The problems are the same: a backward selection and a replacement with a symbol or just deleting the selection eats lines at the bottom. I also checked it on Ubuntu and different terminals on Windows (cmd, PowerShell). |
Could you just check using the simple example app above and if possible post some screenshots/video what you are seeing? I've just tested and everything seems to be working as far as I can see? |
Ah, I think I might have reproduced the issue now, specifically after selecting backwards to the very start of the of text: TEST_SELECTION = Selection((2, 5), (0, 0)) After pressing a then Ctrl+z, you lose the last two lines. |
Yes, I have the same behavior. When I don't select (backward) a heading line, stopping at the second, everything works perfectly. |
I've run out of time this evening, but if someone else picks this up the fix might be always to check the edit top/bottom. My quick test seems to pass after this change: diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py
index 4ad2023a..6ff78864 100644
--- a/src/textual/widgets/_text_area.py
+++ b/src/textual/widgets/_text_area.py
@@ -1300,11 +1300,11 @@ TextArea {
end_location = (
edit._edit_result.end_location if edit._edit_result else (0, 0)
)
- if edit.from_location < minimum_from:
- minimum_from = edit.from_location
+ if edit.top < minimum_from:
+ minimum_from = edit.top
if end_location > maximum_old_end:
maximum_old_end = end_location
- if edit.to_location > maximum_new_end:
+ if edit.bottom > maximum_new_end:
maximum_new_end = edit.bottom
new_gutter_width = self.gutter_width Also worth looking at the redo function as it looks like this might have all the same problems as the undo. Try pressing Ctrl+yto redo the replace in the example app above and it will crash with an IndexError. |
Reopening and pinging @darrenburns |
I've finally managed to find some spare time and check the example with Tom's fixes. They solve my problem now. |
Don't forget to star the repository! Follow @textualizeio for Textual updates. |
Hi all,
I was experimenting with the TextArea widget and its Undo/Redo capabilities when I noticed some flaws with the Undo event.
When I select more than one line of any text and press an arbitrary key effectively changing this text with a symbol, the Ctrl+Z replaces the symbol with the previous text, but it chops several lines at the end of the text. I've noticed that it chops a previously replaced number of lines minus one. For example, we have a text of 10 lines, then select three (at the very beginning or in the middle of the text, it does not matter), replace them with a symbol, and strike the Undo. We will lose two lines at the end of the text (9th and 10th lines respectively). There is also an overlapping scenario. When I select 7 lines and replace them, there are 4 lines: one line with a new symbol, and three original lines at the end. 7 > 4. The Undo will return the first 4 lines of the original text, replacing these 4 lines altogether.
Please, find my code via the https://pastebin.com/KTrr00QR. Also, my diagnostic info is below.
Textual Diagnostics
Versions
Python
Operating System
Terminal
Rich Console options
The text was updated successfully, but these errors were encountered: