From 8091a739b4cdb8f96dadf0783c6e80a07ef62215 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 16 Dec 2024 15:14:30 +0000 Subject: [PATCH 1/6] Refactor cursor movement logic in Input widget to handle selection state in manner similar to VSCode, browsers, etc. If there's a selection and the cursor left/right keybinding is pressed (without shift held), the cursor will move to the corresponding "edge" of the selection. --- src/textual/widgets/_input.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 8f343b1df3..8e2bf86506 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -761,11 +761,14 @@ def action_cursor_left(self, select: bool = False) -> None: Args: select: If `True`, select the text to the left of the cursor. """ + start, end = self.selection if select: - start, end = self.selection self.selection = Selection(start, end - 1) else: - self.cursor_position -= 1 + if self.selection.is_empty: + self.cursor_position -= 1 + else: + self.cursor_position = min(start, end) def action_cursor_right(self, select: bool = False) -> None: """Accept an auto-completion or move the cursor one position to the right. @@ -773,15 +776,18 @@ def action_cursor_right(self, select: bool = False) -> None: Args: select: If `True`, select the text to the right of the cursor. """ + start, end = self.selection if select: - start, end = self.selection self.selection = Selection(start, end + 1) else: if self._cursor_at_end and self._suggestion: self.value = self._suggestion self.cursor_position = len(self.value) else: - self.cursor_position += 1 + if self.selection.is_empty: + self.cursor_position += 1 + else: + self.cursor_position = max(start, end) def action_home(self, select: bool = False) -> None: """Move the cursor to the start of the input. From a317a6015f20af0ee45c7aa006f3fa9ef9d18815 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 16 Dec 2024 15:48:01 +0000 Subject: [PATCH 2/6] Cursor left and right standardisation when theres an active selection --- src/textual/widgets/_text_area.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index b011d8d8ae..fc004c231b 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -1838,8 +1838,12 @@ def action_cursor_left(self, select: bool = False) -> None: Args: select: If True, select the text while moving. """ - target = self.get_cursor_left_location() - self.move_cursor(target, select=select) + if self.selection.is_empty: + target = self.get_cursor_left_location() + self.move_cursor(target, select=select) + else: + start, end = self.selection + self.move_cursor(min(start, end), select=select) def get_cursor_left_location(self) -> Location: """Get the location the cursor will move to if it moves left. @@ -1857,8 +1861,12 @@ def action_cursor_right(self, select: bool = False) -> None: Args: select: If True, select the text while moving. """ - target = self.get_cursor_right_location() - self.move_cursor(target, select=select) + if self.selection.is_empty: + target = self.get_cursor_right_location() + self.move_cursor(target, select=select) + else: + start, end = self.selection + self.move_cursor(max(start, end), select=select) def get_cursor_right_location(self) -> Location: """Get the location the cursor will move to if it moves right. From 72395d90b6ea9d20f6f2e5667640257b021467fc Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 16 Dec 2024 17:38:35 +0000 Subject: [PATCH 3/6] Improving logic --- src/textual/widgets/_text_area.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index fc004c231b..a73582eb1c 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -1838,12 +1838,12 @@ def action_cursor_left(self, select: bool = False) -> None: Args: select: If True, select the text while moving. """ - if self.selection.is_empty: - target = self.get_cursor_left_location() - self.move_cursor(target, select=select) - else: - start, end = self.selection - self.move_cursor(min(start, end), select=select) + target = ( + self.get_cursor_left_location() + if select or self.selection.is_empty + else min(*self.selection) + ) + self.move_cursor(target, select=select) def get_cursor_left_location(self) -> Location: """Get the location the cursor will move to if it moves left. @@ -1861,12 +1861,12 @@ def action_cursor_right(self, select: bool = False) -> None: Args: select: If True, select the text while moving. """ - if self.selection.is_empty: - target = self.get_cursor_right_location() - self.move_cursor(target, select=select) - else: - start, end = self.selection - self.move_cursor(max(start, end), select=select) + target = ( + self.get_cursor_right_location() + if select or self.selection.is_empty + else max(*self.selection) + ) + self.move_cursor(target, select=select) def get_cursor_right_location(self) -> Location: """Get the location the cursor will move to if it moves right. From 21ea23cb03eafe0724b8028c2d5ecfd0fecd38c6 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 17 Dec 2024 11:25:46 +0000 Subject: [PATCH 4/6] Update CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46ff52f8ca..96ad71ff17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## Unreleased + +### Changed + +- Updated `TextArea` and `Input` behavior when there is a selection and the user presses left or right https://github.com/Textualize/textual/pull/5400 + ## [1.0.0] - 2024-12-12 ### Added From a5c568b1b726d3db567dcaff6dcd3f4c6c068bac Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 17 Dec 2024 11:26:01 +0000 Subject: [PATCH 5/6] Update test to account for new interactions with selections and cursor movement --- tests/input/test_input_terminal_cursor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/input/test_input_terminal_cursor.py b/tests/input/test_input_terminal_cursor.py index b956a29846..31f1770181 100644 --- a/tests/input/test_input_terminal_cursor.py +++ b/tests/input/test_input_terminal_cursor.py @@ -8,7 +8,9 @@ class InputApp(App): CSS = "Input { padding: 4 8 }" def compose(self) -> ComposeResult: - yield Input("こんにちは!") + # We don't want to select the text on focus, as selected text + # has different interactions with the cursor_left action. + yield Input("こんにちは!", select_on_focus=False) async def test_initial_terminal_cursor_position(): From 50b3677d87faecf2e741803df6fcb7056cc3fab6 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 17 Dec 2024 11:27:09 +0000 Subject: [PATCH 6/6] Improve docstrings --- src/textual/widgets/_text_area.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index a73582eb1c..687ef8107d 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -1835,6 +1835,8 @@ def action_cursor_left(self, select: bool = False) -> None: If the cursor is at the left edge of the document, try to move it to the end of the previous line. + If text is selected, move the cursor to the start of the selection. + Args: select: If True, select the text while moving. """ @@ -1858,6 +1860,8 @@ def action_cursor_right(self, select: bool = False) -> None: If the cursor is at the end of a line, attempt to go to the start of the next line. + If text is selected, move the cursor to the end of the selection. + Args: select: If True, select the text while moving. """