diff --git a/client/editor/editor.go b/client/editor/editor.go index b1d5520..a2a3d57 100644 --- a/client/editor/editor.go +++ b/client/editor/editor.go @@ -126,18 +126,146 @@ func (e *Editor) showPositions() { } // MoveCursor updates the Cursor position. -func (e *Editor) MoveCursor(x, _ int) { +func (e *Editor) MoveCursor(x, y int) { + if len(e.Text) == 0 && e.Cursor == 0 { + return + } + // Move cursor horizontally. newCursor := e.Cursor + x - if newCursor < 0 { - newCursor = 0 + // Move cursor vertically. + if y > 0 { + newCursor = e.calcCursorDown() } + + if y < 0 { + newCursor = e.calcCursorUp() + } + + // Reset to bounds. if newCursor > len(e.Text) { newCursor = len(e.Text) } + + if newCursor < 0 { + newCursor = 0 + } + e.Cursor = newCursor } +// For the functions calcCursorUp and calcCursorDown, newline characters are found by iterating +// backward and forward from the current Cursor position. These characters are taken as the "start" +// and "end" of the current line. The "offset" from the start of the current line to the Cursor +// is calculated and used to determine the final Cursor position on the target line, based on whether the +// offset is greater than the length of the target line. "pos" is used as a placeholder variable for +// the Cursor. + +// calcCursorUp calculates the intended Cursor position after moving the Cursor up one line. +func (e *Editor) calcCursorUp() int { + pos := e.Cursor + offset := 0 + + // If the initial cursor is out of the bounds of the Text or already on a newline, move it. + if pos == len(e.Text) || e.Text[pos] == '\n' { + offset++ + pos-- + } + + if pos < 0 { + pos = 0 + } + + start, end := pos, pos + + // Find the start of the current line. + for start > 0 && e.Text[start] != '\n' { + start-- + } + + // If the Cursor is already on the first line, move to the beginning of the Text. + if start == 0 { + return 0 + } + + // Find the end of the current line. + for end < len(e.Text) && e.Text[end] != '\n' { + end++ + } + + // Find the start of the previous line. + prevStart := start - 1 + for prevStart >= 0 && e.Text[prevStart] != '\n' { + prevStart-- + } + + // Calculate the distance from the start of the current line to the Cursor. + offset += pos - start + if offset <= start-prevStart { + return prevStart + offset + } else { + return start + } +} + +func (e *Editor) calcCursorDown() int { + pos := e.Cursor + offset := 0 + + // If the initial Cursor is out of the bounds of the Text or already on a newline, move it. + if pos == len(e.Text) || e.Text[pos] == '\n' { + offset++ + pos-- + } + + if pos < 0 { + pos = 0 + } + + start, end := pos, pos + + // Find the start of the current line. + for start > 0 && e.Text[start] != '\n' { + start-- + } + + // This handles the case where the Cursor is on the first line. This is necessary because the start + // of the first line is not a newline character, unlike the other lines in the Text. + if start == 0 && e.Text[start] != '\n' { + offset++ + } + + // Find the end of the current line. + for end < len(e.Text) && e.Text[end] != '\n' { + end++ + } + + // This handles the case where the Cursor is on a newline. end has to be incremented, otherwise + // start == end. + if e.Text[pos] == '\n' && e.Cursor != 0 { + end++ + } + + // If the Cursor is already on the last line, move to the end of the Text. + if end == len(e.Text) { + return len(e.Text) + } + + // Find the end of the next line. + nextEnd := end + 1 + for nextEnd < len(e.Text) && e.Text[nextEnd] != '\n' { + nextEnd++ + } + + // Calculate the distance from the start of the current line to the Cursor. + offset += pos - start + if offset < nextEnd-end { + return end + offset + } else { + return nextEnd + } +} + // calcCursorXY calculates Cursor position from the index obtained from the content. func (e *Editor) calcCursorXY(index int) (int, int) { x := 1 diff --git a/client/editor/editor_test.go b/client/editor/editor_test.go index 1ec1fcf..86bc67e 100644 --- a/client/editor/editor_test.go +++ b/client/editor/editor_test.go @@ -65,20 +65,92 @@ func TestMoveCursor(t *testing.T) { description string cursor int x int + y int expectedCursor int + text []rune }{ - {description: "move forward", cursor: 0, x: 1, expectedCursor: 1}, - {description: "move backward", cursor: 1, x: -1, expectedCursor: 0}, - {description: "negative (out of bounds)", cursor: 0, x: -10, expectedCursor: 0}, - {description: "positive (out of bounds)", cursor: 12, x: 2, expectedCursor: 12}, + // test horizontal movement + {description: "move forward (empty document)", cursor: 0, x: 1, expectedCursor: 0, + text: []rune("")}, + {description: "move backward (empty document)", cursor: 0, x: -1, expectedCursor: 0, + text: []rune("")}, + {description: "move forward", cursor: 0, x: 1, expectedCursor: 1, + text: []rune("foo\n")}, + {description: "move backward", cursor: 1, x: -1, expectedCursor: 0, + text: []rune("foo\n")}, + {description: "move backward (out of bounds)", cursor: 0, x: -10, expectedCursor: 0, + text: []rune("foo\n")}, + {description: "move forward (out of bounds)", cursor: 3, x: 2, expectedCursor: 4, + text: []rune("foo\n")}, + // test vertical movement + {description: "move up", cursor: 6, y: -1, expectedCursor: 2, + text: []rune("foo\nbar")}, + {description: "move down", cursor: 1, y: 2, expectedCursor: 5, + text: []rune("foo\nbar")}, + {description: "move up (empty document)", cursor: 0, y: -1, expectedCursor: 0, + text: []rune("")}, + {description: "move down (empty document)", cursor: 0, y: 1, expectedCursor: 0, + text: []rune("")}, + {description: "move up (first line)", cursor: 1, y: -1, expectedCursor: 0, + text: []rune("foo\nbar")}, + {description: "move down (last line)", cursor: 4, y: 1, expectedCursor: 7, + text: []rune("foo\nbar")}, + {description: "move up (middle line)", cursor: 5, y: -1, expectedCursor: 1, + text: []rune("foo\nbar\nbaz")}, + {description: "move down (middle line)", cursor: 5, y: 1, expectedCursor: 9, + text: []rune("foo\nbar\nbaz")}, + {description: "move up (on newline)", cursor: 3, y: -1, expectedCursor: 0, + text: []rune("foo\nbar\nbaz")}, + {description: "move down (on newline)", cursor: 3, y: 1, expectedCursor: 7, + text: []rune("foo\nbar\nbaz")}, + {description: "move up (on newline, first line)", cursor: 3, y: -1, expectedCursor: 0, + text: []rune("foo\nbar\nbaz")}, + {description: "move down (on newline, last line)", cursor: 7, y: 1, expectedCursor: 11, + text: []rune("foo\nbar\nbaz")}, + {description: "move up (different lengths, short to long)", cursor: 8, y: -1, expectedCursor: 3, + text: []rune("fool\nbar\nbaz")}, + {description: "move down (different lengths, short to long)", cursor: 3, y: 1, expectedCursor: 7, + text: []rune("foo\nbare\nbaz")}, + {description: "move up (different lengths, long to short)", cursor: 8, y: -1, expectedCursor: 3, + text: []rune("foo\nbare\nbaz")}, + {description: "move down (different lengths, long to short)", cursor: 4, y: 1, expectedCursor: 8, + text: []rune("fool\nbar\nbaz")}, + {description: "move up (from empty line)", cursor: 4, y: -1, expectedCursor: 0, + text: []rune("foo\n\nbaz")}, + {description: "move down (from empty line)", cursor: 4, y: 1, expectedCursor: 5, + text: []rune("fool\n\nbaz")}, + {description: "move up (from empty line to empty line)", cursor: 5, y: -1, expectedCursor: 4, + text: []rune("foo\n\n\n")}, + {description: "move down (from empty line to empty line)", cursor: 1, y: 1, expectedCursor: 2, + text: []rune("\n\n\nfoo")}, + {description: "move up (from empty last line to empty line)", cursor: 3, y: -1, expectedCursor: 2, + text: []rune("\n\n\n")}, + {description: "move down (from empty first line to empty line)", cursor: 0, y: 1, expectedCursor: 1, + text: []rune("\n\n\n")}, + {description: "move up (from empty last line)", cursor: 6, y: -1, expectedCursor: 2, + text: []rune("\n\nfoo\n")}, + {description: "move down (from empty first line)", cursor: 0, y: 1, expectedCursor: 1, + text: []rune("\nfoo\n\n")}, + {description: "move up (from empty first line)", cursor: 0, y: -1, expectedCursor: 0, + text: []rune("\n\nfoo\n")}, + {description: "move down (from empty last line)", cursor: 6, y: 1, expectedCursor: 6, + text: []rune("\nfoo\n\n")}, + {description: "move up (from last line to empty line)", cursor: 2, y: -1, expectedCursor: 1, + text: []rune("\n\nfoo")}, + {description: "move down (from first line to empty line)", cursor: 2, y: 1, expectedCursor: 4, + text: []rune("foo\n\n")}, + {description: "move up (from empty line to empty line 2)", cursor: 2, y: -1, expectedCursor: 1, + text: []rune("\n\n\n\n\n")}, + {description: "move down (from empty line to empty line 2)", cursor: 2, y: 1, expectedCursor: 3, + text: []rune("\n\n\n\n\n")}, } e := NewEditor() - e.Text = []rune("content\ntest") for _, tc := range tests { e.Cursor = tc.cursor - e.MoveCursor(tc.x, 0) + e.Text = tc.text + e.MoveCursor(tc.x, tc.y) got := e.Cursor expected := tc.expectedCursor diff --git a/client/main.go b/client/main.go index 23efa1b..d461b93 100644 --- a/client/main.go +++ b/client/main.go @@ -295,6 +295,10 @@ func handleTermboxEvent(ev termbox.Event, conn *websocket.Conn) error { e.MoveCursor(-1, 0) case termbox.KeyArrowRight, termbox.KeyCtrlF: e.MoveCursor(1, 0) + case termbox.KeyArrowUp, termbox.KeyCtrlP: + e.MoveCursor(0, -1) + case termbox.KeyArrowDown, termbox.KeyCtrlN: + e.MoveCursor(0, 1) case termbox.KeyHome: e.SetX(0) case termbox.KeyEnd: @@ -352,8 +356,8 @@ func performOperation(opType int, ev termbox.Event, conn *websocket.Conn) { msg = message{Type: "operation", Operation: Operation{Type: "insert", Position: e.Cursor, Value: ch}} case OperationDelete: logger.Infof("LOCAL DELETE: cursor position %v\n", e.Cursor) - if e.Cursor-1 <= 0 { - e.Cursor = 1 + if e.Cursor-1 < 0 { + e.Cursor = 0 } text := doc.Delete(e.Cursor) e.SetText(text) @@ -361,9 +365,6 @@ func performOperation(opType int, ev termbox.Event, conn *websocket.Conn) { e.MoveCursor(-1, 0) } - // Print document state to logs. - printDoc(doc) - err := conn.WriteJSON(msg) if err != nil { e.StatusMsg = "lost connection!" diff --git a/go.mod b/go.mod index be9ef29..972ce90 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( ) require ( + github.com/Pallinder/go-randomdata v1.2.0 // indirect github.com/mattn/go-colorable v0.1.9 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/rivo/uniseg v0.2.0 // indirect