1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
|
package lsp
import (
"bytes"
"encoding/json"
"io"
"log"
"strings"
"testing"
"codeberg.org/snonux/hexai/internal/appconfig"
)
// local copy of captureResponse for this test file
func capResp(t *testing.T, buf *bytes.Buffer) Response {
t.Helper()
raw := buf.String()
idx := strings.Index(raw, "\r\n\r\n")
if idx < 0 {
t.Fatalf("no header/body separator in %q", raw)
}
body := raw[idx+4:]
var resp Response
if err := json.Unmarshal([]byte(body), &resp); err != nil {
t.Fatalf("unmarshal response: %v", err)
}
return resp
}
func TestHandleCodeAction_ListsCustomActions(t *testing.T) {
var out bytes.Buffer
cfg := appconfig.App{
InlineOpen: ">!",
InlineClose: ">",
ChatSuffix: ">",
ChatPrefixes: []string{"?", "!", ":", ";"},
CustomActions: []appconfig.CustomAction{
{ID: "extract", Title: "Extract function", Scope: "selection", Kind: "refactor.extract", Instruction: "Extract into function"},
{ID: "fix", Title: "Fix diagnostics", Scope: "diagnostics", Kind: "quickfix", User: "Fix:\n{{diagnostics}}\n\n{{selection}}"},
},
}
s := &Server{
logger: log.New(io.Discard, "", 0),
docs: make(map[string]*document),
out: &out,
cfg: cfg,
}
s.llmClient = fakeLLM{resp: "ok"}
// Prepare document and params
uri := "file:///t.go"
s.setDocument(uri, "package x\n\nfunc f(){}\n")
p := CodeActionParams{TextDocument: TextDocumentIdentifier{URI: uri}, Range: Range{Start: Position{Line: 2}, End: Position{Line: 2, Character: 5}}}
// Include diagnostics context so diagnostics-scoped action appears
ctx := CodeActionContext{Diagnostics: []Diagnostic{{Range: Range{Start: Position{Line: 2}}, Message: "warn"}}}
raw, _ := json.Marshal(ctx)
p.Context = json.RawMessage(raw)
// Call handler
req := Request{JSONRPC: "2.0", ID: json.RawMessage("1"), Method: "textDocument/codeAction"}
req.Params, _ = json.Marshal(p)
out.Reset()
s.handleCodeAction(req)
resp := capResp(t, &out)
var actions []CodeAction
rb, _ := json.Marshal(resp.Result)
if err := json.Unmarshal(rb, &actions); err != nil {
t.Fatalf("decode: %v", err)
}
var seenSel, seenDiag bool
for _, a := range actions {
if a.Title == "Hexai: Extract function" {
seenSel = true
}
if a.Title == "Hexai: Fix diagnostics" {
seenDiag = true
}
}
if !seenSel || !seenDiag {
t.Fatalf("expected both custom actions, got %+v", actions)
}
}
func TestResolveCodeAction_CustomInstructionAndUser(t *testing.T) {
s := newTestServer()
s.llmClient = fakeLLM{resp: "REPLACED"}
cfg := s.cfg
cfg.CustomActions = []appconfig.CustomAction{
{ID: "extract", Title: "Extract function", Scope: "selection", Kind: "refactor.extract", Instruction: "Extract into function"},
{ID: "fix", Title: "Fix diagnostics", Scope: "diagnostics", Kind: "quickfix", User: "Fix: {{diagnostics}}\n{{selection}}"},
}
s.cfg = cfg
uri := "file:///t.go"
p := CodeActionParams{TextDocument: TextDocumentIdentifier{URI: uri}, Range: Range{Start: Position{Line: 1}, End: Position{Line: 1, Character: 3}}}
// Build selection-scoped custom action payload
selPayload := struct {
Type string `json:"type"`
ID string `json:"id"`
URI string `json:"uri"`
Range Range `json:"range"`
Selection string `json:"selection"`
}{Type: "custom", ID: "extract", URI: uri, Range: p.Range, Selection: "abc"}
raw1, _ := json.Marshal(selPayload)
ca1 := CodeAction{Title: "Hexai: Extract function", Data: raw1}
if resolved, ok := s.resolveCodeAction(ca1); !ok || resolved.Edit == nil {
t.Fatalf("expected resolve for instruction-based custom action")
}
// Build diagnostics-scoped custom action payload
diagPayload := struct {
Type string `json:"type"`
ID string `json:"id"`
URI string `json:"uri"`
Range Range `json:"range"`
Selection string `json:"selection"`
Diagnostics []Diagnostic `json:"diagnostics"`
}{Type: "custom", ID: "fix", URI: uri, Range: p.Range, Selection: "abc", Diagnostics: []Diagnostic{{Message: "lint"}}}
raw2, _ := json.Marshal(diagPayload)
ca2 := CodeAction{Title: "Hexai: Fix diagnostics", Data: raw2}
if resolved, ok := s.resolveCodeAction(ca2); !ok || resolved.Edit == nil {
t.Fatalf("expected resolve for user-based custom action")
}
}
|