329 lines
7.8 KiB
Go
329 lines
7.8 KiB
Go
package buffer
|
|
|
|
import (
|
|
"bytes"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
dmp "github.com/sergi/go-diff/diffmatchpatch"
|
|
"github.com/zyedidia/micro/internal/config"
|
|
ulua "github.com/zyedidia/micro/internal/lua"
|
|
"github.com/zyedidia/micro/internal/screen"
|
|
luar "layeh.com/gopher-luar"
|
|
)
|
|
|
|
const (
|
|
// Opposite and undoing events must have opposite values
|
|
|
|
// TextEventInsert represents an insertion event
|
|
TextEventInsert = 1
|
|
// TextEventRemove represents a deletion event
|
|
TextEventRemove = -1
|
|
// TextEventReplace represents a replace event
|
|
TextEventReplace = 0
|
|
|
|
undoThreshold = 1000 // If two events are less than n milliseconds apart, undo both of them
|
|
)
|
|
|
|
// TextEvent holds data for a manipulation on some text that can be undone
|
|
type TextEvent struct {
|
|
C Cursor
|
|
|
|
EventType int
|
|
Deltas []Delta
|
|
Time time.Time
|
|
}
|
|
|
|
// A Delta is a change to the buffer
|
|
type Delta struct {
|
|
Text []byte
|
|
Start Loc
|
|
End Loc
|
|
}
|
|
|
|
// ExecuteTextEvent runs a text event
|
|
func ExecuteTextEvent(t *TextEvent, buf *SharedBuffer) {
|
|
if t.EventType == TextEventInsert {
|
|
for _, d := range t.Deltas {
|
|
buf.insert(d.Start, d.Text)
|
|
}
|
|
} else if t.EventType == TextEventRemove {
|
|
for i, d := range t.Deltas {
|
|
t.Deltas[i].Text = buf.remove(d.Start, d.End)
|
|
}
|
|
} else if t.EventType == TextEventReplace {
|
|
for i, d := range t.Deltas {
|
|
t.Deltas[i].Text = buf.remove(d.Start, d.End)
|
|
buf.insert(d.Start, d.Text)
|
|
t.Deltas[i].Start = d.Start
|
|
t.Deltas[i].End = Loc{d.Start.X + utf8.RuneCount(d.Text), d.Start.Y}
|
|
}
|
|
for i, j := 0, len(t.Deltas)-1; i < j; i, j = i+1, j-1 {
|
|
t.Deltas[i], t.Deltas[j] = t.Deltas[j], t.Deltas[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
// UndoTextEvent undoes a text event
|
|
func UndoTextEvent(t *TextEvent, buf *SharedBuffer) {
|
|
t.EventType = -t.EventType
|
|
ExecuteTextEvent(t, buf)
|
|
}
|
|
|
|
// EventHandler executes text manipulations and allows undoing and redoing
|
|
type EventHandler struct {
|
|
buf *SharedBuffer
|
|
cursors []*Cursor
|
|
active int
|
|
UndoStack *TEStack
|
|
RedoStack *TEStack
|
|
}
|
|
|
|
// NewEventHandler returns a new EventHandler
|
|
func NewEventHandler(buf *SharedBuffer, cursors []*Cursor) *EventHandler {
|
|
eh := new(EventHandler)
|
|
eh.UndoStack = new(TEStack)
|
|
eh.RedoStack = new(TEStack)
|
|
eh.buf = buf
|
|
eh.cursors = cursors
|
|
return eh
|
|
}
|
|
|
|
// ApplyDiff takes a string and runs the necessary insertion and deletion events to make
|
|
// the buffer equal to that string
|
|
// This means that we can transform the buffer into any string and still preserve undo/redo
|
|
// through insert and delete events
|
|
func (eh *EventHandler) ApplyDiff(new string) {
|
|
differ := dmp.New()
|
|
diff := differ.DiffMain(string(eh.buf.Bytes()), new, false)
|
|
loc := eh.buf.Start()
|
|
for _, d := range diff {
|
|
if d.Type == dmp.DiffDelete {
|
|
eh.Remove(loc, loc.MoveLA(utf8.RuneCountInString(d.Text), eh.buf.LineArray))
|
|
} else {
|
|
if d.Type == dmp.DiffInsert {
|
|
eh.Insert(loc, d.Text)
|
|
}
|
|
loc = loc.MoveLA(utf8.RuneCountInString(d.Text), eh.buf.LineArray)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Insert creates an insert text event and executes it
|
|
func (eh *EventHandler) Insert(start Loc, textStr string) {
|
|
text := []byte(textStr)
|
|
eh.InsertBytes(start, text)
|
|
}
|
|
|
|
// InsertBytes creates an insert text event and executes it
|
|
func (eh *EventHandler) InsertBytes(start Loc, text []byte) {
|
|
start = clamp(start, eh.buf.LineArray)
|
|
e := &TextEvent{
|
|
C: *eh.cursors[eh.active],
|
|
EventType: TextEventInsert,
|
|
Deltas: []Delta{{text, start, Loc{0, 0}}},
|
|
Time: time.Now(),
|
|
}
|
|
oldl := eh.buf.LinesNum()
|
|
eh.Execute(e)
|
|
linecount := eh.buf.LinesNum() - oldl
|
|
textcount := utf8.RuneCount(text)
|
|
lastnl := bytes.LastIndex(text, []byte{'\n'})
|
|
var endX int
|
|
var textX int
|
|
if lastnl >= 0 {
|
|
endX = utf8.RuneCount(text[lastnl+1:])
|
|
textX = endX
|
|
} else {
|
|
endX = start.X + textcount
|
|
textX = textcount
|
|
}
|
|
|
|
e.Deltas[0].End = clamp(Loc{endX, start.Y + linecount}, eh.buf.LineArray)
|
|
end := e.Deltas[0].End
|
|
|
|
for _, c := range eh.cursors {
|
|
move := func(loc Loc) Loc {
|
|
if start.Y != end.Y && loc.GreaterThan(start) {
|
|
loc.Y += end.Y - start.Y
|
|
} else if loc.Y == start.Y && loc.GreaterEqual(start) {
|
|
loc.Y += end.Y - start.Y
|
|
if lastnl >= 0 {
|
|
loc.X = textX
|
|
} else {
|
|
loc.X += textX
|
|
}
|
|
}
|
|
return loc
|
|
}
|
|
c.Loc = move(c.Loc)
|
|
c.Relocate()
|
|
c.CurSelection[0] = move(c.CurSelection[0])
|
|
c.CurSelection[1] = move(c.CurSelection[1])
|
|
c.OrigSelection[0] = move(c.OrigSelection[0])
|
|
c.OrigSelection[1] = move(c.OrigSelection[1])
|
|
c.LastVisualX = c.GetVisualX()
|
|
}
|
|
}
|
|
|
|
// Remove creates a remove text event and executes it
|
|
func (eh *EventHandler) Remove(start, end Loc) {
|
|
start = clamp(start, eh.buf.LineArray)
|
|
end = clamp(end, eh.buf.LineArray)
|
|
e := &TextEvent{
|
|
C: *eh.cursors[eh.active],
|
|
EventType: TextEventRemove,
|
|
Deltas: []Delta{{[]byte{}, start, end}},
|
|
Time: time.Now(),
|
|
}
|
|
eh.Execute(e)
|
|
|
|
for _, c := range eh.cursors {
|
|
move := func(loc Loc) Loc {
|
|
if start.Y != end.Y && loc.GreaterThan(end) {
|
|
loc.Y -= end.Y - start.Y
|
|
} else if loc.Y == end.Y && loc.GreaterEqual(end) {
|
|
loc = loc.MoveLA(-DiffLA(start, end, eh.buf.LineArray), eh.buf.LineArray)
|
|
}
|
|
return loc
|
|
}
|
|
c.Loc = move(c.Loc)
|
|
c.CurSelection[0] = move(c.CurSelection[0])
|
|
c.CurSelection[1] = move(c.CurSelection[1])
|
|
c.OrigSelection[0] = move(c.OrigSelection[0])
|
|
c.OrigSelection[1] = move(c.OrigSelection[1])
|
|
c.LastVisualX = c.GetVisualX()
|
|
}
|
|
}
|
|
|
|
// MultipleReplace creates an multiple insertions executes them
|
|
func (eh *EventHandler) MultipleReplace(deltas []Delta) {
|
|
e := &TextEvent{
|
|
C: *eh.cursors[eh.active],
|
|
EventType: TextEventReplace,
|
|
Deltas: deltas,
|
|
Time: time.Now(),
|
|
}
|
|
eh.Execute(e)
|
|
}
|
|
|
|
// Replace deletes from start to end and replaces it with the given string
|
|
func (eh *EventHandler) Replace(start, end Loc, replace string) {
|
|
eh.Remove(start, end)
|
|
eh.Insert(start, replace)
|
|
}
|
|
|
|
// Execute a textevent and add it to the undo stack
|
|
func (eh *EventHandler) Execute(t *TextEvent) {
|
|
if eh.RedoStack.Len() > 0 {
|
|
eh.RedoStack = new(TEStack)
|
|
}
|
|
eh.UndoStack.Push(t)
|
|
|
|
b, err := config.RunPluginFnBool("onBeforeTextEvent", luar.New(ulua.L, eh.buf), luar.New(ulua.L, t))
|
|
if err != nil {
|
|
screen.TermMessage(err)
|
|
}
|
|
|
|
if !b {
|
|
return
|
|
}
|
|
|
|
ExecuteTextEvent(t, eh.buf)
|
|
}
|
|
|
|
// Undo the first event in the undo stack
|
|
func (eh *EventHandler) Undo() {
|
|
t := eh.UndoStack.Peek()
|
|
if t == nil {
|
|
return
|
|
}
|
|
|
|
startTime := t.Time.UnixNano() / int64(time.Millisecond)
|
|
endTime := startTime - (startTime % undoThreshold)
|
|
|
|
for {
|
|
t = eh.UndoStack.Peek()
|
|
if t == nil {
|
|
return
|
|
}
|
|
|
|
if t.Time.UnixNano()/int64(time.Millisecond) < endTime {
|
|
return
|
|
}
|
|
|
|
eh.UndoOneEvent()
|
|
}
|
|
}
|
|
|
|
// UndoOneEvent undoes one event
|
|
func (eh *EventHandler) UndoOneEvent() {
|
|
// This event should be undone
|
|
// Pop it off the stack
|
|
t := eh.UndoStack.Pop()
|
|
if t == nil {
|
|
return
|
|
}
|
|
|
|
// Undo it
|
|
// Modifies the text event
|
|
UndoTextEvent(t, eh.buf)
|
|
|
|
// Set the cursor in the right place
|
|
teCursor := t.C
|
|
if teCursor.Num >= 0 && teCursor.Num < len(eh.cursors) {
|
|
t.C = *eh.cursors[teCursor.Num]
|
|
eh.cursors[teCursor.Num].Goto(teCursor)
|
|
} else {
|
|
teCursor.Num = -1
|
|
}
|
|
|
|
// Push it to the redo stack
|
|
eh.RedoStack.Push(t)
|
|
}
|
|
|
|
// Redo the first event in the redo stack
|
|
func (eh *EventHandler) Redo() {
|
|
t := eh.RedoStack.Peek()
|
|
if t == nil {
|
|
return
|
|
}
|
|
|
|
startTime := t.Time.UnixNano() / int64(time.Millisecond)
|
|
endTime := startTime - (startTime % undoThreshold) + undoThreshold
|
|
|
|
for {
|
|
t = eh.RedoStack.Peek()
|
|
if t == nil {
|
|
return
|
|
}
|
|
|
|
if t.Time.UnixNano()/int64(time.Millisecond) > endTime {
|
|
return
|
|
}
|
|
|
|
eh.RedoOneEvent()
|
|
}
|
|
}
|
|
|
|
// RedoOneEvent redoes one event
|
|
func (eh *EventHandler) RedoOneEvent() {
|
|
t := eh.RedoStack.Pop()
|
|
if t == nil {
|
|
return
|
|
}
|
|
|
|
// Modifies the text event
|
|
UndoTextEvent(t, eh.buf)
|
|
|
|
teCursor := t.C
|
|
if teCursor.Num >= 0 && teCursor.Num < len(eh.cursors) {
|
|
t.C = *eh.cursors[teCursor.Num]
|
|
eh.cursors[teCursor.Num].Goto(teCursor)
|
|
} else {
|
|
teCursor.Num = -1
|
|
}
|
|
|
|
eh.UndoStack.Push(t)
|
|
}
|