Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/micro/initlua.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}))
Expand Down
6 changes: 3 additions & 3 deletions internal/action/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines 1620 to +1623
Copy link
Contributor

@Neko-Box-Coder Neko-Box-Coder Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously, when you have multiple selections and perform a paste, the undo will undo all of them at one go.

With this branch, the undo only undos one replacement at a time, which will be undesirable if I paste replaced many selections and decide to undo them.

My guess is because of this change here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you see this commit 8b32508?

The last commit (Always stop Undo(), Redo() after TextEventReplace) is totally subjective and up for debate. For me personally it feels much better to have more control for TextEventReplace undos/redos. Let me know what you think.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed it, mb.

I can sort of see scenarios for both cases.

Though I would prefer an undo in one go since it is a single "action", at least from the user's perspective.
I can't imagine having to smash my undo button many times if I have many multi-cursors.

I would prefer it to be a toggle option instead if we decide to keep it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't imagine having to smash my undo button many times if I have many multi-cursors.

Once you pressed it your finger is already on the right key, so pressing it again is not too bad.
For me, it felt good to have more control. But that's completely subjective, which is why I was interested in other people's opinions.

I would prefer it to be a toggle option instead if we decide to keep it.

I'm not sure if we really need to make this an option. I'm fine with removing it again.
Sometimes you experiment with something, put it up for discussion, and then realize that it was actually a good idea. But I also can imagine that there are people who prefer the original functionality.

}

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")
Expand Down
14 changes: 9 additions & 5 deletions internal/action/bufpane.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 13 additions & 4 deletions internal/buffer/buffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
112 changes: 67 additions & 45 deletions internal/buffer/eventhandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
}
Expand All @@ -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 {
Expand All @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -274,6 +286,11 @@ func (eh *EventHandler) Undo() bool {
}

eh.UndoOneEvent()

t = eh.UndoStack.Peek()
if t == nil || t.EventType == TextEventReplace {
break
}
}
return true
}
Expand Down Expand Up @@ -321,6 +338,11 @@ func (eh *EventHandler) Redo() bool {
}

eh.RedoOneEvent()

t = eh.UndoStack.Peek()
if t == nil || t.EventType == TextEventReplace {
break
}
}
return true
}
Expand Down
5 changes: 3 additions & 2 deletions internal/buffer/line_array.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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
Expand Down
12 changes: 12 additions & 0 deletions internal/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading