From c49ebaf87b1d67e048bda1203bb9c289747e9f67 Mon Sep 17 00:00:00 2001 From: Zachary Yedidia Date: Sat, 26 Mar 2016 17:23:52 -0400 Subject: [PATCH 1/2] Completely rework the cursor --- src/cursor.go | 143 ++++++++++++++++++--------------------------- src/highlighter.go | 2 +- src/view.go | 40 ++++++------- 3 files changed, 77 insertions(+), 108 deletions(-) diff --git a/src/cursor.go b/src/cursor.go index 8e78fe55..254a02e0 100644 --- a/src/cursor.go +++ b/src/cursor.go @@ -4,28 +4,63 @@ import ( "strings" ) +// FromCharPos converts from a character position to an x, y position +func FromCharPos(loc int, buf *Buffer) (int, int) { + charNum := 0 + x, y := 0, 0 + + for charNum+Count(buf.lines[y])+1 < loc { + charNum += Count(buf.lines[y]) + 1 + y++ + } + + for charNum+x < loc { + x++ + } + + return x, y +} + +// ToCharPos converts from an x, y position to a character position +func ToCharPos(x, y int, buf *Buffer) int { + loc := 0 + for i := 0; i < y; i++ { + // + 1 for the newline + loc += Count(buf.lines[i]) + 1 + } + loc += x + return loc +} + // The Cursor struct stores the location of the cursor in the view +// The complicated part about the cursor is storing its location. +// The cursor must be displayed at an x, y location, but since the buffer +// uses a rope to store text, to insert text we must have an index. It +// is also simpler to use character indicies for other tasks such as +// selection. type Cursor struct { v *View - // We need three variables here because we insert text at loc but - // display the cursor at x, y - x int - y int - loc int + // The cursor display location + x int + y int - // Start of the selection in charNum selectionStart int + selectionEnd int +} - // We store the x, y of the start because when the user deletes the selection - // the cursor needs to go back to the start, and this is the simplest way - selectionStartX int - selectionStartY int +// SetLoc sets the location of the cursor in terms of character number +// and not x, y location +// It's just a simple wrapper of FromCharPos +func (c *Cursor) SetLoc(loc int) { + c.x, c.y = FromCharPos(loc, c.v.buf) +} - // End of the selection in charNum - // We don't need to store the x, y here because when if the user is selecting backwards - // and they delete the selection, the cursor is already in the right place - selectionEnd int +// Loc gets the cursor location in terms of character number instead +// of x, y location +// It's just a simple wrapper of ToCharPos +func (c *Cursor) Loc() int { + return ToCharPos(c.x, c.y, c.v.buf) } // ResetSelection resets the user's selection @@ -43,12 +78,10 @@ func (c *Cursor) HasSelection() bool { func (c *Cursor) DeleteSelection() { if c.selectionStart > c.selectionEnd { c.v.eh.Remove(c.selectionEnd, c.selectionStart+1) - // Since the cursor is already at the selection start we don't need to move + c.SetLoc(c.selectionEnd) } else { c.v.eh.Remove(c.selectionStart, c.selectionEnd+1) - c.loc -= c.selectionEnd - c.selectionStart - c.x = c.selectionStartX - c.y = c.selectionStartY + c.SetLoc(c.selectionStart) } } @@ -72,46 +105,33 @@ func (c *Cursor) RuneUnder() rune { // Up moves the cursor up one line (if possible) func (c *Cursor) Up() { if c.y > 0 { - runes := []rune(c.v.buf.lines[c.y]) - c.loc -= len(runes[:c.x]) - // Count the newline - c.loc-- c.y-- - runes = []rune(c.v.buf.lines[c.y]) + runes := []rune(c.v.buf.lines[c.y]) if c.x > len(runes) { c.x = len(runes) } - - c.loc -= len(runes[c.x:]) } } // Down moves the cursor down one line (if possible) func (c *Cursor) Down() { if c.y < len(c.v.buf.lines)-1 { - runes := []rune(c.v.buf.lines[c.y]) - c.loc += len(runes[c.x:]) - // Count the newline - c.loc++ c.y++ - runes = []rune(c.v.buf.lines[c.y]) + runes := []rune(c.v.buf.lines[c.y]) if c.x > len(runes) { c.x = len(runes) } - - c.loc += len(runes[:c.x]) } } // Left moves the cursor left one cell (if possible) or to the last line if it is at the beginning func (c *Cursor) Left() { - if c.loc == 0 { + if c.Loc() == 0 { return } if c.x > 0 { - c.loc-- c.x-- } else { c.Up() @@ -121,11 +141,10 @@ func (c *Cursor) Left() { // Right moves the cursor right one cell (if possible) or to the next line if it is at the end func (c *Cursor) Right() { - if c.loc == c.v.buf.Len() { + if c.Loc() == c.v.buf.Len() { return } if c.x < Count(c.v.buf.lines[c.y]) { - c.loc++ c.x++ } else { c.Down() @@ -135,21 +154,19 @@ func (c *Cursor) Right() { // End moves the cursor to the end of the line it is on func (c *Cursor) End() { - runes := []rune(c.v.buf.lines[c.y]) - c.loc += len(runes[c.x:]) - c.x = len(runes) + c.x = Count(c.v.buf.lines[c.y]) } // Start moves the cursor to the start of the line it is on func (c *Cursor) Start() { - runes := []rune(c.v.buf.lines[c.y]) - c.loc -= len(runes[:c.x]) c.x = 0 } // GetCharPosInLine gets the char position of a visual x y coordinate (this is necessary because tabs are 1 char but 4 visual spaces) func (c *Cursor) GetCharPosInLine(lineNum, visualPos int) int { + // Get the tab size tabSize := options["tabsize"].(int) + // This is the visual line -- every \t replaced with the correct number of spaces visualLine := strings.Replace(c.v.buf.lines[lineNum], "\t", "\t"+Spaces(tabSize-1), -1) if visualPos > Count(visualLine) { visualPos = Count(visualLine) @@ -168,50 +185,6 @@ func (c *Cursor) GetVisualX() int { return c.x + NumOccurences(string(runes[:c.x]), '\t')*(tabSize-1) } -// Distance returns the distance between the cursor and x, y in runes -func (c *Cursor) Distance(x, y int) int { - // Same line - if y == c.y { - return x - c.x - } - - var distance int - runes := []rune(c.v.buf.lines[c.y]) - if y > c.y { - distance += len(runes[c.x:]) - // Newline - distance++ - i := 1 - for y != c.y+i { - distance += Count(c.v.buf.lines[c.y+i]) - // Newline - distance++ - i++ - } - if x < Count(c.v.buf.lines[y]) { - distance += len([]rune(c.v.buf.lines[y])[:x]) - } else { - distance += Count(c.v.buf.lines[y]) - } - return distance - } - - distance -= len(runes[:c.x]) - // Newline - distance-- - i := 1 - for y != c.y-i { - distance -= Count(c.v.buf.lines[c.y-i]) - // Newline - distance-- - i++ - } - if x >= 0 { - distance -= len([]rune(c.v.buf.lines[y])[x:]) - } - return distance -} - // Display draws the cursor to the screen at the correct position func (c *Cursor) Display() { if c.y-c.v.topline < 0 || c.y-c.v.topline > c.v.height-1 { diff --git a/src/highlighter.go b/src/highlighter.go index 86d9a0bc..77d34eba 100644 --- a/src/highlighter.go +++ b/src/highlighter.go @@ -239,7 +239,7 @@ func Match(v *View) SyntaxMatches { } str := strings.Join(buf.lines[totalStart:totalEnd], "\n") - startNum := v.cursor.loc + v.cursor.Distance(0, totalStart) + startNum := ToCharPos(0, totalStart, v.buf) for _, rule := range rules { if rule.startend { diff --git a/src/view.go b/src/view.go index 824f7903..2884d0cc 100644 --- a/src/view.go +++ b/src/view.go @@ -72,10 +72,9 @@ func NewViewWidthHeight(buf *Buffer, w, h int) *View { v.topline = 0 // Put the cursor at the first spot v.cursor = Cursor{ - x: 0, - y: 0, - loc: 0, - v: v, + x: 0, + y: 0, + v: v, } v.eh = NewEventHandler(v) @@ -238,7 +237,7 @@ func (v *View) Paste() { v.cursor.ResetSelection() } clip, _ := clipboard.ReadAll() - v.eh.Insert(v.cursor.loc, clip) + v.eh.Insert(v.cursor.Loc(), clip) // This is a bit weird... Not sure if there's a better way for i := 0; i < Count(clip); i++ { v.cursor.Right() @@ -255,7 +254,6 @@ func (v *View) SelectAll() { // Put the cursor at the beginning v.cursor.x = 0 v.cursor.y = 0 - v.cursor.loc = 0 } // OpenFile opens a new file in the current view @@ -306,8 +304,6 @@ func (v *View) MoveToMouseClick(x, y int) { if x > Count(v.buf.lines[y]) { x = Count(v.buf.lines[y]) } - d := v.cursor.Distance(x, y) - v.cursor.loc += d v.cursor.x = x v.cursor.y = y } @@ -339,12 +335,12 @@ func (v *View) HandleEvent(event tcell.Event) { v.cursor.Right() case tcell.KeyEnter: // Insert a newline - v.eh.Insert(v.cursor.loc, "\n") + v.eh.Insert(v.cursor.Loc(), "\n") v.cursor.Right() v.UpdateLines(v.cursor.y-1, v.cursor.y) case tcell.KeySpace: // Insert a space - v.eh.Insert(v.cursor.loc, " ") + v.eh.Insert(v.cursor.Loc(), " ") v.cursor.Right() v.UpdateLines(v.cursor.y, v.cursor.y) case tcell.KeyBackspace2: @@ -354,22 +350,23 @@ func (v *View) HandleEvent(event tcell.Event) { v.cursor.ResetSelection() // Rehighlight the entire buffer v.UpdateLines(v.topline, v.topline+v.height) - } else if v.cursor.loc > 0 { + } else if v.cursor.Loc() > 0 { // We have to do something a bit hacky here because we want to // delete the line by first moving left and then deleting backwards // but the undo redo would place the cursor in the wrong place // So instead we move left, save the position, move back, delete // and restore the position v.cursor.Left() - cx, cy, cloc := v.cursor.x, v.cursor.y, v.cursor.loc + cx, cy := v.cursor.x, v.cursor.y v.cursor.Right() - v.eh.Remove(v.cursor.loc-1, v.cursor.loc) - v.cursor.x, v.cursor.y, v.cursor.loc = cx, cy, cloc + loc := v.cursor.Loc() + v.eh.Remove(loc-1, loc) + v.cursor.x, v.cursor.y = cx, cy v.UpdateLines(v.cursor.y, v.cursor.y+1) } case tcell.KeyTab: // Insert a tab - v.eh.Insert(v.cursor.loc, "\t") + v.eh.Insert(v.cursor.Loc(), "\t") v.cursor.Right() v.UpdateLines(v.cursor.y, v.cursor.y) case tcell.KeyCtrlS: @@ -418,7 +415,7 @@ func (v *View) HandleEvent(event tcell.Event) { v.cursor.DeleteSelection() v.cursor.ResetSelection() } - v.eh.Insert(v.cursor.loc, string(e.Rune())) + v.eh.Insert(v.cursor.Loc(), string(e.Rune())) v.cursor.Right() v.UpdateLines(v.cursor.y, v.cursor.y) } @@ -437,12 +434,11 @@ func (v *View) HandleEvent(event tcell.Event) { // Left click v.MoveToMouseClick(x, y) + loc := v.cursor.Loc() if v.mouseReleased { - v.cursor.selectionStart = v.cursor.loc - v.cursor.selectionStartX = v.cursor.x - v.cursor.selectionStartY = v.cursor.y + v.cursor.selectionStart = loc } - v.cursor.selectionEnd = v.cursor.loc + v.cursor.selectionEnd = loc v.mouseReleased = false case tcell.ButtonNone: // Mouse event with no click @@ -455,7 +451,7 @@ func (v *View) HandleEvent(event tcell.Event) { // events, this still allows the user to make selections, except only after they // release the mouse v.MoveToMouseClick(x, y) - v.cursor.selectionEnd = v.cursor.loc + v.cursor.selectionEnd = v.cursor.Loc() v.mouseReleased = true } // We don't want to relocate because otherwise the view will be relocated @@ -486,7 +482,7 @@ func (v *View) DisplayView() { matches := make(SyntaxMatches, len(v.matches)) // The character number of the character in the top left of the screen - charNum := v.cursor.loc + v.cursor.Distance(0, v.topline) + charNum := ToCharPos(0, v.topline, v.buf) // Convert the length of buffer to a string, and get the length of the string // We are going to have to offset by that amount From d9ab074276f1e6f352c488d571693ba5037bc952 Mon Sep 17 00:00:00 2001 From: Zachary Yedidia Date: Sun, 27 Mar 2016 12:27:26 -0400 Subject: [PATCH 2/2] Fix cursor bug --- src/cursor.go | 9 ++++----- src/rope.go | 11 ++++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/cursor.go b/src/cursor.go index 254a02e0..09497d90 100644 --- a/src/cursor.go +++ b/src/cursor.go @@ -1,6 +1,8 @@ package main import ( + "io/ioutil" + "strconv" "strings" ) @@ -9,14 +11,11 @@ func FromCharPos(loc int, buf *Buffer) (int, int) { charNum := 0 x, y := 0, 0 - for charNum+Count(buf.lines[y])+1 < loc { + for charNum+Count(buf.lines[y])+1 <= loc { charNum += Count(buf.lines[y]) + 1 y++ } - - for charNum+x < loc { - x++ - } + x = loc - charNum return x, y } diff --git a/src/rope.go b/src/rope.go index d1cecd48..90fff63c 100644 --- a/src/rope.go +++ b/src/rope.go @@ -5,9 +5,9 @@ import ( ) const ( - // RopeSplitLength defines how large can a string be before it is split into two nodes - RopeSplitLength = 1000 - // RopeJoinLength defines how short can a string be before it is joined + // RopeSplitLength is the threshold used to split a leaf node into two child nodes. + RopeSplitLength = 1000000000 + // RopeJoinLength is the threshold used to join two child nodes into one leaf node. RopeJoinLength = 500 // RopeRebalanceRatio = 1.2 ) @@ -39,8 +39,9 @@ func (r *Rope) Adjust() { if !r.valueNil { if r.len > RopeSplitLength { divide := int(math.Floor(float64(r.len) / 2)) - r.left = NewRope(r.value[:divide]) - r.right = NewRope(r.value[divide:]) + runes := []rune(r.value) + r.left = NewRope(string(runes[:divide])) + r.right = NewRope(string(runes[divide:])) r.valueNil = true } } else {