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

📦 Turbo Core f3ade8d · on v1.0.0 · k33g · 8h ago
server.go · 167 lines · 5.1 KBGo Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
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
}