Skip to content

Commit 8dcf9a8

Browse files
committed
fix: relocate cursor when inside removed text
Previously, if a cursor was positioned strictly inside a range of text being removed (start < cursor < end), the event handler failed to update its position. This resulted in the cursor retaining its old X/Y coordinates, effectively "jumping" forward into subsequent text. The fix ensures that any cursor inside the removed region is clamped to the start of the deletion. Also adds regression tests covering: - Multiple cursors inside removed regions - Cursors spanning across removal boundaries - Multiline replacements
1 parent 5bae228 commit 8dcf9a8

File tree

2 files changed

+251
-0
lines changed

2 files changed

+251
-0
lines changed

internal/buffer/eventhandler.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ func (eh *EventHandler) DoTextEvent(t *TextEvent, useUndo bool) {
9494
loc.Y -= end.Y - start.Y
9595
} else if loc.Y == end.Y && loc.GreaterEqual(end) {
9696
loc = loc.MoveLA(-DiffLA(start, end, eh.buf.LineArray), eh.buf.LineArray)
97+
} else if loc.GreaterThan(start) {
98+
loc = start
9799
}
98100
return loc
99101
}

internal/buffer/eventhandler_event_test.go

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,31 @@ func TestReplaceWithSelection(t *testing.T) {
269269
}
270270
}
271271

272+
func TestReplaceMultiLineCursorMovement(t *testing.T) {
273+
buf := NewBufferFromString("0123456789abcdefghijkLMNOPQrstuvwxyz", "", BTDefault)
274+
cursor := makeCursorWithSelection(buf, 27, 0, 21, 0, 27, 0)
275+
buf.SetCursors([]*Cursor{cursor})
276+
277+
buf.Replace(Loc{20, 0}, Loc{23, 0}, "ABCDE")
278+
279+
expectedLoc := Loc{29, 0}
280+
if cursor.Loc != expectedLoc {
281+
t.Errorf("Expected cursor.Loc at %v, got %v", expectedLoc, cursor.Loc)
282+
}
283+
284+
expectedSelStart := Loc{25, 0}
285+
expectedSelEnd := Loc{29, 0}
286+
if cursor.CurSelection[0] != expectedSelStart || cursor.CurSelection[1] != expectedSelEnd {
287+
t.Errorf("Expected selection [%v,%v], got [%v,%v]",
288+
expectedSelStart, expectedSelEnd, cursor.CurSelection[0], cursor.CurSelection[1])
289+
}
290+
291+
expectedText := "0123456789abcdefghijABCDENOPQrstuvwxyz"
292+
if string(buf.Bytes()) != expectedText {
293+
t.Errorf("Expected text '%s', got '%s'", expectedText, string(buf.Bytes()))
294+
}
295+
}
296+
272297
func TestReplaceSelectedTextCursorPosition(t *testing.T) {
273298
buf := NewBufferFromString("hello world test", "", BTDefault)
274299

@@ -457,6 +482,134 @@ func TestRemoveMultipleCursorsSameLine(t *testing.T) {
457482
}
458483
}
459484

485+
func TestRemoveMultipleCursorsBeforeAndAfter(t *testing.T) {
486+
buf := NewBufferFromString("hello world test", "", BTDefault)
487+
cursor1 := NewCursor(buf, Loc{3, 0})
488+
cursor2 := NewCursor(buf, Loc{8, 0})
489+
cursor3 := NewCursor(buf, Loc{16, 0})
490+
buf.SetCursors([]*Cursor{cursor1, cursor2, cursor3})
491+
492+
buf.Remove(Loc{0, 0}, Loc{6, 0})
493+
494+
expectedLoc1 := Loc{0, 0}
495+
if cursor1.Loc != expectedLoc1 {
496+
t.Errorf("Expected cursor1 at %v, got %v", expectedLoc1, cursor1.Loc)
497+
}
498+
499+
expectedLoc2 := Loc{2, 0}
500+
if cursor2.Loc != expectedLoc2 {
501+
t.Errorf("Expected cursor2 at %v, got %v", expectedLoc2, cursor2.Loc)
502+
}
503+
504+
expectedLoc3 := Loc{10, 0}
505+
if cursor3.Loc != expectedLoc3 {
506+
t.Errorf("Expected cursor3 at %v, got %v", expectedLoc3, cursor3.Loc)
507+
}
508+
509+
expectedText := "world test"
510+
if string(buf.Bytes()) != expectedText {
511+
t.Errorf("Expected text '%s', got '%s'", expectedText, string(buf.Bytes()))
512+
}
513+
}
514+
515+
func TestRemoveMultipleCursorsDifferentLines(t *testing.T) {
516+
buf := NewBufferFromString("line1\nline2\nline3\nline4", "", BTDefault)
517+
cursor1 := NewCursor(buf, Loc{5, 0})
518+
cursor2 := NewCursor(buf, Loc{5, 1})
519+
cursor3 := NewCursor(buf, Loc{5, 2})
520+
cursor4 := NewCursor(buf, Loc{5, 3})
521+
buf.SetCursors([]*Cursor{cursor1, cursor2, cursor3, cursor4})
522+
523+
buf.Remove(Loc{0, 1}, Loc{0, 3})
524+
525+
expectedLoc1 := Loc{5, 0}
526+
if cursor1.Loc != expectedLoc1 {
527+
t.Errorf("Expected cursor1 at %v, got %v", expectedLoc1, cursor1.Loc)
528+
}
529+
530+
expectedLoc2 := Loc{0, 1}
531+
if cursor2.Loc != expectedLoc2 {
532+
t.Errorf("Expected cursor2 at %v, got %v", expectedLoc2, cursor2.Loc)
533+
}
534+
535+
expectedLoc3 := Loc{0, 1}
536+
if cursor3.Loc != expectedLoc3 {
537+
t.Errorf("Expected cursor3 at %v, got %v", expectedLoc3, cursor3.Loc)
538+
}
539+
540+
expectedLoc4 := Loc{5, 1}
541+
if cursor4.Loc != expectedLoc4 {
542+
t.Errorf("Expected cursor4 at %v, got %v", expectedLoc4, cursor4.Loc)
543+
}
544+
545+
expectedText := "line1\nline4"
546+
if string(buf.Bytes()) != expectedText {
547+
t.Errorf("Expected text '%s', got '%s'", expectedText, string(buf.Bytes()))
548+
}
549+
}
550+
551+
func TestRemoveMultipleCursorsMultiline(t *testing.T) {
552+
buf := NewBufferFromString("hello\nworld\ntest\nmore", "", BTDefault)
553+
cursor1 := NewCursor(buf, Loc{3, 0})
554+
cursor2 := NewCursor(buf, Loc{3, 2})
555+
cursor3 := NewCursor(buf, Loc{4, 3})
556+
buf.SetCursors([]*Cursor{cursor1, cursor2, cursor3})
557+
558+
buf.Remove(Loc{2, 0}, Loc{2, 2})
559+
560+
expectedLoc1 := Loc{2, 0}
561+
if cursor1.Loc != expectedLoc1 {
562+
t.Errorf("Expected cursor1 at %v, got %v", expectedLoc1, cursor1.Loc)
563+
}
564+
565+
expectedLoc2 := Loc{3, 0}
566+
if cursor2.Loc != expectedLoc2 {
567+
t.Errorf("Expected cursor2 at %v, got %v", expectedLoc2, cursor2.Loc)
568+
}
569+
570+
expectedLoc3 := Loc{4, 1}
571+
if cursor3.Loc != expectedLoc3 {
572+
t.Errorf("Expected cursor3 at %v, got %v", expectedLoc3, cursor3.Loc)
573+
}
574+
575+
expectedText := "hest\nmore"
576+
if string(buf.Bytes()) != expectedText {
577+
t.Errorf("Expected text '%s', got '%s'", expectedText, string(buf.Bytes()))
578+
}
579+
}
580+
581+
func TestRemoveMultipleCursorsWithSelections(t *testing.T) {
582+
buf := NewBufferFromString("hello world test end", "", BTDefault)
583+
cursor1 := makeCursorWithSelection(buf, 5, 0, 0, 0, 5, 0)
584+
cursor2 := makeCursorWithSelection(buf, 16, 0, 12, 0, 16, 0)
585+
buf.SetCursors([]*Cursor{cursor1, cursor2})
586+
587+
buf.Remove(Loc{0, 0}, Loc{6, 0})
588+
589+
expectedLoc1 := Loc{0, 0}
590+
if cursor1.Loc != expectedLoc1 {
591+
t.Errorf("Expected cursor1 at %v, got %v", expectedLoc1, cursor1.Loc)
592+
}
593+
expectedSel1 := [2]Loc{{0, 0}, {0, 0}}
594+
if cursor1.CurSelection != expectedSel1 {
595+
t.Errorf("Expected cursor1 selection %v, got %v", expectedSel1, cursor1.CurSelection)
596+
}
597+
598+
expectedLoc2 := Loc{10, 0}
599+
if cursor2.Loc != expectedLoc2 {
600+
t.Errorf("Expected cursor2 at %v, got %v", expectedLoc2, cursor2.Loc)
601+
}
602+
expectedSel2 := [2]Loc{{6, 0}, {10, 0}}
603+
if cursor2.CurSelection != expectedSel2 {
604+
t.Errorf("Expected cursor2 selection %v, got %v", expectedSel2, cursor2.CurSelection)
605+
}
606+
607+
expectedText := "world test end"
608+
if string(buf.Bytes()) != expectedText {
609+
t.Errorf("Expected text '%s', got '%s'", expectedText, string(buf.Bytes()))
610+
}
611+
}
612+
460613
func TestReplaceMultipleCursorsSameLine(t *testing.T) {
461614
buf := NewBufferFromString("hello world test", "", BTDefault)
462615
cursor1 := NewCursor(buf, Loc{5, 0})
@@ -517,6 +670,42 @@ func TestReplaceMultipleCursorsDifferentLines(t *testing.T) {
517670
}
518671
}
519672

673+
func TestReplaceMultipleCursorsMultilineToSingleLine(t *testing.T) {
674+
buf := NewBufferFromString("line1\nline2\nline3\nline4", "", BTDefault)
675+
cursor1 := NewCursor(buf, Loc{3, 0})
676+
cursor2 := NewCursor(buf, Loc{3, 1})
677+
cursor3 := NewCursor(buf, Loc{3, 2})
678+
cursor4 := NewCursor(buf, Loc{3, 3})
679+
buf.SetCursors([]*Cursor{cursor1, cursor2, cursor3, cursor4})
680+
681+
buf.Replace(Loc{0, 0}, Loc{0, 2}, "REPLACED")
682+
683+
expectedLoc1 := Loc{8, 0}
684+
if cursor1.Loc != expectedLoc1 {
685+
t.Errorf("Expected cursor1 at %v, got %v", expectedLoc1, cursor1.Loc)
686+
}
687+
688+
expectedLoc2 := Loc{8, 0}
689+
if cursor2.Loc != expectedLoc2 {
690+
t.Errorf("Expected cursor2 at %v, got %v", expectedLoc2, cursor2.Loc)
691+
}
692+
693+
expectedLoc3 := Loc{11, 0}
694+
if cursor3.Loc != expectedLoc3 {
695+
t.Errorf("Expected cursor3 at %v, got %v", expectedLoc3, cursor3.Loc)
696+
}
697+
698+
expectedLoc4 := Loc{3, 1}
699+
if cursor4.Loc != expectedLoc4 {
700+
t.Errorf("Expected cursor4 at %v, got %v", expectedLoc4, cursor4.Loc)
701+
}
702+
703+
expectedText := "REPLACEDline3\nline4"
704+
if string(buf.Bytes()) != expectedText {
705+
t.Errorf("Expected text '%s', got '%s'", expectedText, string(buf.Bytes()))
706+
}
707+
}
708+
520709
func TestReplaceMultipleCursorsSingleLineToMultiline(t *testing.T) {
521710
buf := NewBufferFromString("hello world", "", BTDefault)
522711
cursor1 := NewCursor(buf, Loc{5, 0})
@@ -579,6 +768,36 @@ func TestReplaceMultipleCursorsWithSelections(t *testing.T) {
579768
}
580769
}
581770

771+
func TestReplaceMultipleCursorsComplexMultiline(t *testing.T) {
772+
buf := NewBufferFromString("A\nB\nC\nD\nE", "", BTDefault)
773+
cursor1 := NewCursor(buf, Loc{1, 0})
774+
cursor2 := NewCursor(buf, Loc{1, 2})
775+
cursor3 := NewCursor(buf, Loc{1, 4})
776+
buf.SetCursors([]*Cursor{cursor1, cursor2, cursor3})
777+
778+
buf.Replace(Loc{0, 1}, Loc{0, 3}, "X\nY\nZ\n")
779+
780+
expectedLoc1 := Loc{1, 0}
781+
if cursor1.Loc != expectedLoc1 {
782+
t.Errorf("Expected cursor1 at %v, got %v", expectedLoc1, cursor1.Loc)
783+
}
784+
785+
expectedLoc2 := Loc{0, 4}
786+
if cursor2.Loc != expectedLoc2 {
787+
t.Errorf("Expected cursor2 at %v, got %v", expectedLoc2, cursor2.Loc)
788+
}
789+
790+
expectedLoc3 := Loc{1, 5}
791+
if cursor3.Loc != expectedLoc3 {
792+
t.Errorf("Expected cursor3 at %v, got %v", expectedLoc3, cursor3.Loc)
793+
}
794+
795+
expectedText := "A\nX\nY\nZ\nD\nE"
796+
if string(buf.Bytes()) != expectedText {
797+
t.Errorf("Expected text '%s', got '%s'", expectedText, string(buf.Bytes()))
798+
}
799+
}
800+
582801
func TestMultipleCursorsInsertAtSameLocation(t *testing.T) {
583802
buf := NewBufferFromString("hello world", "", BTDefault)
584803
cursor1 := NewCursor(buf, Loc{6, 0})
@@ -607,6 +826,36 @@ func TestMultipleCursorsInsertAtSameLocation(t *testing.T) {
607826
}
608827
}
609828

829+
func TestMultipleCursorsRemoveAcrossCursors(t *testing.T) {
830+
buf := NewBufferFromString("0123456789", "", BTDefault)
831+
cursor1 := NewCursor(buf, Loc{2, 0})
832+
cursor2 := NewCursor(buf, Loc{5, 0})
833+
cursor3 := NewCursor(buf, Loc{8, 0})
834+
buf.SetCursors([]*Cursor{cursor1, cursor2, cursor3})
835+
836+
buf.Remove(Loc{3, 0}, Loc{7, 0})
837+
838+
expectedLoc1 := Loc{2, 0}
839+
if cursor1.Loc != expectedLoc1 {
840+
t.Errorf("Expected cursor1 at %v, got %v", expectedLoc1, cursor1.Loc)
841+
}
842+
843+
expectedLoc2 := Loc{3, 0}
844+
if cursor2.Loc != expectedLoc2 {
845+
t.Errorf("Expected cursor2 at %v, got %v", expectedLoc2, cursor2.Loc)
846+
}
847+
848+
expectedLoc3 := Loc{4, 0}
849+
if cursor3.Loc != expectedLoc3 {
850+
t.Errorf("Expected cursor3 at %v, got %v", expectedLoc3, cursor3.Loc)
851+
}
852+
853+
expectedText := "012789"
854+
if string(buf.Bytes()) != expectedText {
855+
t.Errorf("Expected text '%s', got '%s'", expectedText, string(buf.Bytes()))
856+
}
857+
}
858+
610859
func TestUpdateTrailingWsInsertAtEOL(t *testing.T) {
611860
buf := NewBufferFromString("hello", "", BTDefault)
612861
cursor := NewCursor(buf, Loc{5, 0})

0 commit comments

Comments
 (0)