turbo-editors/turbo-corepublic Fork 0
v1.0.2
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

language.go · 409 lines · 12.6 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 18h ago1package app
2
3import (
4 "context"
5 "errors"
6 "sort"
7 "sync"
8
📦 Turbo Core f3ade8d k33g 10h ago9 "rickub.com/turbo-editors/turbo-core/lsp"
10 "rickub.com/turbo-editors/turbo-core/profile"
🛟 Updated. 28d5985 k33g 18h ago11)
12
13// Language is the editor's side of the language-server conversation.
14//
15// Everything here is optional: when the language server is not installed, or
16// fails to start, every method is a no-op and the editor carries on without
17// completion. That is the whole point of the type — the rest of the app never
18// has to ask whether a language server exists.
19type Language struct {
20 mu sync.Mutex
21 server *lsp.Server // nil when a client was attached without a process
22 client *lsp.Client
23
24 // wanted is the server this editor talks to: gopls, rust-analyzer. It is
25 // fixed when the editor starts and never changes.
26 wanted profile.Server
27 // clientName is how the editor introduces itself in the handshake.
28 clientName string
29
30 serverPath string
31 root string
32 status string
33
34 diagnostics map[string][]lsp.Diagnostic
35 documents map[string]bool // what the server has been told about
36
37 // OnUpdate is called when something changed that the screen should show:
38 // new diagnostics, or a new status.
39 OnUpdate func()
40}
41
42// NewLanguage returns a Language that is not connected to anything, and that
43// will look for the given server when it is started, introducing itself as
44// clientName.
45func NewLanguage(server profile.Server, clientName string) *Language {
46 return &Language{
47 wanted: server,
48 clientName: clientName,
49 status: "LSP: off",
50 diagnostics: map[string][]lsp.Diagnostic{},
51 documents: map[string]bool{},
52 }
53}
54
55// Start looks for the editor's language server and, if it is there, starts it
56// in root.
57//
58// A missing server is reported through Status rather than as an error: it is a
59// perfectly ordinary state for the editor to run in.
60func (l *Language) Start(ctx context.Context, root string) {
61 l.mu.Lock()
62 l.root = root
63 l.mu.Unlock()
64 l.setStatus("LSP: starting…")
65
66 // The path is found first so that the status can name the executable, even
67 // when starting it then fails.
68 path, err := lsp.FindServer(l.wanted)
69 if err != nil {
70 l.setStatus(l.startupMessage(err))
71 return
72 }
73 l.mu.Lock()
74 l.serverPath = path
75 l.mu.Unlock()
76
77 server, err := lsp.StartServerCommand(ctx, path, l.wanted.Args, root, l.clientName)
78 if err != nil {
79 l.setStatus(l.startupMessage(err))
80 return
81 }
82
83 l.mu.Lock()
84 l.server = server
85 l.mu.Unlock()
86
87 l.attach(server.Client())
88}
89
90// attach adopts an initialised client and marks the editor ready to talk to
91// it. Start uses it for the client of a process it launched; a test uses it
92// for a client wired straight to a server in the same process.
93func (l *Language) attach(client *lsp.Client) {
94 client.OnDiagnostics = l.receiveDiagnostics
95
96 l.mu.Lock()
97 l.client = client
98 l.mu.Unlock()
99
100 l.setStatus("LSP: ready")
101}
102
103// startupMessage turns a start-up failure into something worth reading on a
104// status bar.
105//
106// A missing server is reported with the one command that installs it, because
107// "no rust-analyzer" on its own leaves the reader to go and look it up.
108func (l *Language) startupMessage(err error) string {
109 if errors.Is(err, lsp.ErrServerNotFound) {
110 return "LSP: no " + l.wanted.Command + " — " + l.wanted.InstallHint
111 }
112 return "LSP: " + err.Error()
113}
114
115// Stop shuts the language server down, if there is one.
116func (l *Language) Stop(ctx context.Context) {
117 l.mu.Lock()
118 server := l.server
119 l.server, l.client = nil, nil
120 l.mu.Unlock()
121
122 if server != nil {
123 _ = server.Stop(ctx)
124 }
125}
126
127// Report is everything worth knowing about the language server's state, in
128// enough detail to explain why a completion produced nothing.
129type Report struct {
130 Status string
131 ServerPath string
132 Root string
133 Ready bool
134}
135
136// Report returns the current state of the conversation.
137func (l *Language) Report() Report {
138 l.mu.Lock()
139 report := Report{Status: l.status, ServerPath: l.serverPath, Root: l.root}
140 l.mu.Unlock()
141
142 report.Ready = l.Ready()
143 return report
144}
145
146// Status returns the one-line state to show on the status bar.
147func (l *Language) Status() string {
148 l.mu.Lock()
149 defer l.mu.Unlock()
150 return l.status
151}
152
153// setStatus records a new status and asks for a redraw.
154func (l *Language) setStatus(status string) {
155 l.mu.Lock()
156 l.status = status
157 l.mu.Unlock()
158 l.notify()
159}
160
161// Ready reports whether a language server is connected and initialised.
162func (l *Language) Ready() bool {
163 client := l.connected()
164 return client != nil && client.Ready()
165}
166
167// connected returns the client to talk to, or nil when there is none.
168func (l *Language) connected() *lsp.Client {
169 l.mu.Lock()
170 defer l.mu.Unlock()
171 return l.client
172}
173
174// DidOpen tells the server about a file the editor has opened.
175func (l *Language) DidOpen(path, text string) {
176 client := l.connected()
177 if client == nil || path == "" {
178 return
179 }
180 if err := client.DidOpen(path, text); err != nil {
181 return
182 }
183
184 l.mu.Lock()
185 l.documents[pathKey(path)] = true
186 l.mu.Unlock()
187}
188
189// Knows reports whether the server has been told about a file. It is what the
190// status box uses to explain a completion that produced nothing, and what
191// saving uses to decide between announcing a document and reporting a write.
192//
193// The answer does not depend on how the path is spelt: a file opened as
194// `main.go` is known under its absolute name too, for the same reason
195// diagnostics are keyed that way.
196func (l *Language) Knows(path string) bool {
197 l.mu.Lock()
198 defer l.mu.Unlock()
199 return l.documents[pathKey(path)]
200}
201
202// DidChange tells the server about an edit.
203func (l *Language) DidChange(path, text string) {
204 if client := l.connected(); client != nil && path != "" {
205 _ = client.DidChange(path, text)
206 }
207}
208
209// DidSave tells the server a file has been written.
210func (l *Language) DidSave(path, text string) {
211 if client := l.connected(); client != nil && path != "" {
212 _ = client.DidSave(path, text)
213 }
214}
215
📦 Turbo Core — a save that creates a file tells the server (workspace/didChangeWatchedFiles), so moon-lsp diagnoses a new .mbt from its first save 3561e52 k33g 8h ago216// FileCreated tells the server a file has appeared on disk — what a save under
217// a new name does, and what a server that lists a package's files from the
218// directory needs to hear before it will diagnose the document.
219func (l *Language) FileCreated(path string) {
220 if client := l.connected(); client != nil && path != "" {
221 _ = client.FileCreated(path)
222 }
223}
224
🛟 Updated. 28d5985 k33g 18h ago225// DidClose tells the server a file is no longer open, and forgets its
226// diagnostics.
227func (l *Language) DidClose(path string) {
228 if client := l.connected(); client != nil && path != "" {
229 _ = client.DidClose(path)
230 }
231
232 l.mu.Lock()
233 delete(l.diagnostics, pathKey(path))
234 delete(l.documents, pathKey(path))
235 l.mu.Unlock()
236}
237
238// Complete asks what could be typed at a place in a file, and returns nothing
239// at all when there is no server to ask.
240func (l *Language) Complete(ctx context.Context, path string, line, column int, lineText string) ([]lsp.CompletionItem, error) {
241 client := l.connected()
242 if client == nil {
243 return nil, lsp.ErrNotReady
244 }
245 return client.Complete(ctx, path, line, column, lineText)
246}
247
248// Hover asks what the thing under the cursor is.
249func (l *Language) Hover(ctx context.Context, path string, line, column int, lineText string) (string, error) {
250 client := l.connected()
251 if client == nil {
252 return "", lsp.ErrNotReady
253 }
254 return client.Hover(ctx, path, line, column, lineText)
255}
256
257// Definition asks where the thing under the cursor is declared.
258func (l *Language) Definition(ctx context.Context, path string, line, column int, lineText string) ([]lsp.Location, error) {
259 client := l.connected()
260 if client == nil {
261 return nil, lsp.ErrNotReady
262 }
263 return client.Definition(ctx, path, line, column, lineText)
264}
265
266// TypeDefinition asks where the type of the thing under the cursor is declared.
267func (l *Language) TypeDefinition(ctx context.Context, path string, line, column int, lineText string) ([]lsp.Location, error) {
268 client := l.connected()
269 if client == nil {
270 return nil, lsp.ErrNotReady
271 }
272 return client.TypeDefinition(ctx, path, line, column, lineText)
273}
274
275// Implementation asks what implements the thing under the cursor.
276func (l *Language) Implementation(ctx context.Context, path string, line, column int, lineText string) ([]lsp.Location, error) {
277 client := l.connected()
278 if client == nil {
279 return nil, lsp.ErrNotReady
280 }
281 return client.Implementation(ctx, path, line, column, lineText)
282}
283
284// References asks where the thing under the cursor is used, counting its
285// declaration as one of the answers.
286//
287// The declaration is included because the question a reader is asking is
288// "where does this name appear", and a list that leaves out the one place they
289// are already looking at reads as a list with something missing.
290func (l *Language) References(ctx context.Context, path string, line, column int, lineText string) ([]lsp.Location, error) {
291 client := l.connected()
292 if client == nil {
293 return nil, lsp.ErrNotReady
294 }
295 return client.References(ctx, path, line, column, lineText, true)
296}
297
298// DocumentSymbols asks what one file declares.
299func (l *Language) DocumentSymbols(ctx context.Context, path string) ([]lsp.Symbol, error) {
300 client := l.connected()
301 if client == nil {
302 return nil, lsp.ErrNotReady
303 }
304 return client.DocumentSymbols(ctx, path)
305}
306
307// WorkspaceSymbols searches the project for symbols matching a query.
308func (l *Language) WorkspaceSymbols(ctx context.Context, query string) ([]lsp.Symbol, error) {
309 client := l.connected()
310 if client == nil {
311 return nil, lsp.ErrNotReady
312 }
313 return client.WorkspaceSymbols(ctx, query)
314}
315
316// AllDiagnostics returns every problem the server has reported, for every file
317// it has spoken about, sorted by file and then by line.
318//
319// Every file, not only the one in front: a language server publishes for the
320// whole package it has loaded, so the file with the error is often not the
321// file being edited — which is exactly when a list is worth having.
322//
323// for _, problem := range editor.Language().AllDiagnostics() {
324// fmt.Println(problem.Path, problem.Diagnostic.Message)
325// }
326func (l *Language) AllDiagnostics() []FileDiagnostic {
327 l.mu.Lock()
328 var out []FileDiagnostic
329 for path, diagnostics := range l.diagnostics {
330 for _, diagnostic := range diagnostics {
331 out = append(out, FileDiagnostic{Path: path, Diagnostic: diagnostic})
332 }
333 }
334 l.mu.Unlock()
335
336 // Sorted here rather than by the caller because the map's order is
337 // random, and a list that reshuffles itself every time it is opened is
338 // one nobody can use twice.
339 sort.Slice(out, func(i, j int) bool {
340 if out[i].Path != out[j].Path {
341 return out[i].Path < out[j].Path
342 }
343 return out[i].Diagnostic.Range.Start.Line < out[j].Diagnostic.Range.Start.Line
344 })
345 return out
346}
347
348// FileDiagnostic is one problem, with the file it is in — which the diagnostic
349// itself does not carry.
350type FileDiagnostic struct {
351 Path string
352 Diagnostic lsp.Diagnostic
353}
354
355// Diagnostics returns the problems the server reported for a file.
356//
357// for _, problem := range editor.Language().Diagnostics(path) {
358// fmt.Println(problem.Range.Start.Line, problem.Message)
359// }
360func (l *Language) Diagnostics(path string) []lsp.Diagnostic {
361 l.mu.Lock()
362 defer l.mu.Unlock()
363 return l.diagnostics[pathKey(path)]
364}
365
366// receiveDiagnostics records a file's problems. It is called from the
367// connection's read loop, which is why everything here is behind the lock.
368func (l *Language) receiveDiagnostics(path string, diagnostics []lsp.Diagnostic) {
369 l.mu.Lock()
370 l.diagnostics[pathKey(path)] = diagnostics
371 l.mu.Unlock()
372 l.notify()
373}
374
375// pathKey is how a file is named in the maps of problems and of open
376// documents.
377//
📦 Turbo Core — canonical paths: symbolic links resolved in URIs and document keys (moon-lsp on macOS) b91316e k33g 9h ago378// Canonical, always — absolute, links resolved — because the two sides spell
379// it differently. A server publishes absolute URIs; a buffer opened from the
380// command line — `turbo-go main.go` — holds the relative path it was given.
381// Keyed by whatever arrived, the two never meet: the status bar showed no
382// error, and the gutter no mark, for a file the server had plenty to say
383// about. And a server that resolves symbolic links — moon-lsp does — publishes
384// under /private/var/… what the editor opened as /var/…, which on macOS is
385// every temporary directory; absolute alone left those two apart as well.
🛟 Updated. 28d5985 k33g 18h ago386//
📦 Turbo Core — canonical paths: symbolic links resolved in URIs and document keys (moon-lsp on macOS) b91316e k33g 9h ago387// The same function builds the URIs the server is sent (lsp.PathToURI), so a
388// diagnostic comes back under the spelling it was announced by.
🛟 Updated. 28d5985 k33g 18h ago389func pathKey(path string) string {
📦 Turbo Core — canonical paths: symbolic links resolved in URIs and document keys (moon-lsp on macOS) b91316e k33g 9h ago390 return lsp.CanonicalPath(path)
🛟 Updated. 28d5985 k33g 18h ago391}
392
393// FirstError returns the first error-level diagnostic for a file, which is
394// what the status bar shows.
395func (l *Language) FirstError(path string) (lsp.Diagnostic, bool) {
396 for _, diagnostic := range l.Diagnostics(path) {
397 if diagnostic.Severity == lsp.SeverityError {
398 return diagnostic, true
399 }
400 }
401 return lsp.Diagnostic{}, false
402}
403
404// notify asks the editor to redraw.
405func (l *Language) notify() {
406 if l.OnUpdate != nil {
407 l.OnUpdate()
408 }
409}