Skip to content

Commit d46498d

Browse files
authored
Text area cut line (#5374)
* 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
1 parent 3c120c0 commit d46498d

File tree

3 files changed

+80
-20
lines changed

3 files changed

+80
-20
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1010
### Added
1111

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

1818
### Changed
1919

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

2526
### Fixed

src/textual/widgets/_text_area.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,6 @@ class TextArea(ScrollView):
226226
Binding(
227227
"ctrl+f", "delete_word_right", "Delete right to start of word", show=False
228228
),
229-
Binding("ctrl+shift+x", "delete_line", "Delete line", show=False),
230229
Binding("ctrl+x", "cut", "Cut", show=False),
231230
Binding("ctrl+c", "copy", "Copy", show=False),
232231
Binding("ctrl+v", "paste", "Paste", show=False),
@@ -239,9 +238,14 @@ class TextArea(ScrollView):
239238
"Delete to line end",
240239
show=False,
241240
),
241+
Binding(
242+
"ctrl+shift+k",
243+
"delete_line",
244+
"Delete line",
245+
show=False,
246+
),
242247
Binding("ctrl+z", "undo", "Undo", show=False),
243248
Binding("ctrl+y", "redo", "Redo", show=False),
244-
Binding("ctrl+c", "copy_selection", "Copy selected text", show=False),
245249
]
246250
"""
247251
| Key(s) | Description |
@@ -268,13 +272,16 @@ class TextArea(ScrollView):
268272
| ctrl+w | Delete from cursor to start of the word. |
269273
| delete,ctrl+d | Delete character to the right of cursor. |
270274
| ctrl+f | Delete from cursor to end of the word. |
271-
| ctrl+shift+x | Delete the current line. |
275+
| ctrl+shift+k | Delete the current line. |
272276
| ctrl+u | Delete from cursor to the start of the line. |
273277
| ctrl+k | Delete from cursor to the end of the line. |
274278
| f6 | Select the current line. |
275279
| f7 | Select all text in the document. |
276280
| ctrl+z | Undo. |
277281
| ctrl+y | Redo. |
282+
| ctrl+x | Cut selection or line if no selection. |
283+
| ctrl+c | Copy selection to clipboard. |
284+
| ctrl+v | Paste from clipboard. |
278285
"""
279286

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

21862193
def action_delete_line(self) -> None:
21872194
"""Deletes the lines which intersect with the selection."""
2195+
self._delete_cursor_line()
2196+
2197+
def _delete_cursor_line(self) -> EditResult | None:
2198+
"""Deletes the line (including the line terminator) that the cursor is on."""
21882199
start, end = self.selection
21892200
start, end = sorted((start, end))
21902201
start_row, _start_column = start
@@ -2201,25 +2212,26 @@ def action_delete_line(self) -> None:
22012212
deletion = self._delete_via_keyboard(from_location, to_location)
22022213
if deletion is not None:
22032214
self.move_cursor_relative(columns=end_column, record_width=False)
2215+
return deletion
22042216

22052217
def action_cut(self) -> None:
22062218
"""Cut text (remove and copy to clipboard)."""
22072219
if self.read_only:
22082220
return
22092221
start, end = self.selection
22102222
if start == end:
2211-
return
2212-
copy_text = self.get_text_range(start, end)
2213-
self.app.copy_to_clipboard(copy_text)
2214-
self._delete_via_keyboard(start, end)
2223+
edit_result = self._delete_cursor_line()
2224+
else:
2225+
edit_result = self._delete_via_keyboard(start, end)
2226+
2227+
if edit_result is not None:
2228+
self.app.copy_to_clipboard(edit_result.replaced_text)
22152229

22162230
def action_copy(self) -> None:
22172231
"""Copy selection to clipboard."""
2218-
start, end = self.selection
2219-
if start == end:
2220-
return
2221-
copy_text = self.get_text_range(start, end)
2222-
self.app.copy_to_clipboard(copy_text)
2232+
selected_text = self.selected_text
2233+
if selected_text:
2234+
self.app.copy_to_clipboard(selected_text)
22232235

