all repos

clerk @ 6106d588c3cc8b0aa2d551af7c80a0e8268ae14a

missing tooling for ledger/hledger

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

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