all repos

clerk @ d12c0794480a4e65333d20e93d8a9f24a53ea056

missing tooling for ledger/hledger

clerk/internal/settings/settings.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
run gofumpt && goimports, 13 days ago
1
package settings
2
3
import (
4
	"errors"
5
	"fmt"
6
	"io/fs"
7
	"os"
8
	"strings"
9
10
	"github.com/pelletier/go-toml/v2"
11
12
	"olexsmir.xyz/clerk/internal/linter"
13
	"olexsmir.xyz/clerk/journal/printer"
14
)
15
16
type Settings struct {
17
	// SemanticHighlighting enables LSP semantic tokens.
18
	SemanticHighlighting bool
19
20
	// LatinToCyrillicCompletion matches Latin input against Cyrillic labels.
21
	LatinToCyrillicCompletion bool
22
23
	Linter linter.Config
24
	Format printer.Config
25
}
26
27
var DefaultConfig = Settings{
28
	SemanticHighlighting:      true,
29
	LatinToCyrillicCompletion: false,
30
	Linter:                    linter.DefaultConfig,
31
	Format:                    printer.DefaultConfig,
32
}
33
34
// Load reads and parses the TOML config file at path.
35
// A missing file yields defaults without error.
36
func Load(fpath string) (Settings, []string, error) {
37
	data, err := os.ReadFile(fpath)
38
	if err != nil {
39
		if errors.Is(err, fs.ErrNotExist) {
40
			return DefaultConfig, nil, nil
41
		}
42
		return DefaultConfig, nil, err
43
	}
44
	var raw map[string]any
45
	if err := toml.Unmarshal(data, &raw); err != nil {
46
		return DefaultConfig, nil, err
47
	}
48
	return parse(raw)
49
}
50
51
func parse(raw map[string]any) (settings Settings, warns []string, err error) {
52
	s := DefaultConfig
53
	warns, err = s.Apply(raw)
54
	return s, warns, err
55
}
56
57
// Apply merges raw setting from a config file into Settings object.
58
func (s *Settings) Apply(raw map[string]any) ([]string, error) {
59
	return applyMap(raw, s.applyFileField)
60
}
61
62
func (s *Settings) applyFileField(name string, val any) ([]string, error) {
63
	switch normalizeKey(name) {
64
	case "lint":
65
		return s.setLint(val)
66
	case "format":
67
		return s.setFormat(val)
68
	default:
69
		return []string{fmt.Sprintf("unknown setting %q", name)}, nil
70
	}
71
}
72
73
// ApplyLSP merges raw settings from lsp server config into Settings object.
74
func (s *Settings) ApplyLSP(raw map[string]any) ([]string, error) {
75
	return applyMap(raw, s.applyLSPField)
76
}
77
78
func (s *Settings) applyLSPField(name string, val any) ([]string, error) {
79
	switch normalizeKey(name) {
80
	case "semantic_highlighting":
81
		return nil, setBool(&s.SemanticHighlighting, val)
82
	case "latin_to_cyrillic_completion":
83
		return nil, setBool(&s.LatinToCyrillicCompletion, val)
84
	default:
85
		return s.applyFileField(name, val)
86
	}
87
}
88
89
func applyMap(m map[string]any, fn func(k string, val any) ([]string, error)) ([]string, error) {
90
	count := make(map[string]int, len(m))
91
	for k := range m {
92
		count[normalizeKey(k)]++
93
	}
94
	var (
95
		warns []string
96
		errs  []error
97
	)
98
	for k, val := range m {
99
		norm := normalizeKey(k)
100
		if count[norm] > 1 {
101
			errs = append(errs, fmt.Errorf("%s: duplicate setting %q not applied", k, norm))
102
			continue
103
		}
104
		ws, err := fn(k, val)
105
		warns = append(warns, ws...)
106
		if err != nil {
107
			errs = append(errs, prefixLines(k, err))
108
		}
109
	}
110
	return warns, errors.Join(errs...)
111
}
112
113
func applyTable(v any, fn func(k string, val any) ([]string, error)) ([]string, error) {
114
	m, ok := v.(map[string]any)
115
	if !ok {
116
		return nil, fmt.Errorf("invalid value %v (want table)", v)
117
	}
118
	return applyMap(m, fn)
119
}
120
121
func normalizeKey(s string) string {
122
	// already canonical(lowercase, digits, underscores)
123
	canonical := true
124
	for i := 0; i < len(s); i++ {
125
		c := s[i]
126
		if c != '_' && (c < 'a' || c > 'z') && (c < '0' || c > '9') {
127
			canonical = false
128
			break
129
		}
130
	}
131
	if canonical {
132
		return s
133
	}
134
135
	var b strings.Builder
136
	b.Grow(len(s))
137
	prevLower := false
138
	for i := 0; i < len(s); i++ {
139
		c := s[i]
140
		switch {
141
		case c == '-' || c == '_':
142
			b.WriteByte('_')
143
			prevLower = false
144
		case c >= 'A' && c <= 'Z':
145
			if prevLower {
146
				b.WriteByte('_')
147
			}
148
			b.WriteByte(c + 'a' - 'A')
149
			prevLower = false
150
		case c >= 'a' && c <= 'z':
151
			b.WriteByte(c)
152
			prevLower = true
153
		default:
154
			b.WriteByte(c)
155
			prevLower = false
156
		}
157
	}
158
	return b.String()
159
}
160
161
func prefixLines(prefix string, err error) error {
162
	lines := strings.Split(err.Error(), "\n")
163
	for i, line := range lines {
164
		if line != "" {
165
			lines[i] = prefix + ": " + line
166
		}
167
	}
168
	return errors.New(strings.Join(lines, "\n"))
169
}