Skip to content

Commit f570a9b

Browse files
committed
fix(masked input): fix bindings to move and select
Fix bindings like `shift+right` that should move the cursor and select not selecting text in `MaskedInput`.
1 parent ac817e3 commit f570a9b

File tree

2 files changed

+84
-13
lines changed

2 files changed

+84
-13
lines changed

src/textual/widgets/_masked_input.py

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
from textual.reactive import Reactive, var
2020
from textual.validation import ValidationResult, Validator
21-
from textual.widgets._input import Input
21+
from textual.widgets._input import Input, Selection
2222

2323
InputValidationOn = Literal["blur", "changed", "submitted"]
2424
"""Possible messages that trigger input validation."""
@@ -703,17 +703,31 @@ def action_cursor_left(self, select: bool = False) -> None:
703703
Args:
704704
select: If `True`, select the text to the left of the cursor.
705705
"""
706+
start, end = self.selection
706707
cursor_position = self._template.move_cursor(-1)
707-
self.cursor_position = cursor_position
708+
if select:
709+
self.selection = Selection(start, cursor_position)
710+
else:
711+
if self.selection.is_empty:
712+
self.cursor_position = cursor_position
713+
else:
714+
self.cursor_position = min(start, end)
708715

709716
def action_cursor_right(self, select: bool = False) -> None:
710717
"""Move the cursor one position to the right; separators are skipped.
711718
712719
Args:
713720
select: If `True`, select the text to the right of the cursor.
714721
"""
722+
start, end = self.selection
715723
cursor_position = self._template.move_cursor(1)
716-
self.cursor_position = cursor_position
724+
if select:
725+
self.selection = Selection(start, cursor_position)
726+
else:
727+
if self.selection.is_empty:
728+
self.cursor_position = cursor_position
729+
else:
730+
self.cursor_position = max(start, end)
717731

718732
def action_home(self, select: bool = False) -> None:
719733
"""Move the cursor to the start of the input.
@@ -722,7 +736,10 @@ def action_home(self, select: bool = False) -> None:
722736
select: If `True`, select the text between the old and new cursor positions.
723737
"""
724738
cursor_position = self._template.move_cursor(-len(self.template))
725-
self.cursor_position = cursor_position
739+
if select:
740+
self.selection = Selection(self.cursor_position, cursor_position)
741+
else:
742+
self.cursor_position = cursor_position
726743

727744
def action_cursor_left_word(self, select: bool = False) -> None:
728745
"""Move the cursor left next to the previous separator. If no previous
@@ -732,12 +749,22 @@ def action_cursor_left_word(self, select: bool = False) -> None:
732749
select: If `True`, select the text between the old and new cursor positions.
733750
"""
734751
if self._template.at_separator(self.cursor_position - 1):
735-
position = self._template.prev_separator_position(self.cursor_position - 1)
752+
separator_position = self._template.prev_separator_position(
753+
self.cursor_position - 1
754+
)
736755
else:
737-
position = self._template.prev_separator_position()
738-
if position:
739-
position += 1
740-
self.cursor_position = position or 0
756+
separator_position = self._template.prev_separator_position()
757+
758+
if separator_position is None:
759+
cursor_position = 0
760+
else:
761+
cursor_position = separator_position + 1
762+
763+
if select:
764+
start, _ = self.selection
765+
self.selection = Selection(start, cursor_position)
766+
else:
767+
self.cursor_position = cursor_position
741768

742769
def action_cursor_right_word(self, select: bool = False) -> None:
743770
"""Move the cursor right next to the next separator. If no next
@@ -746,11 +773,17 @@ def action_cursor_right_word(self, select: bool = False) -> None:
746773
Args:
747774
select: If `True`, select the text between the old and new cursor positions.
748775
"""
749-
position = self._template.next_separator_position()
750-
if position is None:
751-
self.cursor_position = len(self._template.mask)
776+
separator_position = self._template.next_separator_position()
777+
if separator_position is None:
778+
cursor_position = len(self._template.mask)
752779
else:
753-
self.cursor_position = position + 1
780+
cursor_position = separator_position + 1
781+
782+
if select:
783+
start, _ = self.selection
784+
self.selection = Selection(start, cursor_position)
785+
else:
786+
self.cursor_position = cursor_position
754787

755788
def action_delete_right(self) -> None:
756789
"""Delete one character at the current cursor position."""

tests/test_masked_input.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,3 +323,41 @@ async def test_replace_selection_with_invalid_value():
323323
assert input.selection == (0, len(input.value)) # Sanity check
324324
await pilot.press("a")
325325
assert input.value == "2025-12"
326+
327+
328+
async def test_movement_actions_with_select():
329+
app = MaskedInputApp(
330+
template=">NNNNN-NNNNN-NNNNN-NNNNN;_",
331+
value="ABCDE-FGHIJ-KLMNO-PQRST",
332+
select_on_focus=False,
333+
)
334+
async with app.run_test():
335+
input = app.query_one(MaskedInput)
336+
337+
input.action_home(select=True)
338+
assert input.selection == (len(input.value), 0)
339+
340+
input.action_cursor_left()
341+
assert input.selection.is_empty
342+
assert input.cursor_position == 0
343+
344+
input.action_cursor_right_word(select=True)
345+
assert input.selection == (0, 6)
346+
347+
input.action_cursor_right()
348+
assert input.selection.is_empty
349+
assert input.cursor_position == 6
350+
351+
input.action_cursor_left(select=True)
352+
assert input.selection == (6, 4)
353+
354+
input.action_cursor_left()
355+
input.action_cursor_right(select=True)
356+
assert input.selection == (4, 6)
357+
358+
input.action_end(select=True)
359+
assert input.selection == (6, len(input.value))
360+
361+
input.action_cursor_right()
362+
input.action_cursor_left_word(select=True)
363+
assert input.selection == (len(input.value), 18)

0 commit comments

Comments
 (0)