summaryrefslogtreecommitdiff
path: root/internal/lsp/handlers_test.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-08-16 15:35:02 +0300
committerPaul Buetow <paul@buetow.org>2025-08-16 15:35:02 +0300
commit6c8eb6876fe87553770de114ebd34649a0c6ec10 (patch)
tree064517edaf9d59522bec7191a61362a853c195bd /internal/lsp/handlers_test.go
parent1e1df8c204f6771719f85d8402128d72138bb863 (diff)
lsp: split monolithic server.go into modules; add configurable max tokens and context strategies (minimal|window|file-on-new-func|always-full); provide flags/env fallbacks; add unit tests for helpers and context; update README; remove obsolete files
Diffstat (limited to 'internal/lsp/handlers_test.go')
-rw-r--r--internal/lsp/handlers_test.go126
1 files changed, 126 insertions, 0 deletions
diff --git a/internal/lsp/handlers_test.go b/internal/lsp/handlers_test.go
new file mode 100644
index 0000000..bde9b82
--- /dev/null
+++ b/internal/lsp/handlers_test.go
@@ -0,0 +1,126 @@
+package lsp
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestInParamList(t *testing.T) {
+ line := "func foo(a int, b string) int {"
+ if !inParamList(line, 15) { // inside params
+ t.Fatalf("expected inParamList true for cursor inside params")
+ }
+ if inParamList(line, 2) { // before 'func'
+ t.Fatalf("expected inParamList false for cursor before params")
+ }
+ if inParamList(line, len(line)) { // after ')'
+ t.Fatalf("expected inParamList false for cursor after params")
+ }
+}
+
+func TestComputeWordStart(t *testing.T) {
+ current := "fmt.Prin"
+ // Cursor after the word (index 8)
+ got := computeWordStart(current, 8)
+ // should stop after the dot at index 4
+ if want := 4; got != want {
+ t.Fatalf("computeWordStart got %d want %d", got, want)
+ }
+}
+
+func TestComputeTextEditAndFilter_InParams(t *testing.T) {
+ current := "func foo(a int, b string) {" // ')' at index 26
+ p := CompletionParams{Position: Position{Line: 10, Character: 20}}
+ te, filter := computeTextEditAndFilter("x int, y string", true, current, p)
+
+ if te == nil {
+ t.Fatalf("expected TextEdit")
+ }
+ // left should be after '(' which is at index 8
+ if te.Range.Start.Line != 10 || te.Range.Start.Character != 9 {
+ t.Fatalf("start got line=%d char=%d want line=10 char=9", te.Range.Start.Line, te.Range.Start.Character)
+ }
+ // right should clamp to cursor (20)
+ if te.Range.End.Line != 10 || te.Range.End.Character != 20 {
+ t.Fatalf("end got line=%d char=%d want line=10 char=20", te.Range.End.Line, te.Range.End.Character)
+ }
+ if filter == "" {
+ t.Fatalf("expected non-empty filter inside params")
+ }
+}
+
+func TestComputeTextEditAndFilter_Word(t *testing.T) {
+ current := "fmt.Prin"
+ p := CompletionParams{Position: Position{Line: 2, Character: len(current)}}
+ te, filter := computeTextEditAndFilter("Println", false, current, p)
+ if te == nil {
+ t.Fatalf("expected TextEdit")
+ }
+ if te.Range.Start.Character != 4 || te.Range.End.Character != len(current) {
+ t.Fatalf("range chars got %d..%d want 4..%d", te.Range.Start.Character, te.Range.End.Character, len(current))
+ }
+ if filter != "Prin" {
+ t.Fatalf("filter got %q want %q", filter, "Prin")
+ }
+}
+
+func TestLabelForCompletion(t *testing.T) {
+ if got := labelForCompletion("Println", "Pri"); got != "Println" {
+ t.Fatalf("label mismatch got %q want %q", got, "Println")
+ }
+ if got := labelForCompletion("Println", "X"); got != "X" {
+ t.Fatalf("label mismatch with filter got %q want %q", got, "X")
+ }
+ if got := labelForCompletion("Println\nmore", ""); got != "Println" {
+ t.Fatalf("label firstLine got %q want %q", got, "Println")
+ }
+}
+
+func TestBuildPrompts_InParams(t *testing.T) {
+ p := CompletionParams{TextDocument: TextDocumentIdentifier{URI: "file:///t.go"}, Position: Position{Line: 1, Character: 12}}
+ sys, user := buildPrompts(true, p, "above", "func foo(", "below", "func foo(")
+ if sys == "" || user == "" {
+ t.Fatalf("expected non-empty prompts")
+ }
+ if want := "function signatures"; !contains(sys, want) {
+ t.Fatalf("system prompt missing %q: %q", want, sys)
+ }
+ if want := "parameter list"; !contains(user, want) {
+ t.Fatalf("user prompt missing %q: %q", want, user)
+ }
+}
+
+func TestBuildPrompts_Outside(t *testing.T) {
+ p := CompletionParams{TextDocument: TextDocumentIdentifier{URI: "file:///t.go"}, Position: Position{Line: 1, Character: 5}}
+ sys, user := buildPrompts(false, p, "ab", "cur", "be", "fnctx")
+ if sys == "" || user == "" {
+ t.Fatalf("expected non-empty prompts")
+ }
+ if want := "completion engine"; !contains(sys, want) {
+ t.Fatalf("system prompt missing %q: %q", want, sys)
+ }
+ if want := "Provide the next likely code"; !contains(user, want) {
+ t.Fatalf("user prompt missing %q: %q", want, user)
+ }
+}
+
+func TestComputeTextEditAndFilter_NoParensFallback(t *testing.T) {
+ current := "func foo bar" // no parentheses
+ cursor := len(current)
+ p := CompletionParams{Position: Position{Line: 0, Character: cursor}}
+ te, filter := computeTextEditAndFilter("baz", true, current, p)
+ if te == nil {
+ t.Fatalf("expected TextEdit from fallback path")
+ }
+ // fallback should behave like word edit; start at last space + 1
+ lastSpace := strings.LastIndex(current, " ")
+ if te.Range.Start.Character != lastSpace+1 || te.Range.End.Character != cursor {
+ t.Fatalf("range got %d..%d want %d..%d", te.Range.Start.Character, te.Range.End.Character, lastSpace+1, cursor)
+ }
+ if filter != "bar" {
+ t.Fatalf("filter got %q want %q", filter, "bar")
+ }
+}
+
+// small helper to avoid importing strings
+func contains(s, sub string) bool { return len(s) >= len(sub) && (func() bool { i := 0; for i+len(sub) <= len(s) { if s[i:i+len(sub)] == sub { return true }; i++ }; return false })() }