diff --git a/editor/buf.py b/editor/buf.py deleted file mode 100644 index 42843ad..0000000 --- a/editor/buf.py +++ /dev/null @@ -1,65 +0,0 @@ -import sys -from typing import Optional -from typing import Sequence - - -class Buffer: - MAX_CX = sys.maxsize - - def __init__( - self, - lines: Optional[Sequence[str]] = None, - cx: int = 0, - cy: int = 0, - ): - self._lines = lines or [] - self.cx = cx - self.cy = cy - - self._cx_hint = cx - - def up(self) -> "Buffer": - if self.cy > 0: - self.cy -= 1 - self._set_cx_after_vertical_movement() - return self - - def down(self) -> "Buffer": - if self.cy < len(self._lines) - 1: - self.cy += 1 - self._set_cx_after_vertical_movement() - return self - - def _set_cx_after_vertical_movement(self) -> None: - if self.cx > self._max_cx: - # Cursor exceeded the line - if self.cx > self._cx_hint: - self._cx_hint = self.cx - self.cx = max(self._max_cx, 0) - else: - self.cx = min(self._cx_hint, self._max_cx) - - def left(self) -> "Buffer": - if self.cx > 0: - self.cx -= 1 - self._cx_hint = self.cx - return self - - def right(self) -> "Buffer": - if self.cx < self._max_cx: - self.cx += 1 - self._cx_hint = self.cx - return self - - def home(self) -> "Buffer": - self.cx = self._cx_hint = 0 - return self - - def end(self) -> "Buffer": - self._cx_hint = self.MAX_CX - self.cx = min(self._cx_hint, self._max_cx) - return self - - @property - def _max_cx(self) -> int: - return len(self._lines[self.cy]) - 1 diff --git a/editor/main.py b/editor/main.py index cae0c9e..9376d78 100644 --- a/editor/main.py +++ b/editor/main.py @@ -3,7 +3,6 @@ import curses from typing import Optional from typing import Sequence -from .buf import Buffer from .window import Window @@ -19,30 +18,30 @@ def c_main(stdscr: "curses._CursesWindow", filename: str) -> int: with open(filename) as f: lines = f.read().split("\n") - buf = Buffer(lines) - window = Window(buf, curses.COLS, curses.LINES) + win = Window(lines, curses.COLS, curses.LINES) while True: # Update screen - for y, line in enumerate(window.lines): + for y, line in enumerate(lines[: curses.LINES]): stdscr.addstr(y, 0, line) - stdscr.move(window.cy, window.cx) + cx, cy = win.screen_cursor() + stdscr.move(cy, cx) # Handle keypresses c = stdscr.getkey() if c == "q": break elif c == "k": - buf.up() + win.up() elif c == "j": - buf.down() + win.down() elif c == "h": - buf.left() + win.left() elif c == "l": - buf.right() + win.right() elif c == "0": - buf.home() + win.home() elif c == "$": - buf.end() + win.end() return 0 diff --git a/editor/window.py b/editor/window.py index 9e74c56..d81c330 100644 --- a/editor/window.py +++ b/editor/window.py @@ -1,31 +1,80 @@ +import sys +from typing import Optional from typing import Sequence - -from .buf import Buffer +from typing import Tuple class Window: + MAX_CX = sys.maxsize + def __init__( self, - buf: Buffer, - width: int, - height: int, + lines: Optional[Sequence[str]] = None, + width: int = 0, + height: int = 0, + cx: int = 0, + cy: int = 0, bx: int = 0, by: int = 0, ): - self._buf = buf + self._lines = lines or [] self.width = width self.height = height + self.cx = cx + self.cy = cy self.bx = bx self.by = by - @property - def cx(self) -> int: - return self.bx + self._buf.cx + self._cx_hint = cx + + def up(self) -> "Window": + if self.cy > 0: + self.cy -= 1 + self._set_cx_after_vertical_movement() + return self + + def down(self) -> "Window": + if self.cy < len(self._lines) - 1: + self.cy += 1 + self._set_cx_after_vertical_movement() + return self + + def _set_cx_after_vertical_movement(self) -> None: + if self.cx > self._max_cx: + # Cursor exceeded the line + if self.cx > self._cx_hint: + self._cx_hint = self.cx + self.cx = max(self._max_cx, 0) + else: + self.cx = min(self._cx_hint, self._max_cx) + + def left(self) -> "Window": + if self.cx > 0: + self.cx -= 1 + self._cx_hint = self.cx + return self + + def right(self) -> "Window": + if self.cx < self._max_cx: + self.cx += 1 + self._cx_hint = self.cx + return self + + def home(self) -> "Window": + self.cx = self._cx_hint = 0 + return self + + def end(self) -> "Window": + self._cx_hint = self.MAX_CX + self.cx = min(self._cx_hint, self._max_cx) + return self @property - def cy(self) -> int: - return self.by + self._buf.cy + def _max_cx(self) -> int: + return len(self._lines[self.cy]) - 1 - @property - def lines(self) -> Sequence[str]: - return self._buf._lines[-self.by : (self.height - self.by)] + def screen_cursor(self) -> Tuple[int, int]: + return (self.bx + self.cx, self.by + self.cy) + + def screen_lines(self) -> Sequence[str]: + return self._lines[-self.by : (self.height - self.by)] diff --git a/tests/buf_test.py b/tests/buf_test.py deleted file mode 100644 index 2609e6b..0000000 --- a/tests/buf_test.py +++ /dev/null @@ -1,130 +0,0 @@ -from editor.main import Buffer - - -def test_buffer_up(): - assert Buffer(["foo", "bar"], cy=1).up().cy == 0 - - -def test_buffer_up_at_first_line(): - assert Buffer(["foo"]).up().cy == 0 - - -def test_buffer_up_passed_shorter_line(): - # It correctly resets to a shorter and then longer line - buf = Buffer(["longer line", "short", "long line"], cx=8, cy=2) - buf.up() - assert buf.cx == 4 - assert buf.cy == 1 - buf.up() - assert buf.cx == 8 - assert buf.cy == 0 - - # It correctly resets to a shorter and then medium line - buf = Buffer(["long line", "short", "longer line"], cx=10, cy=2) - buf.up() - assert buf.cx == 4 - assert buf.cy == 1 - buf.up() - assert buf.cx == 8 - assert buf.cy == 0 - - # It correctly resets cx on empty lines - buf = Buffer(["", "foo"], cx=4, cy=1) - buf.up() - assert buf.cx == 0 - assert buf.cy == 0 - - # It correctly resets cx on lines of the same length - buf = Buffer(["short", "short"], cx=4, cy=1) - buf.up() - assert buf.cx == 4 - assert buf.cy == 0 - - # It correctly resets cx hint after horizontal movement - buf = Buffer(["foo", "", "bar"], cy=2) - buf.right().up().up() - assert buf.cx == 1 - assert buf.cy == 0 - buf.left().down().down() - assert buf.cx == 0 - assert buf.cy == 2 - - -def test_buffer_down(): - assert Buffer(["foo", "bar"]).down().cy == 1 - - -def test_buffer_down_passed_shorter_line(): - # It correctly resets to a shorter and then longer line - buf = Buffer(["long line", "short", "longer line"], cx=8) - buf.down() - assert buf.cx == 4 - assert buf.cy == 1 - buf.down() - assert buf.cx == 8 - assert buf.cy == 2 - - # It correctly resets to a shorter and then medium line - buf = Buffer(["longer line", "short", "long line"], cx=10) - buf.down() - assert buf.cx == 4 - assert buf.cy == 1 - buf.down() - assert buf.cx == 8 - assert buf.cy == 2 - - # It correctly resets cx on empty lines - buf = Buffer(["foo", ""], cx=4) - buf.down() - assert buf.cx == 0 - assert buf.cy == 1 - - # It correctly resets cx on lines of the same length - buf = Buffer(["short", "short"], cx=4) - buf.down() - assert buf.cx == 4 - assert buf.cy == 1 - - # It correctly resets cx hint after horizontal movement - buf = Buffer(["foo", "", "bar"]) - buf.right().down().down() - assert buf.cx == 1 - assert buf.cy == 2 - buf.left().up().up() - assert buf.cx == 0 - assert buf.cy == 0 - - -def test_buffer_down_at_last_line(): - assert Buffer(["foo"]).down().cy == 0 - - -def test_buffer_left(): - assert Buffer(cx=1).left().cx == 0 - - -def test_buffer_left_at_first_char(): - assert Buffer().left().cx == 0 - - -def test_buffer_right(): - assert Buffer(["foo"]).right().cx == 1 - - -def test_buffer_right_at_last_char(): - assert Buffer(["foo"], cx=2).right().cx == 2 - - -def test_buffer_home(): - assert Buffer(["foo"], cx=2).home().cx == 0 - - -def test_buffer_end(): - assert Buffer(["foo"]).end().cx == 2 - - -def test_buffer_end_makes_vertical_movement_always_move_to_last_char(): - buf = Buffer(["short", "longer line", "long line"]) - assert buf.end().cx == 4 - assert buf.down().cx == 10 - assert buf.down().cx == 8 diff --git a/tests/window_test.py b/tests/window_test.py index 267148d..9bf0cd8 100644 --- a/tests/window_test.py +++ b/tests/window_test.py @@ -1,21 +1,148 @@ -from editor.buf import Buffer from editor.window import Window -def test_window_cx_cy(): - buf = Buffer(["foo", "bar", "baz"], cx=2, cy=2) - window = Window(buf, width=3, height=3, bx=-1, by=-1) - assert window.cx == 1 - assert window.cy == 1 +def test_window_up(): + assert Window(["foo", "bar"], cy=1).up().cy == 0 -def test_window_lines_vertical_scroll(): - buf = Buffer(["foo", "bar", "baz"], cx=2, cy=2) - window = Window(buf, width=3, height=3, by=-1) - assert window.lines == ["bar", "baz"] +def test_window_up_at_first_line(): + assert Window(["foo"]).up().cy == 0 -def test_window_lines_horizontal_scroll(): - buf = Buffer(["foo", "bar", "baz"], cx=2, cy=2) - window = Window(buf, width=3, height=3, bx=-1) - assert window.lines == ["ar", "az"] +def test_window_up_passed_shorter_line(): + # It correctly resets to a shorter and then longer line + win = Window(["longer line", "short", "long line"], cx=8, cy=2) + win.up() + assert win.cx == 4 + assert win.cy == 1 + win.up() + assert win.cx == 8 + assert win.cy == 0 + + # It correctly resets to a shorter and then medium line + win = Window(["long line", "short", "longer line"], cx=10, cy=2) + win.up() + assert win.cx == 4 + assert win.cy == 1 + win.up() + assert win.cx == 8 + assert win.cy == 0 + + # It correctly resets cx on empty lines + win = Window(["", "foo"], cx=4, cy=1) + win.up() + assert win.cx == 0 + assert win.cy == 0 + + # It correctly resets cx on lines of the same length + win = Window(["short", "short"], cx=4, cy=1) + win.up() + assert win.cx == 4 + assert win.cy == 0 + + # It correctly resets cx hint after horizontal movement + win = Window(["foo", "", "bar"], cy=2) + win.right().up().up() + assert win.cx == 1 + assert win.cy == 0 + win.left().down().down() + assert win.cx == 0 + assert win.cy == 2 + + +def test_window_down(): + assert Window(["foo", "bar"]).down().cy == 1 + + +def test_window_down_passed_shorter_line(): + # It correctly resets to a shorter and then longer line + win = Window(["long line", "short", "longer line"], cx=8) + win.down() + assert win.cx == 4 + assert win.cy == 1 + win.down() + assert win.cx == 8 + assert win.cy == 2 + + # It correctly resets to a shorter and then medium line + win = Window(["longer line", "short", "long line"], cx=10) + win.down() + assert win.cx == 4 + assert win.cy == 1 + win.down() + assert win.cx == 8 + assert win.cy == 2 + + # It correctly resets cx on empty lines + win = Window(["foo", ""], cx=4) + win.down() + assert win.cx == 0 + assert win.cy == 1 + + # It correctly resets cx on lines of the same length + win = Window(["short", "short"], cx=4) + win.down() + assert win.cx == 4 + assert win.cy == 1 + + # It correctly resets cx hint after horizontal movement + win = Window(["foo", "", "bar"]) + win.right().down().down() + assert win.cx == 1 + assert win.cy == 2 + win.left().up().up() + assert win.cx == 0 + assert win.cy == 0 + + +def test_window_down_at_last_line(): + assert Window(["foo"]).down().cy == 0 + + +def test_window_left(): + assert Window(cx=1).left().cx == 0 + + +def test_window_left_at_first_char(): + assert Window().left().cx == 0 + + +def test_window_right(): + assert Window(["foo"]).right().cx == 1 + + +def test_window_right_at_last_char(): + assert Window(["foo"], cx=2).right().cx == 2 + + +def test_window_home(): + assert Window(["foo"], cx=2).home().cx == 0 + + +def test_window_end(): + assert Window(["foo"]).end().cx == 2 + + +def test_window_end_makes_vertical_movement_always_move_to_last_char(): + win = Window(["short", "longer line", "long line"]) + assert win.end().cx == 4 + assert win.down().cx == 10 + assert win.down().cx == 8 + + +def test_window_screen_cursor(): + lines = ["foo", "bar", "baz"] + window = Window(lines, bx=-1, by=-1, cx=2, cy=2) + assert window.screen_cursor() == (1, 1) + + +def test_window_screen_lines_vertical_scroll(): + lines = ["foo", "bar", "baz"] + window = Window(lines, width=3, height=3, by=-1, cx=2, cy=2) + assert window.screen_lines() == ["bar", "baz"] + + +# def test_window_screen_lines_horizontal_scroll(): +# lines = ["foo", "bar", "baz"] +# window = Window(lines, width=3, height=3, bx=-1, cx=2, cy=2) +# assert window.screen_lines() == ["oo", "ar", "az"]