diff --git a/babi.py b/babi.py index 18c1355..68266ab 100644 --- a/babi.py +++ b/babi.py @@ -693,6 +693,58 @@ class File: self.cursor_y = max(self.cursor_y, self.file_y) 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 def go_to_line(self, lineno: int, margin: Margin) -> None: self.x = self.x_hint = 0 @@ -725,7 +777,7 @@ class File: if search.wrapped: status.update('search wrapped') self.cursor_y = line_y - self.x = match.start() + self.x = self.x_hint = match.start() self._scroll_screen_if_needed(margin) def replace( @@ -750,7 +802,7 @@ class File: search = _SearchIter(self, reg, offset=0) for line_y, match in search: self.cursor_y = line_y - self.x = match.start() + self.x = self.x_hint = match.start() self._scroll_screen_if_needed(screen.margin) if res != 'a': # make `a` replace the rest of them screen.draw() @@ -895,6 +947,8 @@ class File: b'kEND5': ctrl_end, b'kUP5': ctrl_up, b'kDN5': ctrl_down, + b'kRIT5': ctrl_right, + b'kLFT5': ctrl_left, } @edit_action('text', final=False) @@ -1146,6 +1200,8 @@ SEQUENCE_KEYNAME = { '\x1b[1;3D': b'kLFT3', # M-Left '\x1b[1;5A': b'kUP5', # ^Up '\x1b[1;5B': b'kDN5', # ^Down + '\x1b[1;5C': b'kRIT5', # ^Right + '\x1b[1;5D': b'kLFT5', # ^Left } diff --git a/tests/movement_test.py b/tests/movement_test.py index 9cfb4f3..d945fae 100644 --- a/tests/movement_test.py +++ b/tests/movement_test.py @@ -267,3 +267,83 @@ def test_page_up_does_not_go_negative(ten_lines): h.press('^Y') h.await_cursor_position(x=0, y=1) 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' diff --git a/tests/replace_test.py b/tests/replace_test.py index cf68fe2..c58864b 100644 --- a/tests/replace_test.py +++ b/tests/replace_test.py @@ -46,6 +46,28 @@ def test_replace_actual_contents(ten_lines): 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): with run(str(ten_lines)) as h, and_exit(h): h.press('^\\') diff --git a/tests/search_test.py b/tests/search_test.py index c77acd9..fbebba5 100644 --- a/tests/search_test.py +++ b/tests/search_test.py @@ -49,6 +49,24 @@ def test_search_only_one_match_already_at_that_match(ten_lines): 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): with run(str(ten_lines)) as h, and_exit(h): h.press('^W')