all repos

clerk @ 9369172

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
linter/lsp: scope checks to include tree, 20 days 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"
14
	"olexsmir.xyz/clerk/journal/semantic"
15
	"olexsmir.xyz/clerk/journal/token"
16
)
17
18
func (s *server) rebuildWorkspace(ctx context.Context) {
19
	s.logger.Debug("rebuilding workspace")
20
21
	s.mu.Lock()
22
	defer s.mu.Unlock()
23
24
	if len(s.openDocs) == 0 {
25
		return
26
	}
27
28
	// reload all open docs from in-memory content
29
	for duri, state := range s.openDocs {
30
		path := duri.Path()
31
		if _, err := s.loader.Reload(path, []byte(state.text)); err != nil {
32
			s.logger.Warn("failed to relod open document", "uri", duri, "err", err)
33
		}
34
	}
35
36
	roots := s.loader.Roots()
37
	if len(roots) == 0 {
38
		s.logger.Debug("no files in workspace")
39
		return
40
	}
41
42
	var finds []linter.Find
43
	for _, root := range roots {
44
		finds = append(finds, s.linter.Run(semantic.Build(journal.CollectFiles(root)))...)
45
	}
46
	finds = dedupFinds(finds)
47
48
	diagsByFile := groupFindsByFile(finds)
49
	activeFiles := activeFileSet(s.openDocs, s.loader.Ordered())
50
	for fpath := range activeFiles {
51
		diags := diagsByFile[fpath]
52
		if diags == nil { // TODO: not sure if it's needed
53
			diags = []protocol.Diagnostic{}
54
		}
55
		if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
56
			URI:         uri.File(fpath),
57
			Diagnostics: diags,
58
		}); err != nil {
59
			s.logger.Warn("publish diagnostics failed", "uri", uri.File(fpath), "err", err)
60
		}
61
	}
62
63
	s.logger.Debug("workspace rebuild complete", "files", len(s.loader.Ordered()), "findings", len(finds))
64
}
65
66
func (s *server) scheduleWorkspaceRebuild(ctx context.Context) {
67
	s.mu.Lock()
68
	if s.debouncer != nil {
69
		s.debouncer.Stop()
70
	}
71
	ctx = jsonrpc2.DetachContext(ctx)
72
	s.debouncer = time.AfterFunc(200*time.Millisecond, func() {
73
		s.rebuildWorkspace(ctx)
74
	})
75
	s.mu.Unlock()
76
}
77
78
func (s *server) cancelWorkspaceRebuild() {
79
	s.mu.Lock()
80
	if s.debouncer != nil {
81
		s.debouncer.Stop()
82
		s.debouncer = nil
83
	}
84
	s.mu.Unlock()
85
}
86
87
func activeFileSet(docs map[uri.URI]docState, ordered []*journal.ParsedFile) map[string]bool {
88
	active := make(map[string]bool)
89
	for duri := range docs {
90
		active[duri.Path()] = true
91
	}
92
93
	visited := make(map[string]bool)
94
	var walk func(*journal.ParsedFile)
95
	walk = func(pf *journal.ParsedFile) {
96
		if visited[pf.Path] {
97
			return
98
		}
99
		active[pf.Path] = true
100
		visited[pf.Path] = true
101
		for _, inc := range pf.Includes {
102
			walk(inc)
103
		}
104
	}
105
106
	byPath := make(map[string]*journal.ParsedFile, len(ordered))
107
	for _, pf := range ordered {
108
		byPath[pf.Path] = pf
109
	}
110
	for duri := range docs {
111
		if pf, ok := byPath[duri.Path()]; ok {
112
			walk(pf)
113
		}
114
	}
115
116
	return active
117
}
118
119
func groupFindsByFile(finds []linter.Find) map[string][]protocol.Diagnostic {
120
	diags := make(map[string][]protocol.Diagnostic)
121
	for _, find := range finds {
122
		file := find.Span.Start.File
123
		if file == "" {
124
			continue
125
		}
126
		diags[file] = append(diags[file], findToDiagnostic(find))
127
	}
128
	return diags
129
}
130
131
func dedupFinds(finds []linter.Find) []linter.Find {
132
	seen := make(map[string]bool)
133
	dedup := make([]linter.Find, 0, len(finds))
134
	for _, f := range finds {
135
		s := f.Span.Start
136
		key := fmt.Sprintf("%s:%d:%d:%s", s.File, s.Line, s.Col, f.Code)
137
		if seen[key] {
138
			continue
139
		}
140
		seen[key] = true
141
		dedup = append(dedup, f)
142
	}
143
	return dedup
144
}
145
146
func findToDiagnostic(find linter.Find) protocol.Diagnostic {
147
	return protocol.Diagnostic{
148
		Range:    spanToRange(find.Span),
149
		Severity: severityToLSP(find.Severity),
150
		Message:  protocol.String(find.Message),
151
		Source:   protocol.NewOptional(name),
152
		Code:     protocol.String(string(find.Code)),
153
	}
154
}
155
156
func spanToRange(span token.Span) protocol.Range {
157
	return protocol.Range{
158
		Start: protocol.Position{
159
			Line:      max(0, uint32(span.Start.Line-1)),
160
			Character: max(0, uint32(span.Start.Col-1)),
161
		},
162
		End: protocol.Position{
163
			Line:      max(0, uint32(span.End.Line-1)),
164
			Character: uint32(max(0, span.End.Col-1)),
165
		},
166
	}
167
}
168
169
func severityToLSP(s linter.Severity) protocol.DiagnosticSeverity {
170
	switch s {
171
	case linter.SeverityError:
172
		return protocol.DiagnosticSeverityError
173
	case linter.SeverityWarning:
174
		return protocol.DiagnosticSeverityWarning
175
	case linter.SeverityInfo:
176
		return protocol.DiagnosticSeverityInformation
177
	case linter.SeverityHint:
178
		return protocol.DiagnosticSeverityHint
179
	default:
180
		panic("impossible diagnostic severity")
181
	}
182
}