package lsp import ( "context" "errors" "fmt" "io" "os" "os/exec" "path/filepath" "strings" "rickub.com/turbo-editors/turbo-core/profile" ) // ErrServerNotFound is returned when the language server is nowhere to be // found. It is not a failure of the editor: colouring and editing work // perfectly well without a language server, and only completion is lost. var ErrServerNotFound = errors.New("lsp: language server not found") // FindServer returns the path to the language server an editor is configured // for. // // PATH is searched first, then the directories the profile names — GOPATH/bin // for Go, ~/.cargo/bin for Rust — which are where each language's own installer // puts things and which are very often not on PATH. // // path, err := lsp.FindServer(p.Server) // if errors.Is(err, lsp.ErrServerNotFound) { // status.SetMessage("no completion: " + p.Server.InstallHint) // } func FindServer(server profile.Server) (string, error) { if path, err := exec.LookPath(server.Command); err == nil { return path, nil } for _, dir := range server.Dirs { if dir == "" { continue } candidate := filepath.Join(dir, server.Command) if info, err := os.Stat(candidate); err == nil && !info.IsDir() { return candidate, nil } } return "", fmt.Errorf("%w: %s is not in PATH%s", ErrServerNotFound, server.Command, orInDirs(server.Dirs)) } // orInDirs names the extra directories that were searched, so the error says // where the editor actually looked rather than only that it failed. func orInDirs(dirs []string) string { var named []string for _, dir := range dirs { if dir != "" { named = append(named, dir) } } if len(named) == 0 { return "" } return " or " + strings.Join(named, " or ") } // Server is a language server running as a child process, and the client // talking to it. type Server struct { client *Client command *exec.Cmd stream io.ReadWriteCloser } // StartServer launches the editor's language server in root and returns a // client that has finished initialising. // // The returned server must be stopped with Stop, which shuts the language // server down politely and then makes sure the process is gone. func StartServer(ctx context.Context, server profile.Server, root, clientName string) (*Server, error) { path, err := FindServer(server) if err != nil { return nil, err } return StartServerCommand(ctx, path, server.Args, root, clientName) } // StartServerCommand is StartServer with the executable named explicitly, which // is what lets a test drive a server it built itself. // // args are the arguments the server is started with: gopls wants "serve", // rust-analyzer wants none. clientName is how the editor introduces itself in // the handshake. func StartServerCommand(ctx context.Context, command string, args []string, root, clientName string) (*Server, error) { cmd := exec.Command(command, args...) cmd.Dir = root cmd.Stderr = nil // the server's own logging is not the editor's business stream, err := pipeTo(cmd) if err != nil { return nil, err } if err := cmd.Start(); err != nil { return nil, fmt.Errorf("lsp: starting %s: %w", command, err) } server := &Server{command: cmd, stream: stream, client: NewClient(stream, root, clientName)} go server.client.Run() //nolint:errcheck // the read loop's error surfaces as a failed request if err := server.client.Initialize(ctx); err != nil { _ = server.Stop(ctx) return nil, err } return server, nil } // Client returns the client talking to this server. func (s *Server) Client() *Client { return s.client } // Stop shuts the language server down and waits for the process to end. func (s *Server) Stop(ctx context.Context) error { shutdownErr := s.client.Shutdown(ctx) // Closing the pipes makes the server see end-of-input, which is what makes it // exit even when the polite shutdown did not land. _ = s.stream.Close() if err := s.command.Wait(); err != nil && shutdownErr == nil { // A server killed by its pipes closing is expected, not an error worth // reporting over the shutdown one. return nil } return shutdownErr } // processStream is a child process's standard input and output, seen as one // stream so the connection does not have to know it is talking to a process. type processStream struct { in io.WriteCloser out io.ReadCloser } // Read reads what the server wrote. func (p *processStream) Read(b []byte) (int, error) { return p.out.Read(b) } // Write sends to the server. func (p *processStream) Write(b []byte) (int, error) { return p.in.Write(b) } // Close shuts both halves, reporting the first failure. func (p *processStream) Close() error { inErr := p.in.Close() outErr := p.out.Close() if inErr != nil { return inErr } return outErr } // pipeTo wires a command's standard input and output into one stream. func pipeTo(cmd *exec.Cmd) (io.ReadWriteCloser, error) { in, err := cmd.StdinPipe() if err != nil { return nil, fmt.Errorf("lsp: connecting to the server's input: %w", err) } out, err := cmd.StdoutPipe() if err != nil { in.Close() return nil, fmt.Errorf("lsp: connecting to the server's output: %w", err) } return &processStream{in: in, out: out}, nil }