diff options
| author | Paul Buetow <paul@buetow.org> | 2025-09-08 12:02:40 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-09-08 12:02:40 +0300 |
| commit | 75cf6abd55bfb60324fc47cf91eac08dbb8b87b4 (patch) | |
| tree | 6ef90d8014fe4d9a757d3f7e95bf736b70e4c685 /internal/lsp | |
| parent | 0dcf347c3fbc6e4ffb7e46294f5dd92dbbcd98ef (diff) | |
docs: move tmux documentation to its own file
Diffstat (limited to 'internal/lsp')
| -rw-r--r-- | internal/lsp/codeaction_gotest_int_test.go | 37 | ||||
| -rw-r--r-- | internal/lsp/coverage_add_test.go | 156 | ||||
| -rw-r--r-- | internal/lsp/handlers.go | 112 | ||||
| -rw-r--r-- | internal/lsp/handlers_codeaction.go | 110 | ||||
| -rw-r--r-- | internal/lsp/handlers_completion.go | 9 | ||||
| -rw-r--r-- | internal/lsp/handlers_document.go | 2 | ||||
| -rw-r--r-- | internal/lsp/handlers_init_more_test.go | 15 | ||||
| -rw-r--r-- | internal/lsp/server.go | 28 |
8 files changed, 238 insertions, 231 deletions
diff --git a/internal/lsp/codeaction_gotest_int_test.go b/internal/lsp/codeaction_gotest_int_test.go index 6bb1c45..04a73e0 100644 --- a/internal/lsp/codeaction_gotest_int_test.go +++ b/internal/lsp/codeaction_gotest_int_test.go @@ -1,26 +1,25 @@ package lsp import ( - "os" - "path/filepath" - "testing" + "os" + "path/filepath" + "testing" ) func TestResolveGoTest_CreatesTestFile(t *testing.T) { - dir := t.TempDir() - src := filepath.Join(dir, "x.go") - if err := os.WriteFile(src, []byte("package x\n\nfunc Sum(a,b int) int { return a+b }\n"), 0o644); err != nil { - t.Fatalf("write: %v", err) - } - s := &Server{} // minimal server with nil llmClient to trigger stub - uri := "file://" + src - we, jumpURI, jumpRange, ok := s.resolveGoTest(uri, Position{Line: 2}) - if !ok || jumpURI == "" || jumpRange.Start.Line < 0 { - t.Fatalf("resolveGoTest failed: ok=%v uri=%q range=%v", ok, jumpURI, jumpRange) - } - // Expect documentChanges to include a create and an edit - if len(we.DocumentChanges) == 0 && len(we.Changes) == 0 { - t.Fatalf("expected edits to create or append test file: %+v", we) - } + dir := t.TempDir() + src := filepath.Join(dir, "x.go") + if err := os.WriteFile(src, []byte("package x\n\nfunc Sum(a,b int) int { return a+b }\n"), 0o644); err != nil { + t.Fatalf("write: %v", err) + } + s := &Server{} // minimal server with nil llmClient to trigger stub + uri := "file://" + src + we, jumpURI, jumpRange, ok := s.resolveGoTest(uri, Position{Line: 2}) + if !ok || jumpURI == "" || jumpRange.Start.Line < 0 { + t.Fatalf("resolveGoTest failed: ok=%v uri=%q range=%v", ok, jumpURI, jumpRange) + } + // Expect documentChanges to include a create and an edit + if len(we.DocumentChanges) == 0 && len(we.Changes) == 0 { + t.Fatalf("expected edits to create or append test file: %+v", we) + } } - diff --git a/internal/lsp/coverage_add_test.go b/internal/lsp/coverage_add_test.go index f4b0f00..7701a5e 100644 --- a/internal/lsp/coverage_add_test.go +++ b/internal/lsp/coverage_add_test.go @@ -1,103 +1,103 @@ package lsp import ( - "encoding/json" - "testing" + "encoding/json" + "testing" ) func TestInParamListAndComputeWordStart(t *testing.T) { - line := "func add(a int, b int) int { return a + b }" - if !inParamList(line, 15) { // inside params - t.Fatalf("expected inParamList true") - } - if inParamList("not a func", 3) { - t.Fatalf("expected inParamList false") - } - if n := computeWordStart("helloWorld", 10); n != 0 { - t.Fatalf("computeWordStart wrong: %d", n) - } + line := "func add(a int, b int) int { return a + b }" + if !inParamList(line, 15) { // inside params + t.Fatalf("expected inParamList true") + } + if inParamList("not a func", 3) { + t.Fatalf("expected inParamList false") + } + if n := computeWordStart("helloWorld", 10); n != 0 { + t.Fatalf("computeWordStart wrong: %d", n) + } } func TestStripInlineAndLabel(t *testing.T) { - if got := stripInlineCodeSpan("`abc`def"); got != "abc" { - t.Fatalf("stripInlineCodeSpan: %q", got) - } - if lbl := labelForCompletion("First line\nSecond", "fir"); lbl != "First line" { - t.Fatalf("labelForCompletion: %q", lbl) - } - if lbl := labelForCompletion("Other", "zzz"); lbl != "zzz" { - t.Fatalf("label fallback: %q", lbl) - } + if got := stripInlineCodeSpan("`abc`def"); got != "abc" { + t.Fatalf("stripInlineCodeSpan: %q", got) + } + if lbl := labelForCompletion("First line\nSecond", "fir"); lbl != "First line" { + t.Fatalf("labelForCompletion: %q", lbl) + } + if lbl := labelForCompletion("Other", "zzz"); lbl != "zzz" { + t.Fatalf("label fallback: %q", lbl) + } } func TestRangeComparators(t *testing.T) { - a := Range{Start: Position{Line: 1, Character: 5}, End: Position{Line: 3, Character: 0}} - b := Range{Start: Position{Line: 2, Character: 0}, End: Position{Line: 4, Character: 0}} - if !rangesOverlap(a, b) { - t.Fatalf("expected overlap") - } - if !lessPos(Position{Line: 1, Character: 0}, Position{Line: 1, Character: 1}) { - t.Fatalf("lessPos") - } - if !greaterPos(Position{Line: 2, Character: 0}, Position{Line: 1, Character: 10}) { - t.Fatalf("greaterPos") - } - if !isIdentChar('A') || isIdentChar('-') { - t.Fatalf("isIdentChar") - } + a := Range{Start: Position{Line: 1, Character: 5}, End: Position{Line: 3, Character: 0}} + b := Range{Start: Position{Line: 2, Character: 0}, End: Position{Line: 4, Character: 0}} + if !rangesOverlap(a, b) { + t.Fatalf("expected overlap") + } + if !lessPos(Position{Line: 1, Character: 0}, Position{Line: 1, Character: 1}) { + t.Fatalf("lessPos") + } + if !greaterPos(Position{Line: 2, Character: 0}, Position{Line: 1, Character: 10}) { + t.Fatalf("greaterPos") + } + if !isIdentChar('A') || isIdentChar('-') { + t.Fatalf("isIdentChar") + } } func TestFindGoFunctionAtLine_NoBody(t *testing.T) { - lines := []string{"func X(a int)", "// comment"} - start, end := findGoFunctionAtLine(lines, 0) - if start != 0 || end != 0 { - t.Fatalf("expected single-line prototype, got %d,%d", start, end) - } + lines := []string{"func X(a int)", "// comment"} + start, end := findGoFunctionAtLine(lines, 0) + if start != 0 || end != 0 { + t.Fatalf("expected single-line prototype, got %d,%d", start, end) + } } func TestLineHasInlinePrompt(t *testing.T) { - if !lineHasInlinePrompt(">do>") { - t.Fatalf("expected inline prompt") - } + if !lineHasInlinePrompt(">do>") { + t.Fatalf("expected inline prompt") + } } func TestDiagnosticsInRange_Overlap(t *testing.T) { - s := &Server{} - ctx := CodeActionContext{Diagnostics: []Diagnostic{{ - Range: Range{Start: Position{Line: 10, Character: 0}, End: Position{Line: 12, Character: 0}}, - Message: "x", - }}} - raw, _ := json.Marshal(ctx) - sel := Range{Start: Position{Line: 11, Character: 0}, End: Position{Line: 11, Character: 1}} - out := s.diagnosticsInRange(raw, sel) - if len(out) != 1 { - t.Fatalf("expected 1 diag overlap, got %d", len(out)) - } - // no diagnostics - var empty json.RawMessage - if o2 := s.diagnosticsInRange(empty, sel); len(o2) != 0 { - t.Fatalf("expected 0 with empty ctx") - } + s := &Server{} + ctx := CodeActionContext{Diagnostics: []Diagnostic{{ + Range: Range{Start: Position{Line: 10, Character: 0}, End: Position{Line: 12, Character: 0}}, + Message: "x", + }}} + raw, _ := json.Marshal(ctx) + sel := Range{Start: Position{Line: 11, Character: 0}, End: Position{Line: 11, Character: 1}} + out := s.diagnosticsInRange(raw, sel) + if len(out) != 1 { + t.Fatalf("expected 1 diag overlap, got %d", len(out)) + } + // no diagnostics + var empty json.RawMessage + if o2 := s.diagnosticsInRange(empty, sel); len(o2) != 0 { + t.Fatalf("expected 0 with empty ctx") + } } func TestIndentHelpersAndPromptRemoval(t *testing.T) { - if ind := leadingIndent("\t ab"); ind == "" { - t.Fatalf("expected indent") - } - if out := applyIndent(" ", "x\ny"); out != " x\n y" { - t.Fatalf("applyIndent: %q", out) - } - // double-open trigger removes whole line - edits := promptRemovalEditsForLine(">>ask>", 3) - if len(edits) != 1 || edits[0].Range.Start.Line != 3 { - t.Fatalf("unexpected edits: %#v", edits) - } - // temporarily switch to semicolon tags and test collection - oldOpen, oldClose := inlineOpenChar, inlineCloseChar - inlineOpenChar, inlineCloseChar = ';', ';' - t.Cleanup(func() { inlineOpenChar, inlineCloseChar = oldOpen, oldClose }) - edits2 := collectSemicolonMarkers("pre;do;post", 1) - if len(edits2) != 1 { - t.Fatalf("expected one semicolon edit, got %#v", edits2) - } + if ind := leadingIndent("\t ab"); ind == "" { + t.Fatalf("expected indent") + } + if out := applyIndent(" ", "x\ny"); out != " x\n y" { + t.Fatalf("applyIndent: %q", out) + } + // double-open trigger removes whole line + edits := promptRemovalEditsForLine(">>ask>", 3) + if len(edits) != 1 || edits[0].Range.Start.Line != 3 { + t.Fatalf("unexpected edits: %#v", edits) + } + // temporarily switch to semicolon tags and test collection + oldOpen, oldClose := inlineOpenChar, inlineCloseChar + inlineOpenChar, inlineCloseChar = ';', ';' + t.Cleanup(func() { inlineOpenChar, inlineCloseChar = oldOpen, oldClose }) + edits2 := collectSemicolonMarkers("pre;do;post", 1) + if len(edits2) != 1 { + t.Fatalf("expected one semicolon edit, got %#v", edits2) + } } diff --git a/internal/lsp/handlers.go b/internal/lsp/handlers.go index 420a694..e85065b 100644 --- a/internal/lsp/handlers.go +++ b/internal/lsp/handlers.go @@ -2,9 +2,9 @@ package lsp import ( - "encoding/json" - "fmt" - "strings" + "encoding/json" + "fmt" + "strings" ) func (s *Server) handle(req Request) { @@ -26,14 +26,14 @@ func (s *Server) handle(req Request) { // a line comment (//, #, --). Returns the instruction string and the selection // text cleaned of the matched instruction marker or comment. func instructionFromSelection(sel string) (string, string) { - lines := splitLines(sel) - for idx, line := range lines { - if instr, cleaned, ok := findFirstInstructionInLine(line); ok && strings.TrimSpace(instr) != "" { - lines[idx] = cleaned - return instr, strings.Join(lines, "\n") - } - } - return "", sel + lines := splitLines(sel) + for idx, line := range lines { + if instr, cleaned, ok := findFirstInstructionInLine(line); ok && strings.TrimSpace(instr) != "" { + lines[idx] = cleaned + return instr, strings.Join(lines, "\n") + } + } + return "", sel } // findFirstInstructionInLine returns the earliest instruction marker on the @@ -46,51 +46,51 @@ func instructionFromSelection(sel string) (string, string) { // - # text // - -- text func findFirstInstructionInLine(line string) (instr string, cleaned string, ok bool) { - type cand struct { - start, end int - text string - } - cands := []cand{} - if t, l, r, ok := findStrictInlineTag(line); ok { - cands = append(cands, cand{start: l, end: r, text: t}) - } - if i := strings.Index(line, "/*"); i >= 0 { - if j := strings.Index(line[i+2:], "*/"); j >= 0 { - start := i - end := i + 2 + j + 2 - text := strings.TrimSpace(line[i+2 : i+2+j]) - cands = append(cands, cand{start: start, end: end, text: text}) - } - } - if i := strings.Index(line, "<!--"); i >= 0 { - if j := strings.Index(line[i+4:], "-->"); j >= 0 { - start := i - end := i + 4 + j + 3 - text := strings.TrimSpace(line[i+4 : i+4+j]) - cands = append(cands, cand{start: start, end: end, text: text}) - } - } - if i := strings.Index(line, "//"); i >= 0 { - cands = append(cands, cand{start: i, end: len(line), text: strings.TrimSpace(line[i+2:])}) - } - if i := strings.Index(line, "#"); i >= 0 { - cands = append(cands, cand{start: i, end: len(line), text: strings.TrimSpace(line[i+1:])}) - } - if i := strings.Index(line, "--"); i >= 0 { - cands = append(cands, cand{start: i, end: len(line), text: strings.TrimSpace(line[i+2:])}) - } - if len(cands) == 0 { - return "", line, false - } - // pick earliest start index - best := cands[0] - for _, c := range cands[1:] { - if c.start >= 0 && (best.start < 0 || c.start < best.start) { - best = c - } - } - cleaned = strings.TrimRight(line[:best.start]+line[best.end:], " \t") - return best.text, cleaned, true + type cand struct { + start, end int + text string + } + cands := []cand{} + if t, l, r, ok := findStrictInlineTag(line); ok { + cands = append(cands, cand{start: l, end: r, text: t}) + } + if i := strings.Index(line, "/*"); i >= 0 { + if j := strings.Index(line[i+2:], "*/"); j >= 0 { + start := i + end := i + 2 + j + 2 + text := strings.TrimSpace(line[i+2 : i+2+j]) + cands = append(cands, cand{start: start, end: end, text: text}) + } + } + if i := strings.Index(line, "<!--"); i >= 0 { + if j := strings.Index(line[i+4:], "-->"); j >= 0 { + start := i + end := i + 4 + j + 3 + text := strings.TrimSpace(line[i+4 : i+4+j]) + cands = append(cands, cand{start: start, end: end, text: text}) + } + } + if i := strings.Index(line, "//"); i >= 0 { + cands = append(cands, cand{start: i, end: len(line), text: strings.TrimSpace(line[i+2:])}) + } + if i := strings.Index(line, "#"); i >= 0 { + cands = append(cands, cand{start: i, end: len(line), text: strings.TrimSpace(line[i+1:])}) + } + if i := strings.Index(line, "--"); i >= 0 { + cands = append(cands, cand{start: i, end: len(line), text: strings.TrimSpace(line[i+2:])}) + } + if len(cands) == 0 { + return "", line, false + } + // pick earliest start index + best := cands[0] + for _, c := range cands[1:] { + if c.start >= 0 && (best.start < 0 || c.start < best.start) { + best = c + } + } + cleaned = strings.TrimRight(line[:best.start]+line[best.end:], " \t") + return best.text, cleaned, true } // diagnosticsInRange parses the CodeAction context and returns diagnostics diff --git a/internal/lsp/handlers_codeaction.go b/internal/lsp/handlers_codeaction.go index d8dba38..e1c2720 100644 --- a/internal/lsp/handlers_codeaction.go +++ b/internal/lsp/handlers_codeaction.go @@ -31,40 +31,40 @@ func (s *Server) handleCodeAction(req Request) { } sel := extractRangeText(d, p.Range) - actions := make([]CodeAction, 0, 5) + actions := make([]CodeAction, 0, 5) if a := s.buildRewriteCodeAction(p, sel); a != nil { actions = append(actions, *a) } if a := s.buildDiagnosticsCodeAction(p, sel); a != nil { actions = append(actions, *a) } - if a := s.buildDocumentCodeAction(p, sel); a != nil { - actions = append(actions, *a) - } - if a := s.buildGoUnitTestCodeAction(p); a != nil { - actions = append(actions, *a) - } - if a := s.buildSimplifyCodeAction(p, sel); a != nil { - actions = append(actions, *a) - } + if a := s.buildDocumentCodeAction(p, sel); a != nil { + actions = append(actions, *a) + } + if a := s.buildGoUnitTestCodeAction(p); a != nil { + actions = append(actions, *a) + } + if a := s.buildSimplifyCodeAction(p, sel); a != nil { + actions = append(actions, *a) + } if len(req.ID) != 0 { s.reply(req.ID, actions, nil) } } func (s *Server) buildSimplifyCodeAction(p CodeActionParams, sel string) *CodeAction { - if strings.TrimSpace(sel) == "" { - return nil - } - payload := struct { - Type string `json:"type"` - URI string `json:"uri"` - Range Range `json:"range"` - Selection string `json:"selection"` - }{Type: "simplify", URI: p.TextDocument.URI, Range: p.Range, Selection: sel} - raw, _ := json.Marshal(payload) - ca := CodeAction{Title: "Hexai: simplify and improve", Kind: "refactor", Data: raw} - return &ca + if strings.TrimSpace(sel) == "" { + return nil + } + payload := struct { + Type string `json:"type"` + URI string `json:"uri"` + Range Range `json:"range"` + Selection string `json:"selection"` + }{Type: "simplify", URI: p.TextDocument.URI, Range: p.Range, Selection: sel} + raw, _ := json.Marshal(payload) + ca := CodeAction{Title: "Hexai: simplify and improve", Kind: "refactor", Data: raw} + return &ca } func (s *Server) buildRewriteCodeAction(p CodeActionParams, sel string) *CodeAction { @@ -115,7 +115,7 @@ func (s *Server) resolveCodeAction(ca CodeAction) (CodeAction, bool) { if err := json.Unmarshal(ca.Data, &payload); err != nil { return ca, false } - switch payload.Type { + switch payload.Type { case "rewrite": sys := s.promptRewriteSystem user := renderTemplate(s.promptRewriteUser, map[string]string{"instruction": payload.Instruction, "selection": payload.Selection}) @@ -123,7 +123,7 @@ func (s *Server) resolveCodeAction(ca CodeAction) (CodeAction, bool) { defer cancel() messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} opts := s.llmRequestOpts() - if text, err := s.llmClient.Chat(ctx, messages, opts...); err == nil { + if text, err := s.chatWithStats(ctx, messages, opts...); err == nil { if out := stripCodeFences(strings.TrimSpace(text)); out != "" { edit := WorkspaceEdit{Changes: map[string][]TextEdit{payload.URI: {{Range: payload.Range, NewText: out}}}} ca.Edit = &edit @@ -148,7 +148,7 @@ func (s *Server) resolveCodeAction(ca CodeAction) (CodeAction, bool) { defer cancel() messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} opts := s.llmRequestOpts() - if text, err := s.llmClient.Chat(ctx, messages, opts...); err == nil { + if text, err := s.chatWithStats(ctx, messages, opts...); err == nil { if out := stripCodeFences(strings.TrimSpace(text)); out != "" { edit := WorkspaceEdit{Changes: map[string][]TextEdit{payload.URI: {{Range: payload.Range, NewText: out}}}} ca.Edit = &edit @@ -164,7 +164,7 @@ func (s *Server) resolveCodeAction(ca CodeAction) (CodeAction, bool) { defer cancel() messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} opts := s.llmRequestOpts() - if text, err := s.llmClient.Chat(ctx, messages, opts...); err == nil { + if text, err := s.chatWithStats(ctx, messages, opts...); err == nil { if out := stripCodeFences(strings.TrimSpace(text)); out != "" { edit := WorkspaceEdit{Changes: map[string][]TextEdit{payload.URI: {{Range: payload.Range, NewText: out}}}} ca.Edit = &edit @@ -173,34 +173,34 @@ func (s *Server) resolveCodeAction(ca CodeAction) (CodeAction, bool) { } else { logging.Logf("lsp ", "codeAction document llm error: %v", err) } - case "go_test": - if edit, jumpURI, jumpRange, ok := s.resolveGoTest(payload.URI, payload.Range.Start); ok { - ca.Edit = &edit - // After edit is applied, ask client to jump to new test function - ca.Command = &Command{Title: "Jump to generated test", Command: "hexai.showDocument", Arguments: []any{jumpURI, jumpRange}} - // Also send a server-initiated showDocument shortly after resolve to cover - // clients that do not execute commands from code actions. - s.deferShowDocument(jumpURI, jumpRange) - return ca, true - } - case "simplify": - sys := s.promptRewriteSystem - // Reuse rewrite user template with a fixed instruction - user := renderTemplate(s.promptRewriteUser, map[string]string{"instruction": "Simplify and improve the code while preserving behavior. Return only the improved code.", "selection": payload.Selection}) - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} - opts := s.llmRequestOpts() - if text, err := s.llmClient.Chat(ctx, messages, opts...); err == nil { - if out := stripCodeFences(strings.TrimSpace(text)); out != "" { - edit := WorkspaceEdit{Changes: map[string][]TextEdit{payload.URI: {{Range: payload.Range, NewText: out}}}} - ca.Edit = &edit - return ca, true - } - } else { - logging.Logf("lsp ", "codeAction simplify llm error: %v", err) - } - } + case "go_test": + if edit, jumpURI, jumpRange, ok := s.resolveGoTest(payload.URI, payload.Range.Start); ok { + ca.Edit = &edit + // After edit is applied, ask client to jump to new test function + ca.Command = &Command{Title: "Jump to generated test", Command: "hexai.showDocument", Arguments: []any{jumpURI, jumpRange}} + // Also send a server-initiated showDocument shortly after resolve to cover + // clients that do not execute commands from code actions. + s.deferShowDocument(jumpURI, jumpRange) + return ca, true + } + case "simplify": + sys := s.promptRewriteSystem + // Reuse rewrite user template with a fixed instruction + user := renderTemplate(s.promptRewriteUser, map[string]string{"instruction": "Simplify and improve the code while preserving behavior. Return only the improved code.", "selection": payload.Selection}) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} + opts := s.llmRequestOpts() + if text, err := s.chatWithStats(ctx, messages, opts...); err == nil { + if out := stripCodeFences(strings.TrimSpace(text)); out != "" { + edit := WorkspaceEdit{Changes: map[string][]TextEdit{payload.URI: {{Range: payload.Range, NewText: out}}}} + ca.Edit = &edit + return ca, true + } + } else { + logging.Logf("lsp ", "codeAction simplify llm error: %v", err) + } + } return ca, false } @@ -508,7 +508,7 @@ func (s *Server) generateGoTestFunction(funcCode string) string { defer cancel() messages := []llm.Message{{Role: "system", Content: sys}, {Role: "user", Content: user}} opts := s.llmRequestOpts() - if out, err := s.llmClient.Chat(ctx, messages, opts...); err == nil { + if out, err := s.chatWithStats(ctx, messages, opts...); err == nil { cleaned := strings.TrimSpace(stripCodeFences(out)) if cleaned != "" { return cleaned diff --git a/internal/lsp/handlers_completion.go b/internal/lsp/handlers_completion.go index 06c44fb..14c5f3e 100644 --- a/internal/lsp/handlers_completion.go +++ b/internal/lsp/handlers_completion.go @@ -250,8 +250,14 @@ func (s *Server) tryProviderNativeCompletion(current string, p CompletionParams, if !s.waitForThrottle(ctx2) { return nil, false } + // Count approximate payload sizes: prompt+after sent; first suggestion received + sentBytes := len(prompt) + len(after) suggestions, err := cc.CodeCompletion(ctx2, prompt, after, 1, lang, temp) if err == nil && len(suggestions) > 0 { + // Update counters and heartbeat + s.incSentCounters(sentBytes) + s.incRecvCounters(len(suggestions[0])) + s.logLLMStats() cleaned := strings.TrimSpace(suggestions[0]) if cleaned != "" { cleaned = stripDuplicateAssignmentPrefix(current[:p.Position.Character], cleaned) @@ -272,6 +278,9 @@ func (s *Server) tryProviderNativeCompletion(current string, p CompletionParams, } } else if err != nil { logging.Logf("lsp ", "completion path=codex error=%v (falling back to chat)", err) + // Still emit a heartbeat for visibility, even on error + s.incSentCounters(sentBytes) + s.logLLMStats() } return nil, false } diff --git a/internal/lsp/handlers_document.go b/internal/lsp/handlers_document.go index f3648b2..14642c7 100644 --- a/internal/lsp/handlers_document.go +++ b/internal/lsp/handlers_document.go @@ -162,7 +162,7 @@ func (s *Server) detectAndHandleChat(uri string) { msgs := append([]llm.Message{{Role: "system", Content: sys}}, history...) opts := s.llmRequestOpts() logging.Logf("lsp ", "chat llm=requesting model=%s", s.llmClient.DefaultModel()) - text, err := s.llmClient.Chat(ctx, msgs, opts...) + text, err := s.chatWithStats(ctx, msgs, opts...) if err != nil { logging.Logf("lsp ", "chat llm error: %v", err) return diff --git a/internal/lsp/handlers_init_more_test.go b/internal/lsp/handlers_init_more_test.go index 230c773..8b8aa55 100644 --- a/internal/lsp/handlers_init_more_test.go +++ b/internal/lsp/handlers_init_more_test.go @@ -1,15 +1,14 @@ package lsp import ( - "bytes" - "log" - "testing" + "bytes" + "log" + "testing" ) func TestHandleInitialized_Logs(t *testing.T) { - // Minimal server with a logger; just ensure it doesn't panic. - var buf bytes.Buffer - s := NewServer(bytes.NewBuffer(nil), &buf, log.New(&buf, "", 0), ServerOptions{}) - s.handleInitialized() + // Minimal server with a logger; just ensure it doesn't panic. + var buf bytes.Buffer + s := NewServer(bytes.NewBuffer(nil), &buf, log.New(&buf, "", 0), ServerOptions{}) + s.handleInitialized() } - diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 796d6f4..97d7de7 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -78,11 +78,11 @@ type Server struct { promptDocumentSystem string promptRewriteUser string promptDiagnosticsUser string - promptDocumentUser string - promptGoTestSystem string - promptGoTestUser string - promptSimplifySystem string - promptSimplifyUser string + promptDocumentUser string + promptGoTestSystem string + promptGoTestUser string + promptSimplifySystem string + promptSimplifyUser string } // ServerOptions collects configuration for NewServer to avoid long parameter lists. @@ -121,10 +121,10 @@ type ServerOptions struct { PromptRewriteUser string PromptDiagnosticsUser string PromptDocumentUser string - PromptGoTestSystem string - PromptGoTestUser string - PromptSimplifySystem string - PromptSimplifyUser string + PromptGoTestSystem string + PromptGoTestUser string + PromptSimplifySystem string + PromptSimplifyUser string } func NewServer(r io.Reader, w io.Writer, logger *log.Logger, opts ServerOptions) *Server { @@ -203,11 +203,11 @@ func NewServer(r io.Reader, w io.Writer, logger *log.Logger, opts ServerOptions) s.promptDocumentSystem = opts.PromptDocumentSystem s.promptRewriteUser = opts.PromptRewriteUser s.promptDiagnosticsUser = opts.PromptDiagnosticsUser - s.promptDocumentUser = opts.PromptDocumentUser - s.promptGoTestSystem = opts.PromptGoTestSystem - s.promptGoTestUser = opts.PromptGoTestUser - s.promptSimplifySystem = opts.PromptSimplifySystem - s.promptSimplifyUser = opts.PromptSimplifyUser + s.promptDocumentUser = opts.PromptDocumentUser + s.promptGoTestSystem = opts.PromptGoTestSystem + s.promptGoTestUser = opts.PromptGoTestUser + s.promptSimplifySystem = opts.PromptSimplifySystem + s.promptSimplifyUser = opts.PromptSimplifyUser // Assign package-level inline trigger chars for free helper functions if s.inlineOpen != "" { |
