summaryrefslogtreecommitdiff
path: root/internal/lsp
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-08 12:02:40 +0300
committerPaul Buetow <paul@buetow.org>2025-09-08 12:02:40 +0300
commit75cf6abd55bfb60324fc47cf91eac08dbb8b87b4 (patch)
tree6ef90d8014fe4d9a757d3f7e95bf736b70e4c685 /internal/lsp
parent0dcf347c3fbc6e4ffb7e46294f5dd92dbbcd98ef (diff)
docs: move tmux documentation to its own file
Diffstat (limited to 'internal/lsp')
-rw-r--r--internal/lsp/codeaction_gotest_int_test.go37
-rw-r--r--internal/lsp/coverage_add_test.go156
-rw-r--r--internal/lsp/handlers.go112
-rw-r--r--internal/lsp/handlers_codeaction.go110
-rw-r--r--internal/lsp/handlers_completion.go9
-rw-r--r--internal/lsp/handlers_document.go2
-rw-r--r--internal/lsp/handlers_init_more_test.go15
-rw-r--r--internal/lsp/server.go28
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 != "" {