summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/lsp/extract_range_text_test.go83
-rw-r--r--internal/lsp/handlers_utils.go57
2 files changed, 126 insertions, 14 deletions
diff --git a/internal/lsp/extract_range_text_test.go b/internal/lsp/extract_range_text_test.go
new file mode 100644
index 0000000..1fe29ae
--- /dev/null
+++ b/internal/lsp/extract_range_text_test.go
@@ -0,0 +1,83 @@
+package lsp
+
+import "testing"
+
+func TestExtractRangeText_NilDocument(t *testing.T) {
+ got := extractRangeText(nil, Range{})
+ if got != "" {
+ t.Fatalf("expected empty, got %q", got)
+ }
+}
+
+func TestExtractRangeText_EmptyLines(t *testing.T) {
+ d := &document{lines: []string{}}
+ got := extractRangeText(d, Range{Start: Position{0, 0}, End: Position{0, 5}})
+ if got != "" {
+ t.Fatalf("expected empty, got %q", got)
+ }
+}
+
+func TestExtractRangeText_NegativeStartLine(t *testing.T) {
+ d := &document{lines: []string{"hello"}}
+ got := extractRangeText(d, Range{Start: Position{-1, 0}, End: Position{0, 3}})
+ if got != "" {
+ t.Fatalf("expected empty, got %q", got)
+ }
+}
+
+func TestExtractRangeText_NegativeEndLine(t *testing.T) {
+ d := &document{lines: []string{"hello"}}
+ got := extractRangeText(d, Range{Start: Position{0, 0}, End: Position{-1, 3}})
+ if got != "" {
+ t.Fatalf("expected empty, got %q", got)
+ }
+}
+
+func TestExtractRangeText_StartLineBeyondEnd(t *testing.T) {
+ d := &document{lines: []string{"hello", "world"}}
+ got := extractRangeText(d, Range{Start: Position{5, 0}, End: Position{6, 0}})
+ if got != "" {
+ t.Fatalf("expected empty, got %q", got)
+ }
+}
+
+func TestExtractRangeText_EndLineBeyondEnd(t *testing.T) {
+ // End line is clamped to the last valid line.
+ d := &document{lines: []string{"hello", "world"}}
+ got := extractRangeText(d, Range{Start: Position{0, 0}, End: Position{10, 5}})
+ if got != "hello\nworld" {
+ t.Fatalf("expected clamped result, got %q", got)
+ }
+}
+
+func TestExtractRangeText_StartAfterEnd(t *testing.T) {
+ d := &document{lines: []string{"aaa", "bbb"}}
+ got := extractRangeText(d, Range{Start: Position{1, 0}, End: Position{0, 2}})
+ if got != "" {
+ t.Fatalf("expected empty, got %q", got)
+ }
+}
+
+func TestExtractRangeText_SingleLine(t *testing.T) {
+ d := &document{lines: []string{"hello world"}}
+ got := extractRangeText(d, Range{Start: Position{0, 6}, End: Position{0, 11}})
+ if got != "world" {
+ t.Fatalf("expected %q, got %q", "world", got)
+ }
+}
+
+func TestExtractRangeText_SingleLineNegativeChar(t *testing.T) {
+ d := &document{lines: []string{"hello"}}
+ got := extractRangeText(d, Range{Start: Position{0, -3}, End: Position{0, 3}})
+ if got != "hel" {
+ t.Fatalf("expected %q, got %q", "hel", got)
+ }
+}
+
+func TestExtractRangeText_MultiLine(t *testing.T) {
+ d := &document{lines: []string{"aaa", "bbb", "ccc"}}
+ got := extractRangeText(d, Range{Start: Position{0, 1}, End: Position{2, 2}})
+ if got != "aa\nbbb\ncc" {
+ t.Fatalf("expected %q, got %q", "aa\nbbb\ncc", got)
+ }
+}
diff --git a/internal/lsp/handlers_utils.go b/internal/lsp/handlers_utils.go
index 4a5bf4a..620b3a9 100644
--- a/internal/lsp/handlers_utils.go
+++ b/internal/lsp/handlers_utils.go
@@ -550,23 +550,52 @@ func labelForCompletion(cleaned, filter string) string {
}
// extractRangeText returns the exact text within the given document range.
+// It performs bounds checks on line indices and character offsets, returning
+// an empty string when the range is invalid (e.g. negative lines, out-of-bounds
+// lines, or an empty document).
func extractRangeText(d *document, r Range) string {
+ if d == nil || len(d.lines) == 0 {
+ return ""
+ }
+ // Clamp line indices to valid bounds.
+ if r.Start.Line < 0 || r.End.Line < 0 || r.Start.Line >= len(d.lines) {
+ return ""
+ }
+ if r.End.Line >= len(d.lines) {
+ r.End.Line = len(d.lines) - 1
+ r.End.Character = len(d.lines[r.End.Line])
+ }
+ if r.Start.Line > r.End.Line {
+ return ""
+ }
+
if r.Start.Line == r.End.Line {
- line := d.lines[r.Start.Line]
- if r.Start.Character < 0 {
- r.Start.Character = 0
- }
- if r.End.Character > len(line) {
- r.End.Character = len(line)
- }
- if r.Start.Character > r.End.Character {
- return ""
- }
- return line[r.Start.Character:r.End.Character]
+ return extractSingleLineRange(d.lines[r.Start.Line], r)
+ }
+ return extractMultiLineRange(d.lines, r)
+}
+
+// extractSingleLineRange handles the case where start and end are on the same line.
+// Character offsets are clamped to the line length.
+func extractSingleLineRange(line string, r Range) string {
+ if r.Start.Character < 0 {
+ r.Start.Character = 0
}
+ if r.End.Character > len(line) {
+ r.End.Character = len(line)
+ }
+ if r.Start.Character > r.End.Character {
+ return ""
+ }
+ return line[r.Start.Character:r.End.Character]
+}
+
+// extractMultiLineRange handles ranges spanning multiple lines, clamping
+// character offsets on the first and last lines.
+func extractMultiLineRange(lines []string, r Range) string {
var b strings.Builder
// first line
- first := d.lines[r.Start.Line]
+ first := lines[r.Start.Line]
if r.Start.Character < 0 {
r.Start.Character = 0
}
@@ -577,13 +606,13 @@ func extractRangeText(d *document, r Range) string {
b.WriteString("\n")
// middle lines
for i := r.Start.Line + 1; i < r.End.Line; i++ {
- b.WriteString(d.lines[i])
+ b.WriteString(lines[i])
if i+1 <= r.End.Line {
b.WriteString("\n")
}
}
// last line
- last := d.lines[r.End.Line]
+ last := lines[r.End.Line]
if r.End.Character < 0 {
r.End.Character = 0
}