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 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` // NewBuffer creates a new buffer from `txt` with path and name `path`
func NewBuffer(txt, path string) *Buffer { func NewBuffer(txt, path string) *Buffer {
b := new(Buffer) b := new(Buffer)
@@ -61,35 +67,36 @@ func NewBuffer(txt, path string) *Buffer {
os.Mkdir(configDir+"/buffers/", os.ModePerm) 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) absPath, _ := filepath.Abs(b.Path)
file, err := os.Open(configDir + "/buffers/" + EscapePath(absPath)) file, err := os.Open(configDir + "/buffers/" + EscapePath(absPath))
if err == nil { if err == nil {
var cursor Cursor var buffer SerializedBuffer
decoder := gob.NewDecoder(file) decoder := gob.NewDecoder(file)
err = decoder.Decode(&cursor) gob.Register(TextEvent{})
err = decoder.Decode(&buffer)
if err != nil { if err != nil {
TermMessage(err.Error()) TermMessage(err.Error())
} }
b.Cursor = cursor if settings["savecursor"].(bool) {
b.Cursor.buf = b b.Cursor = buffer.Cursor
b.Cursor.Clamp() b.Cursor.buf = b
} else { b.Cursor.Clamp()
// Put the cursor at the first spot }
b.Cursor = Cursor{
X: 0, if settings["saveundo"].(bool) {
Y: 0, b.EventHandler = buffer.EventHandler
buf: b, b.EventHandler.buf = b
} }
} }
file.Close() file.Close()
} else {
// Put the cursor at the first spot
b.Cursor = Cursor{
X: 0,
Y: 0,
buf: b,
}
} }
return b return b
@@ -121,12 +128,17 @@ func (b *Buffer) Save() error {
// Serialize serializes the buffer to configDir/buffers // Serialize serializes the buffer to configDir/buffers
func (b *Buffer) Serialize() error { func (b *Buffer) Serialize() error {
if settings["savecursor"].(bool) { if settings["savecursor"].(bool) || settings["saveundo"].(bool) {
absPath, _ := filepath.Abs(b.Path) absPath, _ := filepath.Abs(b.Path)
file, err := os.Create(configDir + "/buffers/" + EscapePath(absPath)) file, err := os.Create(configDir + "/buffers/" + EscapePath(absPath))
if err == nil { if err == nil {
enc := gob.NewEncoder(file) 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() file.Close()
return err return err
@@ -141,7 +153,7 @@ func (b *Buffer) SaveAs(filename string) error {
err := ioutil.WriteFile(filename, data, 0644) err := ioutil.WriteFile(filename, data, 0644)
if err == nil { if err == nil {
b.IsModified = false b.IsModified = false
err = b.Serialize() return b.Serialize()
} }
return err 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 // SetLoc sets the location of the cursor in terms of character number
// and not x, y location // and not x, y location
// It's just a simple wrapper of FromCharPos // 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 // TextEvent holds data for a manipulation on some text that can be undone
type TextEvent struct { type TextEvent struct {
c Cursor C Cursor
eventType int EventType int
text string Text string
start int Start int
end int End int
time time.Time Time time.Time
} }
// ExecuteTextEvent runs a text event // ExecuteTextEvent runs a text event
func ExecuteTextEvent(t *TextEvent, buf *Buffer) { func ExecuteTextEvent(t *TextEvent, buf *Buffer) {
if t.eventType == TextEventInsert { if t.EventType == TextEventInsert {
buf.insert(t.start, t.text) buf.insert(t.Start, t.Text)
} else if t.eventType == TextEventRemove { } else if t.EventType == TextEventRemove {
t.text = buf.remove(t.start, t.end) t.Text = buf.remove(t.Start, t.End)
} }
} }
// UndoTextEvent undoes a text event // UndoTextEvent undoes a text event
func UndoTextEvent(t *TextEvent, buf *Buffer) { func UndoTextEvent(t *TextEvent, buf *Buffer) {
t.eventType = -t.eventType t.EventType = -t.EventType
ExecuteTextEvent(t, buf) ExecuteTextEvent(t, buf)
} }
// EventHandler executes text manipulations and allows undoing and redoing // EventHandler executes text manipulations and allows undoing and redoing
type EventHandler struct { type EventHandler struct {
buf *Buffer buf *Buffer
undo *Stack UndoStack *Stack
redo *Stack RedoStack *Stack
} }
// NewEventHandler returns a new EventHandler // NewEventHandler returns a new EventHandler
func NewEventHandler(buf *Buffer) *EventHandler { func NewEventHandler(buf *Buffer) *EventHandler {
eh := new(EventHandler) eh := new(EventHandler)
eh.undo = new(Stack) eh.UndoStack = new(Stack)
eh.redo = new(Stack) eh.RedoStack = new(Stack)
eh.buf = buf eh.buf = buf
return eh return eh
} }
@@ -58,12 +58,12 @@ func NewEventHandler(buf *Buffer) *EventHandler {
// Insert creates an insert text event and executes it // Insert creates an insert text event and executes it
func (eh *EventHandler) Insert(start int, text string) { func (eh *EventHandler) Insert(start int, text string) {
e := &TextEvent{ e := &TextEvent{
c: eh.buf.Cursor, C: eh.buf.Cursor,
eventType: TextEventInsert, EventType: TextEventInsert,
text: text, Text: text,
start: start, Start: start,
end: start + Count(text), End: start + Count(text),
time: time.Now(), Time: time.Now(),
} }
eh.Execute(e) eh.Execute(e)
} }
@@ -71,11 +71,11 @@ func (eh *EventHandler) Insert(start int, text string) {
// Remove creates a remove text event and executes it // Remove creates a remove text event and executes it
func (eh *EventHandler) Remove(start, end int) { func (eh *EventHandler) Remove(start, end int) {
e := &TextEvent{ e := &TextEvent{
c: eh.buf.Cursor, C: eh.buf.Cursor,
eventType: TextEventRemove, EventType: TextEventRemove,
start: start, Start: start,
end: end, End: end,
time: time.Now(), Time: time.Now(),
} }
eh.Execute(e) 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 // Execute a textevent and add it to the undo stack
func (eh *EventHandler) Execute(t *TextEvent) { func (eh *EventHandler) Execute(t *TextEvent) {
if eh.redo.Len() > 0 { if eh.RedoStack.Len() > 0 {
eh.redo = new(Stack) eh.RedoStack = new(Stack)
} }
eh.undo.Push(t) eh.UndoStack.Push(t)
ExecuteTextEvent(t, eh.buf) ExecuteTextEvent(t, eh.buf)
} }
// Undo the first event in the undo stack // Undo the first event in the undo stack
func (eh *EventHandler) Undo() { func (eh *EventHandler) Undo() {
t := eh.undo.Peek() t := eh.UndoStack.Peek()
if t == nil { if t == nil {
return return
} }
te := t.(*TextEvent) startTime := t.Time.UnixNano() / int64(time.Millisecond)
startTime := t.(*TextEvent).time.UnixNano() / int64(time.Millisecond)
eh.UndoOneEvent() eh.UndoOneEvent()
for { for {
t = eh.undo.Peek() t = eh.UndoStack.Peek()
if t == nil { if t == nil {
return return
} }
te = t.(*TextEvent) if startTime-(t.Time.UnixNano()/int64(time.Millisecond)) > undoThreshold {
if startTime-(te.time.UnixNano()/int64(time.Millisecond)) > undoThreshold {
return return
} else {
startTime = t.(*TextEvent).time.UnixNano() / int64(time.Millisecond)
} }
startTime = t.Time.UnixNano() / int64(time.Millisecond)
eh.UndoOneEvent() eh.UndoOneEvent()
} }
@@ -129,46 +125,42 @@ func (eh *EventHandler) Undo() {
func (eh *EventHandler) UndoOneEvent() { func (eh *EventHandler) UndoOneEvent() {
// This event should be undone // This event should be undone
// Pop it off the stack // Pop it off the stack
t := eh.undo.Pop() t := eh.UndoStack.Pop()
if t == nil { if t == nil {
return return
} }
te := t.(*TextEvent)
// Undo it // Undo it
// Modifies the text event // Modifies the text event
UndoTextEvent(te, eh.buf) UndoTextEvent(t, eh.buf)
// Set the cursor in the right place // Set the cursor in the right place
teCursor := te.c teCursor := t.C
te.c = eh.buf.Cursor t.C = eh.buf.Cursor
eh.buf.Cursor = teCursor eh.buf.Cursor.Goto(teCursor)
// Push it to the redo stack // Push it to the redo stack
eh.redo.Push(te) eh.RedoStack.Push(t)
} }
// Redo the first event in the redo stack // Redo the first event in the redo stack
func (eh *EventHandler) Redo() { func (eh *EventHandler) Redo() {
t := eh.redo.Peek() t := eh.RedoStack.Peek()
if t == nil { if t == nil {
return return
} }
te := t.(*TextEvent) startTime := t.Time.UnixNano() / int64(time.Millisecond)
startTime := t.(*TextEvent).time.UnixNano() / int64(time.Millisecond)
eh.RedoOneEvent() eh.RedoOneEvent()
for { for {
t = eh.redo.Peek() t = eh.RedoStack.Peek()
if t == nil { if t == nil {
return return
} }
te = t.(*TextEvent) if (t.Time.UnixNano()/int64(time.Millisecond))-startTime > undoThreshold {
if (te.time.UnixNano()/int64(time.Millisecond))-startTime > undoThreshold {
return return
} }
@@ -178,18 +170,17 @@ func (eh *EventHandler) Redo() {
// RedoOneEvent redoes one event // RedoOneEvent redoes one event
func (eh *EventHandler) RedoOneEvent() { func (eh *EventHandler) RedoOneEvent() {
t := eh.redo.Pop() t := eh.RedoStack.Pop()
if t == nil { if t == nil {
return return
} }
te := t.(*TextEvent)
// Modifies the text event // Modifies the text event
UndoTextEvent(te, eh.buf) UndoTextEvent(t, eh.buf)
teCursor := te.c teCursor := t.C
te.c = eh.buf.Cursor t.C = eh.buf.Cursor
eh.buf.Cursor = teCursor 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": " ", "indentchar": " ",
"ruler": true, "ruler": true,
"savecursor": false, "savecursor": false,
"saveundo": false,
"scrollspeed": float64(2), "scrollspeed": float64(2),
"scrollmargin": float64(3), "scrollmargin": float64(3),
"statusline": true, "statusline": true,

View File

@@ -1,43 +1,43 @@
package main 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 { type Stack struct {
top *Element Top *Element
size int Size int
} }
// An Element which is stored in the Stack // An Element which is stored in the Stack
type Element struct { type Element struct {
value interface{} // All types satisfy the empty interface, so we can store anything here. Value *TextEvent
next *Element Next *Element
} }
// Len returns the stack's length // Len returns the stack's length
func (s *Stack) Len() int { func (s *Stack) Len() int {
return s.size return s.Size
} }
// Push a new element onto the stack // Push a new element onto the stack
func (s *Stack) Push(value interface{}) { func (s *Stack) Push(value *TextEvent) {
s.top = &Element{value, s.top} s.Top = &Element{value, s.Top}
s.size++ s.Size++
} }
// Pop removes the top element from the stack and returns its value // Pop removes the top element from the stack and returns its value
// If the stack is empty, return nil // If the stack is empty, return nil
func (s *Stack) Pop() (value interface{}) { func (s *Stack) Pop() (value *TextEvent) {
if s.size > 0 { if s.Size > 0 {
value, s.top = s.top.value, s.top.next value, s.Top = s.Top.Value, s.Top.Next
s.size-- s.Size--
return return
} }
return nil return nil
} }
// Peek returns the top element of the stack without removing it // Peek returns the top element of the stack without removing it
func (s *Stack) Peek() interface{} { func (s *Stack) Peek() *TextEvent {
if s.size > 0 { if s.Size > 0 {
return s.top.value return s.Top.Value
} }
return nil 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` 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 * `scrollmargin`: amount of lines you would like to see above and below the cursor
default value: `3` default value: `3`

View File

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