Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Text area cut line #5374

Merged
merged 7 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ 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

### 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
Loading