diff --git a/buffer.go b/buffer.go index d3671f4f..3194dcf6 100644 --- a/buffer.go +++ b/buffer.go @@ -65,9 +65,12 @@ func (b *Buffer) Insert(idx int, value string) { } // Remove a slice of the rope from start to end (exclusive) -func (b *Buffer) Remove(start, end int) { +// Returns the string that was removed +func (b *Buffer) Remove(start, end int) string { + removed := b.text[start:end] b.r.Remove(start, end) b.Update() + return removed } // Len gives the length of the buffer diff --git a/cursor.go b/cursor.go index 80423dd0..6258b399 100644 --- a/cursor.go +++ b/cursor.go @@ -42,10 +42,10 @@ func (c *Cursor) HasSelection() bool { // DeleteSelected deletes the currently selected text func (c *Cursor) DeleteSelected() { if c.selectionStart > c.selectionEnd { - c.v.buf.Remove(c.selectionEnd, c.selectionStart+1) + c.v.eh.Remove(c.selectionEnd, c.selectionStart+1) // Since the cursor is already at the selection start we don't need to move } else { - c.v.buf.Remove(c.selectionStart, c.selectionEnd+1) + c.v.eh.Remove(c.selectionStart, c.selectionEnd+1) c.loc -= c.selectionEnd - c.selectionStart c.x = c.selectionStartX c.y = c.selectionStartY diff --git a/eventhandler.go b/eventhandler.go new file mode 100644 index 00000000..ff886c0a --- /dev/null +++ b/eventhandler.go @@ -0,0 +1,117 @@ +package main + +import ( + "time" +) + +const ( + // TextEventInsert repreasents an insertion event + TextEventInsert = 1 + // TextEventRemove represents a deletion event + TextEventRemove = -1 +) + +// TextEvent holds data for a manipulation on some text that can be undone +type TextEvent struct { + c Cursor + + eventType int + text string + start int + end int + buf *Buffer + time time.Time +} + +// ExecuteTextEvent runs a text event +func ExecuteTextEvent(t *TextEvent) { + if t.eventType == TextEventInsert { + t.buf.Insert(t.start, t.text) + } else if t.eventType == TextEventRemove { + t.text = t.buf.Remove(t.start, t.end) + } +} + +// UndoTextEvent undoes a text event +func UndoTextEvent(t *TextEvent) { + t.eventType = -t.eventType + ExecuteTextEvent(t) +} + +// EventHandler executes text manipulations and allows undoing and redoing +type EventHandler struct { + v *View + undo *Stack + redo *Stack +} + +// NewEventHandler returns a new EventHandler +func NewEventHandler(v *View) *EventHandler { + eh := new(EventHandler) + eh.undo = new(Stack) + eh.redo = new(Stack) + eh.v = v + 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, + eventType: TextEventInsert, + text: text, + start: start, + end: start + len(text), + buf: eh.v.buf, + time: time.Now(), + } + eh.Execute(e) +} + +// Remove creates a remove text event and executes it +func (eh *EventHandler) Remove(start, end int) { + e := &TextEvent{ + c: eh.v.cursor, + eventType: TextEventRemove, + start: start, + end: end, + buf: eh.v.buf, + time: time.Now(), + } + eh.Execute(e) +} + +// Execute a textevent and add it to the undo stack +func (eh *EventHandler) Execute(t *TextEvent) { + eh.undo.Push(t) + ExecuteTextEvent(t) +} + +// Undo the first event in the undo stack +func (eh *EventHandler) Undo() { + t := eh.undo.Pop() + if t == nil { + return + } + + te := t.(*TextEvent) + // Modifies the text event + UndoTextEvent(te) + eh.redo.Push(t) + + eh.v.cursor = te.c +} + +// Redo the first event in the redo stack +func (eh *EventHandler) Redo() { + t := eh.redo.Pop() + if t == nil { + return + } + + te := t.(*TextEvent) + // Modifies the text event + UndoTextEvent(te) + eh.undo.Push(t) + eh.v.cursor = te.c +} diff --git a/stack.go b/stack.go new file mode 100644 index 00000000..7b1dd668 --- /dev/null +++ b/stack.go @@ -0,0 +1,43 @@ +package main + +// Stack is a simple implementation of a LIFO stack +type Stack struct { + top *Element + size int +} + +// An Element which is stored in the Stack +type Element struct { + value interface{} // All types satisfy the empty interface, so we can store anything here. + next *Element +} + +// Len returns the stack's length +func (s *Stack) Len() int { + return s.size +} + +// Push a new element onto the stack +func (s *Stack) Push(value interface{}) { + s.top = &Element{value, s.top} + s.size++ +} + +// Pop removes the top element from the stack and returns its value +// If the stack is empty, return nil +func (s *Stack) Pop() (value interface{}) { + if s.size > 0 { + value, s.top = s.top.value, s.top.next + s.size-- + return + } + return nil +} + +// Peek lets you see and edit the top value of the stack without popping it +func (s *Stack) Peek() *interface{} { + if s.size > 0 { + return &s.top.value + } + return nil +} diff --git a/todolist.md b/todolist.md index 81a108ea..2847e7aa 100644 --- a/todolist.md +++ b/todolist.md @@ -22,7 +22,7 @@ - [ ] Opened with Ctrl-h - [ ] Undo/redo - - [ ] Undo/redo stack + - [x] Undo/redo stack - [ ] Functionality similar to nano - [ ] Command execution diff --git a/view.go b/view.go index 73b3c66d..a3adbeb2 100644 --- a/view.go +++ b/view.go @@ -15,6 +15,8 @@ type View struct { width int lineNumOffset int + eh *EventHandler + buf *Buffer sl Statusline @@ -46,6 +48,8 @@ func NewViewWidthHeight(buf *Buffer, s tcell.Screen, w, h int) *View { v: v, } + v.eh = NewEventHandler(v) + v.sl = Statusline{ v: v, } @@ -132,11 +136,11 @@ func (v *View) HandleEvent(event tcell.Event) int { v.cursor.Right() ret = 1 case tcell.KeyEnter: - v.buf.Insert(v.cursor.loc, "\n") + v.eh.Insert(v.cursor.loc, "\n") v.cursor.Right() ret = 2 case tcell.KeySpace: - v.buf.Insert(v.cursor.loc, " ") + v.eh.Insert(v.cursor.loc, " ") v.cursor.Right() ret = 2 case tcell.KeyBackspace2: @@ -145,12 +149,20 @@ func (v *View) HandleEvent(event tcell.Event) int { v.cursor.ResetSelection() ret = 2 } 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() - v.buf.Remove(v.cursor.loc, v.cursor.loc+1) + cx, cy, cloc := v.cursor.x, v.cursor.y, v.cursor.loc + 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 ret = 2 } case tcell.KeyTab: - v.buf.Insert(v.cursor.loc, "\t") + v.eh.Insert(v.cursor.loc, "\t") v.cursor.Right() ret = 2 case tcell.KeyCtrlS: @@ -160,6 +172,12 @@ func (v *View) HandleEvent(event tcell.Event) int { } // Need to redraw the status line ret = 1 + case tcell.KeyCtrlZ: + v.eh.Undo() + ret = 2 + case tcell.KeyCtrlY: + v.eh.Redo() + ret = 2 case tcell.KeyPgUp: v.PageUp() return 2 @@ -177,7 +195,7 @@ func (v *View) HandleEvent(event tcell.Event) int { v.cursor.DeleteSelected() v.cursor.ResetSelection() } - v.buf.Insert(v.cursor.loc, string(e.Rune())) + v.eh.Insert(v.cursor.loc, string(e.Rune())) v.cursor.Right() ret = 2 }