Skip to content

Commit

Permalink
Text area cut line (#5374)
Browse files Browse the repository at this point in the history
* Updating the behaviour of cut in the TextArea to cut the whole line if there is no selection

* Update bindings in TextArea

* Tidying up, remove unused action, include cut/copy/paste in keybinds table

* Docs fix

* Changelog
  • Loading branch information
darrenburns authored Dec 11, 2024
1 parent 3c120c0 commit d46498d
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 20 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Added

- Added `App.clipboard` https://github.com/Textualize/textual/pull/5352
- Added standard cut/copy/paste (ctrl+x, ctrl+c, ctrl+v) bindings to Input / TextArea https://github.com/Textualize/textual/pull/5352
- Added standard cut/copy/paste (ctrl+x, ctrl+c, ctrl+v) bindings to Input / TextArea https://github.com/Textualize/textual/pull/5352 & https://github.com/Textualize/textual/pull/5374
- Added `system` boolean to Binding, which hides the binding from the help panel https://github.com/Textualize/textual/pull/5352
- Added support for double/triple/etc clicks via `chain` attribute on `Click` events https://github.com/Textualize/textual/pull/5369
- Added `times` parameter to `Pilot.click` method, for simulating rapid clicks https://github.com/Textualize/textual/pull/5369

### Changed

- Change default quit key to `ctrl+q` https://github.com/Textualize/textual/pull/5352
- Changed delete line binding on TextArea to use `ctrl+shift+x` https://github.com/Textualize/textual/pull/5352
- The command palette will now select the top item automatically https://github.com/Textualize/textual/pull/5361
- `ctrl+shift+k` now deletes the current line in `TextArea`, and `ctrl+x` will cut
the selection if there is one, otherwise it will cut the current line https://github.com/Textualize/textual/pull/5374
- Implemented a better matching algorithm for the command palette https://github.com/Textualize/textual/pull/5365

### Fixed
Expand Down
40 changes: 24 additions & 16 deletions src/textual/widgets/_text_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,6 @@ class TextArea(ScrollView):
Binding(
"ctrl+f", "delete_word_right", "Delete right to start of word", show=False
),
Binding("ctrl+shift+x", "delete_line", "Delete line", show=False),
Binding("ctrl+x", "cut", "Cut", show=False),
Binding("ctrl+c", "copy", "Copy", show=False),
Binding("ctrl+v", "paste", "Paste", show=False),
Expand All @@ -239,9 +238,14 @@ class TextArea(ScrollView):
"Delete to line end",
show=False,
),
Binding(
"ctrl+shift+k",
"delete_line",
"Delete line",
show=False,
),
Binding("ctrl+z", "undo", "Undo", show=False),
Binding("ctrl+y", "redo", "Redo", show=False),
Binding("ctrl+c", "copy_selection", "Copy selected text", show=False),
]
"""
| Key(s) | Description |
Expand All @@ -268,13 +272,16 @@ class TextArea(ScrollView):
| ctrl+w | Delete from cursor to start of the word. |
| delete,ctrl+d | Delete character to the right of cursor. |
| ctrl+f | Delete from cursor to end of the word. |
| ctrl+shift+x | Delete the current line. |
| ctrl+shift+k | Delete the current line. |
| ctrl+u | Delete from cursor to the start of the line. |
| ctrl+k | Delete from cursor to the end of the line. |
| f6 | Select the current line. |
| f7 | Select all text in the document. |
| ctrl+z | Undo. |
| ctrl+y | Redo. |
| ctrl+x | Cut selection or line if no selection. |
| ctrl+c | Copy selection to clipboard. |
| ctrl+v | Paste from clipboard. |
"""

language: Reactive[str | None] = reactive(None, always_update=True, init=False)
Expand Down Expand Up @@ -2185,6 +2192,10 @@ def action_delete_right(self) -> None:

def action_delete_line(self) -> None:
"""Deletes the lines which intersect with the selection."""
self._delete_cursor_line()

def _delete_cursor_line(self) -> EditResult | None:
"""Deletes the line (including the line terminator) that the cursor is on."""
start, end = self.selection
start, end = sorted((start, end))
start_row, _start_column = start
Expand All @@ -2201,25 +2212,26 @@ def action_delete_line(self) -> None:
deletion = self._delete_via_keyboard(from_location, to_location)
if deletion is not None:
self.move_cursor_relative(columns=end_column, record_width=False)
return deletion

