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 }