| 🛟 Updated. 28d5985 k33g 18h ago | 1 | package lsp |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "fmt" |
| 7 | "io" |
| 8 | "os" |
| 9 | "os/exec" |
| 10 | "path/filepath" |
| 11 | "strings" |
| 12 | |
| 📦 Turbo Core f3ade8d k33g 11h ago | 13 | "rickub.com/turbo-editors/turbo-core/profile" |
| 🛟 Updated. 28d5985 k33g 18h ago | 14 | ) |
| 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. |
| 19 | var 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 | // } |
| 32 | func 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. |
| 51 | func 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. |
| 66 | type 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. |
| 77 | func 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. |
| 91 | func 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. |
| 115 | func (s *Server) Client() *Client { return s.client } |
| 116 | |
| 117 | // Stop shuts the language server down and waits for the process to end. |
| 118 | func (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. |
| 134 | type processStream struct { |
| 135 | in io.WriteCloser |
| 136 | out io.ReadCloser |
| 137 | } |
| 138 | |
| 139 | // Read reads what the server wrote. |
| 140 | func (p *processStream) Read(b []byte) (int, error) { return p.out.Read(b) } |
| 141 | |
| 142 | // Write sends to the server. |
| 143 | func (p *processStream) Write(b []byte) (int, error) { return p.in.Write(b) } |
| 144 | |
| 145 | // Close shuts both halves, reporting the first failure. |
| 146 | func (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. |
| 156 | func 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 | } |