diff options
| author | Paul Buetow <paul@buetow.org> | 2025-08-16 15:35:02 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-08-16 15:35:02 +0300 |
| commit | 6c8eb6876fe87553770de114ebd34649a0c6ec10 (patch) | |
| tree | 064517edaf9d59522bec7191a61362a853c195bd /internal/lsp/handlers_test.go | |
| parent | 1e1df8c204f6771719f85d8402128d72138bb863 (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.go | 126 |
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 })() } |
