diff --git a/cmd/micro/initlua.go b/cmd/micro/initlua.go index 7eac563763..513007c031 100644 --- a/cmd/micro/initlua.go +++ b/cmd/micro/initlua.go @@ -157,6 +157,7 @@ func luaImportMicroUtil() *lua.LTable { ulua.L.SetField(pkg, "SemVersion", luar.New(ulua.L, util.SemVersion)) ulua.L.SetField(pkg, "HttpRequest", luar.New(ulua.L, util.HttpRequest)) ulua.L.SetField(pkg, "CharacterCountInString", luar.New(ulua.L, util.CharacterCountInString)) + ulua.L.SetField(pkg, "GetTextLengthAfterLastLinebreak", luar.New(ulua.L, util.GetTextLengthAfterLastLinebreak)) ulua.L.SetField(pkg, "RuneStr", luar.New(ulua.L, func(r rune) string { return string(r) })) diff --git a/internal/action/actions.go b/internal/action/actions.go index be2c1c05ee..4a635d8993 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -1618,11 +1618,11 @@ func (h *BufPane) paste(clip string) { } if h.Cursor.HasSelection() { - h.Cursor.DeleteSelection() - h.Cursor.ResetSelection() + h.Buf.Replace(h.Cursor.CurSelection[0], h.Cursor.CurSelection[1], clip) + } else { + h.Buf.Insert(h.Cursor.Loc, clip) } - h.Buf.Insert(h.Cursor.Loc, clip) // h.Cursor.Loc = h.Cursor.Loc.Move(Count(clip), h.Buf) h.freshClip = false InfoBar.Message("Pasted clipboard") diff --git a/internal/action/bufpane.go b/internal/action/bufpane.go index 3f0c4b1eec..c03d6f6a1d 100644 --- a/internal/action/bufpane.go +++ b/internal/action/bufpane.go @@ -627,17 +627,21 @@ func (h *BufPane) DoRuneInsert(r rune) { if !h.PluginCB("preRune", string(r)) { continue } - if c.HasSelection() { - c.DeleteSelection() - c.ResetSelection() - } if h.Buf.OverwriteMode { + if c.HasSelection() { + c.DeleteSelection() + c.ResetSelection() + } next := c.Loc next.X++ h.Buf.Replace(c.Loc, next, string(r)) } else { - h.Buf.Insert(c.Loc, string(r)) + if c.HasSelection() { + h.Buf.Replace(c.CurSelection[0], c.CurSelection[1], string(r)) + } else { + h.Buf.Insert(c.Loc, string(r)) + } } if recordingMacro { curmacro = append(curmacro, r) diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index ce36988bb1..5f018aa698 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -123,13 +123,13 @@ type SharedBuffer struct { origHash [md5.Size]byte } -func (b *SharedBuffer) insert(pos Loc, value []byte) { +func (b *SharedBuffer) insert(pos Loc, value []byte) Loc { b.HasSuggestions = false - b.LineArray.insert(pos, value) + endPos := b.LineArray.insert(pos, value) b.setModified() - inslines := bytes.Count(value, []byte{'\n'}) - b.MarkModified(pos.Y, pos.Y+inslines) + b.MarkModified(pos.Y, endPos.Y) + return endPos } func (b *SharedBuffer) remove(start, end Loc) []byte { @@ -573,6 +573,15 @@ func (b *Buffer) Remove(start, end Loc) { } } +// Replace replaces the characters between the start and end locations with the given text +func (b *Buffer) Replace(start, end Loc, text string) { + if !b.Type.Readonly { + b.EventHandler.cursors = b.cursors + b.EventHandler.active = b.curCursor + b.EventHandler.Replace(start, end, text) + } +} + // FileType returns the buffer's filetype func (b *Buffer) FileType() string { return b.Settings["filetype"].(string) diff --git a/internal/buffer/eventhandler.go b/internal/buffer/eventhandler.go index e739f25011..20ef1625af 100644 --- a/internal/buffer/eventhandler.go +++ b/internal/buffer/eventhandler.go @@ -43,7 +43,8 @@ type Delta struct { // DoTextEvent runs a text event func (eh *EventHandler) DoTextEvent(t *TextEvent, useUndo bool) { - oldl := eh.buf.LinesNum() + oldend := t.Deltas[0].End + oldtext := t.Deltas[0].Text if useUndo { eh.Execute(t) @@ -55,54 +56,60 @@ func (eh *EventHandler) DoTextEvent(t *TextEvent, useUndo bool) { return } + moveCursorInsert := func(loc, start, end Loc, textX int, isMultiLine bool) Loc { + 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 isMultiLine { + loc.X += textX - start.X + } else { + loc.X += textX + } + } + return loc + } + + moveCursorRemove := func(loc, start, end Loc) Loc { + 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 + } + text := t.Deltas[0].Text start := t.Deltas[0].Start - lastnl := -1 - var endX int + end := t.Deltas[0].End var textX int + var isMultiLine bool 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 + textX, isMultiLine = util.GetTextLengthAfterLastLinebreak(text) + } else if t.EventType == TextEventReplace { + textX, isMultiLine = util.GetTextLengthAfterLastLinebreak(oldtext) + } + + moveCursor := func(loc Loc) Loc { + if t.EventType == TextEventInsert { + return moveCursorInsert(loc, start, end, textX, isMultiLine) + } else if t.EventType == TextEventRemove { + return moveCursorRemove(loc, start, end) } else { - endX = start.X + textcount - textX = textcount + loc = moveCursorRemove(loc, start, oldend) + return moveCursorInsert(loc, start, end, textX, isMultiLine) } - 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 - } + if c.Num < t.C.Num { + continue } - 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.Loc = moveCursor(c.Loc) + c.CurSelection[0] = moveCursor(c.CurSelection[0]) + c.CurSelection[1] = moveCursor(c.CurSelection[1]) + c.OrigSelection[0] = moveCursor(c.OrigSelection[0]) + c.OrigSelection[1] = moveCursor(c.OrigSelection[1]) c.Relocate() c.StoreVisualX() } @@ -115,8 +122,8 @@ func (eh *EventHandler) DoTextEvent(t *TextEvent, useUndo bool) { // 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) + for i, d := range t.Deltas { + t.Deltas[i].End = buf.insert(d.Start, d.Text) } } else if t.EventType == TextEventRemove { for i, d := range t.Deltas { @@ -125,9 +132,8 @@ func ExecuteTextEvent(t *TextEvent, buf *SharedBuffer) { } 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} + t.Deltas[i].End = buf.insert(d.Start, d.Text) } 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] @@ -230,8 +236,14 @@ func (eh *EventHandler) MultipleReplace(deltas []Delta) { // 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) + text := []byte(replace) + e := &TextEvent{ + C: *eh.cursors[eh.active], + EventType: TextEventReplace, + Deltas: []Delta{{text, start, end}}, + Time: time.Now(), + } + eh.DoTextEvent(e, true) } // Execute a textevent and add it to the undo stack @@ -274,6 +286,11 @@ func (eh *EventHandler) Undo() bool { } eh.UndoOneEvent() + + t = eh.UndoStack.Peek() + if t == nil || t.EventType == TextEventReplace { + break + } } return true } @@ -321,6 +338,11 @@ func (eh *EventHandler) Redo() bool { } eh.RedoOneEvent() + + t = eh.UndoStack.Peek() + if t == nil || t.EventType == TextEventReplace { + break + } } return true } diff --git a/internal/buffer/line_array.go b/internal/buffer/line_array.go index b65213b805..c4e3ceb13b 100644 --- a/internal/buffer/line_array.go +++ b/internal/buffer/line_array.go @@ -200,8 +200,8 @@ func (la *LineArray) newlineBelow(y int) { } } -// Inserts a byte array at a given location -func (la *LineArray) insert(pos Loc, value []byte) { +// Inserts a byte array at a given location and returns the location where the insertion ends +func (la *LineArray) insert(pos Loc, value []byte) Loc { la.lock.Lock() defer la.lock.Unlock() @@ -221,6 +221,7 @@ func (la *LineArray) insert(pos Loc, value []byte) { la.insertByte(Loc{x, y}, value[i]) x++ } + return Loc{x, y} } // InsertByte inserts a byte at a given location diff --git a/internal/util/util.go b/internal/util/util.go index d0f2002283..f37f81240b 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -521,6 +521,18 @@ func HasTrailingWhitespace(b []byte) bool { return IsWhitespace(r) } +// GetTextLengthAfterLastLinebreak returns the length of the remaining +// characters after the last line break in the given byte array and whether it +// contains line breaks. +// If no line breaks were found, it returns the length of all characters in the byte array. +func GetTextLengthAfterLastLinebreak(text []byte) (int, bool) { + lastnl := bytes.LastIndex(text, []byte{'\n'}) + if lastnl >= 0 { + return CharacterCount(text[lastnl+1:]), true + } + return CharacterCount(text), false +} + // IntOpt turns a float64 setting to an int func IntOpt(opt any) int { return int(opt.(float64))