mirror of
https://github.com/zyedidia/micro.git
synced 2025-06-18 14:55:38 -04:00

Since we already have the StoreVisualX() helper, use it all over the place instead of setting LastVisualX directly. This will allow us to add more logic to StoreVisualX() add let this extra logic apply everywhere automatically.
400 lines
9.9 KiB
Go
400 lines
9.9 KiB
Go
package buffer
|
|
|
|
import (
|
|
"bytes"
|
|
"time"
|
|
|
|
dmp "github.com/sergi/go-diff/diffmatchpatch"
|
|
"github.com/zyedidia/micro/v2/internal/config"
|
|
ulua "github.com/zyedidia/micro/v2/internal/lua"
|
|
"github.com/zyedidia/micro/v2/internal/screen"
|
|
"github.com/zyedidia/micro/v2/internal/util"
|
|
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
|
|
}
|
|
|
|
// DoTextEvent runs a text event
|
|
func (eh *EventHandler) DoTextEvent(t *TextEvent, useUndo bool) {
|
|
oldl := eh.buf.LinesNum()
|
|
|
|
if useUndo {
|
|
eh.Execute(t)
|
|
} else {
|
|
ExecuteTextEvent(t, eh.buf)
|
|
}
|
|
|
|
if len(t.Deltas) != 1 {
|
|
return
|
|
}
|
|
|
|
text := t.Deltas[0].Text
|
|
start := t.Deltas[0].Start
|
|
lastnl := -1
|
|
var endX int
|
|
var textX int
|
|
if t.EventType == TextEventInsert {
|
|
linecount := eh.buf.LinesNum() - oldl
|
|
textcount := util.CharacterCount(text)
|
|
lastnl = bytes.LastIndex(text, []byte{'\n'})
|
|
if lastnl >= 0 {
|
|
endX = util.CharacterCount(text[lastnl+1:])
|
|
textX = endX
|
|
} else {
|
|
endX = start.X + textcount
|
|
textX = textcount
|
|
}
|
|
t.Deltas[0].End = clamp(Loc{endX, start.Y + linecount}, eh.buf.LineArray)
|
|
}
|
|
end := t.Deltas[0].End
|
|
|
|
for _, c := range eh.cursors {
|
|
move := func(loc Loc) Loc {
|
|
if t.EventType == TextEventInsert {
|
|
if start.Y != loc.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 - start.X
|
|
} else {
|
|
loc.X += textX
|
|
}
|
|
}
|
|
return loc
|
|
} else {
|
|
if loc.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.Relocate()
|
|
c.StoreVisualX()
|
|
}
|
|
|
|
if useUndo {
|
|
eh.updateTrailingWs(t)
|
|
}
|
|
}
|
|
|
|
// 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 + util.CharacterCount(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 (eh *EventHandler) UndoTextEvent(t *TextEvent) {
|
|
t.EventType = -t.EventType
|
|
eh.DoTextEvent(t, false)
|
|
}
|
|
|
|
// 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(util.CharacterCountInString(d.Text), eh.buf.LineArray))
|
|
} else {
|
|
if d.Type == dmp.DiffInsert {
|
|
eh.Insert(loc, d.Text)
|
|
}
|
|
loc = loc.MoveLA(util.CharacterCountInString(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) {
|
|
if len(text) == 0 {
|
|
return
|
|
}
|
|
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(),
|
|
}
|
|
eh.DoTextEvent(e, true)
|
|
}
|
|
|
|
// Remove creates a remove text event and executes it
|
|
func (eh *EventHandler) Remove(start, end Loc) {
|
|
if start == end {
|
|
return
|
|
}
|
|
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.DoTextEvent(e, true)
|
|
}
|
|
|
|
// 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(nil, "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. Returns false if the stack is empty.
|
|
func (eh *EventHandler) Undo() bool {
|
|
t := eh.UndoStack.Peek()
|
|
if t == nil {
|
|
return false
|
|
}
|
|
|
|
startTime := t.Time.UnixNano() / int64(time.Millisecond)
|
|
endTime := startTime - (startTime % undoThreshold)
|
|
|
|
for {
|
|
t = eh.UndoStack.Peek()
|
|
if t == nil {
|
|
break
|
|
}
|
|
|
|
if t.Time.UnixNano()/int64(time.Millisecond) < endTime {
|
|
break
|
|
}
|
|
|
|
eh.UndoOneEvent()
|
|
}
|
|
return true
|
|
}
|
|
|
|
// 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
|
|
eh.UndoTextEvent(t)
|
|
|
|
// Set the cursor in the right place
|
|
if t.C.Num >= 0 && t.C.Num < len(eh.cursors) {
|
|
eh.cursors[t.C.Num].Goto(t.C)
|
|
eh.cursors[t.C.Num].NewTrailingWsY = t.C.NewTrailingWsY
|
|
}
|
|
|
|
// Push it to the redo stack
|
|
eh.RedoStack.Push(t)
|
|
}
|
|
|
|
// Redo the first event in the redo stack. Returns false if the stack is empty.
|
|
func (eh *EventHandler) Redo() bool {
|
|
t := eh.RedoStack.Peek()
|
|
if t == nil {
|
|
return false
|
|
}
|
|
|
|
startTime := t.Time.UnixNano() / int64(time.Millisecond)
|
|
endTime := startTime - (startTime % undoThreshold) + undoThreshold
|
|
|
|
for {
|
|
t = eh.RedoStack.Peek()
|
|
if t == nil {
|
|
break
|
|
}
|
|
|
|
if t.Time.UnixNano()/int64(time.Millisecond) > endTime {
|
|
break
|
|
}
|
|
|
|
eh.RedoOneEvent()
|
|
}
|
|
return true
|
|
}
|
|
|
|
// RedoOneEvent redoes one event
|
|
func (eh *EventHandler) RedoOneEvent() {
|
|
t := eh.RedoStack.Pop()
|
|
if t == nil {
|
|
return
|
|
}
|
|
|
|
if t.C.Num >= 0 && t.C.Num < len(eh.cursors) {
|
|
eh.cursors[t.C.Num].Goto(t.C)
|
|
eh.cursors[t.C.Num].NewTrailingWsY = t.C.NewTrailingWsY
|
|
}
|
|
|
|
// Modifies the text event
|
|
eh.UndoTextEvent(t)
|
|
|
|
eh.UndoStack.Push(t)
|
|
}
|
|
|
|
// updateTrailingWs updates the cursor's trailing whitespace status after a text event
|
|
func (eh *EventHandler) updateTrailingWs(t *TextEvent) {
|
|
if len(t.Deltas) != 1 {
|
|
return
|
|
}
|
|
text := t.Deltas[0].Text
|
|
start := t.Deltas[0].Start
|
|
end := t.Deltas[0].End
|
|
|
|
c := eh.cursors[eh.active]
|
|
isEol := func(loc Loc) bool {
|
|
return loc.X == util.CharacterCount(eh.buf.LineBytes(loc.Y))
|
|
}
|
|
if t.EventType == TextEventInsert && c.Loc == end && isEol(end) {
|
|
var addedTrailingWs bool
|
|
addedAfterWs := false
|
|
addedWsOnly := false
|
|
if start.Y == end.Y {
|
|
addedTrailingWs = util.HasTrailingWhitespace(text)
|
|
addedWsOnly = util.IsBytesWhitespace(text)
|
|
addedAfterWs = start.X > 0 && util.IsWhitespace(c.buf.RuneAt(Loc{start.X - 1, start.Y}))
|
|
} else {
|
|
lastnl := bytes.LastIndex(text, []byte{'\n'})
|
|
addedTrailingWs = util.HasTrailingWhitespace(text[lastnl+1:])
|
|
}
|
|
|
|
if addedTrailingWs && !(addedAfterWs && addedWsOnly) {
|
|
c.NewTrailingWsY = c.Y
|
|
} else if !addedTrailingWs {
|
|
c.NewTrailingWsY = -1
|
|
}
|
|
} else if t.EventType == TextEventRemove && c.Loc == start && isEol(start) {
|
|
removedAfterWs := util.HasTrailingWhitespace(eh.buf.LineBytes(start.Y))
|
|
var removedWsOnly bool
|
|
if start.Y == end.Y {
|
|
removedWsOnly = util.IsBytesWhitespace(text)
|
|
} else {
|
|
firstnl := bytes.Index(text, []byte{'\n'})
|
|
removedWsOnly = util.IsBytesWhitespace(text[:firstnl])
|
|
}
|
|
|
|
if removedAfterWs && !removedWsOnly {
|
|
c.NewTrailingWsY = c.Y
|
|
} else if !removedAfterWs {
|
|
c.NewTrailingWsY = -1
|
|
}
|
|
} else if c.NewTrailingWsY != -1 && start.Y != end.Y && c.Loc.GreaterThan(start) &&
|
|
((t.EventType == TextEventInsert && c.Y == c.NewTrailingWsY+(end.Y-start.Y)) ||
|
|
(t.EventType == TextEventRemove && c.Y == c.NewTrailingWsY-(end.Y-start.Y))) {
|
|
// The cursor still has its new trailingws
|
|
// but its line number was shifted by insert or remove of lines above
|
|
c.NewTrailingWsY = c.Y
|
|
}
|
|
}
|