all repos

clerk @ 8575d55

missing tooling for ledger/hledger

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

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