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
|
package grammar
import "encoding/json"
// Action is one grammar fix harper offers for a diagnostic: a human-readable title
// (e.g. `Replace with: "an"`) and the buffer edits that apply it, already mapped to
// glint's rune coordinates. Only replacement (quickfix) actions are surfaced; harper's
// ignore/telemetry commands are not — ignore is handled glint-side.
type Action struct {
Title string
Edits []TextEdit
}
// TextEdit replaces the rune range [Start..End) of the document with NewText. Ranges
// are inclusive of the start position and exclusive of the end, in rune columns per
// line. Grammar fixes are single-line in practice, but a multi-line range (Start and
// End on different lines) is represented faithfully so callers can apply it correctly.
type TextEdit struct {
StartLine, StartCol int
EndLine, EndCol int
NewText string
}
// rawAction mirrors the subset of an LSP CodeAction glint needs: the title and, for
// quickfix actions, a WorkspaceEdit's per-URI TextEdits. Bare Command actions decode
// with an empty Edit and are dropped.
type rawAction struct {
Title string `json:"title"`
Edit struct {
Changes map[string][]struct {
Range struct {
Start struct{ Line, Character int } `json:"start"`
End struct{ Line, Character int } `json:"end"`
} `json:"range"`
NewText string `json:"newText"`
} `json:"changes"`
} `json:"edit"`
}
// parseActions decodes a textDocument/codeAction result into replacement Actions,
// mapping each edit's UTF-16 ranges to rune columns against lines (the authoritative
// document text). Actions without edits (ignore/telemetry commands) are skipped, and
// duplicate titles — harper returns one per overlapping lint — are collapsed to the
// first, preserving order.
func parseActions(result json.RawMessage, lines []string) ([]Action, error) {
var raws []rawAction
if err := json.Unmarshal(result, &raws); err != nil {
return nil, err
}
lineAt := func(i int) string {
if i >= 0 && i < len(lines) {
return lines[i]
}
return ""
}
var out []Action
seen := map[string]bool{}
for _, ra := range raws {
if seen[ra.Title] {
continue
}
var edits []TextEdit
for _, tes := range ra.Edit.Changes {
for _, te := range tes {
edits = append(edits, TextEdit{
StartLine: te.Range.Start.Line,
StartCol: utf16ToRuneCol(lineAt(te.Range.Start.Line), te.Range.Start.Character),
EndLine: te.Range.End.Line,
EndCol: utf16ToRuneCol(lineAt(te.Range.End.Line), te.Range.End.Character),
NewText: te.NewText,
})
}
}
if len(edits) == 0 {
continue // ignore / telemetry command, not a replacement
}
seen[ra.Title] = true
out = append(out, Action{Title: ra.Title, Edits: edits})
}
return out, nil
}
|