Add persistent undo as the option

This commit is contained in:
Zachary Yedidia 2016-05-29 11:02:56 -04:00
parent 1fe18eecb7
commit ee9f2a3d9c
9 changed files with 116 additions and 141 deletions

View file

@ -41,6 +41,12 @@ type Buffer struct {
FileType string
}
// The SerializedBuffer holds the types that get serialized when a buffer is saved
type SerializedBuffer struct {
EventHandler *EventHandler
Cursor Cursor
}
// NewBuffer creates a new buffer from `txt` with path and name `path`
func NewBuffer(txt, path string) *Buffer {
b := new(Buffer)
@ -61,35 +67,36 @@ func NewBuffer(txt, path string) *Buffer {
os.Mkdir(configDir+"/buffers/", os.ModePerm)
}
if settings["savecursor"].(bool) {
// Put the cursor at the first spot
b.Cursor = Cursor{
X: 0,
Y: 0,
buf: b,
}
if settings["savecursor"].(bool) || settings["saveundo"].(bool) {
absPath, _ := filepath.Abs(b.Path)
file, err := os.Open(configDir + "/buffers/" + EscapePath(absPath))
if err == nil {
var cursor Cursor
var buffer SerializedBuffer
decoder := gob.NewDecoder(file)
err = decoder.Decode(&cursor)
gob.Register(TextEvent{})
err = decoder.Decode(&buffer)
if err != nil {
TermMessage(err.Error())
}
b.Cursor = cursor
b.Cursor.buf = b
b.Cursor.Clamp()
} else {
// Put the cursor at the first spot
b.Cursor = Cursor{
X: 0,
Y: 0,
buf: b,
if settings["savecursor"].(bool) {
b.Cursor = buffer.Cursor
b.Cursor.buf = b
b.Cursor.Clamp()
}
if settings["saveundo"].(bool) {
b.EventHandler = buffer.EventHandler
b.EventHandler.buf = b
}
}
file.Close()
} else {
// Put the cursor at the first spot
b.Cursor = Cursor{
X: 0,
Y: 0,
buf: b,
}
}
return b
@ -121,12 +128,17 @@ func (b *Buffer) Save() error {
// Serialize serializes the buffer to configDir/buffers
func (b *Buffer) Serialize() error {
if settings["savecursor"].(bool) {
if settings["savecursor"].(bool) || settings["saveundo"].(bool) {
absPath, _ := filepath.Abs(b.Path)
file, err := os.Create(configDir + "/buffers/" + EscapePath(absPath))
if err == nil {
enc := gob.NewEncoder(file)
err = enc.Encode(b.Cursor)
gob.Register(TextEvent{})
err = enc.Encode(SerializedBuffer{
b.EventHandler,
b.Cursor,
})
// err = enc.Encode(b.Cursor)
}
file.Close()
return err
@ -141,7 +153,7 @@ func (b *Buffer) SaveAs(filename string) error {
err := ioutil.WriteFile(filename, data, 0644)
if err == nil {
b.IsModified = false
err = b.Serialize()
return b.Serialize()
}
return err
}

View file

@ -74,6 +74,11 @@ func (c *Cursor) Clamp() {
}
}
func (c *Cursor) Goto(b Cursor) {
c.X, c.Y, c.LastVisualX = b.X, b.Y, b.LastVisualX
c.OrigSelection, c.CurSelection = b.OrigSelection, b.CurSelection
}
// 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

View file

@ -15,42 +15,42 @@ const (
// TextEvent holds data for a manipulation on some text that can be undone
type TextEvent struct {
c Cursor
C Cursor
eventType int
text string
start int
end int
time time.Time
EventType int
Text string
Start int
End int
Time time.Time
}
// ExecuteTextEvent runs a text event
func ExecuteTextEvent(t *TextEvent, buf *Buffer) {
if t.eventType == TextEventInsert {
buf.insert(t.start, t.text)
} else if t.eventType == TextEventRemove {
t.text = buf.remove(t.start, t.end)
if t.EventType == TextEventInsert {
buf.insert(t.Start, t.Text)
} else if t.EventType == TextEventRemove {
t.Text = buf.remove(t.Start, t.End)
}
}
// UndoTextEvent undoes a text event
func UndoTextEvent(t *TextEvent, buf *Buffer) {
t.eventType = -t.eventType
t.EventType = -t.EventType
ExecuteTextEvent(t, buf)
}
// EventHandler executes text manipulations and allows undoing and redoing
type EventHandler struct {
buf *Buffer
undo *Stack
redo *Stack
buf *Buffer
UndoStack *Stack
RedoStack *Stack
}
// NewEventHandler returns a new EventHandler
func NewEventHandler(buf *Buffer) *EventHandler {
eh := new(EventHandler)
eh.undo = new(Stack)
eh.redo = new(Stack)
eh.UndoStack = new(Stack)
eh.RedoStack = new(Stack)
eh.buf = buf
return eh
}
@ -58,12 +58,12 @@ func NewEventHandler(buf *Buffer) *EventHandler {
// Insert creates an insert text event and executes it
func (eh *EventHandler) Insert(start int, text string) {
e := &TextEvent{
c: eh.buf.Cursor,
eventType: TextEventInsert,
text: text,
start: start,
end: start + Count(text),
time: time.Now(),
C: eh.buf.Cursor,
EventType: TextEventInsert,
Text: text,
Start: start,
End: start + Count(text),
Time: time.Now(),
}
eh.Execute(e)
}
@ -71,11 +71,11 @@ 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.buf.Cursor,
eventType: TextEventRemove,
start: start,
end: end,
time: time.Now(),
C: eh.buf.Cursor,
EventType: TextEventRemove,
Start: start,
End: end,
Time: time.Now(),
}
eh.Execute(e)
}
@ -88,38 +88,34 @@ func (eh *EventHandler) Replace(start, end int, replace string) {
// Execute a textevent and add it to the undo stack
func (eh *EventHandler) Execute(t *TextEvent) {
if eh.redo.Len() > 0 {
eh.redo = new(Stack)
if eh.RedoStack.Len() > 0 {
eh.RedoStack = new(Stack)
}
eh.undo.Push(t)
eh.UndoStack.Push(t)
ExecuteTextEvent(t, eh.buf)
}
// Undo the first event in the undo stack
func (eh *EventHandler) Undo() {
t := eh.undo.Peek()
t := eh.UndoStack.Peek()
if t == nil {
return
}
te := t.(*TextEvent)
startTime := t.(*TextEvent).time.UnixNano() / int64(time.Millisecond)
startTime := t.Time.UnixNano() / int64(time.Millisecond)
eh.UndoOneEvent()
for {
t = eh.undo.Peek()
t = eh.UndoStack.Peek()
if t == nil {
return
}
te = t.(*TextEvent)
if startTime-(te.time.UnixNano()/int64(time.Millisecond)) > undoThreshold {
if startTime-(t.Time.UnixNano()/int64(time.Millisecond)) > undoThreshold {
return
} else {
startTime = t.(*TextEvent).time.UnixNano() / int64(time.Millisecond)
}
startTime = t.Time.UnixNano() / int64(time.Millisecond)
eh.UndoOneEvent()
}
@ -129,46 +125,42 @@ func (eh *EventHandler) Undo() {
func (eh *EventHandler) UndoOneEvent() {
// This event should be undone
// Pop it off the stack
t := eh.undo.Pop()
t := eh.UndoStack.Pop()
if t == nil {
return
}
te := t.(*TextEvent)
// Undo it
// Modifies the text event
UndoTextEvent(te, eh.buf)
UndoTextEvent(t, eh.buf)
// Set the cursor in the right place
teCursor := te.c
te.c = eh.buf.Cursor
eh.buf.Cursor = teCursor
teCursor := t.C
t.C = eh.buf.Cursor
eh.buf.Cursor.Goto(teCursor)
// Push it to the redo stack
eh.redo.Push(te)
eh.RedoStack.Push(t)
}
// Redo the first event in the redo stack
func (eh *EventHandler) Redo() {
t := eh.redo.Peek()
t := eh.RedoStack.Peek()
if t == nil {
return
}
te := t.(*TextEvent)
startTime := t.(*TextEvent).time.UnixNano() / int64(time.Millisecond)
startTime := t.Time.UnixNano() / int64(time.Millisecond)
eh.RedoOneEvent()
for {
t = eh.redo.Peek()
t = eh.RedoStack.Peek()
if t == nil {
return
}
te = t.(*TextEvent)
if (te.time.UnixNano()/int64(time.Millisecond))-startTime > undoThreshold {
if (t.Time.UnixNano()/int64(time.Millisecond))-startTime > undoThreshold {
return
}
@ -178,18 +170,17 @@ func (eh *EventHandler) Redo() {
// RedoOneEvent redoes one event
func (eh *EventHandler) RedoOneEvent() {
t := eh.redo.Pop()
t := eh.RedoStack.Pop()
if t == nil {
return
}
te := t.(*TextEvent)
// Modifies the text event
UndoTextEvent(te, eh.buf)
UndoTextEvent(t, eh.buf)
teCursor := te.c
te.c = eh.buf.Cursor
eh.buf.Cursor = teCursor
teCursor := t.C
t.C = eh.buf.Cursor
eh.buf.Cursor.Goto(teCursor)
eh.undo.Push(te)
eh.UndoStack.Push(t)
}

File diff suppressed because one or more lines are too long

View file

@ -78,6 +78,7 @@ func DefaultSettings() map[string]interface{} {
"indentchar": " ",
"ruler": true,
"savecursor": false,
"saveundo": false,
"scrollspeed": float64(2),
"scrollmargin": float64(3),
"statusline": true,

View file

@ -1,43 +1,43 @@
package main
// Stack is a simple implementation of a LIFO stack
// Stack is a simple implementation of a LIFO stack for text events
type Stack struct {
top *Element
size int
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
Value *TextEvent
Next *Element
}
// Len returns the stack's length
func (s *Stack) Len() int {
return s.size
return s.Size
}
// Push a new element onto the stack
func (s *Stack) Push(value interface{}) {
s.top = &Element{value, s.top}
s.size++
func (s *Stack) Push(value *TextEvent) {
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--
func (s *Stack) Pop() (value *TextEvent) {
if s.Size > 0 {
value, s.Top = s.Top.Value, s.Top.Next
s.Size--
return
}
return nil
}
// Peek returns the top element of the stack without removing it
func (s *Stack) Peek() interface{} {
if s.size > 0 {
return s.top.value
func (s *Stack) Peek() *TextEvent {
if s.Size > 0 {
return s.Top.Value
}
return nil
}

View file

@ -1,39 +0,0 @@
package main
import "testing"
func TestStack(t *testing.T) {
stack := new(Stack)
if stack.Len() != 0 {
t.Errorf("Len failed")
}
stack.Push(5)
stack.Push("test")
stack.Push(10)
if stack.Len() != 3 {
t.Errorf("Len failed")
}
var popped interface{}
popped = stack.Pop()
if popped != 10 {
t.Errorf("Pop failed")
}
popped = stack.Pop()
if popped != "test" {
t.Errorf("Pop failed")
}
stack.Push("test")
popped = stack.Pop()
if popped != "test" {
t.Errorf("Pop failed")
}
stack.Pop()
popped = stack.Pop()
if popped != nil {
t.Errorf("Pop failed")
}
}

View file

@ -219,6 +219,11 @@ Here are the options that you can set:
default value: `off`
* `saveundo`: when this option is on, undo is saved even after you close a file
so if you close and reopen a file, you can keep undoing
default value: `off`
* `scrollmargin`: amount of lines you would like to see above and below the cursor
default value: `3`

View file

@ -10,12 +10,12 @@
- [ ] Horizontal splits
- [ ] Vertical splits
- [ ] Persistent undo/redo (saved between open and closing micro)
- [ ] Wrap lines
### Done
- [x] Persistent undo/redo (saved between open and closing micro)
- [x] Auto indent
- [x] Custom bindings