all repos

clerk @ master

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
improve loader, 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/token"
15
)
16
17
const diagDebounce = 200 * time.Millisecond
18
19
func (s *server) scheduleDiagnostics(ctx context.Context) {
20
	s.diagMu.Lock()
21
	defer s.diagMu.Unlock()
22
23
	if s.diagCancel != nil {
24
		s.diagCancel()
25
	}
26
27
	ctx, cancel := context.WithCancel(context.Background())
28
	s.diagCancel = cancel
29
30
	time.AfterFunc(diagDebounce, func() {
31
		if ctx.Err() != nil {
32
			return
33
		}
34
		s.publishDiagnostics(jsonrpc2.DetachContext(ctx))
35
	})
36
}
37
38
func (s *server) publishDiagnostics(ctx context.Context) {
39
	s.log.Debug("publishing diagnostics")
40
41
	s.mu.Lock()
42
	openDocs := make(map[uri.URI]docState, len(s.openDocs))
43
	for u, st := range s.openDocs {
44
		openDocs[u] = st
45
	}
46
	s.mu.Unlock()
47
48
	if ctx.Err() != nil {
49
		return
50
	}
51
52
	if len(openDocs) == 0 {
53
		return
54
	}
55
56
	activePaths := make(map[string]bool)
57
58
	var a *analyzer.Analysis
59
	for duri, state := range openDocs {
60
		path := duri.Path()
61
		rj := s.loader.ResolveBytes(path, []byte(state.text))
62
		if a == nil {
63
			a = analyzer.Build(rj)
64
		} else {
65
			// Merge: only one root in practice for now.
66
		}
67
		for _, pf := range rj.Occurrences {
68
			activePaths[pf.Path] = true
69
		}
70
	}
71
72
	if a == nil {
73
		s.log.Debug("no files in workspace")
74
		return
75
	}
76
77
	if ctx.Err() != nil {
78
		return
79
	}
80
81
	finds := dedupFinds(s.linter.Run(a))
82
83
	s.mu.Lock()
84
	s.current = a
85
	s.mu.Unlock()
86
87
	if ctx.Err() != nil {
88
		return
89
	}
90
91
	diagsByFile := s.groupFindsByFile(finds)
92
93
	for fpath := range activePaths {
94
		if ctx.Err() != nil {
95
			return
96
		}
97
		if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
98
			URI:         uri.File(fpath),
99
			Diagnostics: diagsByFile[fpath],
100
		}); err != nil {
101
			s.log.Warn("publish diagnostics failed", "uri", uri.File(fpath), "err", err)
102
		}
103
	}
104
105
	s.log.Debug("diagnostics published", "files", len(a.Files), "findings", len(finds))
106
}
107
108
func (s *server) groupFindsByFile(finds []linter.Find) map[string][]protocol.Diagnostic {
109
	diags := make(map[string][]protocol.Diagnostic)
110
	for _, find := range finds {
111
		file := find.Span.Start.File
112
		if file == "" {
113
			continue
114
		}
115
		diags[file] = append(diags[file], s.findToDiagnostic(find))
116
	}
117
	return diags
118
}
119
120
func (s *server) findToDiagnostic(find linter.Find) protocol.Diagnostic {
121
	return protocol.Diagnostic{
122
		Range:    spanToRange(find.Span),
123
		Severity: severityToLSP(find.Severity),
124
		Message:  protocol.String(find.Message),
125
		Source:   protocol.NewOptional(s.name),
126
		Code:     protocol.String(string(find.Code)),
127
	}
128
}
129
130
func dedupFinds(finds []linter.Find) []linter.Find {
131
	seen := make(map[string]bool)
132
	dedup := make([]linter.Find, 0, len(finds))
133
	for _, f := range finds {
134
		s := f.Span.Start
135
		key := fmt.Sprintf("%s:%d:%d:%s", s.File, s.Line, s.Col, f.Code) // TODO: performace
136
		if seen[key] {
137
			continue
138
		}
139
		seen[key] = true
140
		dedup = append(dedup, f)
141
	}
142
	return dedup
143
}
144
145
func spanToRange(span token.Span) protocol.Range {
146
	return protocol.Range{
147
		Start: protocol.Position{
148
			Line:      max(0, uint32(span.Start.Line-1)),
149
			Character: max(0, uint32(span.Start.Col-1)),
150
		},
151
		End: protocol.Position{
152
			Line:      max(0, uint32(span.End.Line-1)),
153
			Character: uint32(max(0, span.End.Col-1)),
154
		},
155
	}
156
}
157
158
func severityToLSP(s linter.Severity) protocol.DiagnosticSeverity {
159
	switch s {
160
	case linter.SeverityError:
161
		return protocol.DiagnosticSeverityError
162
	case linter.SeverityWarning:
163
		return protocol.DiagnosticSeverityWarning
164
	case linter.SeverityInfo:
165
		return protocol.DiagnosticSeverityInformation
166
	case linter.SeverityHint:
167
		return protocol.DiagnosticSeverityHint
168
	default:
169
		panic("impossible diagnostic severity")
170
	}
171
}