turbo-editors/turbo-corepublic Fork 0
v1.0.1
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.

server.go · 167 lines · 5.1 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 16h ago1package lsp
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "strings"
12
📦 Turbo Core f3ade8d k33g 8h ago13 "rickub.com/turbo-editors/turbo-core/profile"
🛟 Updated. 28d5985 k33g 16h ago14)
15
16// ErrServerNotFound is returned when the language server is nowhere to be
17// found. It is not a failure of the editor: colouring and editing work
18// perfectly well without a language server, and only completion is lost.
19var ErrServerNotFound = errors.New("lsp: language server not found")
20
21// FindServer returns the path to the language server an editor is configured
22// for.
23//
24// PATH is searched first, then the directories the profile names — GOPATH/bin
25// for Go, ~/.cargo/bin for Rust — which are where each language's own installer
26// puts things and which are very often not on PATH.
27//
28// path, err := lsp.FindServer(p.Server)
29// if errors.Is(err, lsp.ErrServerNotFound) {
30// status.SetMessage("no completion: " + p.Server.InstallHint)
31// }
32func FindServer(server profile.Server) (string, error) {
33 if path, err := exec.LookPath(server.Command); err == nil {
34 return path, nil
35 }
36
37 for _, dir := range server.Dirs {
38 if dir == "" {
39 continue
40 }
41 candidate := filepath.Join(dir, server.Command)
42 if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
43 return candidate, nil
44 }
45 }
46 return "", fmt.Errorf("%w: %s is not in PATH%s", ErrServerNotFound, server.Command, orInDirs(server.Dirs))
47}
48
49// orInDirs names the extra directories that were searched, so the error says
50// where the editor actually looked rather than only that it failed.
51func orInDirs(dirs []string) string {
52 var named []string
53 for _, dir := range dirs {
54 if dir != "" {
55 named = append(named, dir)
56 }
57 }
58 if len(named) == 0 {
59 return ""
60 }
61 return " or " + strings.Join(named, " or ")
62}
63
64// Server is a language server running as a child process, and the client
65// talking to it.
66type Server struct {
67 client *Client
68 command *exec.Cmd
69 stream io.ReadWriteCloser
70}
71
72// StartServer launches the editor's language server in root and returns a
73// client that has finished initialising.
74//
75// The returned server must be stopped with Stop, which shuts the language
76// server down politely and then makes sure the process is gone.
77func StartServer(ctx context.Context, server profile.Server, root, clientName string) (*Server, error) {
78 path, err := FindServer(server)
79 if err != nil {
80 return nil, err
81 }
82 return StartServerCommand(ctx, path, server.Args, root, clientName)
83}
84
85// StartServerCommand is StartServer with the executable named explicitly, which
86// is what lets a test drive a server it built itself.
87//
88// args are the arguments the server is started with: gopls wants "serve",
89// rust-analyzer wants none. clientName is how the editor introduces itself in
90// the handshake.
91func StartServerCommand(ctx context.Context, command string, args []string, root, clientName string) (*Server, error) {
92 cmd := exec.Command(command, args...)
93 cmd.Dir = root
94 cmd.Stderr = nil // the server's own logging is not the editor's business
95
96 stream, err := pipeTo(cmd)
97 if err != nil {
98 return nil, err
99 }
100 if err := cmd.Start(); err != nil {
101 return nil, fmt.Errorf("lsp: starting %s: %w", command, err)
102 }
103
104 server := &Server{command: cmd, stream: stream, client: NewClient(stream, root, clientName)}
105 go server.client.Run() //nolint:errcheck // the read loop's error surfaces as a failed request
106
107 if err := server.client.Initialize(ctx); err != nil {
108 _ = server.Stop(ctx)
109 return nil, err
110 }
111 return server, nil
112}
113
114// Client returns the client talking to this server.
115func (s *Server) Client() *Client { return s.client }
116
117// Stop shuts the language server down and waits for the process to end.
118func (s *Server) Stop(ctx context.Context) error {
119 shutdownErr := s.client.Shutdown(ctx)
120
121 // Closing the pipes makes the server see end-of-input, which is what makes it
122 // exit even when the polite shutdown did not land.
123 _ = s.stream.Close()
124 if err := s.command.Wait(); err != nil && shutdownErr == nil {
125 // A server killed by its pipes closing is expected, not an error worth
126 // reporting over the shutdown one.
127 return nil
128 }
129 return shutdownErr
130}
131
132// processStream is a child process's standard input and output, seen as one
133// stream so the connection does not have to know it is talking to a process.
134type processStream struct {
135 in io.WriteCloser
136 out io.ReadCloser
137}
138
139// Read reads what the server wrote.
140func (p *processStream) Read(b []byte) (int, error) { return p.out.Read(b) }
141
142// Write sends to the server.
143func (p *processStream) Write(b []byte) (int, error) { return p.in.Write(b) }
144
145// Close shuts both halves, reporting the first failure.
146func (p *processStream) Close() error {
147 inErr := p.in.Close()
148 outErr := p.out.Close()
149 if inErr != nil {
150 return inErr
151 }
152 return outErr
153}
154
155// pipeTo wires a command's standard input and output into one stream.
156func pipeTo(cmd *exec.Cmd) (io.ReadWriteCloser, error) {
157 in, err := cmd.StdinPipe()
158 if err != nil {
159 return nil, fmt.Errorf("lsp: connecting to the server's input: %w", err)
160 }
161 out, err := cmd.StdoutPipe()
162 if err != nil {
163 in.Close()
164 return nil, fmt.Errorf("lsp: connecting to the server's output: %w", err)
165 }
166 return &processStream{in: in, out: out}, nil
167}