Something went wrong. Try again.
Fork of micro-editor/micro
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400package buffer
import ( "bytes" "time"
"github.com/micro-editor/micro/v2/internal/config" ulua "github.com/micro-editor/micro/v2/internal/lua" "github.com/micro-editor/micro/v2/internal/screen" "github.com/micro-editor/micro/v2/internal/util" dmp "github.com/sergi/go-diff/diffmatchpatch" 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 undonetype TextEvent struct { C Cursor
EventType int Deltas []Delta Time time.Time}
// A Delta is a change to the buffertype Delta struct { Text []byte Start Loc End Loc}
// DoTextEvent runs a text eventfunc (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 eventfunc 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 eventfunc (eh *EventHandler) UndoTextEvent(t *TextEvent) { t.EventType = -t.EventType eh.DoTextEvent(t, false)}
// EventHandler executes text manipulations and allows undoing and redoingtype EventHandler struct { buf *SharedBuffer cursors []*Cursor active int UndoStack *TEStack RedoStack *TEStack}
// NewEventHandler returns a new EventHandlerfunc 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 eventsfunc (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 itfunc (eh *EventHandler) Insert(start Loc, textStr string) { text := []byte(textStr) eh.InsertBytes(start, text)}
// InsertBytes creates an insert text event and executes itfunc (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 itfunc (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 a replace text event with multiple deltas and executes itfunc (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 stringfunc (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 stackfunc (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 eventfunc (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 eventfunc (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 eventfunc (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 }}