summaryrefslogtreecommitdiff
path: root/internal/lsp/codeaction_custom_test.go
blob: 36f99d47cf86aed1db1a6c254d371d5d351ed039 (plain)
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")
	}
}