all repos

clerk @ 225a2d3

missing tooling for ledger/hledger

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

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