summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJonathan Slenders <jonathan@slenders.be>2018-06-05 21:07:09 +0200
committerJonathan Slenders <jonathan@slenders.be>2018-06-05 22:07:12 +0200
commita54fc0bd7579b11e290ccd4d70037d68c92f7be3 (patch)
treee2a16f3f307f5100ae3133aaa92012b92b5a6612
parentc0a44da1898876340b5de262cbc4f78498d072cc (diff)
Fixed off by one error in Vi line selection.
-rw-r--r--prompt_toolkit/key_binding/bindings/vi.py11
-rw-r--r--tests/test_cli.py34
2 files changed, 44 insertions, 1 deletions
diff --git a/prompt_toolkit/key_binding/bindings/vi.py b/prompt_toolkit/key_binding/bindings/vi.py
index 68084e24..22d0f018 100644
--- a/prompt_toolkit/key_binding/bindings/vi.py
+++ b/prompt_toolkit/key_binding/bindings/vi.py
@@ -84,6 +84,9 @@ class TextObject(object):
Return a (start, end) tuple with start <= end that indicates the range
operators should operate on.
`buffer` is used to get start and end of line positions.
+
+ This should return something that can be used in a slice, so the `end`
+ position is *not* included.
"""
start, end = self.sorted()
doc = document
@@ -126,7 +129,13 @@ class TextObject(object):
from_ += buffer.cursor_position
to += buffer.cursor_position
- to -= 1 # SelectionState does not include the end position, `operator_range` does.
+
+ # For Vi mode, the SelectionState does include the upper position,
+ # while `self.operator_range` does not. So, go one to the left, unless
+ # we're in the line mode, then we don't want to risk going to the
+ # previous line, and missing one line in the selection.
+ if self.type != TextObjectType.LINEWISE:
+ to -= 1
document = Document(buffer.text, to, SelectionState(
original_cursor_position=from_, type=self.selection_type))
diff --git a/tests/test_cli.py b/tests/test_cli.py
index c5320b4a..8befba95 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -773,6 +773,40 @@ def test_vi_visual_line_copy():
assert (result.text ==
'-line1\n-line2\n-line3\n-line4\n-line2\n-line3\n-line2\n-line3\n-line5\n-line6')
+
+def test_vi_visual_empty_line():
+ """
+ Test edge case with an empty line in Visual-line mode.
+ """
+ feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI,
+ multiline=True)
+
+ # 1. Delete first two lines.
+ operations = (
+ # Three lines of text. The middle one is empty.
+ 'hello\r\rworld'
+ # Go to the start.
+ '\x1bgg'
+ # Visual line and move down.
+ 'Vj'
+ # Delete.
+ 'd\r')
+ result, cli = feed(operations)
+ assert result.text == 'world'
+
+ # 1. Delete middle line.
+ operations = (
+ # Three lines of text. The middle one is empty.
+ 'hello\r\rworld'
+ # Go to middle line.
+ '\x1bggj'
+ # Delete line
+ 'Vd\r')
+
+ result, cli = feed(operations)
+ assert result.text == 'hello\nworld'
+
+
def test_vi_character_delete_after_cursor():
" Test 'x' keypress. "
feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI,