Implement jump by word (^Left/^Right)

This commit is contained in:
Anthony Sottile
2019-12-15 15:03:05 -08:00
parent 070c1002f8
commit 2f1f64537d
4 changed files with 178 additions and 2 deletions

60
babi.py
View File

@@ -693,6 +693,58 @@ class File:
self.cursor_y = max(self.cursor_y, self.file_y) self.cursor_y = max(self.cursor_y, self.file_y)
self._set_x_after_vertical_movement() self._set_x_after_vertical_movement()
@action
def ctrl_right(self, margin: Margin) -> None:
line = self.lines[self.cursor_y]
# if we're at the second to last character, jump to end of line
if self.x == len(line) - 1:
self.x = self.x_hint = self.x + 1
# if we're at the end of the line, jump forward to the next non-ws
elif self.x == len(line):
while (
self.cursor_y < len(self.lines) - 1 and (
self.x == len(self.lines[self.cursor_y]) or
self.lines[self.cursor_y][self.x].isspace()
)
):
if self.x == len(self.lines[self.cursor_y]):
self.cursor_y += 1
self.maybe_scroll_down(margin)
self.x = self.x_hint = 0
else:
self.x = self.x_hint = self.x + 1
# if we're inside the line, jump to next position that's not our type
else:
self.x = self.x_hint = self.x + 1
isalnum = line[self.x].isalnum()
while self.x < len(line) and isalnum == line[self.x].isalnum():
self.x = self.x_hint = self.x + 1
@action
def ctrl_left(self, margin: Margin) -> None:
line = self.lines[self.cursor_y]
# if we're at position 1 and it's not a space, go to the beginning
if self.x == 1 and not line[:self.x].isspace():
self.x = self.x_hint = 0
# if we're at the beginning or it's all space up to here jump to the
# end of the previous non-space line
elif self.x == 0 or line[:self.x].isspace():
self.x = self.x_hint = 0
while (
self.cursor_y > 0 and (
self.x == 0 or
not self.lines[self.cursor_y]
)
):
self.cursor_y -= 1
self._maybe_scroll_up(margin)
self.x = self.x_hint = len(self.lines[self.cursor_y])
else:
self.x = self.x_hint = self.x - 1
isalnum = line[self.x - 1].isalnum()
while self.x > 0 and isalnum == line[self.x - 1].isalnum():
self.x = self.x_hint = self.x - 1
@action @action
def go_to_line(self, lineno: int, margin: Margin) -> None: def go_to_line(self, lineno: int, margin: Margin) -> None:
self.x = self.x_hint = 0 self.x = self.x_hint = 0
@@ -725,7 +777,7 @@ class File:
if search.wrapped: if search.wrapped:
status.update('search wrapped') status.update('search wrapped')
self.cursor_y = line_y self.cursor_y = line_y
self.x = match.start() self.x = self.x_hint = match.start()
self._scroll_screen_if_needed(margin) self._scroll_screen_if_needed(margin)
def replace( def replace(
@@ -750,7 +802,7 @@ class File:
search = _SearchIter(self, reg, offset=0) search = _SearchIter(self, reg, offset=0)
for line_y, match in search: for line_y, match in search:
self.cursor_y = line_y self.cursor_y = line_y
self.x = match.start() self.x = self.x_hint = match.start()
self._scroll_screen_if_needed(screen.margin) self._scroll_screen_if_needed(screen.margin)
if res != 'a': # make `a` replace the rest of them if res != 'a': # make `a` replace the rest of them
screen.draw() screen.draw()
@@ -895,6 +947,8 @@ class File:
b'kEND5': ctrl_end, b'kEND5': ctrl_end,
b'kUP5': ctrl_up, b'kUP5': ctrl_up,
b'kDN5': ctrl_down, b'kDN5': ctrl_down,
b'kRIT5': ctrl_right,
b'kLFT5': ctrl_left,
} }
@edit_action('text', final=False) @edit_action('text', final=False)
@@ -1146,6 +1200,8 @@ SEQUENCE_KEYNAME = {
'\x1b[1;3D': b'kLFT3', # M-Left '\x1b[1;3D': b'kLFT3', # M-Left
'\x1b[1;5A': b'kUP5', # ^Up '\x1b[1;5A': b'kUP5', # ^Up
'\x1b[1;5B': b'kDN5', # ^Down '\x1b[1;5B': b'kDN5', # ^Down
'\x1b[1;5C': b'kRIT5', # ^Right
'\x1b[1;5D': b'kLFT5', # ^Left
} }

View File

