Add persistent undo as the option
This commit is contained in:
parent
1fe18eecb7
commit
ee9f2a3d9c
9 changed files with 116 additions and 141 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -78,6 +78,7 @@ func DefaultSettings() map[string]interface{} {
|
|||
"indentchar": " ",
|
||||
"ruler": true,
|
||||
"savecursor": false,
|
||||
"saveundo": false,
|
||||
"scrollspeed": float64(2),
|
||||
"scrollmargin": float64(3),
|
||||
"statusline": true,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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`
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue