▍ humdrum codex / glint v1.1.2
license AGPL-3.0
2.7 KB raw
 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
}