22242236
def action_paste(self) -> None:
22252237
"""Paste from local clipboard."""
@@ -2310,10 +2322,6 @@ def action_delete_word_right(self) -> None:
23102322

23112323
self._delete_via_keyboard(end, to_location)
23122324

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

23182326
@lru_cache(maxsize=128)
23192327
def build_byte_to_codepoint_dict(data: bytes) -> dict[int, int]:

tests/text_area/test_edit_via_bindings.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,57 @@ async def test_delete_right_end_of_line():
167167
assert text_area.text == "helloworld!"
168168

169169

170+
@pytest.mark.parametrize(
171+
"selection,expected_result,expected_clipboard,cursor_end_location",
172+
[
173+
(Selection.cursor((0, 0)), "", "0123456789", (0, 0)),
174+
(Selection.cursor((0, 4)), "", "0123456789", (0, 0)),
175+
(Selection.cursor((0, 10)), "", "0123456789", (0, 0)),
176+
(Selection((0, 2), (0, 4)), "01456789", "23", (0, 2)),
177+
(Selection((0, 4), (0, 2)), "01456789", "23", (0, 2)),
178+
],
179+
)
180+
async def test_cut(selection, expected_result, expected_clipboard, cursor_end_location):
181+
app = TextAreaApp()
182+
async with app.run_test() as pilot:
183+
text_area = app.query_one(TextArea)
184+
text_area.load_text("0123456789")
185+
text_area.selection = selection
186+
187+
await pilot.press("ctrl+x")
188+
189+
assert text_area.selection == Selection.cursor(cursor_end_location)
190+
assert text_area.text == expected_result
191+
assert app.clipboard == expected_clipboard
192+
193+
194+
@pytest.mark.parametrize(
195+
"selection,expected_result",
196+
[
197+
# Cursors
198+
(Selection.cursor((0, 0)), "345\n678\n9\n"),
199+
(Selection.cursor((0, 2)), "345\n678\n9\n"),
200+
(Selection.cursor((3, 1)), "012\n345\n678\n"),
201+
(Selection.cursor((4, 0)), "012\n345\n678\n9\n"),
202+
# Selections
203+
(Selection((1, 1), (1, 2)), "012\n35\n678\n9\n"),
204+
(Selection((1, 2), (2, 1)), "012\n3478\n9\n"),
205+
],
206+
)
207+
async def test_cut_multiline_document(selection, expected_result):
208+
app = TextAreaApp()
209+
async with app.run_test() as pilot:
210+
text_area = app.query_one(TextArea)
211+
text_area.load_text("012\n345\n678\n9\n")
212+
text_area.selection = selection
213+
214+
await pilot.press("ctrl+x")
215+
216+
cursor_row, cursor_column = text_area.cursor_location
217+
assert text_area.selection == Selection.cursor((cursor_row, cursor_column))
218+
assert text_area.text == expected_result
219+
220+
170221
@pytest.mark.parametrize(
171222
"selection,expected_result",
172223
[
@@ -184,7 +235,7 @@ async def test_delete_line(selection, expected_result):
184235
text_area.load_text("0123456789")
185236
text_area.selection = selection
186237

187-
await pilot.press("ctrl+shift+x")
238+
await pilot.press("ctrl+shift+k")
188239

189240
assert text_area.selection == Selection.cursor((0, 0))
190241
assert text_area.text == expected_result
@@ -219,7 +270,7 @@ async def test_delete_line_multiline_document(selection, expected_result):
219270
text_area.load_text("012\n345\n678\n9\n")
220271
text_area.selection = selection
221272

222-
await pilot.press("ctrl+shift+x")
273+
await pilot.press("ctrl+shift+k")
223274

224275
cursor_row, cursor_column = text_area.cursor_location
225276
assert text_area.selection == Selection.cursor((cursor_row, cursor_column))

0 commit comments

Comments
 (0)