From df684ec505032b3f31d48be6426ecc0f4e5016d5 Mon Sep 17 00:00:00 2001 From: Zachary Yedidia Date: Sun, 22 May 2016 15:01:02 -0400 Subject: [PATCH] Store cursor and eventhandler in buffer instead of view This is better design because the cursor and eventhandler are things the buffer should know about directly but the view shouldn't. This should make it easier to add persistent undo or location saving between sessions (see #107). --- cmd/micro/bindings.go | 24 +++++++------- cmd/micro/buffer.go | 21 ++++++++++-- cmd/micro/command.go | 4 +-- cmd/micro/cursor.go | 70 +++++++++++++++++---------------------- cmd/micro/eventhandler.go | 28 ++++++++-------- cmd/micro/view.go | 32 +++++++++--------- 6 files changed, 92 insertions(+), 87 deletions(-) diff --git a/cmd/micro/bindings.go b/cmd/micro/bindings.go index 330be01e..f093fe7b 100644 --- a/cmd/micro/bindings.go +++ b/cmd/micro/bindings.go @@ -506,7 +506,7 @@ func (v *View) InsertSpace() bool { v.Cursor.DeleteSelection() v.Cursor.ResetSelection() } - v.eh.Insert(v.Cursor.Loc(), " ") + v.Buf.Insert(v.Cursor.Loc(), " ") v.Cursor.Right() return true } @@ -519,12 +519,12 @@ func (v *View) InsertEnter() bool { v.Cursor.ResetSelection() } - v.eh.Insert(v.Cursor.Loc(), "\n") + v.Buf.Insert(v.Cursor.Loc(), "\n") ws := GetLeadingWhitespace(v.Buf.Lines[v.Cursor.y]) v.Cursor.Right() if settings["autoindent"].(bool) { - v.eh.Insert(v.Cursor.Loc(), ws) + v.Buf.Insert(v.Cursor.Loc(), ws) for i := 0; i < len(ws); i++ { v.Cursor.Right() } @@ -556,14 +556,14 @@ func (v *View) Backspace() bool { v.Cursor.SetLoc(loc - tabSize) cx, cy := v.Cursor.x, v.Cursor.y v.Cursor.SetLoc(loc) - v.eh.Remove(loc-tabSize, loc) + v.Buf.Remove(loc-tabSize, loc) v.Cursor.x, v.Cursor.y = cx, cy } else { v.Cursor.Left() cx, cy := v.Cursor.x, v.Cursor.y v.Cursor.Right() loc := v.Cursor.Loc() - v.eh.Remove(loc-1, loc) + v.Buf.Remove(loc-1, loc) v.Cursor.x, v.Cursor.y = cx, cy } } @@ -579,7 +579,7 @@ func (v *View) Delete() bool { } else { loc := v.Cursor.Loc() if loc < v.Buf.Len() { - v.eh.Remove(loc, loc+1) + v.Buf.Remove(loc, loc+1) } } return true @@ -594,12 +594,12 @@ func (v *View) InsertTab() bool { } if settings["tabsToSpaces"].(bool) { tabSize := int(settings["tabsize"].(float64)) - v.eh.Insert(v.Cursor.Loc(), Spaces(tabSize)) + v.Buf.Insert(v.Cursor.Loc(), Spaces(tabSize)) for i := 0; i < tabSize; i++ { v.Cursor.Right() } } else { - v.eh.Insert(v.Cursor.Loc(), "\t") + v.Buf.Insert(v.Cursor.Loc(), "\t") v.Cursor.Right() } return true @@ -663,14 +663,14 @@ func (v *View) FindPrevious() bool { // Undo undoes the last action func (v *View) Undo() bool { - v.eh.Undo() + v.Buf.Undo() messenger.Message("Undid action") return true } // Redo redoes the last action func (v *View) Redo() bool { - v.eh.Redo() + v.Buf.Redo() messenger.Message("Redid action") return true } @@ -722,7 +722,7 @@ func (v *View) Cut() bool { // DuplicateLine duplicates the current line func (v *View) DuplicateLine() bool { v.Cursor.End() - v.eh.Insert(v.Cursor.Loc(), "\n"+v.Buf.Lines[v.Cursor.y]) + v.Buf.Insert(v.Cursor.Loc(), "\n"+v.Buf.Lines[v.Cursor.y]) v.Cursor.Right() messenger.Message("Duplicated line") return true @@ -736,7 +736,7 @@ func (v *View) Paste() bool { v.Cursor.ResetSelection() } clip, _ := clipboard.ReadAll() - v.eh.Insert(v.Cursor.Loc(), clip) + v.Buf.Insert(v.Cursor.Loc(), clip) v.Cursor.SetLoc(v.Cursor.Loc() + Count(clip)) v.freshClip = false messenger.Message("Pasted clipboard") diff --git a/cmd/micro/buffer.go b/cmd/micro/buffer.go index da5e6757..df938296 100644 --- a/cmd/micro/buffer.go +++ b/cmd/micro/buffer.go @@ -10,9 +10,14 @@ import ( // It uses a rope to efficiently store the string and contains some // simple functions for saving and wrapper functions for modifying the rope type Buffer struct { + // The eventhandler for undo/redo + *EventHandler + // Stores the text of the buffer r *rope.Rope + Cursor Cursor + // Path to the file on disk Path string // Name of the buffer on the status line @@ -43,6 +48,15 @@ func NewBuffer(txt, path string) *Buffer { b.Path = path b.Name = path + // Put the cursor at the first spot + b.Cursor = Cursor{ + x: 0, + y: 0, + Buf: b, + } + + b.EventHandler = NewEventHandler(b) + b.Update() b.UpdateRules() @@ -84,8 +98,8 @@ func (b *Buffer) SaveAs(filename string) error { return err } -// Insert a string into the rope -func (b *Buffer) Insert(idx int, value string) { +// This directly inserts value at idx, bypassing all undo/redo +func (b *Buffer) insert(idx int, value string) { b.IsModified = true b.r = b.r.Insert(idx, value) b.Update() @@ -93,7 +107,8 @@ func (b *Buffer) Insert(idx int, value string) { // Remove a slice of the rope from start to end (exclusive) // Returns the string that was removed -func (b *Buffer) Remove(start, end int) string { +// This directly removes from start to end from the buffer, bypassing all undo/redo +func (b *Buffer) remove(start, end int) string { b.IsModified = true if start < 0 { start = 0 diff --git a/cmd/micro/command.go b/cmd/micro/command.go index 1e41a7b2..dac6ed58 100644 --- a/cmd/micro/command.go +++ b/cmd/micro/command.go @@ -162,7 +162,7 @@ func HandleCommand(input string, view *View) { } if choice { view.Cursor.DeleteSelection() - view.eh.Insert(match[0], replace) + view.Buf.Insert(match[0], replace) view.Cursor.ResetSelection() messenger.Reset() } else { @@ -174,7 +174,7 @@ func HandleCommand(input string, view *View) { continue } } else { - view.eh.Replace(match[0], match[1], replace) + view.Buf.Replace(match[0], match[1], replace) } } if !found { diff --git a/cmd/micro/cursor.go b/cmd/micro/cursor.go index 5eb43b50..1bb55d61 100644 --- a/cmd/micro/cursor.go +++ b/cmd/micro/cursor.go @@ -43,7 +43,7 @@ func ToCharPos(x, y int, buf *Buffer) int { // is also simpler to use character indicies for other tasks such as // selection. type Cursor struct { - v *View + Buf *Buffer // The cursor display location x int @@ -64,7 +64,7 @@ type Cursor struct { // 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) + c.x, c.y = FromCharPos(loc, c.Buf) c.lastVisualX = c.GetVisualX() } @@ -72,7 +72,7 @@ func (c *Cursor) SetLoc(loc int) { // 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) + return ToCharPos(c.x, c.y, c.Buf) } // ResetSelection resets the user's selection @@ -89,12 +89,12 @@ func (c *Cursor) HasSelection() bool { // DeleteSelection deletes the currently selected text func (c *Cursor) DeleteSelection() { if c.curSelection[0] > c.curSelection[1] { - c.v.eh.Remove(c.curSelection[1], c.curSelection[0]) + c.Buf.Remove(c.curSelection[1], c.curSelection[0]) c.SetLoc(c.curSelection[1]) } else if c.GetSelection() == "" { return } else { - c.v.eh.Remove(c.curSelection[0], c.curSelection[1]) + c.Buf.Remove(c.curSelection[0], c.curSelection[1]) c.SetLoc(c.curSelection[0]) } } @@ -102,9 +102,9 @@ func (c *Cursor) DeleteSelection() { // GetSelection returns the cursor's selection func (c *Cursor) GetSelection() string { if c.curSelection[0] > c.curSelection[1] { - return c.v.Buf.Substr(c.curSelection[1], c.curSelection[0]) + return c.Buf.Substr(c.curSelection[1], c.curSelection[0]) } - return c.v.Buf.Substr(c.curSelection[0], c.curSelection[1]) + return c.Buf.Substr(c.curSelection[0], c.curSelection[1]) } // SelectLine selects the current line @@ -112,7 +112,7 @@ func (c *Cursor) SelectLine() { c.Start() c.curSelection[0] = c.Loc() c.End() - if c.v.Buf.NumLines-1 > c.y { + if c.Buf.NumLines-1 > c.y { c.curSelection[1] = c.Loc() + 1 } else { c.curSelection[1] = c.Loc() @@ -143,7 +143,7 @@ func (c *Cursor) AddLineToSelection() { // SelectWord selects the word the cursor is currently on func (c *Cursor) SelectWord() { - if len(c.v.Buf.Lines[c.y]) == 0 { + if len(c.Buf.Lines[c.y]) == 0 { return } @@ -161,14 +161,14 @@ func (c *Cursor) SelectWord() { backward-- } - c.curSelection[0] = ToCharPos(backward, c.y, c.v.Buf) + c.curSelection[0] = ToCharPos(backward, c.y, c.Buf) c.origSelection[0] = c.curSelection[0] - for forward < Count(c.v.Buf.Lines[c.y])-1 && IsWordChar(string(c.RuneUnder(forward+1))) { + for forward < Count(c.Buf.Lines[c.y])-1 && IsWordChar(string(c.RuneUnder(forward+1))) { forward++ } - c.curSelection[1] = ToCharPos(forward, c.y, c.v.Buf) + 1 + c.curSelection[1] = ToCharPos(forward, c.y, c.Buf) + 1 c.origSelection[1] = c.curSelection[1] c.SetLoc(c.curSelection[1]) } @@ -189,18 +189,18 @@ func (c *Cursor) AddWordToSelection() { backward-- } - c.curSelection[0] = ToCharPos(backward, c.y, c.v.Buf) + c.curSelection[0] = ToCharPos(backward, c.y, c.Buf) c.curSelection[1] = c.origSelection[1] } if loc > c.origSelection[1] { forward := c.x - for forward < Count(c.v.Buf.Lines[c.y])-1 && IsWordChar(string(c.RuneUnder(forward+1))) { + for forward < Count(c.Buf.Lines[c.y])-1 && IsWordChar(string(c.RuneUnder(forward+1))) { forward++ } - c.curSelection[1] = ToCharPos(forward, c.y, c.v.Buf) + 1 + c.curSelection[1] = ToCharPos(forward, c.y, c.Buf) + 1 c.curSelection[0] = c.origSelection[0] } @@ -222,13 +222,13 @@ func (c *Cursor) SelectTo(loc int) { func (c *Cursor) WordRight() { c.Right() for IsWhitespace(c.RuneUnder(c.x)) { - if c.x == Count(c.v.Buf.Lines[c.y]) { + if c.x == Count(c.Buf.Lines[c.y]) { return } c.Right() } for !IsWhitespace(c.RuneUnder(c.x)) { - if c.x == Count(c.v.Buf.Lines[c.y]) { + if c.x == Count(c.Buf.Lines[c.y]) { return } c.Right() @@ -255,7 +255,7 @@ func (c *Cursor) WordLeft() { // RuneUnder returns the rune under the given x position func (c *Cursor) RuneUnder(x int) rune { - line := []rune(c.v.Buf.Lines[c.y]) + line := []rune(c.Buf.Lines[c.y]) if len(line) == 0 { return '\n' } @@ -272,7 +272,7 @@ func (c *Cursor) Up() { if c.y > 0 { c.y-- - runes := []rune(c.v.Buf.Lines[c.y]) + runes := []rune(c.Buf.Lines[c.y]) c.x = c.GetCharPosInLine(c.y, c.lastVisualX) if c.x > len(runes) { c.x = len(runes) @@ -282,10 +282,10 @@ func (c *Cursor) Up() { // Down moves the cursor down one line (if possible) func (c *Cursor) Down() { - if c.y < c.v.Buf.NumLines-1 { + if c.y < c.Buf.NumLines-1 { c.y++ - runes := []rune(c.v.Buf.Lines[c.y]) + runes := []rune(c.Buf.Lines[c.y]) c.x = c.GetCharPosInLine(c.y, c.lastVisualX) if c.x > len(runes) { c.x = len(runes) @@ -309,10 +309,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.Buf.Len() { return } - if c.x < Count(c.v.Buf.Lines[c.y]) { + if c.x < Count(c.Buf.Lines[c.y]) { c.x++ } else { c.Down() @@ -323,7 +323,7 @@ func (c *Cursor) Right() { // End moves the cursor to the end of the line it is on func (c *Cursor) End() { - c.x = Count(c.v.Buf.Lines[c.y]) + c.x = Count(c.Buf.Lines[c.y]) c.lastVisualX = c.GetVisualX() } @@ -338,7 +338,7 @@ func (c *Cursor) GetCharPosInLine(lineNum, visualPos int) int { // Get the tab size tabSize := int(settings["tabsize"].(float64)) // 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) + visualLine := strings.Replace(c.Buf.Lines[lineNum], "\t", "\t"+Spaces(tabSize-1), -1) if visualPos > Count(visualLine) { visualPos = Count(visualLine) } @@ -351,7 +351,7 @@ func (c *Cursor) GetCharPosInLine(lineNum, visualPos int) int { // GetVisualX returns the x value of the cursor in visual spaces func (c *Cursor) GetVisualX() int { - runes := []rune(c.v.Buf.Lines[c.y]) + runes := []rune(c.Buf.Lines[c.y]) tabSize := int(settings["tabsize"].(float64)) return c.x + NumOccurences(string(runes[:c.x]), '\t')*(tabSize-1) } @@ -361,23 +361,13 @@ func (c *Cursor) GetVisualX() int { func (c *Cursor) Relocate() { if c.y < 0 { c.y = 0 - } else if c.y >= c.v.Buf.NumLines { - c.y = c.v.Buf.NumLines - 1 + } else if c.y >= c.Buf.NumLines { + c.y = c.Buf.NumLines - 1 } if c.x < 0 { c.x = 0 - } else if c.x > Count(c.v.Buf.Lines[c.y]) { - c.x = Count(c.v.Buf.Lines[c.y]) - } -} - -// Display draws the cursor to the screen at the correct position -func (c *Cursor) Display() { - // Don't draw the cursor if it is out of the viewport or if it has a selection - if (c.y-c.v.Topline < 0 || c.y-c.v.Topline > c.v.height-1) || c.HasSelection() { - screen.HideCursor() - } else { - screen.ShowCursor(c.GetVisualX()+c.v.lineNumOffset-c.v.leftCol, c.y-c.v.Topline) + } else if c.x > Count(c.Buf.Lines[c.y]) { + c.x = Count(c.Buf.Lines[c.y]) } } diff --git a/cmd/micro/eventhandler.go b/cmd/micro/eventhandler.go index 4904a84e..2c5076d1 100644 --- a/cmd/micro/eventhandler.go +++ b/cmd/micro/eventhandler.go @@ -27,9 +27,9 @@ type TextEvent struct { // ExecuteTextEvent runs a text event func ExecuteTextEvent(t *TextEvent, buf *Buffer) { if t.eventType == TextEventInsert { - buf.Insert(t.start, t.text) + buf.insert(t.start, t.text) } else if t.eventType == TextEventRemove { - t.text = buf.Remove(t.start, t.end) + t.text = buf.remove(t.start, t.end) } } @@ -41,24 +41,24 @@ func UndoTextEvent(t *TextEvent, buf *Buffer) { // EventHandler executes text manipulations and allows undoing and redoing type EventHandler struct { - v *View + buf *Buffer undo *Stack redo *Stack } // NewEventHandler returns a new EventHandler -func NewEventHandler(v *View) *EventHandler { +func NewEventHandler(buf *Buffer) *EventHandler { eh := new(EventHandler) eh.undo = new(Stack) eh.redo = new(Stack) - eh.v = v + eh.buf = buf return eh } // Insert creates an insert text event and executes it func (eh *EventHandler) Insert(start int, text string) { e := &TextEvent{ - c: eh.v.Cursor, + c: eh.buf.Cursor, eventType: TextEventInsert, text: text, start: start, @@ -71,7 +71,7 @@ func (eh *EventHandler) Insert(start int, text string) { // Remove creates a remove text event and executes it func (eh *EventHandler) Remove(start, end int) { e := &TextEvent{ - c: eh.v.Cursor, + c: eh.buf.Cursor, eventType: TextEventRemove, start: start, end: end, @@ -92,7 +92,7 @@ func (eh *EventHandler) Execute(t *TextEvent) { eh.redo = new(Stack) } eh.undo.Push(t) - ExecuteTextEvent(t, eh.v.Buf) + ExecuteTextEvent(t, eh.buf) } // Undo the first event in the undo stack @@ -135,12 +135,12 @@ func (eh *EventHandler) UndoOneEvent() { te := t.(*TextEvent) // Undo it // Modifies the text event - UndoTextEvent(te, eh.v.Buf) + UndoTextEvent(te, eh.buf) // Set the cursor in the right place teCursor := te.c - te.c = eh.v.Cursor - eh.v.Cursor = teCursor + te.c = eh.buf.Cursor + eh.buf.Cursor = teCursor // Push it to the redo stack eh.redo.Push(te) @@ -183,11 +183,11 @@ func (eh *EventHandler) RedoOneEvent() { te := t.(*TextEvent) // Modifies the text event - UndoTextEvent(te, eh.v.Buf) + UndoTextEvent(te, eh.buf) teCursor := te.c - te.c = eh.v.Cursor - eh.v.Cursor = teCursor + te.c = eh.buf.Cursor + eh.buf.Cursor = teCursor eh.undo.Push(te) } diff --git a/cmd/micro/view.go b/cmd/micro/view.go index 078177b3..e6371235 100644 --- a/cmd/micro/view.go +++ b/cmd/micro/view.go @@ -15,7 +15,8 @@ import ( // It stores information about the cursor, and the viewport // that the user sees the buffer from. type View struct { - Cursor Cursor + // A pointer to the buffer's cursor for ease of access + Cursor *Cursor // The topmost line, used for vertical scrolling Topline int @@ -33,9 +34,6 @@ type View struct { // How much to offset because of line numbers lineNumOffset int - // The eventhandler for undo/redo - eh *EventHandler - // Holds the list of gutter messages messages map[string][]GutterMessage @@ -90,8 +88,6 @@ func NewViewWidthHeight(buf *Buffer, w, h int) *View { v.OpenBuffer(buf) - v.eh = NewEventHandler(v) - v.messages = make(map[string][]GutterMessage) v.sline = Statusline{ @@ -162,18 +158,12 @@ func (v *View) CanClose(msg string) bool { // This resets the topline, event handler and cursor. func (v *View) OpenBuffer(buf *Buffer) { v.Buf = buf + v.Cursor = &buf.Cursor v.Topline = 0 v.leftCol = 0 - // Put the cursor at the first spot - v.Cursor = Cursor{ - x: 0, - y: 0, - v: v, - } v.Cursor.ResetSelection() v.messages = make(map[string][]GutterMessage) - v.eh = NewEventHandler(v) v.matches = Match(v) // Set mouseReleased to true because we assume the mouse is not being pressed when @@ -269,7 +259,7 @@ func (v *View) HandleEvent(event tcell.Event) { v.Cursor.DeleteSelection() v.Cursor.ResetSelection() } - v.eh.Insert(v.Cursor.Loc(), string(e.Rune())) + v.Buf.Insert(v.Cursor.Loc(), string(e.Rune())) v.Cursor.Right() } else { for key, action := range bindings { @@ -293,7 +283,7 @@ func (v *View) HandleEvent(event tcell.Event) { v.Cursor.ResetSelection() } clip := e.Text() - v.eh.Insert(v.Cursor.Loc(), clip) + v.Buf.Insert(v.Cursor.Loc(), clip) v.Cursor.SetLoc(v.Cursor.Loc() + Count(clip)) v.freshClip = false case *tcell.EventMouse: @@ -595,10 +585,20 @@ func (v *View) DisplayView() { } } +// DisplayCursor draws the current buffer's cursor to the screen +func (v *View) DisplayCursor() { + // Don't draw the cursor if it is out of the viewport or if it has a selection + if (v.Cursor.y-v.Topline < 0 || v.Cursor.y-v.Topline > v.height-1) || v.Cursor.HasSelection() { + screen.HideCursor() + } else { + screen.ShowCursor(v.Cursor.GetVisualX()+v.lineNumOffset-v.leftCol, v.Cursor.y-v.Topline) + } +} + // Display renders the view, the cursor, and statusline func (v *View) Display() { v.DisplayView() - v.Cursor.Display() + v.DisplayCursor() if settings["statusline"].(bool) { v.sline.Display() }