Skip to content

Commit

Permalink
Ensure "delete line" keybinding doesnt move cursor in read_only mode …
Browse files Browse the repository at this point in the history
…in TextArea
  • Loading branch information
darrenburns committed Feb 12, 2024
1 parent 11000da commit f5fa7ee
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 52 deletions.
5 changes: 3 additions & 2 deletions src/textual/widgets/_text_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -1950,8 +1950,9 @@ def action_delete_line(self) -> None:
from_location = (start_row, 0)
to_location = (end_row + 1, 0)

self._delete_via_keyboard(from_location, to_location)
self.move_cursor_relative(columns=end_column, record_width=False)
deletion = self._delete_via_keyboard(from_location, to_location)
if deletion is not None:
self.move_cursor_relative(columns=end_column, record_width=False)

def action_delete_to_start_of_line(self) -> None:
"""Deletes from the cursor location to the start of the line."""
Expand Down
4 changes: 3 additions & 1 deletion tests/text_area/test_edit_via_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Note that more extensive testing for editing is done at the Document level.
"""

import pytest

from textual.app import App, ComposeResult
Expand Down Expand Up @@ -521,10 +522,11 @@ async def test_replace_fully_within_selection():
)
assert text_area.selected_text == "XX56"


async def test_text_setter():
app = TextAreaApp()
async with app.run_test():
text_area = app.query_one(TextArea)
new_text = "hello\nworld\n"
text_area.text = new_text
assert text_area.text == new_text
assert text_area.text == new_text
31 changes: 31 additions & 0 deletions tests/text_area/test_edit_via_bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,37 @@ async def test_delete_word_right_at_end_of_line():
assert text_area.selection == Selection.cursor((0, 5))


@pytest.mark.parametrize(
"binding",
[
"enter",
"backspace",
"ctrl+u",
"ctrl+f",
"ctrl+w",
"ctrl+k",
"ctrl+x",
"space",
"1",
"tab",
],
)
async def test_edit_read_only_mode_does_nothing(binding):
"""Try out various key-presses and bindings and ensure they don't alter
the document when read_only=True."""
app = TextAreaApp()
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
text_area.read_only = True
selection = Selection.cursor((0, 2))
text_area.selection = selection

await pilot.press(binding)

assert text_area.text == TEXT
assert text_area.selection == selection


@pytest.mark.parametrize(
"selection",
[
Expand Down
94 changes: 45 additions & 49 deletions tests/text_area/test_selection_bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,48 @@


class TextAreaApp(App):
def __init__(self, read_only: bool = False):
super().__init__()
self.read_only = read_only

def compose(self) -> ComposeResult:
text_area = TextArea(show_line_numbers=True)
text_area.load_text(TEXT)
yield text_area
yield TextArea(TEXT, show_line_numbers=True, read_only=self.read_only)


@pytest.fixture(params=[True, False])
async def read_only_mode(request):
"""This parametrised fixture is injected into the app fixture.
It means every test that uses the app fixture will run twice - once
for read_only=True and once for read_only=False.
"""
return request.param


@pytest.fixture
async def app(read_only_mode: bool):
text_area_app = TextAreaApp(read_only_mode)
yield text_area_app

async def test_mouse_click():

async def test_mouse_click(app: TextAreaApp):
"""When you click the TextArea, the cursor moves to the expected location."""
app = TextAreaApp()
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
await pilot.click(TextArea, Offset(x=5, y=2))
assert text_area.selection == Selection.cursor((1, 0))


async def test_mouse_click_clamp_from_right():
async def test_mouse_click_clamp_from_right(app: TextAreaApp):
"""When you click to the right of the document bounds, the cursor is clamped
to within the document bounds."""
app = TextAreaApp()
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
await pilot.click(TextArea, Offset(x=8, y=20))
assert text_area.selection == Selection.cursor((4, 0))


async def test_mouse_click_gutter_clamp():
async def test_mouse_click_gutter_clamp(app: TextAreaApp):
"""When you click the gutter, it selects the start of the line."""
app = TextAreaApp()
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
await pilot.click(TextArea, Offset(x=0, y=3))
Expand All @@ -66,40 +80,36 @@ async def test_cursor_movement_basic():
assert text_area.selection == Selection.cursor((0, 0))


async def test_cursor_selection_right():
async def test_cursor_selection_right(app: TextAreaApp):
"""When you press shift+right the selection is updated correctly."""
app = TextAreaApp()
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
await pilot.press(*["shift+right"] * 3)
assert text_area.selection == Selection((0, 0), (0, 3))


async def test_cursor_selection_right_to_previous_line():
async def test_cursor_selection_right_to_previous_line(app: TextAreaApp):
"""When you press shift+right resulting in the cursor moving to the next line,
the selection is updated correctly."""
app = TextAreaApp()
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
text_area.selection = Selection.cursor((0, 15))
await pilot.press(*["shift+right"] * 4)
assert text_area.selection == Selection((0, 15), (1, 2))


async def test_cursor_selection_left():
async def test_cursor_selection_left(app: TextAreaApp):
"""When you press shift+left the selection is updated correctly."""
app = TextAreaApp()
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
text_area.selection = Selection.cursor((2, 5))
await pilot.press(*["shift+left"] * 3)
assert text_area.selection == Selection((2, 5), (2, 2))


async def test_cursor_selection_left_to_previous_line():
async def test_cursor_selection_left_to_previous_line(app: TextAreaApp):
"""When you press shift+left resulting in the cursor moving back to the previous line,
the selection is updated correctly."""
app = TextAreaApp()
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
text_area.selection = Selection.cursor((2, 2))
Expand All @@ -110,9 +120,8 @@ async def test_cursor_selection_left_to_previous_line():
assert text_area.selection == Selection((2, 2), (1, end_of_previous_line))


async def test_cursor_selection_up():
async def test_cursor_selection_up(app: TextAreaApp):
"""When you press shift+up the selection is updated correctly."""
app = TextAreaApp()
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
text_area.move_cursor((2, 3))
Expand All @@ -121,9 +130,8 @@ async def test_cursor_selection_up():
assert text_area.selection == Selection((2, 3), (1, 3))


async def test_cursor_selection_up_when_cursor_on_first_line():
async def test_cursor_selection_up_when_cursor_on_first_line(app: TextAreaApp):
"""When you press shift+up the on the first line, it selects to the start."""
app = TextAreaApp()
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
text_area.move_cursor((0, 4))
Expand All @@ -134,8 +142,7 @@ async def test_cursor_selection_up_when_cursor_on_first_line():
assert text_area.selection == Selection((0, 4), (0, 0))


async def test_cursor_selection_down():
app = TextAreaApp()
async def test_cursor_selection_down(app: TextAreaApp):
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
text_area.move_cursor((2, 5))
Expand All @@ -144,8 +151,7 @@ async def test_cursor_selection_down():
assert text_area.selection == Selection((2, 5), (3, 5))


async def test_cursor_selection_down_when_cursor_on_last_line():
app = TextAreaApp()
async def test_cursor_selection_down_when_cursor_on_last_line(app: TextAreaApp):
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
text_area.load_text("ABCDEF\nGHIJK")
Expand All @@ -157,8 +163,7 @@ async def test_cursor_selection_down_when_cursor_on_last_line():
assert text_area.selection == Selection((1, 2), (1, 5))


async def test_cursor_word_right():
app = TextAreaApp()
async def test_cursor_word_right(app: TextAreaApp):
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
text_area.load_text("ABC DEF\nGHIJK")
Expand All @@ -168,8 +173,7 @@ async def test_cursor_word_right():
assert text_area.selection == Selection.cursor((0, 3))


async def test_cursor_word_right_select():
app = TextAreaApp()
async def test_cursor_word_right_select(app: TextAreaApp):
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
text_area.load_text("ABC DEF\nGHIJK")
Expand All @@ -179,8 +183,7 @@ async def test_cursor_word_right_select():
assert text_area.selection == Selection((0, 0), (0, 3))


async def test_cursor_word_left():
app = TextAreaApp()
async def test_cursor_word_left(app: TextAreaApp):
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
text_area.load_text("ABC DEF\nGHIJK")
Expand All @@ -191,8 +194,7 @@ async def test_cursor_word_left():
assert text_area.selection == Selection.cursor((0, 4))


async def test_cursor_word_left_select():
app = TextAreaApp()
async def test_cursor_word_left_select(app: TextAreaApp):
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
text_area.load_text("ABC DEF\nGHIJK")
Expand All @@ -204,9 +206,8 @@ async def test_cursor_word_left_select():


@pytest.mark.parametrize("key", ["end", "ctrl+e"])
async def test_cursor_to_line_end(key):
async def test_cursor_to_line_end(key, app: TextAreaApp):
"""You can use the keyboard to jump the cursor to the end of the current line."""
app = TextAreaApp()
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
text_area.selection = Selection.cursor((2, 2))
Expand All @@ -217,9 +218,8 @@ async def test_cursor_to_line_end(key):


@pytest.mark.parametrize("key", ["home", "ctrl+a"])
async def test_cursor_to_line_home_basic_behaviour(key):
async def test_cursor_to_line_home_basic_behaviour(key, app: TextAreaApp):
"""You can use the keyboard to jump the cursor to the start of the current line."""
app = TextAreaApp()
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
text_area.selection = Selection.cursor((2, 2))
Expand All @@ -239,11 +239,12 @@ async def test_cursor_to_line_home_basic_behaviour(key):
((0, 15), (0, 4)),
],
)
async def test_cursor_line_home_smart_home(cursor_start, cursor_destination):
async def test_cursor_line_home_smart_home(
cursor_start, cursor_destination, app: TextAreaApp
):
"""If the line begins with whitespace, pressing home firstly goes
to the start of the (non-whitespace) content. Pressing it again takes you to column
0. If you press it again, it goes back to the first non-whitespace column."""
app = TextAreaApp()
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
text_area.load_text(" hello world")
Expand All @@ -252,9 +253,8 @@ async def test_cursor_line_home_smart_home(cursor_start, cursor_destination):
assert text_area.selection == Selection.cursor(cursor_destination)


async def test_cursor_page_down():
async def test_cursor_page_down(app: TextAreaApp):
"""Pagedown moves the cursor down 1 page, retaining column index."""
app = TextAreaApp()
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
text_area.load_text("XXX\n" * 200)
Expand All @@ -266,9 +266,8 @@ async def test_cursor_page_down():
)


async def test_cursor_page_up():
async def test_cursor_page_up(app: TextAreaApp):
"""Pageup moves the cursor up 1 page, retaining column index."""
app = TextAreaApp()
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
text_area.load_text("XXX\n" * 200)
Expand All @@ -280,10 +279,9 @@ async def test_cursor_page_up():
)


async def test_cursor_vertical_movement_visual_alignment_snapping():
async def test_cursor_vertical_movement_visual_alignment_snapping(app: TextAreaApp):
"""When you move the cursor vertically, it should stay vertically
aligned even when double-width characters are used."""
app = TextAreaApp()
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
text_area.text = "こんにちは\n012345"
Expand All @@ -301,8 +299,7 @@ async def test_cursor_vertical_movement_visual_alignment_snapping():
assert text_area.selection == Selection.cursor((1, 3))


async def test_select_line_binding():
app = TextAreaApp()
async def test_select_line_binding(app: TextAreaApp):
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
text_area.move_cursor((2, 2))
Expand All @@ -312,8 +309,7 @@ async def test_select_line_binding():
assert text_area.selection == Selection((2, 0), (2, 56))


async def test_select_all_binding():
app = TextAreaApp()
async def test_select_all_binding(app: TextAreaApp):
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)

Expand Down

0 comments on commit f5fa7ee

Please sign in to comment.