mirror of
https://github.com/zyedidia/micro.git
synced 2026-03-29 22:27:13 +09:00
Add persistent undo as the option
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`
|
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`
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user