Skip to content

Commit 413c3f9

Browse files
committed
gh-131430: Fix crashes on empty DELETE_WORD_BACKWARDS (^W) followed by CLEAR_TO_START (^K)
1 parent 878e0fb commit 413c3f9

File tree

3 files changed

+58
-1
lines changed

3 files changed

+58
-1
lines changed

Lib/_pyrepl/commands.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def kill_range(self, start: int, end: int) -> None:
6666
b = r.buffer
6767
text = b[start:end]
6868
del b[start:end]
69-
if is_kill(r.last_command):
69+
if is_kill(r.last_command) and r.kill_ring:
7070
if start < r.pos:
7171
r.kill_ring[-1] = text + r.kill_ring[-1]
7272
else:

Lib/test/test_pyrepl/test_reader.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from textwrap import dedent
55
from unittest import TestCase
66
from unittest.mock import MagicMock
7+
from _pyrepl.readline import multiline_input
78
from test.support import force_colorized_test_class, force_not_colorized_test_class
89

910
from .support import handle_all_events, handle_events_narrow_console
@@ -358,6 +359,59 @@ def test_setpos_from_xy_for_non_printing_char(self):
358359
reader.setpos_from_xy(8, 0)
359360
self.assertEqual(reader.pos, 7)
360361

362+
def test_empty_line_control_w_k(self):
363+
"""Test that Control-W followed by Control-K on an empty line doesn't crash."""
364+
events = itertools.chain(
365+
[
366+
Event(evt="key", data="\x17", raw=bytearray(b"\x17")), # Control-W
367+
Event(evt="key", data="\x0b", raw=bytearray(b"\x0b")), # Control-K
368+
],
369+
)
370+
reader, _ = handle_all_events(events)
371+
self.assert_screen_equal(reader, "", clean=True)
372+
self.assertEqual(reader.pos, 0)
373+
374+
def test_control_w_delete_word(self):
375+
"""Test Control-W delete word"""
376+
def test_with_text(text: str, expected: list[str], before_pos: int, after_pos: int):
377+
events = itertools.chain(
378+
code_to_events(text) if len(text) else [],
379+
[Event(evt="key", data="left", raw=bytearray(b"\x1b[D"))] * (len(text) - before_pos), # Move cursor to specified position
380+
[
381+
Event(evt="key", data="\x17", raw=bytearray(b"\x17")), # Control-W
382+
],
383+
)
384+
reader, _ = handle_all_events(events)
385+
self.assertEqual(reader.screen, expected)
386+
self.assertEqual(reader.pos, after_pos)
387+
388+
test_with_text("", [], 0, 0)
389+
test_with_text("a", [""], 1, 0)
390+
test_with_text("abc", [""], 3, 0)
391+
test_with_text("abc def", ["def"], 4, 0)
392+
test_with_text("abc def", ["abc "], 7, 4)
393+
test_with_text("def xxx():xxx\n ", ["def xxx():"], 18, 10)
394+
395+
def test_control_k_delete_to_eol(self):
396+
"""Test Control-K delete from cursor to end of line"""
397+
def test_with_text(text: str, pos: int, expected: list[str]):
398+
events = itertools.chain(
399+
code_to_events(text) if len(text) else [],
400+
[Event(evt="key", data="left", raw=bytearray(b"\x1b[D"))] * (len(text) - pos), # Move cursor to specified position
401+
[
402+
Event(evt="key", data="\x0b", raw=bytearray(b"\x0b")), # Control-K
403+
],
404+
)
405+
reader, _ = handle_all_events(events)
406+
self.assertEqual(reader.screen, expected)
407+
self.assertEqual(reader.pos, pos)
408+
409+
test_with_text("", 0, [""])
410+
test_with_text("a", 0, [""])
411+
test_with_text("abc", 1, ["a"])
412+
test_with_text("abc def", 4, ["abc "])
413+
test_with_text("def xxx():xxx\n pass", 10, ["def xxx():", " pass"])
414+
361415
@force_colorized_test_class
362416
class TestReaderInColor(ScreenEqualMixin, TestCase):
363417
def test_syntax_highlighting_basic(self):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix crashes on an empty DELETE_WORD_BACKWARDS (^W) followed by
2+
CLEAR_TO_START (^K). Added comprehensive test cases for both Control-W and
3+
Control-K functionality.

0 commit comments

Comments
 (0)