all repos

clerk @ 2a7dd08

missing tooling for ledger/hledger

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

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