Implement jump by word (^Left/^Right)
This commit is contained in:
60
babi.py
60
babi.py
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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('^\\')
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user