mirror of
https://github.com/zyedidia/micro.git
synced 2025-06-18 23:05:40 -04:00
288 lines
5.9 KiB
Go
288 lines
5.9 KiB
Go
package lsp
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
lsp "go.lsp.dev/protocol"
|
|
"go.lsp.dev/uri"
|
|
)
|
|
|
|
var activeServers map[string]*Server
|
|
var slock sync.Mutex
|
|
|
|
func init() {
|
|
activeServers = make(map[string]*Server)
|
|
}
|
|
|
|
func GetServer(l Language, dir string) *Server {
|
|
return activeServers[l.Command+"-"+dir]
|
|
}
|
|
|
|
type Server struct {
|
|
cmd *exec.Cmd
|
|
stdin io.WriteCloser
|
|
stdout *bufio.Reader
|
|
language *Language
|
|
capabilities lsp.ServerCapabilities
|
|
root string
|
|
lock sync.Mutex
|
|
Active bool
|
|
requestID int
|
|
responses map[int]chan ([]byte)
|
|
}
|
|
|
|
type RPCRequest struct {
|
|
RPCVersion string `json:"jsonrpc"`
|
|
ID int `json:"id"`
|
|
Method string `json:"method"`
|
|
Params interface{} `json:"params"`
|
|
}
|
|
|
|
type RPCNotification struct {
|
|
RPCVersion string `json:"jsonrpc"`
|
|
Method string `json:"method"`
|
|
Params interface{} `json:"params"`
|
|
}
|
|
|
|
type RPCInit struct {
|
|
RPCVersion string `json:"jsonrpc"`
|
|
ID int `json:"id"`
|
|
Result lsp.InitializeResult `json:"result"`
|
|
}
|
|
|
|
type RPCResult struct {
|
|
RPCVersion string `json:"jsonrpc"`
|
|
ID int `json:"id,omitempty"`
|
|
Method string `json:"method,omitempty"`
|
|
}
|
|
|
|
func StartServer(l Language) (*Server, error) {
|
|
s := new(Server)
|
|
|
|
c := exec.Command(l.Command, l.Args...)
|
|
|
|
c.Stderr = log.Writer()
|
|
|
|
stdin, err := c.StdinPipe()
|
|
if err != nil {
|
|
log.Println("[micro-lsp]", err)
|
|
return nil, err
|
|
}
|
|
|
|
stdout, err := c.StdoutPipe()
|
|
if err != nil {
|
|
log.Println("[micro-lsp]", err)
|
|
return nil, err
|
|
}
|
|
|
|
err = c.Start()
|
|
if err != nil {
|
|
log.Println("[micro-lsp]", err)
|
|
return nil, err
|
|
}
|
|
|
|
s.cmd = c
|
|
s.stdin = stdin
|
|
s.stdout = bufio.NewReader(stdout)
|
|
s.language = &l
|
|
s.responses = make(map[int]chan []byte)
|
|
|
|
return s, nil
|
|
}
|
|
|
|
// Initialize performs the LSP initialization handshake
|
|
// The directory must be an absolute path
|
|
func (s *Server) Initialize(directory string) {
|
|
params := lsp.InitializeParams{
|
|
ProcessID: float64(os.Getpid()),
|
|
RootURI: uri.File(directory),
|
|
Capabilities: lsp.ClientCapabilities{
|
|
Workspace: &lsp.WorkspaceClientCapabilities{
|
|
WorkspaceEdit: &lsp.WorkspaceClientCapabilitiesWorkspaceEdit{
|
|
DocumentChanges: true,
|
|
ResourceOperations: []string{"create", "rename", "delete"},
|
|
},
|
|
ApplyEdit: true,
|
|
},
|
|
TextDocument: &lsp.TextDocumentClientCapabilities{
|
|
Formatting: &lsp.TextDocumentClientCapabilitiesFormatting{
|
|
DynamicRegistration: false,
|
|
},
|
|
Completion: &lsp.TextDocumentClientCapabilitiesCompletion{
|
|
DynamicRegistration: false,
|
|
CompletionItem: &lsp.TextDocumentClientCapabilitiesCompletionItem{
|
|
SnippetSupport: false,
|
|
CommitCharactersSupport: false,
|
|
DocumentationFormat: []lsp.MarkupKind{lsp.PlainText},
|
|
DeprecatedSupport: false,
|
|
PreselectSupport: false,
|
|
},
|
|
ContextSupport: false,
|
|
},
|
|
Hover: &lsp.TextDocumentClientCapabilitiesHover{
|
|
DynamicRegistration: false,
|
|
ContentFormat: []lsp.MarkupKind{lsp.PlainText},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
activeServers[s.language.Command+"-"+directory] = s
|
|
s.Active = true
|
|
s.root = directory
|
|
|
|
go s.receive()
|
|
|
|
s.lock.Lock()
|
|
go func() {
|
|
resp, err := s.sendRequest("initialize", params)
|
|
if err != nil {
|
|
log.Println("[micro-lsp]", err)
|
|
s.Active = false
|
|
s.lock.Unlock()
|
|
return
|
|
}
|
|
|
|
// todo parse capabilities
|
|
log.Println("[micro-lsp] <<<", string(resp))
|
|
|
|
var r RPCInit
|
|
json.Unmarshal(resp, &r)
|
|
|
|
s.lock.Unlock()
|
|
err = s.sendNotification("initialized", struct{}{})
|
|
if err != nil {
|
|
log.Println("[micro-lsp]", err)
|
|
}
|
|
|
|
s.capabilities = r.Result.Capabilities
|
|
}()
|
|
}
|
|
|
|
func (s *Server) receive() {
|
|
for s.Active {
|
|
resp, err := s.receiveMessage()
|
|
if err != nil {
|
|
log.Println("[micro-lsp]", err)
|
|
continue
|
|
}
|
|
log.Println("[micro-lsp] <<<", string(resp))
|
|
|
|
var r RPCResult
|
|
err = json.Unmarshal(resp, &r)
|
|
if err != nil {
|
|
log.Println("[micro-lsp]", err)
|
|
continue
|
|
}
|
|
|
|
switch r.Method {
|
|
case "window/logMessage":
|
|
// TODO
|
|
case "textDocument/publishDiagnostics":
|
|
// TODO
|
|
case "":
|
|
// Response
|
|
if _, ok := s.responses[r.ID]; ok {
|
|
log.Println("[micro-lsp] Got response for", r.ID)
|
|
s.responses[r.ID] <- resp
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) receiveMessage() ([]byte, error) {
|
|
n := -1
|
|
for {
|
|
b, err := s.stdout.ReadBytes('\n')
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
headerline := strings.TrimSpace(string(b))
|
|
if len(headerline) == 0 {
|
|
break
|
|
}
|
|
if strings.HasPrefix(headerline, "Content-Length:") {
|
|
split := strings.Split(headerline, ":")
|
|
if len(split) <= 1 {
|
|
break
|
|
}
|
|
n, err = strconv.Atoi(strings.TrimSpace(split[1]))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
if n <= 0 {
|
|
return []byte{}, nil
|
|
}
|
|
|
|
bytes := make([]byte, n)
|
|
_, err := io.ReadFull(s.stdout, bytes)
|
|
if err != nil {
|
|
log.Println("[micro-lsp]", err)
|
|
}
|
|
return bytes, err
|
|
}
|
|
|
|
func (s *Server) sendNotification(method string, params interface{}) error {
|
|
m := RPCNotification{
|
|
RPCVersion: "2.0",
|
|
Method: method,
|
|
Params: params,
|
|
}
|
|
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
return s.sendMessage(m)
|
|
}
|
|
|
|
func (s *Server) sendRequest(method string, params interface{}) ([]byte, error) {
|
|
id := s.requestID
|
|
s.requestID++
|
|
r := make(chan []byte)
|
|
s.responses[id] = r
|
|
|
|
m := RPCRequest{
|
|
RPCVersion: "2.0",
|
|
ID: id,
|
|
Method: method,
|
|
Params: params,
|
|
}
|
|
|
|
err := s.sendMessage(m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bytes := <-r
|
|
delete(s.responses, id)
|
|
|
|
return bytes, nil
|
|
}
|
|
|
|
func (s *Server) sendMessage(m interface{}) error {
|
|
msg, err := json.Marshal(m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Println("[micro-lsp] >>>", string(msg))
|
|
|
|
// encode header and proper line endings
|
|
msg = append(msg, '\r', '\n')
|
|
header := []byte("Content-Length: " + strconv.Itoa(len(msg)) + "\r\n\r\n")
|
|
msg = append(header, msg...)
|
|
|
|
_, err = s.stdin.Write(msg)
|
|
return err
|
|
}
|