From 5df223f81eb6d47cdcb9f34524b463c3de720d3a Mon Sep 17 00:00:00 2001 From: Andrew Lane Date: Fri, 28 Aug 2020 18:20:49 -0400 Subject: [PATCH] Add :expandtabs and :noexpandtabs --- babi/buf.py | 8 ++++++ babi/file.py | 23 +++++++++++----- babi/screen.py | 8 ++++++ tests/features/expandtabs_test.py | 45 +++++++++++++++++++++++++++++++ tests/features/indent_test.py | 15 +++++++++++ 5 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 tests/features/expandtabs_test.py diff --git a/babi/buf.py b/babi/buf.py index e1b0ee3..9b1cec4 100644 --- a/babi/buf.py +++ b/babi/buf.py @@ -59,6 +59,7 @@ class DelModification(NamedTuple): class Buf: def __init__(self, lines: List[str], tab_size: int = 4) -> None: self._lines = lines + self.expandtabs = True self.tab_size = tab_size self.file_y = self.y = self._x = self._x_hint = 0 @@ -242,6 +243,13 @@ class Buf: # rendered lines + @property + def tab_string(self) -> str: + if self.expandtabs: + return ' ' * self.tab_size + else: + return '\t' + def rendered_line(self, idx: int, margin: Margin) -> str: x = self._cursor_x if idx == self.y else 0 expanded = self._lines[idx].expandtabs(self.tab_size) diff --git a/babi/file.py b/babi/file.py index 823c11e..418a494 100644 --- a/babi/file.py +++ b/babi/file.py @@ -524,20 +524,29 @@ class File: assert self.selection.start is not None sel_y, sel_x = self.selection.start (s_y, _), (e_y, _) = self.selection.get() + tab_string = self.buf.tab_string + tab_size = len(tab_string) for l_y in range(s_y, e_y + 1): if self.buf[l_y]: - self.buf[l_y] = ' ' * self.buf.tab_size + self.buf[l_y] + self.buf[l_y] = tab_string + self.buf[l_y] if l_y == self.buf.y: - self.buf.x += self.buf.tab_size + self.buf.x += tab_size if l_y == sel_y and sel_x != 0: - sel_x += self.buf.tab_size + sel_x += tab_size self.selection.set(sel_y, sel_x, self.buf.y, self.buf.x) @edit_action('insert tab', final=False) def _tab(self, margin: Margin) -> None: - n = self.buf.tab_size - self.buf.x % self.buf.tab_size + tab_string = self.buf.tab_string + if tab_string == '\t': + n = 1 + else: + n = self.buf.tab_size - self.buf.x % self.buf.tab_size + tab_string = tab_string[:n] line = self.buf[self.buf.y] - self.buf[self.buf.y] = line[:self.buf.x] + n * ' ' + line[self.buf.x:] + self.buf[self.buf.y] = ( + line[:self.buf.x] + tab_string + line[self.buf.x:] + ) self.buf.x += n self.buf.restore_eof_invariant() @@ -548,9 +557,9 @@ class File: self._tab(margin) def _dedent_line(self, s: str) -> int: - bound = min(len(s), self.buf.tab_size) + bound = min(len(s), len(self.buf.tab_string)) i = 0 - while i < bound and s[i] == ' ': + while i < bound and s[i] in (' ', '\t'): i += 1 return i diff --git a/babi/screen.py b/babi/screen.py index 3ca493e..81529d1 100644 --- a/babi/screen.py +++ b/babi/screen.py @@ -457,6 +457,14 @@ class Screen: for file in self.files: file.buf.set_tab_size(parsed_tab_size) self.status.update('updated!') + elif response.startswith(':expandtabs'): + for file in self.files: + file.buf.expandtabs = True + self.status.update('updated!') + elif response.startswith(':noexpandtabs'): + for file in self.files: + file.buf.expandtabs = False + self.status.update('updated!') elif response == ':comment' or response.startswith(':comment '): _, _, comment = response.partition(' ') comment = (comment or '#').strip() diff --git a/tests/features/expandtabs_test.py b/tests/features/expandtabs_test.py new file mode 100644 index 0000000..01d4297 --- /dev/null +++ b/tests/features/expandtabs_test.py @@ -0,0 +1,45 @@ +from testing.runner import and_exit +from testing.runner import trigger_command_mode + + +def test_set_expandtabs(run, tmpdir): + f = tmpdir.join('f') + f.write('a') + + with run(str(f)) as h, and_exit(h): + h.press('Left') + trigger_command_mode(h) + h.press_and_enter(':expandtabs') + h.await_text('updated!') + h.press('Tab') + h.press('^S') + assert f.read() == ' a\n' + + +def test_set_noexpandtabs(run, tmpdir): + f = tmpdir.join('f') + f.write('a') + + with run(str(f)) as h, and_exit(h): + h.press('Left') + trigger_command_mode(h) + h.press_and_enter(':noexpandtabs') + h.await_text('updated!') + h.press('Tab') + h.press('^S') + assert f.read() == '\ta\n' + + +def test_indent_with_expandtabs(run, tmpdir): + f = tmpdir.join('f') + f.write('a\nb\nc') + + with run(str(f)) as h, and_exit(h): + trigger_command_mode(h) + h.press_and_enter(':noexpandtabs') + h.await_text('updated!') + for _ in range(3): + h.press('S-Down') + h.press('Tab') + h.press('^S') + assert f.read() == '\ta\n\tb\n\tc\n' diff --git a/tests/features/indent_test.py b/tests/features/indent_test.py index 052dd7f..f81ab6c 100644 --- a/tests/features/indent_test.py +++ b/tests/features/indent_test.py @@ -1,4 +1,5 @@ from testing.runner import and_exit +from testing.runner import trigger_command_mode def test_indent_at_beginning_of_line(run): @@ -87,6 +88,20 @@ def test_dedent_selection(run, tmpdir): h.await_text('\n1\n2\n 3\n') +def test_dedent_selection_with_noexpandtabs(run, tmpdir): + f = tmpdir.join('f') + f.write('1\n\t2\n\t\t3\n') + with run(str(f)) as h, and_exit(h): + trigger_command_mode(h) + h.press_and_enter(':noexpandtabs') + h.await_text('updated!') + for _ in range(3): + h.press('S-Down') + h.press('BTab') + h.press('^S') + assert f.read() == '1\n2\n\t3\n' + + def test_dedent_beginning_of_line(run, tmpdir): f = tmpdir.join('f') f.write(' hi\n')