def action_cut(self) -> None:
"""Cut text (remove and copy to clipboard)."""
if self.read_only:
return
start, end = self.selection
if start == end:
return
copy_text = self.get_text_range(start, end)
self.app.copy_to_clipboard(copy_text)
self._delete_via_keyboard(start, end)
edit_result = self._delete_cursor_line()
else:
edit_result = self._delete_via_keyboard(start, end)

if edit_result is not None:
self.app.copy_to_clipboard(edit_result.replaced_text)

def action_copy(self) -> None:
"""Copy selection to clipboard."""
start, end = self.selection
if start == end:
return
copy_text = self.get_text_range(start, end)
self.app.copy_to_clipboard(copy_text)
selected_text = self.selected_text
if selected_text:
self.app.copy_to_clipboard(selected_text)

def action_paste(self) -> None:
"""Paste from local clipboard."""
Expand Down Expand Up @@ -2310,10 +2322,6 @@ def action_delete_word_right(self) -> None:

self._delete_via_keyboard(end, to_location)

def action_copy_selection(self) -> None:
"""Copy the current selection to the clipboard."""
self.app.copy_to_clipboard(self.selected_text)


@lru_cache(maxsize=128)
def build_byte_to_codepoint_dict(data: bytes) -> dict[int, int]:
Expand Down
55 changes: 53 additions & 2 deletions tests/text_area/test_edit_via_bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,57 @@ async def test_delete_right_end_of_line():
assert text_area.text == "helloworld!"


@pytest.mark.parametrize(
"selection,expected_result,expected_clipboard,cursor_end_location",
[
(Selection.cursor((0, 0)), "", "0123456789", (0, 0)),
(Selection.cursor((0, 4)), "", "0123456789", (0, 0)),
(Selection.cursor((0, 10)), "", "0123456789", (0, 0)),
(Selection((0, 2), (0, 4)), "01456789", "23", (0, 2)),
(Selection((0, 4), (0, 2)), "01456789", "23", (0, 2)),
],
)
async def test_cut(selection, expected_result, expected_clipboard, cursor_end_location):
app = TextAreaApp()
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
text_area.load_text("0123456789")
text_area.selection = selection

await pilot.press("ctrl+x")

assert text_area.selection == Selection.cursor(cursor_end_location)
assert text_area.text == expected_result
assert app.clipboard == expected_clipboard


@pytest.mark.parametrize(
"selection,expected_result",
[
# Cursors
(Selection.cursor((0, 0)), "345\n678\n9\n"),
(Selection.cursor((0, 2)), "345\n678\n9\n"),
(Selection.cursor((3, 1)), "012\n345\n678\n"),
(Selection.cursor((4, 0)), "012\n345\n678\n9\n"),
# Selections
(Selection((1, 1), (1, 2)), "012\n35\n678\n9\n"),
(Selection((1, 2), (2, 1)), "012\n3478\n9\n"),
],
)
async def test_cut_multiline_document(selection, expected_result):
app = TextAreaApp()
async with app.run_test() as pilot:
text_area = app.query_one(TextArea)
text_area.load_text("012\n345\n678\n9\n")
text_area.selection = selection

await pilot.press("ctrl+x")

cursor_row, cursor_column = text_area.cursor_location
assert text_area.selection == Selection.cursor((cursor_row, cursor_column))
assert text_area.text == expected_result


@pytest.mark.parametrize(
"selection,expected_result",
[
Expand All @@ -184,7 +235,7 @@ async def test_delete_line(selection, expected_result):
text_area.load_text("0123456789")
text_area.selection = selection

await pilot.press("ctrl+shift+x")
await pilot.press("ctrl+shift+k")

assert text_area.selection == Selection.cursor((0, 0))
assert text_area.text == expected_result
Expand Down Expand Up @@ -219,7 +270,7 @@ async def test_delete_line_multiline_document(selection, expected_result):
text_area.load_text("012\n345\n678\n9\n")
text_area.selection = selection

await pilot.press("ctrl+shift+x")
await pilot.press("ctrl+shift+k")

cursor_row, cursor_column = text_area.cursor_location
assert text_area.selection == Selection.cursor((cursor_row, cursor_column))
Expand Down

0 comments on commit d46498d

Please sign in to comment.