From c5e2dd14f8199eaabf7b795d9f02fae4dd5a9fea Mon Sep 17 00:00:00 2001 From: burntcarrot Date: Tue, 22 Nov 2022 00:07:48 +0530 Subject: [PATCH 01/28] WIP: add document synchronization --- client/main.go | 52 +++++++++++++++++++++++++++++++++++--------------- server/main.go | 39 ++++++++++++++++++++++++++++++++++--- 2 files changed, 73 insertions(+), 18 deletions(-) diff --git a/client/main.go b/client/main.go index 75f4d11..32e934b 100644 --- a/client/main.go +++ b/client/main.go @@ -8,6 +8,8 @@ import ( "log" "net/url" "os" + "strings" + "time" "github.com/burntcarrot/rowix/crdt" "github.com/gorilla/websocket" @@ -15,10 +17,11 @@ import ( ) type message struct { - Username string `json:"username"` - Text string `json:"text"` - Type string `json:"type"` - Operation Operation `json:"operation"` + Username string `json:"username"` + Text string `json:"text"` + Type string `json:"type"` + Operation Operation `json:"operation"` + Document *crdt.Document `json:"document"` } type Operation struct { @@ -64,9 +67,13 @@ func main() { doc = crdt.New() // Get WebSocket connection. - conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil) + dialer := websocket.Dialer{ + HandshakeTimeout: 2 * time.Minute, + } + + conn, _, err := dialer.Dial(u.String(), nil) if err != nil { - fmt.Printf("Connection error, exiting: %s", err) + fmt.Printf("Connection error, exiting: %s\n", err) os.Exit(0) } defer conn.Close() @@ -75,6 +82,9 @@ func main() { msg := message{Username: name, Text: "has joined the session.", Type: "info"} _ = conn.WriteJSON(msg) + // syncMsg := message{Type: "syncReq"} + // _ = conn.WriteJSON(syncMsg) + // open logging file and create if non-existent file, err := os.OpenFile("help.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { @@ -87,7 +97,11 @@ func main() { err = UI(conn, &doc) if err != nil { - fmt.Printf("TUI error, exiting: %s", err) + if strings.HasPrefix(err.Error(), "rowix") { + fmt.Println("exiting session.") + os.Exit(0) + } + fmt.Printf("TUI error, exiting: %s\n", err) os.Exit(0) } } @@ -127,7 +141,7 @@ func mainLoop(e *Editor, conn *websocket.Conn, doc *crdt.Document) error { } case msg := <-msgChan: - handleMsg(msg, doc) + handleMsg(msg, doc, conn) } } } @@ -138,7 +152,7 @@ func handleTermboxEvent(ev termbox.Event, conn *websocket.Conn) error { case termbox.EventKey: switch ev.Key { case termbox.KeyEsc, termbox.KeyCtrlC: - return errors.New("exiting") + return errors.New("rowix: exiting") case termbox.KeyArrowLeft, termbox.KeyCtrlB: e.MoveCursor(-1, 0) e.Draw() @@ -220,12 +234,20 @@ func getTermboxChan() chan termbox.Event { } // handleMsg updates the CRDT document with the contents of the message. -func handleMsg(msg message, doc *crdt.Document) { - switch msg.Operation.Type { - case "insert": - _, _ = doc.Insert(msg.Operation.Position, msg.Operation.Value) - case "delete": - _ = doc.Delete(msg.Operation.Position) +func handleMsg(msg message, doc *crdt.Document, conn *websocket.Conn) { + if msg.Type == "syncResp" { + *doc = *msg.Document + logger.Printf("%+v\n", msg.Document) + } else if msg.Type == "docReq" { + docMsg := message{Type: "docResp", Document: doc} + conn.WriteJSON(&docMsg) + } else { + switch msg.Operation.Type { + case "insert": + _, _ = doc.Insert(msg.Operation.Position, msg.Operation.Value) + case "delete": + _ = doc.Delete(msg.Operation.Position) + } } e.SetText(crdt.Content(*doc)) diff --git a/server/main.go b/server/main.go index 3e5e553..e0427c1 100644 --- a/server/main.go +++ b/server/main.go @@ -6,6 +6,7 @@ import ( "net/http" "time" + "github.com/burntcarrot/rowix/crdt" "github.com/fatih/color" "github.com/google/uuid" "github.com/gorilla/websocket" @@ -16,7 +17,8 @@ type message struct { Text string `json:"text"` Type string `json:"type"` ID uuid.UUID - Operation Operation `json:"operation"` + Operation Operation `json:"operation"` + Document *crdt.Document `json:"document"` } type Operation struct { @@ -62,6 +64,35 @@ func handleConn(w http.ResponseWriter, r *http.Request) { } defer conn.Close() + // doc := crdt.New() + var doc *crdt.Document + color.Yellow("total active clients: %d\n", len(activeClients)) + if len(activeClients) > 1 { + // at least 2 clients for requesting a document + for clientConn, _ := range activeClients { + // send a docReq message to a client + msg := message{Type: "docReq"} + err = clientConn.WriteJSON(&msg) + if err != nil { + color.Red("Failed to send docReq: %v\n", err) + } + + // wait for a client to send a document + err = clientConn.ReadJSON(&msg) + if err != nil { + color.Red("Failed to receive document: %v, msg: %+v\n", err, msg) + } + doc = msg.Document + break + } + + msg := message{Type: "syncResp", Document: doc} + err = conn.WriteJSON(&msg) + if err != nil { + color.Red("Failed to send syncResp: %v\n", err) + } + } + // Generate a UUID for the client. activeClients[conn] = uuid.New() @@ -94,10 +125,12 @@ func handleMsg() { t := time.Now().Format(time.ANSIC) if msg.Type == "info" { color.Green("%s >> %s %s (ID: %s)\n", t, msg.Username, msg.Text, msg.ID) + } else if msg.Type == "syncReq" { + color.Green("%s >> syncReq sent from ID: %s\n", t, msg.ID) } else if msg.Type == "operation" { color.Green("operation >> %+v from ID=%s\n", msg.Operation, msg.ID) } else { - color.Green("%s >> %s: %s\n", t, msg.Username, msg.Text) + color.Green("%s >> %+v\n", t, msg) } // Broadcast to all active clients. @@ -105,7 +138,7 @@ func handleMsg() { // Check the UUID to prevent sending messages to their origin. if UUID != msg.ID { // Write JSON message. - color.Magenta("writing message to: %s\n", UUID) + color.Magenta("writing message to: %s, msg: %+v\n", UUID, msg) err := client.WriteJSON(msg) if err != nil { color.Red("Error sending message to client: %v\n", err) From 092e47ed46385c8b3669f3b2d1ce8b3df48aca7e Mon Sep 17 00:00:00 2001 From: burntcarrot Date: Tue, 29 Nov 2022 22:31:11 +0530 Subject: [PATCH 02/28] temp commit --- client/main.go | 2 +- server/main.go | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/client/main.go b/client/main.go index 32e934b..1edf6b5 100644 --- a/client/main.go +++ b/client/main.go @@ -235,7 +235,7 @@ func getTermboxChan() chan termbox.Event { // handleMsg updates the CRDT document with the contents of the message. func handleMsg(msg message, doc *crdt.Document, conn *websocket.Conn) { - if msg.Type == "syncResp" { + if msg.Type == "docResp" { *doc = *msg.Document logger.Printf("%+v\n", msg.Document) } else if msg.Type == "docReq" { diff --git a/server/main.go b/server/main.go index e0427c1..ab79332 100644 --- a/server/main.go +++ b/server/main.go @@ -67,7 +67,7 @@ func handleConn(w http.ResponseWriter, r *http.Request) { // doc := crdt.New() var doc *crdt.Document color.Yellow("total active clients: %d\n", len(activeClients)) - if len(activeClients) > 1 { + if len(activeClients) > 0 { // at least 2 clients for requesting a document for clientConn, _ := range activeClients { // send a docReq message to a client @@ -82,11 +82,13 @@ func handleConn(w http.ResponseWriter, r *http.Request) { if err != nil { color.Red("Failed to receive document: %v, msg: %+v\n", err, msg) } + color.Red("received document from other client: %+v", msg) + doc = msg.Document break } - msg := message{Type: "syncResp", Document: doc} + msg := message{Type: "docResp", Document: doc} err = conn.WriteJSON(&msg) if err != nil { color.Red("Failed to send syncResp: %v\n", err) @@ -125,8 +127,8 @@ func handleMsg() { t := time.Now().Format(time.ANSIC) if msg.Type == "info" { color.Green("%s >> %s %s (ID: %s)\n", t, msg.Username, msg.Text, msg.ID) - } else if msg.Type == "syncReq" { - color.Green("%s >> syncReq sent from ID: %s\n", t, msg.ID) + } else if msg.Type == "docReq" { + color.Green("%s >> docReq sent from ID: %s\n", t, msg.ID) } else if msg.Type == "operation" { color.Green("operation >> %+v from ID=%s\n", msg.Operation, msg.ID) } else { From fa3272db30c3ae73be2c860d21abef665bade3c3 Mon Sep 17 00:00:00 2001 From: Ben M <61804926+benmuth@users.noreply.github.com> Date: Tue, 6 Dec 2022 05:14:29 -0600 Subject: [PATCH 03/28] feat: document sync (#12) * Add support for newlines. * Fix newline and deletion bugs in editor. Applied patch that changes Editor struct to have one cursor field instead of having x and y fields. Changed performOperation() to operate on the absolute cursor position, not just the x value. Fixed deletion by making cursor updating happen after the operation and removing some "-1"s from the cursor positions. * Start to implement document syncing. * Consolidate calls to e.Draw. Changed handleTermboxEvent to only call e.Draw once at the end of the function. Removed a call to e.Draw in performOperation. * WIP: adding flag for login process * Make login process automatic. Added a flag to make the manual login process optional for faster iteration. The default behavior is now to use automatically generated random names. Used the github library github.com/Pallinder/go-randomdata for random name generation. * Add document syncing for new session members (WIP) Got document syncing working for new session members, but current architecture is still rough. The intention is to move more document syncing logic to handlMsg. * Refactor doc sync Changed the doc sync code path to be a bit more clear. Some work could still be done, like moving the whole document syncing process into its own function. Slightly modified client-side logging to be a bit more useful. * Improve error handling Incorporated error handling changes from a patch. * Add unique site id generation Server now generates unique site ids and assigns them to clients. Also added more logging. * chore: cleanup * ci: fix linter issues Co-authored-by: Ben Muthalaly Co-authored-by: burntcarrot --- .gitignore | 4 +- client/editor.go | 9 ++++ client/main.go | 111 +++++++++++++++++++++++++-------------- crdt/woot.go | 19 ++++++- go.mod | 1 + go.sum | 2 + server/main.go | 132 ++++++++++++++++++++++++++++++----------------- 7 files changed, 189 insertions(+), 89 deletions(-) diff --git a/.gitignore b/.gitignore index e0ab203..397b4a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1 @@ -TODO -timeline.md -help.log +*.log diff --git a/client/editor.go b/client/editor.go index 6fd6198..39c358a 100644 --- a/client/editor.go +++ b/client/editor.go @@ -124,6 +124,15 @@ func (e *Editor) MoveCursor(x, _ int) { func (e *Editor) calcCursorXY(index int) (int, int) { x := 1 y := 1 + + if index < 0 { + return x, y + } + + if index > len(e.text) { + index = len(e.text) + } + for i := 0; i < index; i++ { if e.text[i] == rune('\n') { x = 1 diff --git a/client/main.go b/client/main.go index 1edf6b5..8744a58 100644 --- a/client/main.go +++ b/client/main.go @@ -8,20 +8,24 @@ import ( "log" "net/url" "os" + "strconv" "strings" "time" + "github.com/Pallinder/go-randomdata" "github.com/burntcarrot/rowix/crdt" + "github.com/google/uuid" "github.com/gorilla/websocket" "github.com/nsf/termbox-go" ) type message struct { - Username string `json:"username"` - Text string `json:"text"` - Type string `json:"type"` - Operation Operation `json:"operation"` - Document *crdt.Document `json:"document"` + Username string `json:"username"` + Text string `json:"text"` + Type string `json:"type"` + ID uuid.UUID `json:"ID"` + Operation Operation `json:"operation"` + Document crdt.Document `json:"document"` } type Operation struct { @@ -40,13 +44,11 @@ var logger *log.Logger var e *Editor func main() { - var name string - var s *bufio.Scanner - // Parse flags. server := flag.String("server", "localhost:8080", "Server network address") path := flag.String("path", "/", "Server path") - secure := flag.Bool("wss", false, "Use wss by default") + secure := flag.Bool("wss", false, "Enable a secure WebSocket connection") + login := flag.Bool("login", false, "Enable the login prompt") flag.Parse() // Construct WebSocket URL. @@ -57,11 +59,18 @@ func main() { u = url.URL{Scheme: "ws", Host: *server, Path: *path} } - // Read username. - fmt.Print("Enter your name: ") - s = bufio.NewScanner(os.Stdin) - s.Scan() - name = s.Text() + var name string + var s *bufio.Scanner + + // Read username based if login flag is set to true, otherwise generate a random name. + if *login { + fmt.Print("Enter your name: ") + s = bufio.NewScanner(os.Stdin) + s.Scan() + name = s.Text() + } else { + name = randomdata.SillyName() + } // Initialize document. doc = crdt.New() @@ -82,19 +91,16 @@ func main() { msg := message{Username: name, Text: "has joined the session.", Type: "info"} _ = conn.WriteJSON(msg) - // syncMsg := message{Type: "syncReq"} - // _ = conn.WriteJSON(syncMsg) - // open logging file and create if non-existent - file, err := os.OpenFile("help.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + file, err := os.OpenFile("rowix.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { fmt.Printf("Logger error, exiting: %s", err) os.Exit(0) } defer file.Close() - logger = log.New(file, "operations:", log.LstdFlags) - + logger = log.New(file, fmt.Sprintf("--- name: %s >> ", name), log.LstdFlags) + // start local session err = UI(conn, &doc) if err != nil { if strings.HasPrefix(err.Error(), "rowix") { @@ -155,27 +161,21 @@ func handleTermboxEvent(ev termbox.Event, conn *websocket.Conn) error { return errors.New("rowix: exiting") case termbox.KeyArrowLeft, termbox.KeyCtrlB: e.MoveCursor(-1, 0) - e.Draw() case termbox.KeyArrowRight, termbox.KeyCtrlF: e.MoveCursor(1, 0) - e.Draw() case termbox.KeyHome: e.SetX(0) - e.Draw() case termbox.KeyEnd: e.SetX(len(e.text)) - e.Draw() case termbox.KeyBackspace, termbox.KeyBackspace2: performOperation(OperationDelete, ev, conn) case termbox.KeyDelete: performOperation(OperationDelete, ev, conn) - case termbox.KeyTab: // TODO: add tabs? + case termbox.KeyTab: case termbox.KeyEnter: - logger.Println("enter value:", ev.Ch) ev.Ch = '\n' performOperation(OperationInsert, ev, conn) case termbox.KeySpace: - logger.Println("space value:", ev.Ch) ev.Ch = ' ' performOperation(OperationInsert, ev, conn) default: @@ -184,6 +184,8 @@ func handleTermboxEvent(ev termbox.Event, conn *websocket.Conn) error { } } } + + e.Draw() return nil } @@ -192,6 +194,7 @@ const ( OperationDelete ) +// performOperation performs a CRDT insert or delete operation on the local document and sends a message over the WebSocket connection func performOperation(opType int, ev termbox.Event, conn *websocket.Conn) { // Get position and value. ch := string(ev.Ch) @@ -201,14 +204,20 @@ func performOperation(opType int, ev termbox.Event, conn *websocket.Conn) { // Modify local state (CRDT) first. switch opType { case OperationInsert: + logger.Printf("LOCAL INSERT: %s at cursor position %v\n", ch, e.cursor) r := []rune(ch) e.AddRune(r[0]) - text, _ := doc.Insert(e.cursor, ch) + text, err := doc.Insert(e.cursor, ch) + if err != nil { + e.SetText(text) + logger.Printf("CRDT error: %v\n", err) + } + e.SetText(text) - // logger.Println(crdt.Content(doc)) msg = message{Type: "operation", Operation: Operation{Type: "insert", Position: e.cursor, Value: ch}} case OperationDelete: + logger.Printf("LOCAL DELETE: cursor position %v\n", e.cursor) if e.cursor-1 <= 0 { e.cursor = 1 } @@ -218,8 +227,18 @@ func performOperation(opType int, ev termbox.Event, conn *websocket.Conn) { e.MoveCursor(-1, 0) } + // Print document state to logs. + printDoc(doc) + _ = conn.WriteJSON(msg) - e.Draw() +} + +// printDoc "prints" the document state to the logs. +func printDoc(doc crdt.Document) { + logger.Printf("---DOCUMENT STATE---") + for i, c := range doc.Characters { + logger.Printf("index: %v value: %s ID: %v IDPrev: %v IDNext: %v ", i, c.Value, c.ID, c.IDPrevious, c.IDNext) + } } // getTermboxChan returns a channel of termbox Events repeatedly waiting on user input. @@ -235,21 +254,35 @@ func getTermboxChan() chan termbox.Event { // handleMsg updates the CRDT document with the contents of the message. func handleMsg(msg message, doc *crdt.Document, conn *websocket.Conn) { - if msg.Type == "docResp" { - *doc = *msg.Document - logger.Printf("%+v\n", msg.Document) - } else if msg.Type == "docReq" { - docMsg := message{Type: "docResp", Document: doc} - conn.WriteJSON(&docMsg) + if msg.Type == "docResp" { //update local document + logger.Printf("DOCRESP RECEIVED, updating local doc%+v\n", msg.Document) + logger.Printf("MESSAGE DOC: %+v\n", msg.Document) + *doc = msg.Document + } else if msg.Type == "docReq" { // send local document as docResp message + logger.Printf("DOCREQ RECEIVED, sending local document to %v\n", msg.ID) + docMsg := message{Type: "docResp", Document: *doc, ID: msg.ID} + _ = conn.WriteJSON(&docMsg) + } else if msg.Type == "SiteID" { + siteID, err := strconv.Atoi(msg.Text) + if err != nil { + logger.Printf("failed to set siteID, err: %v\n", err) + } + crdt.SiteID = siteID + logger.Printf("SITE ID %v, INTENDED SITE ID: %v", crdt.SiteID, siteID) } else { switch msg.Operation.Type { case "insert": - _, _ = doc.Insert(msg.Operation.Position, msg.Operation.Value) + _, err := doc.Insert(msg.Operation.Position, msg.Operation.Value) + if err != nil { + logger.Printf("failed to insert, err: %v\n", err) + } + logger.Printf("REMOTE INSERT: %s at position %v\n", msg.Operation.Value, msg.Operation.Position) case "delete": _ = doc.Delete(msg.Operation.Position) + logger.Printf("REMOTE DELETE: position %v\n", msg.Operation.Position) } } - + printDoc(*doc) e.SetText(crdt.Content(*doc)) e.Draw() } @@ -265,7 +298,7 @@ func getMsgChan(conn *websocket.Conn) chan message { err := conn.ReadJSON(&msg) if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { - log.Printf("websocket error: %v", err) + logger.Printf("websocket error: %v", err) } break } diff --git a/crdt/woot.go b/crdt/woot.go index e90cf6b..7cf5db7 100644 --- a/crdt/woot.go +++ b/crdt/woot.go @@ -47,6 +47,14 @@ func New() Document { // Utility functions ////////////////////// +func (doc *Document) SetText(newDoc Document) { + for _, char := range newDoc.Characters { + // c := Character{ID: fmt.Sprint(SiteID) + fmt.Sprint(LocalClock), Visible: char.Visible, Value: char.Value, IDPrevious: char.IDPrevious, IDNext: char.IDNext} + c := Character{ID: char.ID, Visible: char.Visible, Value: char.Value, IDPrevious: char.IDPrevious, IDNext: char.IDNext} + doc.Characters = append(doc.Characters, c) + } +} + // Content returns the content of the document. func Content(doc Document) string { value := "" @@ -141,6 +149,10 @@ func (doc *Document) Subseq(wcharacterStart, wcharacterEnd Character) ([]Charact return doc.Characters, ErrBoundsNotPresent } + if startPosition > endPosition { + return doc.Characters, ErrBoundsNotPresent + } + if startPosition == endPosition { return []Character{}, nil } @@ -177,7 +189,12 @@ func (doc *Document) LocalInsert(char Character, position int) (*Document, error // Characters based off of the previous & next Character func (doc *Document) IntegrateInsert(char, charPrev, charNext Character) (*Document, error) { // Get the subsequence. - subsequence, _ := doc.Subseq(charPrev, charNext) + + // panic happens when charPrev > charNext + subsequence, err := doc.Subseq(charPrev, charNext) + if err != nil { + return doc, err + } // Get the position of the next character. position := doc.Position(charNext.ID) diff --git a/go.mod b/go.mod index 6244825..574c0bf 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,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 diff --git a/go.sum b/go.sum index 66238d0..f8d8bc6 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Pallinder/go-randomdata v1.2.0 h1:DZ41wBchNRb/0GfsePLiSwb0PHZmT67XY00lCDlaYPg= +github.com/Pallinder/go-randomdata v1.2.0/go.mod h1:yHmJgulpD2Nfrm0cR9tI/+oAgRqCQQixsA8HyRZfV9Y= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= diff --git a/server/main.go b/server/main.go index ab79332..9463305 100644 --- a/server/main.go +++ b/server/main.go @@ -4,6 +4,7 @@ import ( "flag" "log" "net/http" + "strconv" "time" "github.com/burntcarrot/rowix/crdt" @@ -13,12 +14,18 @@ import ( ) type message struct { - Username string `json:"username"` - Text string `json:"text"` - Type string `json:"type"` - ID uuid.UUID - Operation Operation `json:"operation"` - Document *crdt.Document `json:"document"` + Username string `json:"username"` + Text string `json:"text"` + Type string `json:"type"` + ID uuid.UUID `json:"ID"` + Operation Operation `json:"operation"` + Document crdt.Document `json:"document"` +} + +type clientInfo struct { + Username string `json:"username"` + SiteID string `json:"siteID"` + Conn *websocket.Conn } type Operation struct { @@ -30,12 +37,15 @@ type Operation struct { // Upgrader instance to upgrade all HTTP connections to a WebSocket. var upgrader = websocket.Upgrader{} -// Map to store currently active client connections. -var activeClients = make(map[*websocket.Conn]uuid.UUID) +// Map to store active client connections. +var activeClients = make(map[uuid.UUID]clientInfo) // Channel for client messages. var messageChan = make(chan message) +// Channel for document sync messages. +var docChan = make(chan message) + func main() { // Parse flags. addr := flag.String("addr", ":8080", "Server's network address") @@ -44,6 +54,9 @@ func main() { mux := http.NewServeMux() mux.HandleFunc("/", handleConn) + // Handle document syncing + go handleSync() + // Handle incoming messages. go handleMsg() @@ -64,55 +77,64 @@ func handleConn(w http.ResponseWriter, r *http.Request) { } defer conn.Close() - // doc := crdt.New() - var doc *crdt.Document color.Yellow("total active clients: %d\n", len(activeClients)) - if len(activeClients) > 0 { - // at least 2 clients for requesting a document - for clientConn, _ := range activeClients { - // send a docReq message to a client - msg := message{Type: "docReq"} - err = clientConn.WriteJSON(&msg) - if err != nil { - color.Red("Failed to send docReq: %v\n", err) - } - // wait for a client to send a document - err = clientConn.ReadJSON(&msg) + // Generate the UUID and the site ID for the client. + clientID := uuid.New() + siteID := strconv.Itoa(len(activeClients)) + + // Add the client to the map of active clients. + c := clientInfo{Conn: conn, SiteID: siteID} + activeClients[clientID] = c + + color.Magenta("activeClients after SiteID generation: %+v", activeClients) + color.Yellow("Assigning siteID: %s", c.SiteID) + + // Generate a Site ID message. + siteIDMsg := message{Type: "SiteID", Text: c.SiteID, ID: clientID} + if err := conn.WriteJSON(siteIDMsg); err != nil { + color.Red("ERROR: didn't send siteID message") + } + + // send a document request to an existing client + for id, clientInfo := range activeClients { + if id != clientID { + msg := message{Type: "docReq", ID: clientID} + color.Cyan("sending docReq to %s on behalf of %s", id, clientID) + err = clientInfo.Conn.WriteJSON(&msg) if err != nil { - color.Red("Failed to receive document: %v, msg: %+v\n", err, msg) + color.Red("Failed to send docReq: %v\n", err) + continue } - color.Red("received document from other client: %+v", msg) - - doc = msg.Document break } - - msg := message{Type: "docResp", Document: doc} - err = conn.WriteJSON(&msg) - if err != nil { - color.Red("Failed to send syncResp: %v\n", err) - } } - // Generate a UUID for the client. - activeClients[conn] = uuid.New() - + // read messages from the connection and send to channel to broadcast for { var msg message // Read message from the connection. err := conn.ReadJSON(&msg) if err != nil { - color.Red("Closing connection with ID: %v\n", activeClients[conn]) - delete(activeClients, conn) + color.Red("Closing connection with username: %v\n", activeClients[clientID].Username) + delete(activeClients, clientID) break } // Set message ID - msg.ID = activeClients[conn] + msg.ID = clientID + + // Send docResp to handleSync function + if msg.Type == "docResp" { + docChan <- msg + continue + } else { + // Set message ID + msg.ID = clientID + } - // Send message to messageChan. + // Send message to messageChan for logging and broadcasting messageChan <- msg } } @@ -126,9 +148,12 @@ func handleMsg() { // Log each message to stdout. t := time.Now().Format(time.ANSIC) if msg.Type == "info" { + // Set the username received from the client to the clientInfo present in activeClients. + clientInfo := activeClients[msg.ID] + clientInfo.Username = msg.Username + activeClients[msg.ID] = clientInfo + color.Green("%s >> %s %s (ID: %s)\n", t, msg.Username, msg.Text, msg.ID) - } else if msg.Type == "docReq" { - color.Green("%s >> docReq sent from ID: %s\n", t, msg.ID) } else if msg.Type == "operation" { color.Green("operation >> %+v from ID=%s\n", msg.Operation, msg.ID) } else { @@ -136,18 +161,33 @@ func handleMsg() { } // Broadcast to all active clients. - for client, UUID := range activeClients { + for id, clientInfo := range activeClients { // Check the UUID to prevent sending messages to their origin. - if UUID != msg.ID { + if id != msg.ID { // Write JSON message. - color.Magenta("writing message to: %s, msg: %+v\n", UUID, msg) - err := client.WriteJSON(msg) + color.Magenta("writing message to: %s, msg: %+v\n", id, msg) + err := clientInfo.Conn.WriteJSON(msg) if err != nil { color.Red("Error sending message to client: %v\n", err) - client.Close() - delete(activeClients, client) + clientInfo.Conn.Close() + delete(activeClients, id) } } } } } + +func handleSync() { + for { + // Receive document response. + docRespMsg := <-docChan + color.Cyan("got docRespMsg: %+v", docRespMsg) + + for UUID, clientInfo := range activeClients { + if UUID != docRespMsg.ID { + color.Cyan("sending docResp to %s", docRespMsg.ID) + _ = clientInfo.Conn.WriteJSON(docRespMsg) + } + } + } +} From 4e296edc5ea840309671d3aceae14adf0303f694 Mon Sep 17 00:00:00 2001 From: Aadhav Vignesh <53528139+burntcarrot@users.noreply.github.com> Date: Tue, 6 Dec 2022 16:45:09 +0530 Subject: [PATCH 04/28] tests: add editor unit tests (#13) Added unit tests for the termbox-based editor. --- client/editor.go | 4 +- client/editor_test.go | 90 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 client/editor_test.go diff --git a/client/editor.go b/client/editor.go index 39c358a..9d2e640 100644 --- a/client/editor.go +++ b/client/editor.go @@ -72,10 +72,10 @@ func (e *Editor) Draw() { cx, cy := e.calcCursorXY(e.cursor) termbox.SetCursor(cx-1, cy-1) - x, y := 0, 0 + x, y := 1, 1 for i := 0; i < len(e.text); i++ { if e.text[i] == rune('\n') { - x = 0 + x = 1 y++ } else { if x < e.width { diff --git a/client/editor_test.go b/client/editor_test.go new file mode 100644 index 0000000..3ef7e31 --- /dev/null +++ b/client/editor_test.go @@ -0,0 +1,90 @@ +package main + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestAddRune(t *testing.T) { + tests := []struct { + r rune + cursor int + expected []rune + }{ + {r: 'a', cursor: 0, expected: []rune{'a'}}, + {r: 'b', cursor: 1, expected: []rune{'a', 'b'}}, + {r: 'c', cursor: 2, expected: []rune{'a', 'b', 'c'}}, + {r: 'e', cursor: 3, expected: []rune{'a', 'b', 'c', 'e'}}, + {r: 'd', cursor: 3, expected: []rune{'a', 'b', 'c', 'd', 'e'}}, + } + + e := NewEditor() + + for _, tc := range tests { + e.cursor = tc.cursor + e.AddRune(tc.r) + if !cmp.Equal(e.GetText(), tc.expected) { + t.Errorf("got != expected, diff: %v\n", cmp.Diff(e.text, tc.expected)) + } + } +} + +func TestCalcCursorXY(t *testing.T) { + tests := []struct { + description string + cursor int + expectedX int + expectedY int + }{ + {description: "initial position", cursor: 0, expectedX: 1, expectedY: 1}, + {description: "negative index", cursor: -1, expectedX: 1, expectedY: 1}, + {description: "normal editing", cursor: 6, expectedX: 7, expectedY: 1}, + {description: "after newline", cursor: 10, expectedX: 3, expectedY: 2}, + {description: "large number", cursor: 100000, expectedX: 5, expectedY: 2}, + } + + e := NewEditor() + e.text = []rune("content\ntest") + + for _, tc := range tests { + e.cursor = tc.cursor + x, y := e.calcCursorXY(e.cursor) + + got := []int{x, y} + expected := []int{tc.expectedX, tc.expectedY} + + if !cmp.Equal(got, expected) { + t.Errorf("(%s) got != expected, diff: %v\n", tc.description, cmp.Diff(got, expected)) + } + } +} + +func TestMoveCursor(t *testing.T) { + tests := []struct { + description string + cursor int + x int + expectedCursor int + }{ + {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}, + } + + e := NewEditor() + e.text = []rune("content\ntest") + + for _, tc := range tests { + e.cursor = tc.cursor + e.MoveCursor(tc.x, 0) + + got := e.cursor + expected := tc.expectedCursor + + if !cmp.Equal(got, expected) { + t.Errorf("(%s) got != expected, diff: %v\n", tc.description, cmp.Diff(got, expected)) + } + } +} From a9768d372d3df2ec3e14147d99f67860d8b7a932 Mon Sep 17 00:00:00 2001 From: burntcarrot Date: Tue, 6 Dec 2022 19:34:39 +0530 Subject: [PATCH 05/28] feat: add editor status bar * add editor status bar for displaying messages when a new user joins and when the client loses the connection * refactor editor a separate package --- .deepsource.toml | 20 ++++ client/editor.go | 145 ------------------------- client/editor/editor.go | 163 +++++++++++++++++++++++++++++ client/{ => editor}/editor_test.go | 18 ++-- client/main.go | 40 ++++--- 5 files changed, 218 insertions(+), 168 deletions(-) create mode 100644 .deepsource.toml delete mode 100644 client/editor.go create mode 100644 client/editor/editor.go rename client/{ => editor}/editor_test.go (88%) diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 0000000..829ad69 --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,20 @@ +version = 1 + +[[analyzers]] +name = "shell" +enabled = true + +[[analyzers]] +name = "secrets" +enabled = true + +[[analyzers]] +name = "docker" +enabled = true + +[[analyzers]] +name = "go" +enabled = true + + [analyzers.meta] + import_root = "github.com/burntcarrot/rowix" diff --git a/client/editor.go b/client/editor.go deleted file mode 100644 index 9d2e640..0000000 --- a/client/editor.go +++ /dev/null @@ -1,145 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/mattn/go-runewidth" - "github.com/nsf/termbox-go" -) - -type Editor struct { - text []rune - cursor int - width int - height int -} - -func NewEditor() *Editor { - return &Editor{} -} - -func (e *Editor) GetText() []rune { - return e.text -} - -func (e *Editor) SetText(text string) { - e.text = []rune(text) -} - -func (e *Editor) GetX() int { - x, _ := e.calcCursorXY(e.cursor) - return x -} - -func (e *Editor) SetX(x int) { - e.cursor = x -} - -func (e *Editor) GetY() int { - _, y := e.calcCursorXY(e.cursor) - return y -} - -func (e *Editor) GetWidth() int { - return e.width -} - -func (e *Editor) GetHeight() int { - return e.height -} - -func (e *Editor) SetSize(w, h int) { - e.width = w - e.height = h -} - -// AddRune adds a rune to the editor's state and updates position. -func (e *Editor) AddRune(r rune) { - if e.cursor == 0 { - e.text = append([]rune{r}, e.text...) - } else if e.cursor < len(e.text) { - e.text = append(e.text[:e.cursor], e.text[e.cursor-1:]...) - e.text[e.cursor] = r - } else { - e.text = append(e.text[:e.cursor], r) - } - e.cursor++ -} - -// Draw updates the UI by setting cells with the editor's content. -func (e *Editor) Draw() { - _ = termbox.Clear(termbox.ColorDefault, termbox.ColorDefault) - cx, cy := e.calcCursorXY(e.cursor) - termbox.SetCursor(cx-1, cy-1) - - x, y := 1, 1 - for i := 0; i < len(e.text); i++ { - if e.text[i] == rune('\n') { - x = 1 - y++ - } else { - if x < e.width { - // Set cell content. - termbox.SetCell(x, y, e.text[i], termbox.ColorDefault, termbox.ColorDefault) - } - - // Update x by rune's width. - x = x + runewidth.RuneWidth(e.text[i]) - } - } - - // Show position details (for debugging). - e.showPositions() - - // Flush back buffer! - termbox.Flush() -} - -// showPositions shows the positions with other details. -func (e *Editor) showPositions() { - x, y := e.calcCursorXY(e.cursor) - - // Construct message for debugging. - str := fmt.Sprintf("x=%d, y=%d, cursor=%d, len(text)=%d", x, y, e.cursor, len(e.text)) - - for i, r := range []rune(str) { - termbox.SetCell(i, e.height-1, r, termbox.ColorDefault, termbox.ColorDefault) - } -} - -// MoveCursor updates the cursor position. -func (e *Editor) MoveCursor(x, _ int) { - newCursor := e.cursor + x - - if newCursor < 0 { - newCursor = 0 - } - if newCursor > len(e.text) { - newCursor = len(e.text) - } - e.cursor = newCursor -} - -// calcCursorXY calculates cursor position from the index obtained from the content. -func (e *Editor) calcCursorXY(index int) (int, int) { - x := 1 - y := 1 - - if index < 0 { - return x, y - } - - if index > len(e.text) { - index = len(e.text) - } - - for i := 0; i < index; i++ { - if e.text[i] == rune('\n') { - x = 1 - y++ - } else { - x = x + runewidth.RuneWidth(e.text[i]) - } - } - return x, y -} diff --git a/client/editor/editor.go b/client/editor/editor.go new file mode 100644 index 0000000..b1d5520 --- /dev/null +++ b/client/editor/editor.go @@ -0,0 +1,163 @@ +package editor + +import ( + "fmt" + "time" + + "github.com/mattn/go-runewidth" + "github.com/nsf/termbox-go" +) + +type Editor struct { + Text []rune + Cursor int + Width int + Height int + ShowMsg bool + StatusMsg string +} + +func NewEditor() *Editor { + return &Editor{} +} + +func (e *Editor) GetText() []rune { + return e.Text +} + +func (e *Editor) SetText(text string) { + e.Text = []rune(text) +} + +func (e *Editor) GetX() int { + x, _ := e.calcCursorXY(e.Cursor) + return x +} + +func (e *Editor) SetX(x int) { + e.Cursor = x +} + +func (e *Editor) GetY() int { + _, y := e.calcCursorXY(e.Cursor) + return y +} + +func (e *Editor) GetWidth() int { + return e.Width +} + +func (e *Editor) GetHeight() int { + return e.Height +} + +func (e *Editor) SetSize(w, h int) { + e.Width = w + e.Height = h +} + +// AddRune adds a rune to the editor's state and updates position. +func (e *Editor) AddRune(r rune) { + if e.Cursor == 0 { + e.Text = append([]rune{r}, e.Text...) + } else if e.Cursor < len(e.Text) { + e.Text = append(e.Text[:e.Cursor], e.Text[e.Cursor-1:]...) + e.Text[e.Cursor] = r + } else { + e.Text = append(e.Text[:e.Cursor], r) + } + e.Cursor++ +} + +// Draw updates the UI by setting cells with the editor's content. +func (e *Editor) Draw() { + _ = termbox.Clear(termbox.ColorDefault, termbox.ColorDefault) + cx, cy := e.calcCursorXY(e.Cursor) + termbox.SetCursor(cx-1, cy-1) + + x, y := 0, 0 + for i := 0; i < len(e.Text); i++ { + if e.Text[i] == rune('\n') { + x = 0 + y++ + } else { + if x < e.Width { + // Set cell content. + termbox.SetCell(x, y, e.Text[i], termbox.ColorDefault, termbox.ColorDefault) + } + + // Update x by rune's Width. + x = x + runewidth.RuneWidth(e.Text[i]) + } + } + + if e.ShowMsg { + e.SetStatusBar() + } else { + e.showPositions() + } + + // Flush back buffer! + termbox.Flush() +} + +func (e *Editor) SetStatusBar() { + e.ShowMsg = true + + for i, r := range []rune(e.StatusMsg) { + termbox.SetCell(i, e.Height-1, r, termbox.ColorDefault, termbox.ColorDefault) + } + + _ = time.AfterFunc(5*time.Second, func() { + e.ShowMsg = false + }) +} + +// showPositions shows the positions with other details. +func (e *Editor) showPositions() { + x, y := e.calcCursorXY(e.Cursor) + + // Construct message for debugging. + str := fmt.Sprintf("x=%d, y=%d, cursor=%d, len(text)=%d", x, y, e.Cursor, len(e.Text)) + + for i, r := range []rune(str) { + termbox.SetCell(i, e.Height-1, r, termbox.ColorDefault, termbox.ColorDefault) + } +} + +// MoveCursor updates the Cursor position. +func (e *Editor) MoveCursor(x, _ int) { + newCursor := e.Cursor + x + + if newCursor < 0 { + newCursor = 0 + } + if newCursor > len(e.Text) { + newCursor = len(e.Text) + } + e.Cursor = newCursor +} + +// calcCursorXY calculates Cursor position from the index obtained from the content. +func (e *Editor) calcCursorXY(index int) (int, int) { + x := 1 + y := 1 + + if index < 0 { + return x, y + } + + if index > len(e.Text) { + index = len(e.Text) + } + + for i := 0; i < index; i++ { + if e.Text[i] == rune('\n') { + x = 1 + y++ + } else { + x = x + runewidth.RuneWidth(e.Text[i]) + } + } + return x, y +} diff --git a/client/editor_test.go b/client/editor/editor_test.go similarity index 88% rename from client/editor_test.go rename to client/editor/editor_test.go index 3ef7e31..1ec1fcf 100644 --- a/client/editor_test.go +++ b/client/editor/editor_test.go @@ -1,4 +1,4 @@ -package main +package editor import ( "testing" @@ -22,10 +22,10 @@ func TestAddRune(t *testing.T) { e := NewEditor() for _, tc := range tests { - e.cursor = tc.cursor + e.Cursor = tc.cursor e.AddRune(tc.r) if !cmp.Equal(e.GetText(), tc.expected) { - t.Errorf("got != expected, diff: %v\n", cmp.Diff(e.text, tc.expected)) + t.Errorf("got != expected, diff: %v\n", cmp.Diff(e.Text, tc.expected)) } } } @@ -45,11 +45,11 @@ func TestCalcCursorXY(t *testing.T) { } e := NewEditor() - e.text = []rune("content\ntest") + e.Text = []rune("content\ntest") for _, tc := range tests { - e.cursor = tc.cursor - x, y := e.calcCursorXY(e.cursor) + e.Cursor = tc.cursor + x, y := e.calcCursorXY(e.Cursor) got := []int{x, y} expected := []int{tc.expectedX, tc.expectedY} @@ -74,13 +74,13 @@ func TestMoveCursor(t *testing.T) { } e := NewEditor() - e.text = []rune("content\ntest") + e.Text = []rune("content\ntest") for _, tc := range tests { - e.cursor = tc.cursor + e.Cursor = tc.cursor e.MoveCursor(tc.x, 0) - got := e.cursor + got := e.Cursor expected := tc.expectedCursor if !cmp.Equal(got, expected) { diff --git a/client/main.go b/client/main.go index 8744a58..733d6b8 100644 --- a/client/main.go +++ b/client/main.go @@ -13,6 +13,7 @@ import ( "time" "github.com/Pallinder/go-randomdata" + "github.com/burntcarrot/rowix/client/editor" "github.com/burntcarrot/rowix/crdt" "github.com/google/uuid" "github.com/gorilla/websocket" @@ -40,8 +41,11 @@ var doc crdt.Document // Centralized logger. var logger *log.Logger +// WebSocket connection. +var conn *websocket.Conn + // termbox-based editor. -var e *Editor +var e *editor.Editor func main() { // Parse flags. @@ -80,7 +84,8 @@ func main() { HandshakeTimeout: 2 * time.Minute, } - conn, _, err := dialer.Dial(u.String(), nil) + var err error + conn, _, err = dialer.Dial(u.String(), nil) if err != nil { fmt.Printf("Connection error, exiting: %s\n", err) os.Exit(0) @@ -120,7 +125,7 @@ func UI(conn *websocket.Conn, d *crdt.Document) error { } defer termbox.Close() - e = NewEditor() + e = editor.NewEditor() e.SetSize(termbox.Size()) e.Draw() @@ -133,7 +138,7 @@ func UI(conn *websocket.Conn, d *crdt.Document) error { } // mainLoop is the main update loop for the UI. -func mainLoop(e *Editor, conn *websocket.Conn, doc *crdt.Document) error { +func mainLoop(e *editor.Editor, conn *websocket.Conn, doc *crdt.Document) error { termboxChan := getTermboxChan() msgChan := getMsgChan(conn) @@ -166,7 +171,7 @@ func handleTermboxEvent(ev termbox.Event, conn *websocket.Conn) error { case termbox.KeyHome: e.SetX(0) case termbox.KeyEnd: - e.SetX(len(e.text)) + e.SetX(len(e.Text)) case termbox.KeyBackspace, termbox.KeyBackspace2: performOperation(OperationDelete, ev, conn) case termbox.KeyDelete: @@ -204,33 +209,37 @@ func performOperation(opType int, ev termbox.Event, conn *websocket.Conn) { // Modify local state (CRDT) first. switch opType { case OperationInsert: - logger.Printf("LOCAL INSERT: %s at cursor position %v\n", ch, e.cursor) + logger.Printf("LOCAL INSERT: %s at cursor position %v\n", ch, e.Cursor) r := []rune(ch) e.AddRune(r[0]) - text, err := doc.Insert(e.cursor, ch) + text, err := doc.Insert(e.Cursor, ch) if err != nil { e.SetText(text) logger.Printf("CRDT error: %v\n", err) } e.SetText(text) - msg = message{Type: "operation", Operation: Operation{Type: "insert", Position: e.cursor, Value: ch}} + msg = message{Type: "operation", Operation: Operation{Type: "insert", Position: e.Cursor, Value: ch}} case OperationDelete: - logger.Printf("LOCAL DELETE: cursor position %v\n", e.cursor) - if e.cursor-1 <= 0 { - e.cursor = 1 + logger.Printf("LOCAL DELETE: cursor position %v\n", e.Cursor) + if e.Cursor-1 <= 0 { + e.Cursor = 1 } - text := doc.Delete(e.cursor) + text := doc.Delete(e.Cursor) e.SetText(text) - msg = message{Type: "operation", Operation: Operation{Type: "delete", Position: e.cursor}} + msg = message{Type: "operation", Operation: Operation{Type: "delete", Position: e.Cursor}} e.MoveCursor(-1, 0) } // Print document state to logs. printDoc(doc) - _ = conn.WriteJSON(msg) + err := conn.WriteJSON(msg) + if err != nil { + e.StatusMsg = "lost connection!" + e.SetStatusBar() + } } // printDoc "prints" the document state to the logs. @@ -269,6 +278,9 @@ func handleMsg(msg message, doc *crdt.Document, conn *websocket.Conn) { } crdt.SiteID = siteID logger.Printf("SITE ID %v, INTENDED SITE ID: %v", crdt.SiteID, siteID) + } else if msg.Type == "info" { + e.StatusMsg = fmt.Sprintf("%s has joined the session!", msg.Username) + e.SetStatusBar() } else { switch msg.Operation.Type { case "insert": From 01da8c6318efe07d9dce27a31290daa0626d4966 Mon Sep 17 00:00:00 2001 From: burntcarrot Date: Tue, 6 Dec 2022 22:52:44 +0530 Subject: [PATCH 06/28] fix: fix linter issues * fix defer calls on file and WebSocket close * add server read and write timeout * use single layer for running commands in Dockerfile * cleanup control flow --- Dockerfile | 7 +++---- client/main.go | 31 ++++++++++++++++++++----------- server/main.go | 13 +++++++++---- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/Dockerfile b/Dockerfile index bdb5902..36c5c5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,10 +8,9 @@ RUN go mod download COPY ./ ./ -RUN apk add --no-cache bash - -RUN go build -o ./rowix-server ./server/main.go +# skipcq: DOK-DL3018 +RUN apk add --no-cache bash && go build -o ./rowix-server ./server/main.go EXPOSE 8080 -CMD [ "/app/rowix-server" ] \ No newline at end of file +CMD [ "/app/rowix-server" ] diff --git a/client/main.go b/client/main.go index 733d6b8..b996b77 100644 --- a/client/main.go +++ b/client/main.go @@ -96,24 +96,34 @@ func main() { msg := message{Username: name, Text: "has joined the session.", Type: "info"} _ = conn.WriteJSON(msg) - // open logging file and create if non-existent - file, err := os.OpenFile("rowix.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + // open the log file and create if it does not exist + file, err := os.OpenFile("rowix.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) if err != nil { fmt.Printf("Logger error, exiting: %s", err) - os.Exit(0) + return } defer file.Close() logger = log.New(file, fmt.Sprintf("--- name: %s >> ", name), log.LstdFlags) - // start local session + err = UI(conn, &doc) if err != nil { if strings.HasPrefix(err.Error(), "rowix") { fmt.Println("exiting session.") - os.Exit(0) + return } fmt.Printf("TUI error, exiting: %s\n", err) - os.Exit(0) + return + } + + if err := file.Close(); err != nil { + fmt.Printf("Failed to close log file: %s", err) + return + } + + if err := conn.Close(); err != nil { + fmt.Printf("Failed to close websocket connection: %s", err) + return } } @@ -129,7 +139,7 @@ func UI(conn *websocket.Conn, d *crdt.Document) error { e.SetSize(termbox.Size()) e.Draw() - err = mainLoop(e, conn, d) + err = mainLoop(conn, d) if err != nil { return err } @@ -138,7 +148,7 @@ func UI(conn *websocket.Conn, d *crdt.Document) error { } // mainLoop is the main update loop for the UI. -func mainLoop(e *editor.Editor, conn *websocket.Conn, doc *crdt.Document) error { +func mainLoop(conn *websocket.Conn, doc *crdt.Document) error { termboxChan := getTermboxChan() msgChan := getMsgChan(conn) @@ -159,8 +169,7 @@ func mainLoop(e *editor.Editor, conn *websocket.Conn, doc *crdt.Document) error // handleTermboxEvent handles key input by updating the local CRDT document and sending a message over the WebSocket connection. func handleTermboxEvent(ev termbox.Event, conn *websocket.Conn) error { - switch ev.Type { - case termbox.EventKey: + if ev.Type == termbox.EventKey { switch ev.Key { case termbox.KeyEsc, termbox.KeyCtrlC: return errors.New("rowix: exiting") @@ -263,7 +272,7 @@ func getTermboxChan() chan termbox.Event { // handleMsg updates the CRDT document with the contents of the message. func handleMsg(msg message, doc *crdt.Document, conn *websocket.Conn) { - if msg.Type == "docResp" { //update local document + if msg.Type == "docResp" { // update local document logger.Printf("DOCRESP RECEIVED, updating local doc%+v\n", msg.Document) logger.Printf("MESSAGE DOC: %+v\n", msg.Document) *doc = msg.Document diff --git a/server/main.go b/server/main.go index 9463305..ff3f73e 100644 --- a/server/main.go +++ b/server/main.go @@ -62,7 +62,15 @@ func main() { // Start the server. log.Printf("Starting server on %s", *addr) - err := http.ListenAndServe(*addr, mux) + + server := &http.Server{ + Addr: *addr, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + Handler: mux, + } + + err := server.ListenAndServe() if err != nil { log.Fatal("Error starting server, exiting.", err) } @@ -129,9 +137,6 @@ func handleConn(w http.ResponseWriter, r *http.Request) { if msg.Type == "docResp" { docChan <- msg continue - } else { - // Set message ID - msg.ID = clientID } // Send message to messageChan for logging and broadcasting From c1f22483ce5df078b5b3ddc25e2aa7d25fef4470 Mon Sep 17 00:00:00 2001 From: burntcarrot Date: Tue, 6 Dec 2022 22:57:55 +0530 Subject: [PATCH 07/28] fix: fix file close defer call --- client/main.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/main.go b/client/main.go index b996b77..6991ffc 100644 --- a/client/main.go +++ b/client/main.go @@ -102,7 +102,12 @@ func main() { fmt.Printf("Logger error, exiting: %s", err) return } - defer file.Close() + defer func() { + err := file.Close() + if err != nil { + log.Fatalln(err) + } + }() logger = log.New(file, fmt.Sprintf("--- name: %s >> ", name), log.LstdFlags) From b9d1ae6a3cfaa1df606f448dd01805e0eddee440 Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Thu, 8 Dec 2022 08:19:32 -0600 Subject: [PATCH 08/28] (WIP) Add support for up/down arrows. Up and down arrows now have some behavior, but it is currently not as intended. --- client/editor/editor.go | 43 ++++++++++++++++++++++++++++++++++++++++- client/main.go | 4 ++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/client/editor/editor.go b/client/editor/editor.go index b1d5520..33c3133 100644 --- a/client/editor/editor.go +++ b/client/editor/editor.go @@ -126,9 +126,22 @@ func (e *Editor) showPositions() { } // MoveCursor updates the Cursor position. -func (e *Editor) MoveCursor(x, _ int) { +func (e *Editor) MoveCursor(x, y int) { newCursor := e.Cursor + x + // move cursor down y cells + if y > 0 { + cx, cy := e.calcCursorXY(e.Cursor) + cy = cy + y + newCursor = e.calcCursor(cx, cy) + } + // move cursor up y cells + if y < 0 { + cx, cy := e.calcCursorXY(e.Cursor) + cy = cy - y + newCursor = e.calcCursor(cx, cy) + } + if newCursor < 0 { newCursor = 0 } @@ -161,3 +174,31 @@ func (e *Editor) calcCursorXY(index int) (int, int) { } return x, y } + +func (e *Editor) calcCursor(x, y int) int { + ri := 0 + yi := 1 + xi := 1 + + for yi < y { + for _, r := range e.Text { + ri++ + if r == '\n' { + yi++ + break + } + } + if ri > len(e.Text) { + ri = len(e.Text) + } + + for _, r := range e.Text[ri:] { + if xi >= x-runewidth.RuneWidth(r) { + break + } + xi += runewidth.RuneWidth(r) + ri++ + } + } + return ri +} diff --git a/client/main.go b/client/main.go index 6991ffc..db96510 100644 --- a/client/main.go +++ b/client/main.go @@ -182,6 +182,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: From 81ee2fefc53138aa945316c0efab4d0cb2269b98 Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Thu, 8 Dec 2022 10:38:28 -0600 Subject: [PATCH 09/28] (WIP) Change up and down arrow support implementation. --- client/editor/editor.go | 103 +++++++++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 33 deletions(-) diff --git a/client/editor/editor.go b/client/editor/editor.go index 33c3133..41db196 100644 --- a/client/editor/editor.go +++ b/client/editor/editor.go @@ -131,15 +131,52 @@ func (e *Editor) MoveCursor(x, y int) { // move cursor down y cells if y > 0 { - cx, cy := e.calcCursorXY(e.Cursor) - cy = cy + y - newCursor = e.calcCursor(cx, cy) + // for i := 0; i <= y; i++ { + n1 := 0 // index of next newline character + n2 := 0 // index of the newline character after n1 + + // note the position of the next two newline characters + for j := e.Cursor; j < len(e.Text); j++ { + if e.Text[j] == '\n' { + if n1 != 0 { // if the next newline character has already been found + n2 = j + break + } + n1 = j + } + } + newCursor := n2 + for k := 0; k < n1-e.Cursor; k++ { + if newCursor == n1 { + break + } + newCursor-- + } + // } } // move cursor up y cells if y < 0 { - cx, cy := e.calcCursorXY(e.Cursor) - cy = cy - y - newCursor = e.calcCursor(cx, cy) + // for i := 0; i >= y; i-- { + n1 := 0 // index of previous newline character + n2 := 0 // index of the newline character before n1 + // note the position of the previous two newline characters + for j := e.Cursor; j > 0; j-- { + if e.Text[j] == '\n' { + if n2 != 0 { // if the next newline character has already been found + n1 = j + break + } + n2 = j + } + } + newCursor := n1 + for k := 0; k < n1-e.Cursor; k++ { + if newCursor == n1 { + break + } + newCursor++ + } + // } } if newCursor < 0 { @@ -175,30 +212,30 @@ func (e *Editor) calcCursorXY(index int) (int, int) { return x, y } -func (e *Editor) calcCursor(x, y int) int { - ri := 0 - yi := 1 - xi := 1 - - for yi < y { - for _, r := range e.Text { - ri++ - if r == '\n' { - yi++ - break - } - } - if ri > len(e.Text) { - ri = len(e.Text) - } - - for _, r := range e.Text[ri:] { - if xi >= x-runewidth.RuneWidth(r) { - break - } - xi += runewidth.RuneWidth(r) - ri++ - } - } - return ri -} +// func (e *Editor) calcCursor(x, y int) int { +// ri := 0 +// yi := 1 +// xi := 1 + +// for yi < y { +// for _, r := range e.Text { +// ri++ +// if r == '\n' { +// yi++ +// break +// } +// } +// if ri > len(e.Text) { +// ri = len(e.Text) +// } + +// for _, r := range e.Text[ri:] { +// if xi >= x-runewidth.RuneWidth(r) { +// break +// } +// xi += runewidth.RuneWidth(r) +// ri++ +// } +// } +// return ri +// } From 565900c27a93d87262418aa09d4514a87f8659e2 Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Fri, 9 Dec 2022 10:33:37 -0600 Subject: [PATCH 10/28] (WIP) Up and down arrows working. Movement is working, but behavior is still undefined at the and end of files, so there are a few bugs to be fixed. --- client/editor/editor.go | 46 ++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/client/editor/editor.go b/client/editor/editor.go index 41db196..5f72728 100644 --- a/client/editor/editor.go +++ b/client/editor/editor.go @@ -2,6 +2,8 @@ package editor import ( "fmt" + "log" + "os" "time" "github.com/mattn/go-runewidth" @@ -127,15 +129,28 @@ func (e *Editor) showPositions() { // MoveCursor updates the Cursor position. func (e *Editor) MoveCursor(x, y int) { + file, err := os.OpenFile("cursor.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + fmt.Printf("Logger error, exiting: %s", err) + return + } + defer func() { + err := file.Close() + if err != nil { + log.Fatalln(err) + } + }() + logger := log.New(file, "---", log.LstdFlags) + newCursor := e.Cursor + x // move cursor down y cells if y > 0 { - // for i := 0; i <= y; i++ { + logger.Printf("DOWN ARROW PRESSED") n1 := 0 // index of next newline character n2 := 0 // index of the newline character after n1 - // note the position of the next two newline characters + // store the position of the next two newline characters for j := e.Cursor; j < len(e.Text); j++ { if e.Text[j] == '\n' { if n1 != 0 { // if the next newline character has already been found @@ -145,38 +160,43 @@ func (e *Editor) MoveCursor(x, y int) { n1 = j } } - newCursor := n2 + newCursor = n2 + logger.Printf("the next two new line characters are at %v and %v\n.", n1, n2) for k := 0; k < n1-e.Cursor; k++ { if newCursor == n1 { break } newCursor-- } - // } + logger.Printf("After calc, newCursor is at: %v", newCursor) } + // move cursor up y cells if y < 0 { - // for i := 0; i >= y; i-- { + logger.Printf("UP ARROW PRESSED") n1 := 0 // index of previous newline character n2 := 0 // index of the newline character before n1 - // note the position of the previous two newline characters + // store the position of the previous two newline characters for j := e.Cursor; j > 0; j-- { if e.Text[j] == '\n' { - if n2 != 0 { // if the next newline character has already been found - n1 = j + if n1 != 0 || j == 0 { // if the previous newline character has already been found + n2 = j + logger.Printf("this code is getting reached! n2 = %v\n", n2) break } - n2 = j + n1 = j + } } - newCursor := n1 - for k := 0; k < n1-e.Cursor; k++ { + newCursor = n2 + logger.Printf("the previous two new line characters are at %v and %v\n.", n1, n2) + for k := 0; k < e.Cursor-n1; k++ { if newCursor == n1 { break } newCursor++ } - // } + logger.Printf("After calc, newCursor is at: %v", newCursor) } if newCursor < 0 { @@ -185,7 +205,9 @@ func (e *Editor) MoveCursor(x, y int) { if newCursor > len(e.Text) { newCursor = len(e.Text) } + e.Cursor = newCursor + } // calcCursorXY calculates Cursor position from the index obtained from the content. From e48400a43d7675cbca059870b8d5d57f7afe8ab3 Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Sat, 10 Dec 2022 18:24:39 -0600 Subject: [PATCH 11/28] Change init of variables to avoid conflict. Newline index variables n1 and n2 now initialize to -1 to avoid conflict when running into the beginning of the document. --- client/editor/editor.go | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/client/editor/editor.go b/client/editor/editor.go index 5f72728..a8996ab 100644 --- a/client/editor/editor.go +++ b/client/editor/editor.go @@ -147,19 +147,28 @@ func (e *Editor) MoveCursor(x, y int) { // move cursor down y cells if y > 0 { logger.Printf("DOWN ARROW PRESSED") - n1 := 0 // index of next newline character - n2 := 0 // index of the newline character after n1 + n1 := -1 // index of next newline character + n2 := -1 // index of the newline character after n1 // store the position of the next two newline characters for j := e.Cursor; j < len(e.Text); j++ { if e.Text[j] == '\n' { - if n1 != 0 { // if the next newline character has already been found + if n1 > 0 { // if the next newline character has already been found n2 = j break } n1 = j } } + if n1 < 0 && n2 < 0 { + logger.Printf("couldn't find next newline, setting e.Cursor to end of text\n") + e.Cursor = len(e.Text) + return + } + if n2 < 0 { + n2 = len(e.Text) + } + newCursor = n2 logger.Printf("the next two new line characters are at %v and %v\n.", n1, n2) for k := 0; k < n1-e.Cursor; k++ { @@ -174,20 +183,27 @@ func (e *Editor) MoveCursor(x, y int) { // move cursor up y cells if y < 0 { logger.Printf("UP ARROW PRESSED") - n1 := 0 // index of previous newline character - n2 := 0 // index of the newline character before n1 + n1 := -1 // index of previous newline character + n2 := -1 // index of the newline character before n1 // store the position of the previous two newline characters for j := e.Cursor; j > 0; j-- { if e.Text[j] == '\n' { - if n1 != 0 || j == 0 { // if the previous newline character has already been found + if n1 > 0 { // if the previous newline character has already been found n2 = j logger.Printf("this code is getting reached! n2 = %v\n", n2) break } n1 = j - } } + if n1 < 0 && n2 < 0 { + logger.Printf("couldn't find previous newline, setting e.Cursor to 0\n") + e.Cursor = 0 + return + } + if n2 < 0 { + n2 = 0 + } newCursor = n2 logger.Printf("the previous two new line characters are at %v and %v\n.", n1, n2) for k := 0; k < e.Cursor-n1; k++ { From d0d32572f13b418fcf7b671f7402de108a21bccc Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Sun, 11 Dec 2022 01:07:39 -0600 Subject: [PATCH 12/28] (WIP) Fix down arrow behavior Down arrow behavior seems to be working in standard use and some edge cases. Up arrow has not been fixed. --- client/editor/editor.go | 114 ++++++++++++++++++++++------------------ 1 file changed, 64 insertions(+), 50 deletions(-) diff --git a/client/editor/editor.go b/client/editor/editor.go index a8996ab..33ef5e5 100644 --- a/client/editor/editor.go +++ b/client/editor/editor.go @@ -147,35 +147,77 @@ func (e *Editor) MoveCursor(x, y int) { // move cursor down y cells if y > 0 { logger.Printf("DOWN ARROW PRESSED") - n1 := -1 // index of next newline character - n2 := -1 // index of the newline character after n1 + cls := -1 // index of the newline character marking the start of the current line + cle := -1 // index of the newline character marking the end of the current line + nle := -1 // index of the newline character marking the end of the next line + offset := 0 // current offset of cursor from beginning of line + + if newCursor > len(e.Text)-1 { + logger.Printf("cursor out of bounds! cursor reset at %v", len(e.Text)) + newCursor = len(e.Text) - 1 + } + // if cursor is currently on newline, 'move' it + if e.Text[newCursor] == '\n' { + logger.Printf("cursor on new line, moving to prev char") + newCursor-- + offset++ + } - // store the position of the next two newline characters - for j := e.Cursor; j < len(e.Text); j++ { - if e.Text[j] == '\n' { - if n1 > 0 { // if the next newline character has already been found - n2 = j - break - } - n1 = j + // find offset from start of line and set cls to start of line + for i := newCursor; i > 0; i-- { + if e.Text[i] == '\n' { + logger.Println("current line start found at ", i) + cls = i + break } + offset++ + logger.Printf("offset at %v, char %v", offset, e.Text[i]) } - if n1 < 0 && n2 < 0 { - logger.Printf("couldn't find next newline, setting e.Cursor to end of text\n") - e.Cursor = len(e.Text) - return - } - if n2 < 0 { - n2 = len(e.Text) + logger.Printf("offset: %v", offset) + // if start of current line isn't set, assume current line is the first line of the document, + // so the start of the current line is at position 0 + if cls < 0 { + logger.Printf("on first line") + offset++ + cls = 0 } - newCursor = n2 - logger.Printf("the next two new line characters are at %v and %v\n.", n1, n2) - for k := 0; k < n1-e.Cursor; k++ { - if newCursor == n1 { + // cle is used to find length of current line (cle - cls) + for i := cls + 1; i < len(e.Text); i++ { + if e.Text[i] == '\n' { + logger.Println("current line end at ", i) + cle = i break } - newCursor-- + } + + // nle is used to find length of next line (nle - cle) + if cle > 0 { // if the end of the current line isn't set, no need to find nle + for i := cle + 1; i < len(e.Text); i++ { + if e.Text[i] == '\n' { + logger.Println("next line end at ", i) + nle = i + break + } + } + } + // if end of next line isn't set, assume next line is last of the document + if nle < 0 { + nle = len(e.Text) + } + logger.Printf("Current line starts at %v. Current line ends at %v. Next line ends at %v\n", cls, cle, nle) + + if cle < 0 { + newCursor = len(e.Text) + // } else if nle-cle < cle-cls { // if next line is shorter than the current line + // logger.Printf("next line is shorter") + // newCursor = nle + // } else { + } else if nle-cle < offset { // if next line is shorter than the offset + logger.Printf("next line is shorter") + newCursor = nle + } else { + newCursor = cle + offset } logger.Printf("After calc, newCursor is at: %v", newCursor) } @@ -249,31 +291,3 @@ func (e *Editor) calcCursorXY(index int) (int, int) { } return x, y } - -// func (e *Editor) calcCursor(x, y int) int { -// ri := 0 -// yi := 1 -// xi := 1 - -// for yi < y { -// for _, r := range e.Text { -// ri++ -// if r == '\n' { -// yi++ -// break -// } -// } -// if ri > len(e.Text) { -// ri = len(e.Text) -// } - -// for _, r := range e.Text[ri:] { -// if xi >= x-runewidth.RuneWidth(r) { -// break -// } -// xi += runewidth.RuneWidth(r) -// ri++ -// } -// } -// return ri -// } From d9941df4d80588e543067a6108816ff723c1759c Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Mon, 12 Dec 2022 21:56:18 -0600 Subject: [PATCH 13/28] Fix down arrow behavior --- client/editor/editor.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/client/editor/editor.go b/client/editor/editor.go index 33ef5e5..7b385ae 100644 --- a/client/editor/editor.go +++ b/client/editor/editor.go @@ -147,10 +147,11 @@ func (e *Editor) MoveCursor(x, y int) { // move cursor down y cells if y > 0 { logger.Printf("DOWN ARROW PRESSED") - cls := -1 // index of the newline character marking the start of the current line - cle := -1 // index of the newline character marking the end of the current line - nle := -1 // index of the newline character marking the end of the next line - offset := 0 // current offset of cursor from beginning of line + cls := -1 // index of the newline character marking the start of the current line + cle := -1 // index of the newline character marking the end of the current line + nle := -1 // index of the newline character marking the end of the next line + // offset = e.Cursor - cls + offset := 1 // current offset of cursor from beginning of line if newCursor > len(e.Text)-1 { logger.Printf("cursor out of bounds! cursor reset at %v", len(e.Text)) @@ -160,7 +161,6 @@ func (e *Editor) MoveCursor(x, y int) { if e.Text[newCursor] == '\n' { logger.Printf("cursor on new line, moving to prev char") newCursor-- - offset++ } // find offset from start of line and set cls to start of line @@ -170,18 +170,18 @@ func (e *Editor) MoveCursor(x, y int) { cls = i break } - offset++ - logger.Printf("offset at %v, char %v", offset, e.Text[i]) } - logger.Printf("offset: %v", offset) // if start of current line isn't set, assume current line is the first line of the document, // so the start of the current line is at position 0 if cls < 0 { logger.Printf("on first line") - offset++ - cls = 0 + offset = e.Cursor + 1 + } else { + offset = e.Cursor - cls } + logger.Printf("offset: %v", offset) + // cle is used to find length of current line (cle - cls) for i := cls + 1; i < len(e.Text); i++ { if e.Text[i] == '\n' { @@ -214,7 +214,7 @@ func (e *Editor) MoveCursor(x, y int) { // newCursor = nle // } else { } else if nle-cle < offset { // if next line is shorter than the offset - logger.Printf("next line is shorter") + logger.Printf("next line is shorter, moving to end of next line") newCursor = nle } else { newCursor = cle + offset From d509f4fece4545a8717cd66564ff0a360c654281 Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Mon, 12 Dec 2022 23:50:11 -0600 Subject: [PATCH 14/28] Fix up/down arrow bugs Got an implementation of up/down arrow behavior working. The implementation is rather convoluted and hard to reason about. There is definitely room for tightening the logic and improving readability. It's also worth considering a change to the way editor text is currently represented, so that it maps more closely to Termbox's 2D structure. A 2D array of runes might make a more natural interface between the linear CRDT document and the 2D Termbox terminal, and therefore make the code cleaner and more maintainable. Future: - tighten logic - remove logging - refactor (explore alternate ways to store editor text) --- client/editor/editor.go | 125 +++++++++++++++++++++++----------------- 1 file changed, 73 insertions(+), 52 deletions(-) diff --git a/client/editor/editor.go b/client/editor/editor.go index 7b385ae..e0a237e 100644 --- a/client/editor/editor.go +++ b/client/editor/editor.go @@ -147,45 +147,37 @@ func (e *Editor) MoveCursor(x, y int) { // move cursor down y cells if y > 0 { logger.Printf("DOWN ARROW PRESSED") - cls := -1 // index of the newline character marking the start of the current line - cle := -1 // index of the newline character marking the end of the current line - nle := -1 // index of the newline character marking the end of the next line - // offset = e.Cursor - cls + cls, cle, nle := -1, -1, -1 offset := 1 // current offset of cursor from beginning of line + //TODO: check if the below is necessary for avoiding bugs if newCursor > len(e.Text)-1 { - logger.Printf("cursor out of bounds! cursor reset at %v", len(e.Text)) newCursor = len(e.Text) - 1 } + // if cursor is currently on newline, 'move' it - if e.Text[newCursor] == '\n' { - logger.Printf("cursor on new line, moving to prev char") - newCursor-- - } + // if e.Text[newCursor] == '\n' { + // newCursor-- + // } // find offset from start of line and set cls to start of line for i := newCursor; i > 0; i-- { if e.Text[i] == '\n' { - logger.Println("current line start found at ", i) cls = i break } } // if start of current line isn't set, assume current line is the first line of the document, - // so the start of the current line is at position 0 + // and manually set the offset if cls < 0 { - logger.Printf("on first line") offset = e.Cursor + 1 } else { offset = e.Cursor - cls } - logger.Printf("offset: %v", offset) - // cle is used to find length of current line (cle - cls) for i := cls + 1; i < len(e.Text); i++ { if e.Text[i] == '\n' { - logger.Println("current line end at ", i) cle = i break } @@ -195,7 +187,6 @@ func (e *Editor) MoveCursor(x, y int) { if cle > 0 { // if the end of the current line isn't set, no need to find nle for i := cle + 1; i < len(e.Text); i++ { if e.Text[i] == '\n' { - logger.Println("next line end at ", i) nle = i break } @@ -206,66 +197,96 @@ func (e *Editor) MoveCursor(x, y int) { nle = len(e.Text) } logger.Printf("Current line starts at %v. Current line ends at %v. Next line ends at %v\n", cls, cle, nle) - if cle < 0 { newCursor = len(e.Text) - // } else if nle-cle < cle-cls { // if next line is shorter than the current line - // logger.Printf("next line is shorter") - // newCursor = nle - // } else { } else if nle-cle < offset { // if next line is shorter than the offset - logger.Printf("next line is shorter, moving to end of next line") newCursor = nle } else { newCursor = cle + offset } - logger.Printf("After calc, newCursor is at: %v", newCursor) } // move cursor up y cells if y < 0 { logger.Printf("UP ARROW PRESSED") - n1 := -1 // index of previous newline character - n2 := -1 // index of the newline character before n1 - // store the position of the previous two newline characters - for j := e.Cursor; j > 0; j-- { - if e.Text[j] == '\n' { - if n1 > 0 { // if the previous newline character has already been found - n2 = j - logger.Printf("this code is getting reached! n2 = %v\n", n2) - break - } - n1 = j - } + pls, cls, cle := -1, -1, -1 // prev line start, curr line start, curr line end + offset := 1 // current offset of cursor from beginning of line + + // below might be necessary + if newCursor > len(e.Text)-1 { + logger.Printf("cursor out of bounds! cursor reset at %v", len(e.Text)) + newCursor = len(e.Text) - 1 } - if n1 < 0 && n2 < 0 { - logger.Printf("couldn't find previous newline, setting e.Cursor to 0\n") - e.Cursor = 0 - return + + // if cursor is currently on newline, 'move' it + if e.Text[newCursor] == '\n' { + logger.Printf("cursor on new line, moving to prev char") + newCursor-- } - if n2 < 0 { - n2 = 0 + + // set cls to start of line + for i := newCursor; i > 0; i-- { + if e.Text[i] == '\n' { + logger.Println("current line start found at ", i) + cls = i + break + } } - newCursor = n2 - logger.Printf("the previous two new line characters are at %v and %v\n.", n1, n2) - for k := 0; k < e.Cursor-n1; k++ { - if newCursor == n1 { + // if start of current line isn't set, assume current line is the first line of the document, + // and manually set the offset + if cls < 0 { + logger.Printf("on first line") + offset = e.Cursor + 1 + } else { + offset = e.Cursor - cls + } + + logger.Printf("offset: %v", offset) + + // cle is used to find length of current line (cle - cls) + for i := cls + 1; i < len(e.Text); i++ { + if e.Text[i] == '\n' { + logger.Println("current line end at ", i) + cle = i break } - newCursor++ } - logger.Printf("After calc, newCursor is at: %v", newCursor) - } - if newCursor < 0 { - newCursor = 0 + // pls is used to find length of previous line (cle - pls) + if cls > 0 { // only need to find pls if cls is set + for i := cls - 1; i > 0; i-- { + if e.Text[i] == '\n' { + logger.Println("next line start at ", i) + pls = i + break + } + } + } + // if start of previous line isn't set, assume previous line is start of document + if pls < 0 { + pls = 0 + offset-- + } + logger.Printf("Current line starts at %v. Current line ends at %v. Previous line starts at %v\n", cls, cle, pls) + + if cls < 0 { + newCursor = 0 + } else if cls-pls < offset { // if next line is shorter than the offset + logger.Printf("previous line is shorter, moving to end of previous line") + newCursor = cls + } else { + newCursor = pls + offset + } } + if newCursor > len(e.Text) { newCursor = len(e.Text) } - + if newCursor < 0 { + newCursor = 0 + } + logger.Println("cursor moved to ", newCursor) e.Cursor = newCursor - } // calcCursorXY calculates Cursor position from the index obtained from the content. From 725ef9d871810fff954de712cb97c21880a558a8 Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Wed, 21 Dec 2022 22:05:55 -0600 Subject: [PATCH 15/28] Change comments in MoveCursor function. --- client/editor/editor.go | 52 ++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/client/editor/editor.go b/client/editor/editor.go index e0a237e..6112243 100644 --- a/client/editor/editor.go +++ b/client/editor/editor.go @@ -150,32 +150,32 @@ func (e *Editor) MoveCursor(x, y int) { cls, cle, nle := -1, -1, -1 offset := 1 // current offset of cursor from beginning of line - //TODO: check if the below is necessary for avoiding bugs + // reset cursor if out of bounds if newCursor > len(e.Text)-1 { newCursor = len(e.Text) - 1 } - // if cursor is currently on newline, 'move' it - // if e.Text[newCursor] == '\n' { - // newCursor-- - // } + // if cursor is currently on newline, "move" it + if e.Text[newCursor] == '\n' { + newCursor-- + } - // find offset from start of line and set cls to start of line + // find the start of the line the cursor is currently on for i := newCursor; i > 0; i-- { if e.Text[i] == '\n' { cls = i break } } - // if start of current line isn't set, assume current line is the first line of the document, - // and manually set the offset + + // set the cursor offset from the start of the current line if cls < 0 { offset = e.Cursor + 1 } else { offset = e.Cursor - cls } - logger.Printf("offset: %v", offset) - // cle is used to find length of current line (cle - cls) + + // find the end of the current line for i := cls + 1; i < len(e.Text); i++ { if e.Text[i] == '\n' { cle = i @@ -183,8 +183,8 @@ func (e *Editor) MoveCursor(x, y int) { } } - // nle is used to find length of next line (nle - cle) - if cle > 0 { // if the end of the current line isn't set, no need to find nle + // find the end of the next line + if cle > 0 { // if the end of the current line isn't set, no need to find next line end for i := cle + 1; i < len(e.Text); i++ { if e.Text[i] == '\n' { nle = i @@ -192,14 +192,14 @@ func (e *Editor) MoveCursor(x, y int) { } } } - // if end of next line isn't set, assume next line is last of the document + // if end of next line isn't found, assume next line is last of the document and set cursor to end if nle < 0 { nle = len(e.Text) } logger.Printf("Current line starts at %v. Current line ends at %v. Next line ends at %v\n", cls, cle, nle) if cle < 0 { newCursor = len(e.Text) - } else if nle-cle < offset { // if next line is shorter than the offset + } else if nle-cle < offset { // if next line is shorter than the offset, set cursor to end of next line newCursor = nle } else { newCursor = cle + offset @@ -209,22 +209,22 @@ func (e *Editor) MoveCursor(x, y int) { // move cursor up y cells if y < 0 { logger.Printf("UP ARROW PRESSED") - pls, cls, cle := -1, -1, -1 // prev line start, curr line start, curr line end - offset := 1 // current offset of cursor from beginning of line + pls, cls, cle := -1, -1, -1 + offset := 1 // current offset of cursor from beginning of line - // below might be necessary + // reset cursor if out of bounds if newCursor > len(e.Text)-1 { logger.Printf("cursor out of bounds! cursor reset at %v", len(e.Text)) newCursor = len(e.Text) - 1 } - // if cursor is currently on newline, 'move' it + // if cursor is currently on newline, "move" it if e.Text[newCursor] == '\n' { logger.Printf("cursor on new line, moving to prev char") newCursor-- } - // set cls to start of line + // find the start of the line the cursor is currently on for i := newCursor; i > 0; i-- { if e.Text[i] == '\n' { logger.Println("current line start found at ", i) @@ -232,8 +232,7 @@ func (e *Editor) MoveCursor(x, y int) { break } } - // if start of current line isn't set, assume current line is the first line of the document, - // and manually set the offset + // set the cursor offset from the start of the current line if cls < 0 { logger.Printf("on first line") offset = e.Cursor + 1 @@ -241,9 +240,7 @@ func (e *Editor) MoveCursor(x, y int) { offset = e.Cursor - cls } - logger.Printf("offset: %v", offset) - - // cle is used to find length of current line (cle - cls) + // find the end of the current line for i := cls + 1; i < len(e.Text); i++ { if e.Text[i] == '\n' { logger.Println("current line end at ", i) @@ -252,7 +249,7 @@ func (e *Editor) MoveCursor(x, y int) { } } - // pls is used to find length of previous line (cle - pls) + // find the start of the previous line if cls > 0 { // only need to find pls if cls is set for i := cls - 1; i > 0; i-- { if e.Text[i] == '\n' { @@ -262,7 +259,8 @@ func (e *Editor) MoveCursor(x, y int) { } } } - // if start of previous line isn't set, assume previous line is start of document + + // if start of previous line isn't found, assume previous line is first of the document and set cursor to end if pls < 0 { pls = 0 offset-- @@ -271,7 +269,7 @@ func (e *Editor) MoveCursor(x, y int) { if cls < 0 { newCursor = 0 - } else if cls-pls < offset { // if next line is shorter than the offset + } else if cls-pls < offset { // if previous line is shorter than the offset logger.Printf("previous line is shorter, moving to end of previous line") newCursor = cls } else { From 1a9cbb0418349973c7803034be6c2c159f315037 Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Tue, 27 Dec 2022 13:55:37 -0600 Subject: [PATCH 16/28] Remove duplicate line. --- client/main.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/main.go b/client/main.go index 7c14e92..f20aa98 100644 --- a/client/main.go +++ b/client/main.go @@ -48,9 +48,6 @@ var logger *logrus.Logger // WebSocket connection. var conn *websocket.Conn -// WebSocket connection. -var conn *websocket.Conn - // termbox-based editor. var e *editor.Editor From 6a6861c7a573a2aeb56411c052b330e235781f1d Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Tue, 27 Dec 2022 14:01:21 -0600 Subject: [PATCH 17/28] Remove logging and unused variable. --- client/editor/editor.go | 41 +++++++---------------------------------- 1 file changed, 7 insertions(+), 34 deletions(-) diff --git a/client/editor/editor.go b/client/editor/editor.go index 6112243..8b9fb7e 100644 --- a/client/editor/editor.go +++ b/client/editor/editor.go @@ -2,8 +2,6 @@ package editor import ( "fmt" - "log" - "os" "time" "github.com/mattn/go-runewidth" @@ -129,24 +127,10 @@ func (e *Editor) showPositions() { // MoveCursor updates the Cursor position. func (e *Editor) MoveCursor(x, y int) { - file, err := os.OpenFile("cursor.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) - if err != nil { - fmt.Printf("Logger error, exiting: %s", err) - return - } - defer func() { - err := file.Close() - if err != nil { - log.Fatalln(err) - } - }() - logger := log.New(file, "---", log.LstdFlags) - newCursor := e.Cursor + x // move cursor down y cells if y > 0 { - logger.Printf("DOWN ARROW PRESSED") cls, cle, nle := -1, -1, -1 offset := 1 // current offset of cursor from beginning of line @@ -196,7 +180,6 @@ func (e *Editor) MoveCursor(x, y int) { if nle < 0 { nle = len(e.Text) } - logger.Printf("Current line starts at %v. Current line ends at %v. Next line ends at %v\n", cls, cle, nle) if cle < 0 { newCursor = len(e.Text) } else if nle-cle < offset { // if next line is shorter than the offset, set cursor to end of next line @@ -208,52 +191,45 @@ func (e *Editor) MoveCursor(x, y int) { // move cursor up y cells if y < 0 { - logger.Printf("UP ARROW PRESSED") - pls, cls, cle := -1, -1, -1 + pls, cls := -1, -1 offset := 1 // current offset of cursor from beginning of line // reset cursor if out of bounds if newCursor > len(e.Text)-1 { - logger.Printf("cursor out of bounds! cursor reset at %v", len(e.Text)) newCursor = len(e.Text) - 1 } // if cursor is currently on newline, "move" it if e.Text[newCursor] == '\n' { - logger.Printf("cursor on new line, moving to prev char") newCursor-- } // find the start of the line the cursor is currently on for i := newCursor; i > 0; i-- { if e.Text[i] == '\n' { - logger.Println("current line start found at ", i) cls = i break } } // set the cursor offset from the start of the current line if cls < 0 { - logger.Printf("on first line") offset = e.Cursor + 1 } else { offset = e.Cursor - cls } // find the end of the current line - for i := cls + 1; i < len(e.Text); i++ { - if e.Text[i] == '\n' { - logger.Println("current line end at ", i) - cle = i - break - } - } + // for i := cls + 1; i < len(e.Text); i++ { + // if e.Text[i] == '\n' { + // cle = i + // break + // } + // } // find the start of the previous line if cls > 0 { // only need to find pls if cls is set for i := cls - 1; i > 0; i-- { if e.Text[i] == '\n' { - logger.Println("next line start at ", i) pls = i break } @@ -265,12 +241,10 @@ func (e *Editor) MoveCursor(x, y int) { pls = 0 offset-- } - logger.Printf("Current line starts at %v. Current line ends at %v. Previous line starts at %v\n", cls, cle, pls) if cls < 0 { newCursor = 0 } else if cls-pls < offset { // if previous line is shorter than the offset - logger.Printf("previous line is shorter, moving to end of previous line") newCursor = cls } else { newCursor = pls + offset @@ -283,7 +257,6 @@ func (e *Editor) MoveCursor(x, y int) { if newCursor < 0 { newCursor = 0 } - logger.Println("cursor moved to ", newCursor) e.Cursor = newCursor } From 9140b36db14b1b0f2facf60db6e42c541f976441 Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Tue, 27 Dec 2022 15:14:15 -0600 Subject: [PATCH 18/28] Refactor MoveCursor into smaller functions. --- client/editor/editor.go | 208 ++++++++++++++++++++-------------------- 1 file changed, 104 insertions(+), 104 deletions(-) diff --git a/client/editor/editor.go b/client/editor/editor.go index 8b9fb7e..f7c0dd8 100644 --- a/client/editor/editor.go +++ b/client/editor/editor.go @@ -127,137 +127,137 @@ func (e *Editor) showPositions() { // MoveCursor updates the Cursor position. func (e *Editor) MoveCursor(x, y int) { + // move cursor horizontally newCursor := e.Cursor + x - // move cursor down y cells + // move cursor vertically if y > 0 { - cls, cle, nle := -1, -1, -1 - offset := 1 // current offset of cursor from beginning of line + newCursor = e.calcCursorDown(y) + } - // reset cursor if out of bounds - if newCursor > len(e.Text)-1 { - newCursor = len(e.Text) - 1 - } + if y < 0 { + newCursor = e.calcCursorUp(y) + } - // if cursor is currently on newline, "move" it - if e.Text[newCursor] == '\n' { - newCursor-- - } + if newCursor > len(e.Text) { + newCursor = len(e.Text) + } - // find the start of the line the cursor is currently on - for i := newCursor; i > 0; i-- { - if e.Text[i] == '\n' { - cls = i - break - } - } + if newCursor < 0 { + newCursor = 0 + } - // set the cursor offset from the start of the current line - if cls < 0 { - offset = e.Cursor + 1 - } else { - offset = e.Cursor - cls + e.Cursor = newCursor +} + +func (e *Editor) calcCursorUp(y int) int { + pos := e.Cursor + // reset cursor if out of bounds + if pos > len(e.Text)-1 { + pos = len(e.Text) - 1 + } + + // if cursor is currently on newline, "move" it + if e.Text[pos] == '\n' { + pos-- + } + + cls, offset := -1, 1 + // find the start of the line the cursor is currently on + for i := pos; i > 0; i-- { + if e.Text[i] == '\n' { + cls = i + break } + } + // set the cursor offset from the start of the current line + if cls < 0 { + offset = e.Cursor + 1 + } else { + offset = e.Cursor - cls + } - // find the end of the current line - for i := cls + 1; i < len(e.Text); i++ { + pls := -1 + // find the start of the previous line + if cls > 0 { // only need to find pls if cls is set + for i := cls - 1; i > 0; i-- { if e.Text[i] == '\n' { - cle = i + pls = i break } } + } + // if start of previous line isn't found, assume previous line is first of the document and set cursor to end + if pls < 0 { + pls = 0 + offset-- + } - // find the end of the next line - if cle > 0 { // if the end of the current line isn't set, no need to find next line end - for i := cle + 1; i < len(e.Text); i++ { - if e.Text[i] == '\n' { - nle = i - break - } - } - } - // if end of next line isn't found, assume next line is last of the document and set cursor to end - if nle < 0 { - nle = len(e.Text) - } - if cle < 0 { - newCursor = len(e.Text) - } else if nle-cle < offset { // if next line is shorter than the offset, set cursor to end of next line - newCursor = nle - } else { - newCursor = cle + offset - } + if cls < 0 { + return 0 + } else if cls-pls < offset { // if previous line is shorter than the offset + return cls + } else { + return pls + offset } +} - // move cursor up y cells - if y < 0 { - pls, cls := -1, -1 - offset := 1 // current offset of cursor from beginning of line +func (e *Editor) calcCursorDown(y int) int { + pos := e.Cursor + // reset cursor if out of bounds + if pos > len(e.Text)-1 { + pos = len(e.Text) - 1 + } - // reset cursor if out of bounds - if newCursor > len(e.Text)-1 { - newCursor = len(e.Text) - 1 - } + // if cursor is currently on newline, "move" it + if e.Text[pos] == '\n' { + pos-- + } - // if cursor is currently on newline, "move" it - if e.Text[newCursor] == '\n' { - newCursor-- + cls, offset := -1, 1 + // find the start of the line the cursor is currently on + for i := pos; i > 0; i-- { + if e.Text[i] == '\n' { + cls = i + break } + } + // set the cursor offset from the start of the current line + if cls < 0 { + offset = e.Cursor + 1 + } else { + offset = e.Cursor - cls + } - // find the start of the line the cursor is currently on - for i := newCursor; i > 0; i-- { + cle, nle := -1, -1 + // find the end of the current line + for i := cls + 1; i < len(e.Text); i++ { + if e.Text[i] == '\n' { + cle = i + break + } + } + // find the end of the next line + if cle > 0 { // if the end of the current line isn't set, no need to find next line end + for i := cle + 1; i < len(e.Text); i++ { if e.Text[i] == '\n' { - cls = i + nle = i break } } - // set the cursor offset from the start of the current line - if cls < 0 { - offset = e.Cursor + 1 - } else { - offset = e.Cursor - cls - } - - // find the end of the current line - // for i := cls + 1; i < len(e.Text); i++ { - // if e.Text[i] == '\n' { - // cle = i - // break - // } - // } - - // find the start of the previous line - if cls > 0 { // only need to find pls if cls is set - for i := cls - 1; i > 0; i-- { - if e.Text[i] == '\n' { - pls = i - break - } - } - } - - // if start of previous line isn't found, assume previous line is first of the document and set cursor to end - if pls < 0 { - pls = 0 - offset-- - } - - if cls < 0 { - newCursor = 0 - } else if cls-pls < offset { // if previous line is shorter than the offset - newCursor = cls - } else { - newCursor = pls + offset - } } - - if newCursor > len(e.Text) { - newCursor = len(e.Text) + // if end of next line isn't found, assume next line is last of the document and set cursor to end + if nle < 0 { + nle = len(e.Text) } - if newCursor < 0 { - newCursor = 0 + + if cle < 0 { + return len(e.Text) + } else if nle-cle < offset { // if next line is shorter than the offset, set cursor to end of next line + return nle + } else { // default + return cle + offset } - e.Cursor = newCursor } // calcCursorXY calculates Cursor position from the index obtained from the content. From 629c76305453eb18fe297396b81a18a621091479 Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Wed, 28 Dec 2022 10:52:48 -0600 Subject: [PATCH 19/28] Fix unused parameter and duplicate initialization. --- client/editor/editor.go | 16 ++++++++-------- client/main.go | 10 ---------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/client/editor/editor.go b/client/editor/editor.go index f7c0dd8..98f4c1f 100644 --- a/client/editor/editor.go +++ b/client/editor/editor.go @@ -132,11 +132,11 @@ func (e *Editor) MoveCursor(x, y int) { // move cursor vertically if y > 0 { - newCursor = e.calcCursorDown(y) + newCursor = e.calcCursorDown() } if y < 0 { - newCursor = e.calcCursorUp(y) + newCursor = e.calcCursorUp() } if newCursor > len(e.Text) { @@ -150,7 +150,7 @@ func (e *Editor) MoveCursor(x, y int) { e.Cursor = newCursor } -func (e *Editor) calcCursorUp(y int) int { +func (e *Editor) calcCursorUp() int { pos := e.Cursor // reset cursor if out of bounds if pos > len(e.Text)-1 { @@ -179,7 +179,7 @@ func (e *Editor) calcCursorUp(y int) int { pls := -1 // find the start of the previous line - if cls > 0 { // only need to find pls if cls is set + if cls > 0 { // no need to find previous line start if current line start doesn't exist for i := cls - 1; i > 0; i-- { if e.Text[i] == '\n' { pls = i @@ -195,14 +195,14 @@ func (e *Editor) calcCursorUp(y int) int { if cls < 0 { return 0 - } else if cls-pls < offset { // if previous line is shorter than the offset + } else if cls-pls < offset { // if previous line is shorter than the offset, set cursor to start of current line return cls - } else { + } else { // default return pls + offset } } -func (e *Editor) calcCursorDown(y int) int { +func (e *Editor) calcCursorDown() int { pos := e.Cursor // reset cursor if out of bounds if pos > len(e.Text)-1 { @@ -238,7 +238,7 @@ func (e *Editor) calcCursorDown(y int) int { } } // find the end of the next line - if cle > 0 { // if the end of the current line isn't set, no need to find next line end + if cle > 0 { // no need to find next line end if the end of the current line doesn't exist for i := cle + 1; i < len(e.Text); i++ { if e.Text[i] == '\n' { nle = i diff --git a/client/main.go b/client/main.go index f20aa98..6c24fa2 100644 --- a/client/main.go +++ b/client/main.go @@ -84,16 +84,6 @@ func main() { name = randomdata.SillyName() } - // Read username based if login flag is set to true, otherwise generate a random name. - if *login { - fmt.Print("Enter your name: ") - s = bufio.NewScanner(os.Stdin) - s.Scan() - name = s.Text() - } else { - name = randomdata.SillyName() - } - // Get WebSocket connection. dialer := websocket.Dialer{ HandshakeTimeout: 2 * time.Minute, From 559248d51e9fa2a15b926ef564fdabd52b081c8c Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Wed, 28 Dec 2022 10:58:02 -0600 Subject: [PATCH 20/28] Fix ineffassign linting error. --- client/editor/editor.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/editor/editor.go b/client/editor/editor.go index 98f4c1f..92955f0 100644 --- a/client/editor/editor.go +++ b/client/editor/editor.go @@ -162,7 +162,7 @@ func (e *Editor) calcCursorUp() int { pos-- } - cls, offset := -1, 1 + cls := -1 // find the start of the line the cursor is currently on for i := pos; i > 0; i-- { if e.Text[i] == '\n' { @@ -170,6 +170,7 @@ func (e *Editor) calcCursorUp() int { break } } + var offset int // set the cursor offset from the start of the current line if cls < 0 { offset = e.Cursor + 1 @@ -214,7 +215,7 @@ func (e *Editor) calcCursorDown() int { pos-- } - cls, offset := -1, 1 + cls := -1 // find the start of the line the cursor is currently on for i := pos; i > 0; i-- { if e.Text[i] == '\n' { @@ -222,6 +223,7 @@ func (e *Editor) calcCursorDown() int { break } } + var offset int // set the cursor offset from the start of the current line if cls < 0 { offset = e.Cursor + 1 From e425b8595e286b15ded15e424a71573ffe8a9e7e Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Fri, 30 Dec 2022 02:04:22 -0600 Subject: [PATCH 21/28] Fix panics occurring with empty document. Fixed a couple of crashes that happened due to out of bounds errors if the document was empty. MoveCursor would try to do some invalid calculations which are now avoided by checking the text length first. Also, on delete, the cursor was getting reset to 1 instead of 0, which caused a crash when characters were inserted. --- client/editor/editor.go | 3 +++ client/main.go | 5 +---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/editor/editor.go b/client/editor/editor.go index 92955f0..6bddcba 100644 --- a/client/editor/editor.go +++ b/client/editor/editor.go @@ -127,6 +127,9 @@ func (e *Editor) showPositions() { // MoveCursor updates the Cursor position. func (e *Editor) MoveCursor(x, y int) { + if len(e.Text) == 0 { + return + } // move cursor horizontally newCursor := e.Cursor + x diff --git a/client/main.go b/client/main.go index 6c24fa2..881fbdf 100644 --- a/client/main.go +++ b/client/main.go @@ -357,7 +357,7 @@ func performOperation(opType int, ev termbox.Event, conn *websocket.Conn) { case OperationDelete: logger.Infof("LOCAL DELETE: cursor position %v\n", e.Cursor) if e.Cursor-1 <= 0 { - e.Cursor = 1 + e.Cursor = 0 } text := doc.Delete(e.Cursor) e.SetText(text) @@ -365,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!" From 3527b76196c0d62119b3d7f454a403d7a9dbf7b5 Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Wed, 11 Jan 2023 21:53:32 -0600 Subject: [PATCH 22/28] (WIP) Add tests and fix some cursor movement bugs. Added many tests for cursor movement. Still in the process of fixing the bugs found in the tests. Also temporarily added logging to help with bug fixing. --- client/editor/editor.go | 54 ++++++++++++++++++++++++-- client/editor/editor_test.go | 74 +++++++++++++++++++++++++++++++++--- 2 files changed, 118 insertions(+), 10 deletions(-) diff --git a/client/editor/editor.go b/client/editor/editor.go index 6bddcba..291624b 100644 --- a/client/editor/editor.go +++ b/client/editor/editor.go @@ -2,6 +2,8 @@ package editor import ( "fmt" + "log" + "os" "time" "github.com/mattn/go-runewidth" @@ -130,10 +132,10 @@ func (e *Editor) MoveCursor(x, y int) { if len(e.Text) == 0 { return } - // move cursor horizontally + // Move cursor horizontally. newCursor := e.Cursor + x - // move cursor vertically + // Move cursor vertically. if y > 0 { newCursor = e.calcCursorDown() } @@ -142,6 +144,7 @@ func (e *Editor) MoveCursor(x, y int) { newCursor = e.calcCursorUp() } + // Reset to bounds. if newCursor > len(e.Text) { newCursor = len(e.Text) } @@ -153,16 +156,51 @@ func (e *Editor) MoveCursor(x, y int) { e.Cursor = newCursor } +// For the functions calcCursorUp and calcCursorDown, variables like cls (current line start), +// pls (previous line start), and nle (next line end) hold the index of the '\n' characters that separate +// each line. These variables are used to calculate the length of the line to move to. If these variables +// remain at -1, it's assumed that the start or end of the document was found instead of a '\n' character. +// The variable offset is used to calculate the number of characters between the start of the current line +// and the cursor, which should be kept constant as you move between lines. + +// calcCursorUp calculates the new position of the cursor after moving one line up. func (e *Editor) calcCursorUp() int { + file, err := os.OpenFile("cursor.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + fmt.Printf("Logger error, exiting: %s", err) + } + defer func() { + err := file.Close() + if err != nil { + log.Fatalln(err) + } + }() + logger := log.New(file, "---", log.LstdFlags) + pos := e.Cursor + logger.Printf("MOVING UP: initial position: %v\n", pos) // reset cursor if out of bounds if pos > len(e.Text)-1 { pos = len(e.Text) - 1 + if e.Text[pos] == '\n' && e.Text[pos-1] == '\n' { // this covers the case where the previous line is blank + logger.Printf("prev char is new line, setting position to %v\n", pos) + return pos + } } // if cursor is currently on newline, "move" it if e.Text[pos] == '\n' { + logger.Printf("On newline: moving one space back\n") pos-- + if pos < 1 { + logger.Printf("set position to 0") + return 0 + } + if e.Text[pos] == '\n' && e.Text[pos-1] == '\n' { // this covers the case where the previous line is blank + logger.Printf("prev char is new line, setting position to %v\n", pos) + return pos + } + } cls := -1 @@ -173,6 +211,11 @@ func (e *Editor) calcCursorUp() int { break } } + // if pos == len(e.Text) { + // logger.Printf("at end of text, setting cursor to %v\n", cls) + // return cls + // } + logger.Printf("cls is set to %v\n", cls) var offset int // set the cursor offset from the start of the current line if cls < 0 { @@ -180,7 +223,7 @@ func (e *Editor) calcCursorUp() int { } else { offset = e.Cursor - cls } - + logger.Printf("offset is set to %v\n", offset) pls := -1 // find the start of the previous line if cls > 0 { // no need to find previous line start if current line start doesn't exist @@ -196,16 +239,19 @@ func (e *Editor) calcCursorUp() int { pls = 0 offset-- } - + logger.Printf("pls is set to %v\n", pls) if cls < 0 { return 0 } else if cls-pls < offset { // if previous line is shorter than the offset, set cursor to start of current line + logger.Printf("pos is cls (%v)\n", cls) return cls } else { // default + logger.Printf("pos is pls (%v) + offset (%v)\n", pls, offset) return pls + offset } } +// calcCursorDown calculates the new position of the cursor after moving one line down. func (e *Editor) calcCursorDown() int { pos := e.Cursor // reset cursor if out of bounds diff --git a/client/editor/editor_test.go b/client/editor/editor_test.go index 1ec1fcf..2ef4d99 100644 --- a/client/editor/editor_test.go +++ b/client/editor/editor_test.go @@ -65,20 +65,82 @@ 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")}, + {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 to empty line)", cursor: 3, y: -1, expectedCursor: 2, + text: []rune("\n\n\n")}, + {description: "move down (from empty first line)", cursor: 0, y: 1, expectedCursor: 1, + text: []rune("\nfoo\n\n")}, + {description: "move up (from empty last line)", cursor: 6, y: -1, expectedCursor: 2, + text: []rune("\n\nfoo\n")}, + {description: "move down (from first line to empty line)", cursor: 2, y: 1, expectedCursor: 4, + text: []rune("foo\n\n")}, + {description: "move up (from last line to empty line)", cursor: 2, y: -1, expectedCursor: 1, + text: []rune("\n\nfoo")}, } 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 From df2efe3f401555e03fba2e6156159e89131a2e55 Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Mon, 13 Feb 2023 19:03:01 -0600 Subject: [PATCH 23/28] Upward cursor movement now passes tests. Rewrote calcCursor up with slightly different logic. It is simpler and cleaner than the previous attempts. Further refinement might be possible. --- client/editor/editor.go | 233 +++++++++++++++++++++++++++++----------- 1 file changed, 170 insertions(+), 63 deletions(-) diff --git a/client/editor/editor.go b/client/editor/editor.go index 291624b..f2be4c3 100644 --- a/client/editor/editor.go +++ b/client/editor/editor.go @@ -160,10 +160,100 @@ func (e *Editor) MoveCursor(x, y int) { // pls (previous line start), and nle (next line end) hold the index of the '\n' characters that separate // each line. These variables are used to calculate the length of the line to move to. If these variables // remain at -1, it's assumed that the start or end of the document was found instead of a '\n' character. -// The variable offset is used to calculate the number of characters between the start of the current line +// The offset variable is used to calculate the number of characters between the start of the current line // and the cursor, which should be kept constant as you move between lines. // calcCursorUp calculates the new position of the cursor after moving one line up. +// func (e *Editor) calcCursorUp() int { +// file, err := os.OpenFile("cursor.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) +// if err != nil { +// fmt.Printf("Logger error, exiting: %s", err) +// } +// defer func() { +// err := file.Close() +// if err != nil { +// log.Fatalln(err) +// } +// }() +// logger := log.New(file, "---", log.LstdFlags) + +// pos := e.Cursor +// logger.Printf("MOVING UP: initial position: %v\n", pos) +// // reset cursor if out of bounds +// if pos >= len(e.Text) { +// pos = len(e.Text) - 1 +// // if e.Text[pos] == '\n' && e.Text[pos-1] == '\n' { // this covers the case where the previous line is blank +// // logger.Printf("prev char is new line, setting position to %v\n", pos) +// // return pos +// // } +// } + +// // if cursor is currently on newline, "move" it +// if e.Text[pos] == '\n' { +// logger.Printf("On newline: moving one space back\n") +// pos-- +// if pos < 1 { +// logger.Printf("set position to 0") +// return 0 +// } +// // if e.Text[pos] == '\n' && e.Text[pos-1] == '\n' { // this covers the case where the previous line is blank +// // logger.Printf("prev char is new line, setting position to %v\n", pos) +// // return pos +// // } +// } + +// cls := -1 +// // find the start of the line the cursor is currently on +// for i := pos; i >= 0; i-- { +// if e.Text[i] == '\n' { +// cls = i +// break +// } +// } +// // if pos == len(e.Text) { +// // logger.Printf("at end of text, setting cursor to %v\n", cls) +// // return cls +// // } +// logger.Printf("cls is set to %v\n", cls) +// var offset int +// // set the cursor offset from the start of the current line +// if cls < 0 { +// offset = e.Cursor + 1 +// } else { +// offset = e.Cursor - cls +// } +// logger.Printf("offset is set to %v\n", offset) +// pls := -1 +// // find the start of the previous line +// if cls > 0 { // no need to find previous line start if current line start doesn't exist +// for i := cls - 1; i >= 0; i-- { +// if e.Text[i] == '\n' { +// pls = i +// break +// } +// } +// } +// // if start of previous line isn't found, assume previous line is first of the document and set cursor to end +// if pls < 0 { +// pls = 0 +// offset-- +// } +// logger.Printf("pls is set to %v\n", pls) + +// if cls < 0 { +// return 0 +// } else if cls-pls < offset { // if previous line is shorter than the offset, set cursor to start of current line +// logger.Printf("pos is cls (%v)\n", cls) +// return cls +// } else if pos == len(e.Text)-1 { +// return cls + offset +// } else { // default +// logger.Printf("pos is pls (%v) + offset (%v)\n", pls, offset) +// return pls + offset +// } +// } + +// chatGPT func (e *Editor) calcCursorUp() int { file, err := os.OpenFile("cursor.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) if err != nil { @@ -177,80 +267,97 @@ func (e *Editor) calcCursorUp() int { }() logger := log.New(file, "---", log.LstdFlags) + logger.Println("MOVING UP") + logger.Printf("Cursor at %v", e.Cursor) + pos := e.Cursor - logger.Printf("MOVING UP: initial position: %v\n", pos) - // reset cursor if out of bounds - if pos > len(e.Text)-1 { - pos = len(e.Text) - 1 - if e.Text[pos] == '\n' && e.Text[pos-1] == '\n' { // this covers the case where the previous line is blank - logger.Printf("prev char is new line, setting position to %v\n", pos) - return pos - } - } + offset := 0 - // if cursor is currently on newline, "move" it - if e.Text[pos] == '\n' { - logger.Printf("On newline: moving one space back\n") + if pos == len(e.Text) || e.Text[pos] == '\n' { + logger.Println("cursor out of bounds or on newline, moving back one") + offset++ pos-- - if pos < 1 { - logger.Printf("set position to 0") - return 0 - } - if e.Text[pos] == '\n' && e.Text[pos-1] == '\n' { // this covers the case where the previous line is blank - logger.Printf("prev char is new line, setting position to %v\n", pos) - return pos - } - } - cls := -1 - // find the start of the line the cursor is currently on - for i := pos; i > 0; i-- { - if e.Text[i] == '\n' { - cls = i - break - } + start := pos + end := pos + + // Find the end of the current line + for end < len(e.Text) && e.Text[end] != '\n' { + end++ } - // if pos == len(e.Text) { - // logger.Printf("at end of text, setting cursor to %v\n", cls) - // return cls - // } - logger.Printf("cls is set to %v\n", cls) - var offset int - // set the cursor offset from the start of the current line - if cls < 0 { - offset = e.Cursor + 1 - } else { - offset = e.Cursor - cls + + // Find the start of the current line + for start > 0 && e.Text[start] != '\n' { + start-- } - logger.Printf("offset is set to %v\n", offset) - pls := -1 - // find the start of the previous line - if cls > 0 { // no need to find previous line start if current line start doesn't exist - for i := cls - 1; i > 0; i-- { - if e.Text[i] == '\n' { - pls = i - break - } - } + // start++ + + logger.Printf("Found start of current line at %v and end of current line at %v\n", start, end) + + // Check if the cursor is at the first line + if start == 0 { + return 0 } - // if start of previous line isn't found, assume previous line is first of the document and set cursor to end - if pls < 0 { - pls = 0 - offset-- + + // Find the start of the previous line + prevStart := start - 1 + for prevStart >= 0 && e.Text[prevStart] != '\n' { + prevStart-- } - logger.Printf("pls is set to %v\n", pls) - if cls < 0 { - return 0 - } else if cls-pls < offset { // if previous line is shorter than the offset, set cursor to start of current line - logger.Printf("pos is cls (%v)\n", cls) - return cls - } else { // default - logger.Printf("pos is pls (%v) + offset (%v)\n", pls, offset) - return pls + offset + + logger.Printf("Found start of previous line at %v \n", prevStart) + + // Calculate the cursor position in the previous line + offset += pos - start + logger.Printf("offset set at %v", offset) + logger.Printf("length of line is %v", end-start) + if offset <= start-prevStart { + logger.Printf("offset is less than the length of the previous line, placing cursor at %v", prevStart+offset+1) + return prevStart + offset + } else { + logger.Printf("offset is greater than the length of the previous line, placing cursor at %v", prevStart+1) + return start } } +// func (e *Editor) calcCursorUp() int { +// pos := e.Cursor + +// if e.Text[pos] == '\n' { +// pos-- +// } + +// cls, cle := -1, -1 +// for i := pos; i > 0; i-- { +// if e.Text[i] == '\n' { + +// cls = i + +// } + +// } + +// pls := -1 +// for i := cls - 1; i > 0; i-- { +// if e.Text[i] == '\n' { +// pls = i +// } +// } +// offset := 0 +// if cls >= 0 { +// offset = e.Cursor - cls +// } + +// // if cls < 0 { +// // return 0 +// // } else if cls-pls < offset { // if previous line is shorter than the offset, set cursor to start of current line +// // return cls +// // } else { // default +// // return pls + offset +// // } +// } + // calcCursorDown calculates the new position of the cursor after moving one line down. func (e *Editor) calcCursorDown() int { pos := e.Cursor From ac51f10325c7bf396f9eeaa25dbddc030b8796f7 Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Mon, 13 Feb 2023 19:16:34 -0600 Subject: [PATCH 24/28] Remove old functions and extraneous comments. --- client/editor/editor.go | 159 +--------------------------------------- 1 file changed, 2 insertions(+), 157 deletions(-) diff --git a/client/editor/editor.go b/client/editor/editor.go index f2be4c3..2d75fa5 100644 --- a/client/editor/editor.go +++ b/client/editor/editor.go @@ -2,8 +2,6 @@ package editor import ( "fmt" - "log" - "os" "time" "github.com/mattn/go-runewidth" @@ -163,124 +161,17 @@ func (e *Editor) MoveCursor(x, y int) { // The offset variable is used to calculate the number of characters between the start of the current line // and the cursor, which should be kept constant as you move between lines. -// calcCursorUp calculates the new position of the cursor after moving one line up. -// func (e *Editor) calcCursorUp() int { -// file, err := os.OpenFile("cursor.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) -// if err != nil { -// fmt.Printf("Logger error, exiting: %s", err) -// } -// defer func() { -// err := file.Close() -// if err != nil { -// log.Fatalln(err) -// } -// }() -// logger := log.New(file, "---", log.LstdFlags) - -// pos := e.Cursor -// logger.Printf("MOVING UP: initial position: %v\n", pos) -// // reset cursor if out of bounds -// if pos >= len(e.Text) { -// pos = len(e.Text) - 1 -// // if e.Text[pos] == '\n' && e.Text[pos-1] == '\n' { // this covers the case where the previous line is blank -// // logger.Printf("prev char is new line, setting position to %v\n", pos) -// // return pos -// // } -// } - -// // if cursor is currently on newline, "move" it -// if e.Text[pos] == '\n' { -// logger.Printf("On newline: moving one space back\n") -// pos-- -// if pos < 1 { -// logger.Printf("set position to 0") -// return 0 -// } -// // if e.Text[pos] == '\n' && e.Text[pos-1] == '\n' { // this covers the case where the previous line is blank -// // logger.Printf("prev char is new line, setting position to %v\n", pos) -// // return pos -// // } -// } - -// cls := -1 -// // find the start of the line the cursor is currently on -// for i := pos; i >= 0; i-- { -// if e.Text[i] == '\n' { -// cls = i -// break -// } -// } -// // if pos == len(e.Text) { -// // logger.Printf("at end of text, setting cursor to %v\n", cls) -// // return cls -// // } -// logger.Printf("cls is set to %v\n", cls) -// var offset int -// // set the cursor offset from the start of the current line -// if cls < 0 { -// offset = e.Cursor + 1 -// } else { -// offset = e.Cursor - cls -// } -// logger.Printf("offset is set to %v\n", offset) -// pls := -1 -// // find the start of the previous line -// if cls > 0 { // no need to find previous line start if current line start doesn't exist -// for i := cls - 1; i >= 0; i-- { -// if e.Text[i] == '\n' { -// pls = i -// break -// } -// } -// } -// // if start of previous line isn't found, assume previous line is first of the document and set cursor to end -// if pls < 0 { -// pls = 0 -// offset-- -// } -// logger.Printf("pls is set to %v\n", pls) - -// if cls < 0 { -// return 0 -// } else if cls-pls < offset { // if previous line is shorter than the offset, set cursor to start of current line -// logger.Printf("pos is cls (%v)\n", cls) -// return cls -// } else if pos == len(e.Text)-1 { -// return cls + offset -// } else { // default -// logger.Printf("pos is pls (%v) + offset (%v)\n", pls, offset) -// return pls + offset -// } -// } - -// chatGPT +// calcCursorUp func (e *Editor) calcCursorUp() int { - file, err := os.OpenFile("cursor.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) - if err != nil { - fmt.Printf("Logger error, exiting: %s", err) - } - defer func() { - err := file.Close() - if err != nil { - log.Fatalln(err) - } - }() - logger := log.New(file, "---", log.LstdFlags) - - logger.Println("MOVING UP") - logger.Printf("Cursor at %v", e.Cursor) - pos := e.Cursor offset := 0 if pos == len(e.Text) || e.Text[pos] == '\n' { - logger.Println("cursor out of bounds or on newline, moving back one") offset++ pos-- } - start := pos - end := pos + start, end := pos, pos // Find the end of the current line for end < len(e.Text) && e.Text[end] != '\n' { @@ -291,9 +182,6 @@ func (e *Editor) calcCursorUp() int { for start > 0 && e.Text[start] != '\n' { start-- } - // start++ - - logger.Printf("Found start of current line at %v and end of current line at %v\n", start, end) // Check if the cursor is at the first line if start == 0 { @@ -306,58 +194,15 @@ func (e *Editor) calcCursorUp() int { prevStart-- } - logger.Printf("Found start of previous line at %v \n", prevStart) - // Calculate the cursor position in the previous line offset += pos - start - logger.Printf("offset set at %v", offset) - logger.Printf("length of line is %v", end-start) if offset <= start-prevStart { - logger.Printf("offset is less than the length of the previous line, placing cursor at %v", prevStart+offset+1) return prevStart + offset } else { - logger.Printf("offset is greater than the length of the previous line, placing cursor at %v", prevStart+1) return start } } -// func (e *Editor) calcCursorUp() int { -// pos := e.Cursor - -// if e.Text[pos] == '\n' { -// pos-- -// } - -// cls, cle := -1, -1 -// for i := pos; i > 0; i-- { -// if e.Text[i] == '\n' { - -// cls = i - -// } - -// } - -// pls := -1 -// for i := cls - 1; i > 0; i-- { -// if e.Text[i] == '\n' { -// pls = i -// } -// } -// offset := 0 -// if cls >= 0 { -// offset = e.Cursor - cls -// } - -// // if cls < 0 { -// // return 0 -// // } else if cls-pls < offset { // if previous line is shorter than the offset, set cursor to start of current line -// // return cls -// // } else { // default -// // return pls + offset -// // } -// } - // calcCursorDown calculates the new position of the cursor after moving one line down. func (e *Editor) calcCursorDown() int { pos := e.Cursor From 4003bf9166181300e71585c99a4046191e4f86cf Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Tue, 14 Feb 2023 01:35:02 -0600 Subject: [PATCH 25/28] Refactor downward cursor movement. Changed calcCursorDown to match calcCursorUp logic. Both functions now pass tests. --- client/editor/editor.go | 74 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/client/editor/editor.go b/client/editor/editor.go index 2d75fa5..f252576 100644 --- a/client/editor/editor.go +++ b/client/editor/editor.go @@ -2,6 +2,8 @@ package editor import ( "fmt" + "log" + "os" "time" "github.com/mattn/go-runewidth" @@ -203,8 +205,78 @@ func (e *Editor) calcCursorUp() int { } } -// calcCursorDown calculates the new position of the cursor after moving one line down. func (e *Editor) calcCursorDown() int { + file, err := os.OpenFile("cursor.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + fmt.Printf("Logger error, exiting: %s", err) + } + defer func() { + err := file.Close() + if err != nil { + log.Fatalln(err) + } + }() + logger := log.New(file, "---", log.LstdFlags) + + logger.Printf("MOVING DOWN") + pos := e.Cursor + offset := 0 + + logger.Printf("cursor at %v", pos) + if pos == len(e.Text) || e.Text[pos] == '\n' { + offset++ + pos-- + } + + if pos < 0 { + pos = 0 + } + + start, end := pos, pos + + // Find the end of the current line + for end < len(e.Text) && e.Text[end] != '\n' { + end++ + } + + // Find the start of the current line + for start > 0 && e.Text[start] != '\n' { + start-- + } + + if start == 0 && e.Text[start] != '\n' { + offset++ + } + + // Check if the cursor is at the first line + 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++ + } + + logger.Printf("start at %v, end at %v", start, end) + + logger.Printf("next ends at %v", nextEnd) + // Calculate the cursor position in the current line + offset += pos - start + logger.Printf("offset at %v", offset) + logger.Printf("next line length is %v", nextEnd-end) + if offset < nextEnd-end { + logger.Printf("offset is less than length of next line, cursor at %v", end+offset) + return end + offset + } else { + logger.Printf("setting cursor at end of next line") + return nextEnd + } +} + +// calcCursorDown calculates the new position of the cursor after moving one line down. +func (e *Editor) calcCursorDown2() int { pos := e.Cursor // reset cursor if out of bounds if pos > len(e.Text)-1 { From 1bb049ce957c1b68ab39752f9bed4f4e9a944a2d Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Tue, 14 Feb 2023 15:46:32 -0600 Subject: [PATCH 26/28] Add test cases. --- client/editor/editor_test.go | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/client/editor/editor_test.go b/client/editor/editor_test.go index 2ef4d99..86bc67e 100644 --- a/client/editor/editor_test.go +++ b/client/editor/editor_test.go @@ -120,19 +120,29 @@ func TestMoveCursor(t *testing.T) { {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")}, - {description: "move down (from empty first line to empty line)", cursor: 0, y: 1, expectedCursor: 1, - text: []rune("\n\n\n")}, + 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 last line)", cursor: 6, y: -1, expectedCursor: 2, + {description: "move up (from empty first line)", cursor: 0, y: -1, expectedCursor: 0, text: []rune("\n\nfoo\n")}, - {description: "move down (from first line to empty line)", cursor: 2, y: 1, expectedCursor: 4, - text: []rune("foo\n\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() From baca5fe43f25f1bb8a844bd979f6b9af9fb7a87c Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Tue, 14 Feb 2023 16:38:26 -0600 Subject: [PATCH 27/28] Fix cursor movement bugs and add/update comments. Fixed a couple more edge cases with the new cursor movement logic, including some cases where panics occurred. Behavior now seems to match expectations. Also updated comments for both calcCursorUp and calcCursorDown. --- client/editor/editor.go | 147 +++++++++++----------------------------- 1 file changed, 39 insertions(+), 108 deletions(-) diff --git a/client/editor/editor.go b/client/editor/editor.go index f252576..e767f25 100644 --- a/client/editor/editor.go +++ b/client/editor/editor.go @@ -2,8 +2,6 @@ package editor import ( "fmt" - "log" - "os" "time" "github.com/mattn/go-runewidth" @@ -156,47 +154,52 @@ func (e *Editor) MoveCursor(x, y int) { e.Cursor = newCursor } -// For the functions calcCursorUp and calcCursorDown, variables like cls (current line start), -// pls (previous line start), and nle (next line end) hold the index of the '\n' characters that separate -// each line. These variables are used to calculate the length of the line to move to. If these variables -// remain at -1, it's assumed that the start or end of the document was found instead of a '\n' character. -// The offset variable is used to calculate the number of characters between the start of the current line -// and the cursor, which should be kept constant as you move between lines. +// 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 +// 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-- } - start, end := pos, pos - - // Find the end of the current line - for end < len(e.Text) && e.Text[end] != '\n' { - end++ + if pos < 0 { + pos = 0 } - // Find the start of the current line + start, end := pos, pos + + // Find the start of the current line. for start > 0 && e.Text[start] != '\n' { start-- } - // Check if the cursor is at the first line + // If the Cursor is already on the first line, move to the beginning of the Text. if start == 0 { return 0 } - // Find the start of the previous line + // 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 cursor position in the previous line + // Calculate the distance from the start of the current line to the Cursor. offset += pos - start if offset <= start-prevStart { return prevStart + offset @@ -206,23 +209,10 @@ func (e *Editor) calcCursorUp() int { } func (e *Editor) calcCursorDown() int { - file, err := os.OpenFile("cursor.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) - if err != nil { - fmt.Printf("Logger error, exiting: %s", err) - } - defer func() { - err := file.Close() - if err != nil { - log.Fatalln(err) - } - }() - logger := log.New(file, "---", log.LstdFlags) - - logger.Printf("MOVING DOWN") pos := e.Cursor offset := 0 - logger.Printf("cursor at %v", pos) + // 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-- @@ -234,107 +224,48 @@ func (e *Editor) calcCursorDown() int { start, end := pos, pos - // Find the end of the current line - for end < len(e.Text) && e.Text[end] != '\n' { - end++ - } - - // Find the start of the current line + // 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++ } - // Check if the cursor is at the first line + // 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 + // Find the end of the next line. nextEnd := end + 1 for nextEnd < len(e.Text) && e.Text[nextEnd] != '\n' { nextEnd++ } - logger.Printf("start at %v, end at %v", start, end) - - logger.Printf("next ends at %v", nextEnd) - // Calculate the cursor position in the current line + // Calculate the distance from the start of the current line to the Cursor. offset += pos - start - logger.Printf("offset at %v", offset) - logger.Printf("next line length is %v", nextEnd-end) if offset < nextEnd-end { - logger.Printf("offset is less than length of next line, cursor at %v", end+offset) return end + offset } else { - logger.Printf("setting cursor at end of next line") return nextEnd } } -// calcCursorDown calculates the new position of the cursor after moving one line down. -func (e *Editor) calcCursorDown2() int { - pos := e.Cursor - // reset cursor if out of bounds - if pos > len(e.Text)-1 { - pos = len(e.Text) - 1 - } - - // if cursor is currently on newline, "move" it - if e.Text[pos] == '\n' { - pos-- - } - - cls := -1 - // find the start of the line the cursor is currently on - for i := pos; i > 0; i-- { - if e.Text[i] == '\n' { - cls = i - break - } - } - var offset int - // set the cursor offset from the start of the current line - if cls < 0 { - offset = e.Cursor + 1 - } else { - offset = e.Cursor - cls - } - - cle, nle := -1, -1 - // find the end of the current line - for i := cls + 1; i < len(e.Text); i++ { - if e.Text[i] == '\n' { - cle = i - break - } - } - // find the end of the next line - if cle > 0 { // no need to find next line end if the end of the current line doesn't exist - for i := cle + 1; i < len(e.Text); i++ { - if e.Text[i] == '\n' { - nle = i - break - } - } - } - // if end of next line isn't found, assume next line is last of the document and set cursor to end - if nle < 0 { - nle = len(e.Text) - } - - if cle < 0 { - return len(e.Text) - } else if nle-cle < offset { // if next line is shorter than the offset, set cursor to end of next line - return nle - } else { // default - return cle + offset - } -} - // calcCursorXY calculates Cursor position from the index obtained from the content. func (e *Editor) calcCursorXY(index int) (int, int) { x := 1 From 9d8166327a35efcf0d2741cd0985003e08e04d66 Mon Sep 17 00:00:00 2001 From: Ben Muthalaly Date: Mon, 20 Feb 2023 01:15:44 -0600 Subject: [PATCH 28/28] Fix first character not able to be deleted. --- client/editor/editor.go | 2 +- client/main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/editor/editor.go b/client/editor/editor.go index e767f25..a2a3d57 100644 --- a/client/editor/editor.go +++ b/client/editor/editor.go @@ -127,7 +127,7 @@ func (e *Editor) showPositions() { // MoveCursor updates the Cursor position. func (e *Editor) MoveCursor(x, y int) { - if len(e.Text) == 0 { + if len(e.Text) == 0 && e.Cursor == 0 { return } // Move cursor horizontally. diff --git a/client/main.go b/client/main.go index 881fbdf..d461b93 100644 --- a/client/main.go +++ b/client/main.go @@ -356,7 +356,7 @@ 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 { + if e.Cursor-1 < 0 { e.Cursor = 0 } text := doc.Delete(e.Cursor)