diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index b09b96310..5af98629b 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -378,20 +378,32 @@ class Input(Widget, can_focus=True): def action_cursor_left_word(self) -> None: """Move the cursor left to the start of a word.""" - try: - *_, hit = re.finditer(self._WORD_START, self.value[: self.cursor_position]) - except ValueError: - self.cursor_position = 0 + if self.password: + # This is a password field so don't give any hints about word + # boundaries, even during movement. + self.action_home() else: - self.cursor_position = hit.start() + try: + *_, hit = re.finditer( + self._WORD_START, self.value[: self.cursor_position] + ) + except ValueError: + self.cursor_position = 0 + else: + self.cursor_position = hit.start() def action_cursor_right_word(self) -> None: """Move the cursor right to the start of a word.""" - hit = re.search(self._WORD_START, self.value[self.cursor_position :]) - if hit is None: - self.cursor_position = len(self.value) + if self.password: + # This is a password field so don't give any hints about word + # boundaries, even during movement. + self.action_end() else: - self.cursor_position += hit.start() + hit = re.search(self._WORD_START, self.value[self.cursor_position :]) + if hit is None: + self.cursor_position = len(self.value) + else: + self.cursor_position += hit.start() def action_delete_right(self) -> None: """Delete one character at the current cursor position.""" @@ -404,12 +416,19 @@ class Input(Widget, can_focus=True): def action_delete_right_word(self) -> None: """Delete the current character and all rightward to the start of the next word.""" - after = self.value[self.cursor_position :] - hit = re.search(self._WORD_START, after) - if hit is None: - self.value = self.value[: self.cursor_position] + if self.password: + # This is a password field so don't give any hints about word + # boundaries, even during deletion. + self.action_delete_right_all() else: - self.value = f"{self.value[: self.cursor_position]}{after[hit.end()-1 :]}" + after = self.value[self.cursor_position :] + hit = re.search(self._WORD_START, after) + if hit is None: + self.value = self.value[: self.cursor_position] + else: + self.value = ( + f"{self.value[: self.cursor_position]}{after[hit.end()-1 :]}" + ) def action_delete_right_all(self) -> None: """Delete the current character and all characters to the right of the cursor position.""" @@ -437,14 +456,21 @@ class Input(Widget, can_focus=True): """Delete leftward of the cursor position to the start of a word.""" if self.cursor_position <= 0: return - after = self.value[self.cursor_position :] - try: - *_, hit = re.finditer(self._WORD_START, self.value[: self.cursor_position]) - except ValueError: - self.cursor_position = 0 + if self.password: + # This is a password field so don't give any hints about word + # boundaries, even during deletion. + self.action_delete_left_all() else: - self.cursor_position = hit.start() - self.value = f"{self.value[: self.cursor_position]}{after}" + after = self.value[self.cursor_position :] + try: + *_, hit = re.finditer( + self._WORD_START, self.value[: self.cursor_position] + ) + except ValueError: + self.cursor_position = 0 + else: + self.cursor_position = hit.start() + self.value = f"{self.value[: self.cursor_position]}{after}" def action_delete_left_all(self) -> None: """Delete all characters to the left of the cursor position.""" diff --git a/tests/input/test_input_key_modification_actions.py b/tests/input/test_input_key_modification_actions.py index 51b124d7d..68f7e50c8 100644 --- a/tests/input/test_input_key_modification_actions.py +++ b/tests/input/test_input_key_modification_actions.py @@ -66,6 +66,17 @@ async def test_delete_left_word_from_end() -> None: assert input.value == expected[input.id] +async def test_password_delete_left_word_from_end() -> None: + """Deleting word left from end of a password input should delete everything.""" + async with InputTester().run_test() as pilot: + for input in pilot.app.query(Input): + input.action_end() + input.password = True + input.action_delete_left_word() + assert input.cursor_position == 0 + assert input.value == "" + + async def test_delete_left_all_from_home() -> None: """Deleting all left from home should do nothing.""" async with InputTester().run_test() as pilot: @@ -119,6 +130,16 @@ async def test_delete_right_word_from_home() -> None: assert input.value == expected[input.id] +async def test_password_delete_right_word_from_home() -> None: + """Deleting word right from home of a password input should delete everything.""" + async with InputTester().run_test() as pilot: + for input in pilot.app.query(Input): + input.password = True + input.action_delete_right_word() + assert input.cursor_position == 0 + assert input.value == "" + + async def test_delete_right_word_from_end() -> None: """Deleting word right from end should not change the input's value.""" async with InputTester().run_test() as pilot: diff --git a/tests/input/test_input_key_movement_actions.py b/tests/input/test_input_key_movement_actions.py index 2d0bc1338..a6cf13694 100644 --- a/tests/input/test_input_key_movement_actions.py +++ b/tests/input/test_input_key_movement_actions.py @@ -97,6 +97,16 @@ async def test_input_left_word_from_end() -> None: assert input.cursor_position == expected_at[input.id] +async def test_password_input_left_word_from_end() -> None: + """Going left one word from the end in a password field should land at home.""" + async with InputTester().run_test() as pilot: + for input in pilot.app.query(Input): + input.action_end() + input.password = True + input.action_cursor_left_word() + assert input.cursor_position == 0 + + async def test_input_right_word_from_home() -> None: """Going right one word from the start should land correctly..""" async with InputTester().run_test() as pilot: @@ -112,6 +122,15 @@ async def test_input_right_word_from_home() -> None: assert input.cursor_position == expected_at[input.id] +async def test_password_input_right_word_from_home() -> None: + """Going right one word from the start of a password input should go to the end.""" + async with InputTester().run_test() as pilot: + for input in pilot.app.query(Input): + input.password = True + input.action_cursor_right_word() + assert input.cursor_position == len(input.value) + + async def test_input_right_word_from_end() -> None: """Going right one word from the end should do nothing.""" async with InputTester().run_test() as pilot: