diff --git a/internal/lsp/util/edit.go b/internal/lsp/util/edit.go index 8b500ac67489e5fbcd0981a012dcf7a0c871f67e..23e9d479f2223773bbd0db41cc049bf8a02f3357 100644 --- a/internal/lsp/util/edit.go +++ b/internal/lsp/util/edit.go @@ -247,14 +247,18 @@ func ApplyWorkspaceEdit(edit protocol.WorkspaceEdit) error { return nil } +// rangesOverlap checks if two LSP ranges overlap. +// Per the LSP specification, ranges are half-open intervals [start, end), +// so adjacent ranges where one's end equals another's start do NOT overlap. +// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#range func rangesOverlap(r1, r2 protocol.Range) bool { if r1.Start.Line > r2.End.Line || r2.Start.Line > r1.End.Line { return false } - if r1.Start.Line == r2.End.Line && r1.Start.Character > r2.End.Character { + if r1.Start.Line == r2.End.Line && r1.Start.Character >= r2.End.Character { return false } - if r2.Start.Line == r1.End.Line && r2.Start.Character > r1.End.Character { + if r2.Start.Line == r1.End.Line && r2.Start.Character >= r1.End.Character { return false } return true diff --git a/internal/lsp/util/edit_test.go b/internal/lsp/util/edit_test.go new file mode 100644 index 0000000000000000000000000000000000000000..cb32d3498289dbf4cdb69dfc9ca4ee73f10b7a20 --- /dev/null +++ b/internal/lsp/util/edit_test.go @@ -0,0 +1,68 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/charmbracelet/x/powernap/pkg/lsp/protocol" +) + +func TestRangesOverlap(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + r1 protocol.Range + r2 protocol.Range + want bool + }{ + { + name: "adjacent ranges do not overlap", + r1: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 5}, + }, + r2: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 5}, + End: protocol.Position{Line: 0, Character: 10}, + }, + want: false, + }, + { + name: "overlapping ranges", + r1: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 8}, + }, + r2: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 5}, + End: protocol.Position{Line: 0, Character: 10}, + }, + want: true, + }, + { + name: "non-overlapping with gap", + r1: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 3}, + }, + r2: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 7}, + End: protocol.Position{Line: 0, Character: 10}, + }, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := rangesOverlap(tt.r1, tt.r2) + require.Equal(t, tt.want, got, "rangesOverlap(r1, r2)") + // Overlap should be symmetric + got2 := rangesOverlap(tt.r2, tt.r1) + require.Equal(t, tt.want, got2, "rangesOverlap(r2, r1) symmetry") + }) + } +}