all repos

clerk @ 25c8f80b71674079b26e4189b4030a90fffb8c26

missing tooling for ledger/hledger

clerk/internal/lsp/lsp.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
lsp: comply with lsp3.18 lifecycle, 29 days ago
1
package lsp
2
3
import (
4
	"context"
5
	"io"
6
	"log/slog"
7
	"os"
8
	"path/filepath"
9
10
	"go.lsp.dev/jsonrpc2"
11
	"go.lsp.dev/protocol"
12
	"go.lsp.dev/uri"
13
14
	"olexsmir.xyz/clerk/internal/settings"
15
	"olexsmir.xyz/clerk/internal/xdg"
16
	"olexsmir.xyz/clerk/journal"
17
)
18
19
type Server struct{ server *server }
20
21
func NewServer(version string, configPath string) (Server, error) {
22
	logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
23
	if logFile, err := openLogFile(); err == nil {
24
		logger = slog.New(slog.NewTextHandler(logFile, nil))
25
	}
26
27
	srv := &server{
28
		name:    "clerk",
29
		version: version,
30
31
		openDocs: make(map[uri.URI]docState),
32
33
		settings:   settings.DefaultConfig,
34
		configPath: configPath,
35
		loader:     journal.NewLoader(),
36
37
		log: logger,
38
	}
39
	srv.loader.ContentProvider = srv.bufferContent
40
	return Server{srv}, nil
41
}
42
43
func (s *Server) Run(ctx context.Context, stdin io.ReadCloser, stdout io.WriteCloser) error {
44
	stream := jsonrpc2.NewStream(readWriterCloser{
45
		Reader: stdin,
46
		Writer: stdout,
47
		Closer: stdin,
48
	})
49
	conn := jsonrpc2.NewConn(stream, jsonrpc2.WithCodec(lspCodec{}))
50
	s.server.client = protocol.ClientDispatcher(conn)
51
	s.server.conn = conn
52
	conn.Go(ctx, protocol.Handlers(s.server.lifecycle(protocol.ServerHandler(s.server, jsonrpc2.MethodNotFoundHandler))))
53
54
	<-conn.Done()
55
	s.server.stateMu.Lock()
56
	st := s.server.state
57
	s.server.stateMu.Unlock()
58
	if st >= stateExited {
59
		return &ExitError{Code: st.ExitCode()}
60
	}
61
	return conn.Err()
62
}
63
64
// bufferContent returns the open buffer text for a path, if any.
65
// called by the loader during include resolution; must NOT hold the loader lock.
66
func (s *server) bufferContent(path string) ([]byte, bool) {
67
	s.mu.RLock()
68
	defer s.mu.RUnlock()
69
	st, ok := s.openDocs[uri.File(path)]
70
	return []byte(st.text), ok
71
}
72
73
type readWriterCloser struct {
74
	io.Reader
75
	io.Writer
76
	io.Closer
77
}
78
79
func openLogFile() (*os.File, error) {
80
	dir, err := xdg.StateDir()
81
	if err != nil {
82
		return nil, err
83
	}
84
	dir = filepath.Join(dir, "clerk")
85
	if err := os.MkdirAll(dir, 0o700); err != nil {
86
		return nil, err
87
	}
88
	return os.OpenFile(filepath.Join(dir, "lsp.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
89
}
90
91
// lspCodec mirros go.lsp.dev/protocol wire codec, so Run can install the lifecycle guard.
92
type lspCodec struct{}
93
94
func (lspCodec) Marshal(v any) ([]byte, error) {
95
	switch m := v.(type) {
96
	case jsonrpc2.RawMessage:
97
		if m == nil {
98
			return []byte("null"), nil
99
		}
100
		return m, nil
101
	case *jsonrpc2.RawMessage:
102
		if m == nil || *m == nil {
103
			return []byte("null"), nil
104
		}
105
		return *m, nil
106
	}
107
	return protocol.Marshal(v)
108
}
109
110
func (lspCodec) Unmarshal(data []byte, v any) error {
111
	if p, ok := v.(*jsonrpc2.RawMessage); ok {
112
		b := make(jsonrpc2.RawMessage, len(data))
113
		copy(b, data)
114
		*p = b
115
		return nil
116
	}
117
	return protocol.Unmarshal(data, v)
118
}