@@ -267,3 +267,83 @@ def test_page_up_does_not_go_negative(ten_lines):
h.press('^Y') h.press('^Y')
h.await_cursor_position(x=0, y=1) h.await_cursor_position(x=0, y=1)
assert h.get_cursor_line() == 'line_0' assert h.get_cursor_line() == 'line_0'
@pytest.fixture
def jump_word_file(tmpdir):
f = tmpdir.join('f')
contents = '''\
hello world
hi
this(is_some_code) # comment
'''
f.write(contents)
yield f
def test_ctrl_right_jump_by_word(jump_word_file):
with run(str(jump_word_file)) as h, and_exit(h):
h.press('^Right')
h.await_cursor_position(x=5, y=1)
h.press('^Right')
h.await_cursor_position(x=11, y=1)
h.press('Left')
h.await_cursor_position(x=10, y=1)
h.press('^Right')
h.await_cursor_position(x=11, y=1)
h.press('^Right')
h.await_cursor_position(x=0, y=3)
h.press('^Right')
h.await_cursor_position(x=2, y=3)
h.press('^Right')
h.await_cursor_position(x=4, y=5)
h.press('^Right')
h.await_cursor_position(x=8, y=5)
h.press('^Right')
h.await_cursor_position(x=11, y=5)
h.press('Down')
h.press('^Right')
h.await_cursor_position(x=0, y=6)
def test_ctrl_left_jump_by_word(jump_word_file):
with run(str(jump_word_file)) as h, and_exit(h):
h.press('^Left')
h.await_cursor_position(x=0, y=1)
h.press('Right')
h.await_cursor_position(x=1, y=1)
h.press('^Left')
h.await_cursor_position(x=0, y=1)
h.press('PageDown')
h.await_cursor_position(x=0, y=6)
h.press('^Left')
h.await_cursor_position(x=33, y=5)
h.press('^Left')
h.await_cursor_position(x=26, y=5)
h.press('Home')
h.press('Right')
h.await_cursor_position(x=1, y=5)
h.press('^Left')
h.await_cursor_position(x=2, y=3)
def test_ctrl_right_triggering_scroll(jump_word_file):
with run(str(jump_word_file), height=4) as h, and_exit(h):
h.press('Down')
h.await_cursor_position(x=0, y=2)
h.press('^Right')
h.await_cursor_position(x=0, y=1)
assert h.get_cursor_line() == 'hi'
def test_ctrl_left_triggering_scroll(jump_word_file):
with run(str(jump_word_file)) as h, and_exit(h):
h.press('Down')
h.await_cursor_position(x=0, y=2)
h.press('^Down')
h.await_cursor_position(x=0, y=1)
h.press('^Left')
h.await_cursor_position(x=11, y=1)
assert h.get_cursor_line() == 'hello world'

View File

@@ -46,6 +46,28 @@ def test_replace_actual_contents(ten_lines):
h.await_text('replaced 1 occurrence') h.await_text('replaced 1 occurrence')
def test_replace_sets_x_hint_properly(tmpdir):
f = tmpdir.join('f')
contents = '''\
beginning_line
match me!
'''
f.write(contents)
with run(str(f)) as h, and_exit(h):
h.press('^\\')
h.await_text('search (to replace):')
h.press_and_enter('me!')
h.await_text('replace with:')
h.press_and_enter('youuuu')
h.await_text('replace [y(es), n(o), a(ll)]?')
h.press('y')
h.await_cursor_position(x=6, y=3)
h.press('Up')
h.press('Up')
h.await_cursor_position(x=6, y=1)
def test_replace_cancel_at_individual_replace(ten_lines): def test_replace_cancel_at_individual_replace(ten_lines):
with run(str(ten_lines)) as h, and_exit(h): with run(str(ten_lines)) as h, and_exit(h):
h.press('^\\') h.press('^\\')

View File

@@ -49,6 +49,24 @@ def test_search_only_one_match_already_at_that_match(ten_lines):
h.await_cursor_position(x=0, y=2) h.await_cursor_position(x=0, y=2)
def test_search_sets_x_hint_properly(tmpdir):
f = tmpdir.join('f')
contents = '''\
beginning_line
match me!
'''
f.write(contents)
with run(str(f)) as h, and_exit(h):
h.press('^W')
h.await_text('search:')
h.press_and_enter('me!')
h.await_cursor_position(x=6, y=3)
h.press('Up')
h.press('Up')
h.await_cursor_position(x=6, y=1)
def test_search_not_found(ten_lines): def test_search_not_found(ten_lines):
with run(str(ten_lines)) as h, and_exit(h): with run(str(ten_lines)) as h, and_exit(h):
h.press('^W') h.press('^W')