diff options
Diffstat (limited to 'internal/lsp/completion_prefix_strip_test.go')
| -rw-r--r-- | internal/lsp/completion_prefix_strip_test.go | 210 |
1 files changed, 124 insertions, 86 deletions
diff --git a/internal/lsp/completion_prefix_strip_test.go b/internal/lsp/completion_prefix_strip_test.go index 9953714..64cca49 100644 --- a/internal/lsp/completion_prefix_strip_test.go +++ b/internal/lsp/completion_prefix_strip_test.go @@ -1,120 +1,158 @@ package lsp import ( - "encoding/json" - "testing" + "encoding/json" + "testing" ) func TestStripDuplicateGeneralPrefix_ExactOverlap(t *testing.T) { - prefix := "func New " - sugg := "func New() *CustData" - got := stripDuplicateGeneralPrefix(prefix, sugg) - // We expect the already typed prefix to be removed from the suggestion. - if got == sugg { - t.Fatalf("expected duplicate prefix to be stripped; got unchanged: %q", got) - } - if got != "() *CustData" { - t.Fatalf("got %q want %q", got, "() *CustData") - } + prefix := "func New " + sugg := "func New() *CustData" + got := stripDuplicateGeneralPrefix(prefix, sugg) + // We expect the already typed prefix to be removed from the suggestion. + if got == sugg { + t.Fatalf("expected duplicate prefix to be stripped; got unchanged: %q", got) + } + if got != "() *CustData" { + t.Fatalf("got %q want %q", got, "() *CustData") + } } func TestStripDuplicateGeneralPrefix_TokenBoundarySuffix(t *testing.T) { - prefix := "db." - sugg := "db.Query()" - got := stripDuplicateGeneralPrefix(prefix, sugg) - if got != "Query()" { - t.Fatalf("got %q want %q", got, "Query()") - } + prefix := "db." + sugg := "db.Query()" + got := stripDuplicateGeneralPrefix(prefix, sugg) + if got != "Query()" { + t.Fatalf("got %q want %q", got, "Query()") + } } func TestStripDuplicateAssignmentPrefix_AssignAndWalrus(t *testing.T) { - // walrus - if out := stripDuplicateAssignmentPrefix("name := ", "name := compute()" ); out != "compute()" { - t.Fatalf(":= expected compute(), got %q", out) - } - // equals - if out := stripDuplicateAssignmentPrefix("x = ", "x = y+1" ); out != "y+1" { - t.Fatalf("= expected y+1, got %q", out) - } + // walrus + if out := stripDuplicateAssignmentPrefix("name := ", "name := compute()"); out != "compute()" { + t.Fatalf(":= expected compute(), got %q", out) + } + // equals + if out := stripDuplicateAssignmentPrefix("x = ", "x = y+1"); out != "y+1" { + t.Fatalf("= expected y+1, got %q", out) + } } func TestTryLLMCompletion_ManualInvokeAfterWhitespace_Allows(t *testing.T) { - s := &Server{ maxTokens: 32, triggerChars: []string{".", ":", "/", "_"}, compCache: make(map[string]string) } - s.llmClient = fakeLLM{resp: "() *CustData"} - line := "func fib(i int) " // cursor after space - p := CompletionParams{ Position: Position{ Line: 0, Character: len(line) }, TextDocument: TextDocumentIdentifier{URI: "file://x.go"} } - // Simulate manual user invocation (TriggerKind=1) - p.Context = json.RawMessage([]byte(`{"triggerKind":1}`)) - items, ok := s.tryLLMCompletion(p, "", line, "", "", "", false, "") - if !ok { t.Fatalf("expected ok=true for manual invoke after whitespace") } - if len(items) == 0 { t.Fatalf("expected at least one completion item") } + s := &Server{maxTokens: 32, triggerChars: []string{".", ":", "/", "_"}, compCache: make(map[string]string)} + s.llmClient = fakeLLM{resp: "() *CustData"} + line := "func fib(i int) " // cursor after space + p := CompletionParams{Position: Position{Line: 0, Character: len(line)}, TextDocument: TextDocumentIdentifier{URI: "file://x.go"}} + // Simulate manual user invocation (TriggerKind=1) + p.Context = json.RawMessage([]byte(`{"triggerKind":1}`)) + items, ok := s.tryLLMCompletion(p, "", line, "", "", "", false, "") + if !ok { + t.Fatalf("expected ok=true for manual invoke after whitespace") + } + if len(items) == 0 { + t.Fatalf("expected at least one completion item") + } } func TestTryLLMCompletion_InlineSemicolonPromptAlwaysTriggers(t *testing.T) { - s := &Server{ maxTokens: 32, triggerChars: []string{".", ":", "/", "_"}, compCache: make(map[string]string) } - s.llmClient = fakeLLM{resp: "replacement"} - line := "prefix ;do something; suffix" - // No trigger char immediately before cursor; place cursor at end - p := CompletionParams{ Position: Position{ Line: 0, Character: len(line) }, TextDocument: TextDocumentIdentifier{URI: "file://inline.go"} } - items, ok := s.tryLLMCompletion(p, "", line, "", "", "", false, "") - if !ok || len(items) == 0 { t.Fatalf("expected completion to trigger on inline ;text; prompt") } + s := &Server{maxTokens: 32, triggerChars: []string{".", ":", "/", "_"}, compCache: make(map[string]string)} + s.llmClient = fakeLLM{resp: "replacement"} + line := "prefix ;do something; suffix" + // No trigger char immediately before cursor; place cursor at end + p := CompletionParams{Position: Position{Line: 0, Character: len(line)}, TextDocument: TextDocumentIdentifier{URI: "file://inline.go"}} + items, ok := s.tryLLMCompletion(p, "", line, "", "", "", false, "") + if !ok || len(items) == 0 { + t.Fatalf("expected completion to trigger on inline ;text; prompt") + } } func TestTryLLMCompletion_DoubleSemicolonEmpty_DoesNotAutoTrigger(t *testing.T) { - s := &Server{ maxTokens: 32, triggerChars: []string{".", ":", "/", "_"}, compCache: make(map[string]string) } - fake := &countingLLM{} - s.llmClient = fake - line := ";; " // empty content after ';;' should not force-trigger - p := CompletionParams{ Position: Position{ Line: 0, Character: len(line) }, TextDocument: TextDocumentIdentifier{URI: "file://empty-inline.go"} } - items, ok := s.tryLLMCompletion(p, "", line, "", "", "", false, "") - if !ok { t.Fatalf("expected ok=true for non-trigger path") } - if len(items) != 0 { t.Fatalf("expected no items when inline ';;' is empty") } - if fake.calls != 0 { t.Fatalf("LLM should not be called; calls=%d", fake.calls) } + s := &Server{maxTokens: 32, triggerChars: []string{".", ":", "/", "_"}, compCache: make(map[string]string)} + fake := &countingLLM{} + s.llmClient = fake + line := ";; " // empty content after ';;' should not force-trigger + p := CompletionParams{Position: Position{Line: 0, Character: len(line)}, TextDocument: TextDocumentIdentifier{URI: "file://empty-inline.go"}} + items, ok := s.tryLLMCompletion(p, "", line, "", "", "", false, "") + if !ok { + t.Fatalf("expected ok=true for non-trigger path") + } + if len(items) != 0 { + t.Fatalf("expected no items when inline ';;' is empty") + } + if fake.calls != 0 { + t.Fatalf("LLM should not be called; calls=%d", fake.calls) + } } func TestHasDoubleSemicolonTrigger_Variants(t *testing.T) { - if hasDoubleSemicolonTrigger(";;") { t.Fatalf("bare ';;' should not trigger") } - if hasDoubleSemicolonTrigger(";; ;") { t.Fatalf("';;' followed by space should not trigger") } - if hasDoubleSemicolonTrigger(";;;") { t.Fatalf("';;;' should not trigger (no content)") } - if !hasDoubleSemicolonTrigger(";;x;") { t.Fatalf("expected trigger for ';;x;' pattern") } + if hasDoubleSemicolonTrigger(";;") { + t.Fatalf("bare ';;' should not trigger") + } + if hasDoubleSemicolonTrigger(";; ;") { + t.Fatalf("';;' followed by space should not trigger") + } + if hasDoubleSemicolonTrigger(";;;") { + t.Fatalf("';;;' should not trigger (no content)") + } + if !hasDoubleSemicolonTrigger(";;x;") { + t.Fatalf("expected trigger for ';;x;' pattern") + } } func TestBareDoubleSemicolonPreventsAutoTriggerEvenWithOtherTriggers(t *testing.T) { - s := &Server{ maxTokens: 32, triggerChars: []string{".", ":", "/", "_"}, compCache: make(map[string]string) } - fake := &countingLLM{} - s.llmClient = fake - // Place a '.' earlier but also include bare ';;' at end; should not auto-trigger - line := "obj. call ;;" - p := CompletionParams{ Position: Position{ Line: 0, Character: len(line) }, TextDocument: TextDocumentIdentifier{URI: "file://bare-ds.go"} } - items, ok := s.tryLLMCompletion(p, "", line, "", "", "", false, "") - if !ok { t.Fatalf("expected ok=true (handled), but not auto-triggering") } - if len(items) != 0 { t.Fatalf("expected no items due to bare ';;'") } - if fake.calls != 0 { t.Fatalf("LLM should not be called; calls=%d", fake.calls) } + s := &Server{maxTokens: 32, triggerChars: []string{".", ":", "/", "_"}, compCache: make(map[string]string)} + fake := &countingLLM{} + s.llmClient = fake + // Place a '.' earlier but also include bare ';;' at end; should not auto-trigger + line := "obj. call ;;" + p := CompletionParams{Position: Position{Line: 0, Character: len(line)}, TextDocument: TextDocumentIdentifier{URI: "file://bare-ds.go"}} + items, ok := s.tryLLMCompletion(p, "", line, "", "", "", false, "") + if !ok { + t.Fatalf("expected ok=true (handled), but not auto-triggering") + } + if len(items) != 0 { + t.Fatalf("expected no items due to bare ';;'") + } + if fake.calls != 0 { + t.Fatalf("LLM should not be called; calls=%d", fake.calls) + } } func TestBareDoubleSemicolonOnNextLine_PreventsAutoTrigger(t *testing.T) { - s := &Server{ maxTokens: 32, triggerChars: []string{".", ":", "/", "_"}, compCache: make(map[string]string) } - fake := &countingLLM{} - s.llmClient = fake - current := "expression := flag.String(\"expression\", \"\", \"Expression to evaluate\")" - below := ";;" - p := CompletionParams{ Position: Position{ Line: 0, Character: len(current) }, TextDocument: TextDocumentIdentifier{URI: "file://nextline.go"} } - items, ok := s.tryLLMCompletion(p, "", current, below, "", "", false, "") - if !ok { t.Fatalf("expected ok=true handled") } - if len(items) != 0 { t.Fatalf("expected no items due to bare ';;' on next line") } - if fake.calls != 0 { t.Fatalf("LLM should not be called; calls=%d", fake.calls) } + s := &Server{maxTokens: 32, triggerChars: []string{".", ":", "/", "_"}, compCache: make(map[string]string)} + fake := &countingLLM{} + s.llmClient = fake + current := "expression := flag.String(\"expression\", \"\", \"Expression to evaluate\")" + below := ";;" + p := CompletionParams{Position: Position{Line: 0, Character: len(current)}, TextDocument: TextDocumentIdentifier{URI: "file://nextline.go"}} + items, ok := s.tryLLMCompletion(p, "", current, below, "", "", false, "") + if !ok { + t.Fatalf("expected ok=true handled") + } + if len(items) != 0 { + t.Fatalf("expected no items due to bare ';;' on next line") + } + if fake.calls != 0 { + t.Fatalf("LLM should not be called; calls=%d", fake.calls) + } } func TestBareDoubleSemicolonPreventsManualInvoke(t *testing.T) { - s := &Server{ maxTokens: 32, triggerChars: []string{".", ":", "/", "_"}, compCache: make(map[string]string) } - fake := &countingLLM{} - s.llmClient = fake - line := ";;" - p := CompletionParams{ Position: Position{ Line: 0, Character: len(line) }, TextDocument: TextDocumentIdentifier{URI: "file://bare-ds-manual.go"} } - // Simulate manual invoke - p.Context = json.RawMessage([]byte(`{"triggerKind":1}`)) - items, ok := s.tryLLMCompletion(p, "", line, "", "", "", false, "") - if !ok { t.Fatalf("expected ok=true (handled)") } - if len(items) != 0 { t.Fatalf("expected no items for bare ';;' even with manual invoke") } - if fake.calls != 0 { t.Fatalf("LLM should not be called; calls=%d", fake.calls) } + s := &Server{maxTokens: 32, triggerChars: []string{".", ":", "/", "_"}, compCache: make(map[string]string)} + fake := &countingLLM{} + s.llmClient = fake + line := ";;" + p := CompletionParams{Position: Position{Line: 0, Character: len(line)}, TextDocument: TextDocumentIdentifier{URI: "file://bare-ds-manual.go"}} + // Simulate manual invoke + p.Context = json.RawMessage([]byte(`{"triggerKind":1}`)) + items, ok := s.tryLLMCompletion(p, "", line, "", "", "", false, "") + if !ok { + t.Fatalf("expected ok=true (handled)") + } + if len(items) != 0 { + t.Fatalf("expected no items for bare ';;' even with manual invoke") + } + if fake.calls != 0 { + t.Fatalf("LLM should not be called; calls=%d", fake.calls) + } } |
