all repos

clerk @ e2e9e2b

missing tooling for ledger/hledger

clerk/internal/lsp/diagnostics.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
improve analyzer, 1 day ago
1
package lsp
2
3
import (
4
	"context"
5
	"fmt"
6
	"time"
7
8
	"go.lsp.dev/jsonrpc2"
9
	"go.lsp.dev/protocol"
10
	"go.lsp.dev/uri"
11
12
	"olexsmir.xyz/clerk/internal/analyzer"
13
	"olexsmir.xyz/clerk/internal/linter"
14
	"olexsmir.xyz/clerk/journal"
15
	"olexsmir.xyz/clerk/journal/token"
16
)
17
18
const diagDebounce = 200 * time.Millisecond
19
20
func (s *server) scheduleDiagnostics(ctx context.Context) {
21
	s.diagMu.Lock()
22
	defer s.diagMu.Unlock()
23
24
	if s.diagCancel != nil {
25
		s.diagCancel()
26
	}
27
28
	ctx, cancel := context.WithCancel(context.Background())
29
	s.diagCancel = cancel
30
31
	time.AfterFunc(diagDebounce, func() {
32
		if ctx.Err() != nil {
33
			return
34
		}
35
		s.publishDiagnostics(jsonrpc2.DetachContext(ctx))
36
	})
37
}
38
39
func (s *server) publishDiagnostics(ctx context.Context) {
40
	s.log.Debug("publishing diagnostics")
41
42
	s.mu.Lock()
43
	openDocs := make(map[uri.URI]docState, len(s.openDocs))
44
	for u, st := range s.openDocs {
45
		openDocs[u] = st
46
	}
47
	ordered := s.loader.Ordered()
48
	s.mu.Unlock()
49
50
	if ctx.Err() != nil {
51
		return
52
	}
53
54
	if len(openDocs) == 0 {
55
		return
56
	}
57
58
	for uri, state := range openDocs {
59
		path := uri.Path()
60
		if _, err := s.loader.Reload(path, []byte(state.text)); err != nil {
61
			s.log.Warn("failed to reload open document", "uri", uri, "err", err)
62
		}
63
	}
64
65
	if ctx.Err() != nil {
66
		return
67
	}
68
69
	ordered = s.loader.Ordered()
70
	if len(ordered) == 0 {
71
		s.log.Debug("no files in workspace")
72
		return
73
	}
74
75
	if ctx.Err() != nil {
76
		return
77
	}
78
79
	a := analyzer.Build(ordered)
80
	finds := dedupFinds(s.linter.Run(a))
81
82
	s.mu.Lock()
83
	s.current = a
84
	s.mu.Unlock()
85
86
	if ctx.Err() != nil {
87
		return
88
	}
89
90
	diagsByFile := s.groupFindsByFile(finds)
91
92
	s.mu.Lock()
93
	activeFiles := s.activeFileSet(openDocs, ordered)
94
	s.mu.Unlock()
95
96
	for fpath := range activeFiles {
97
		if ctx.Err() != nil {
98
			return
99
		}
100
		if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
101
			URI:         uri.File(fpath),
102
			Diagnostics: diagsByFile[fpath],
103
		}); err != nil {
104
			s.log.Warn("publish diagnostics failed", "uri", uri.File(fpath), "err", err)
105
		}
106
	}
107
108
	s.log.Debug("diagnostics published", "files", len(ordered), "findings", len(finds))
109
}
110
111
func (s *server) activeFileSet(docs map[uri.URI]docState, ordered []*journal.ParsedFile) map[string]bool {
112
	active := make(map[string]bool)
113
	for duri := range docs {
114
		active[duri.Path()] = true
115
	}
116
	visited := make(map[string]bool)
117
	var walk func(*journal.ParsedFile)
118
	walk = func(pf *journal.ParsedFile) {
119
		if visited[pf.Path] {
120
			return
121
		}
122
		active[pf.Path] = true
123
		visited[pf.Path] = true
124
		for _, inc := range pf.Includes {
125
			walk(inc)
126
		}
127
	}
128
	byPath := make(map[string]*journal.ParsedFile, len(ordered))
129
	for _, pf := range ordered {
130
		byPath[pf.Path] = pf
131
	}
132
	for duri := range docs {
133
		if pf, ok := byPath[duri.Path()]; ok {
134
			walk(pf)
135
		}
136
	}
137
	return active
138
}
139
140
func (s *server) groupFindsByFile(finds []linter.Find) map[string][]protocol.Diagnostic {
141
	diags := make(map[string][]protocol.Diagnostic)
142
	for _, find := range finds {
143
		file := find.Span.Start.File
144
		if file == "" {
145
			continue
146
		}
147
		diags[file] = append(diags[file], s.findToDiagnostic(find))
148
	}
149
	return diags
150
}
151
152
func (s *server) findToDiagnostic(find linter.Find) protocol.Diagnostic {
153
	return protocol.Diagnostic{
154
		Range:    spanToRange(find.Span),
155
		Severity: severityToLSP(find.Severity),
156
		Message:  protocol.String(find.Message),
157
		Source:   protocol.NewOptional(s.name),
158
		Code:     protocol.String(string(find.Code)),
159
	}
160
}
161
162
func dedupFinds(finds []linter.Find) []linter.Find {
163
	seen := make(map[string]bool)
164
	dedup := make([]linter.Find, 0, len(finds))
165
	for _, f := range finds {
166
		s := f.Span.Start
167
		key := fmt.Sprintf("%s:%d:%d:%s", s.File, s.Line, s.Col, f.Code) // TODO: performace
168
		if seen[key] {
169
			continue
170
		}
171
		seen[key] = true
172
		dedup = append(dedup, f)
173
	}
174
	return dedup
175
}
176
177
func spanToRange(span token.Span) protocol.Range {
178
	return protocol.Range{
179
		Start: protocol.Position{
180
			Line:      max(0, uint32(span.Start.Line-1)),
181
			Character: max(0, uint32(span.Start.Col-1)),
182
		},
183
		End: protocol.Position{
184
			Line:      max(0, uint32(span.End.Line-1)),
185
			Character: uint32(max(0, span.End.Col-1)),
186
		},
187
	}
188
}
189
190
func severityToLSP(s linter.Severity) protocol.DiagnosticSeverity {
191
	switch s {
192
	case linter.SeverityError:
193
		return protocol.DiagnosticSeverityError
194
	case linter.SeverityWarning:
195
		return protocol.DiagnosticSeverityWarning
196
	case linter.SeverityInfo:
197
		return protocol.DiagnosticSeverityInformation
198
	case linter.SeverityHint:
199
		return protocol.DiagnosticSeverityHint
200
	default:
201
		panic("impossible diagnostic severity")
202
	}
203
}