From 26442bdbbe93c4e0275914eb2304e181e1af3b78 Mon Sep 17 00:00:00 2001 From: Zachary Yedidia Date: Sun, 9 Aug 2020 23:33:24 -0400 Subject: [PATCH] Basic communication with lsp server --- go.mod | 2 + go.sum | 4 + internal/buffer/buffer.go | 17 +++ internal/lsp/install.go | 72 ++++++++++++ internal/lsp/requests.go | 29 +++++ internal/lsp/server.go | 212 +++++++++++++++++++++++++++++++++++ internal/lsp/servers_toml.go | 79 +++++++++++++ 7 files changed, 415 insertions(+) create mode 100644 internal/lsp/install.go create mode 100644 internal/lsp/requests.go create mode 100644 internal/lsp/server.go create mode 100644 internal/lsp/servers_toml.go diff --git a/go.mod b/go.mod index 9d54b1d6..64e1a94d 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module github.com/zyedidia/micro/v2 require ( + github.com/BurntSushi/toml v0.3.1 github.com/blang/semver v3.5.1+incompatible github.com/dustin/go-humanize v1.0.0 github.com/go-errors/errors v1.0.1 @@ -10,6 +11,7 @@ require ( github.com/mitchellh/go-homedir v1.1.0 github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff github.com/sergi/go-diff v1.1.0 + github.com/sourcegraph/go-lsp v0.0.0-20200429204803-219e11d77f5d github.com/stretchr/testify v1.4.0 github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb github.com/zyedidia/clipboard v1.0.3 diff --git a/go.sum b/go.sum index 30240649..376742e3 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -31,6 +33,8 @@ github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff h1:+6NUiITWwE5q1 github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sourcegraph/go-lsp v0.0.0-20200429204803-219e11d77f5d h1:afLbh+ltiygTOB37ymZVwKlJwWZn+86syPTbrrOAydY= +github.com/sourcegraph/go-lsp v0.0.0-20200429204803-219e11d77f5d/go.mod h1:SULmZY7YNBsvNiQbrb/BEDdEJ84TGnfyUQxaHt8t8rY= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index dfbd3644..8a4940ab 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -10,6 +10,7 @@ import ( "io/ioutil" "os" "path" + gopath "path" "path/filepath" "strconv" "strings" @@ -20,6 +21,7 @@ import ( dmp "github.com/sergi/go-diff/diffmatchpatch" "github.com/zyedidia/micro/v2/internal/config" + "github.com/zyedidia/micro/v2/internal/lsp" ulua "github.com/zyedidia/micro/v2/internal/lua" "github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/util" @@ -123,6 +125,9 @@ type SharedBuffer struct { // Hash of the original buffer -- empty if fastdirty is on origHash [md5.Size]byte + + server *lsp.Server + version int } func (b *SharedBuffer) insert(pos Loc, value []byte) { @@ -369,6 +374,18 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT OpenBuffers = append(OpenBuffers, b) + if !found { + if btype == BTDefault { + ft := b.Settings["filetype"].(string) + l, ok := lsp.GetLanguage(ft) + if ok && l.Installed() { + b.server, _ = lsp.StartServer(l) + b.server.Initialize(gopath.Dir(b.AbsPath)) + b.server.DidOpen(b.AbsPath, ft, string(b.Bytes()), b.version) + } + } + } + return b } diff --git a/internal/lsp/install.go b/internal/lsp/install.go new file mode 100644 index 00000000..33de2b89 --- /dev/null +++ b/internal/lsp/install.go @@ -0,0 +1,72 @@ +package lsp + +import ( + "errors" + "io" + "os/exec" + "strings" + + "github.com/BurntSushi/toml" +) + +var ErrManualInstall = errors.New("Requires manual installation") + +type Config struct { + Languages map[string]Language `toml:"language"` +} + +type Language struct { + Command string `toml:"command"` + Args []string `toml:"args"` + Install [][]string `toml:"install"` +} + +var conf *Config + +func GetLanguage(lang string) (Language, bool) { + l, ok := conf.Languages[lang] + return l, ok +} + +func init() { + conf, _ = LoadConfig([]byte(servers)) +} + +func LoadConfig(data []byte) (*Config, error) { + var conf Config + if _, err := toml.Decode(string(data), &conf); err != nil { + return nil, err + } + + return &conf, nil +} + +func (l *Language) Installed() bool { + _, err := exec.LookPath(l.Command) + if err != nil { + return false + } + + return true +} + +func (l *Language) DoInstall(w io.Writer) error { + if l.Installed() { + return nil + } + + if len(l.Install) == 0 { + return ErrManualInstall + } + + for _, c := range l.Install { + io.WriteString(w, strings.Join(c, " ")+"\n") + cmd := exec.Command(c[0], c[1:]...) + err := cmd.Run() + if err != nil { + return err + } + } + + return nil +} diff --git a/internal/lsp/requests.go b/internal/lsp/requests.go new file mode 100644 index 00000000..bbaa6a2f --- /dev/null +++ b/internal/lsp/requests.go @@ -0,0 +1,29 @@ +package lsp + +import ( + "log" + + "github.com/sourcegraph/go-lsp" +) + +func (s *Server) DidOpen(filename, language, text string, version int) error { + doc := lsp.TextDocumentItem{ + URI: lsp.DocumentURI("file://" + filename), + LanguageID: language, + Version: version, + Text: text, + } + + params := lsp.DidOpenTextDocumentParams{ + TextDocument: doc, + } + + resp, err := s.SendMessage("textDocument/didOpen", params) + if err != nil { + return err + } + + log.Println("Received", string(resp)) + + return nil +} diff --git a/internal/lsp/server.go b/internal/lsp/server.go new file mode 100644 index 00000000..432e0f1c --- /dev/null +++ b/internal/lsp/server.go @@ -0,0 +1,212 @@ +package lsp + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "log" + "os" + "os/exec" + "strconv" + "strings" + + "github.com/sourcegraph/go-lsp" + "github.com/zyedidia/micro/v2/internal/util" +) + +var ActiveServers map[string]*Server + +func init() { + ActiveServers = make(map[string]*Server) +} + +type Server struct { + cmd *exec.Cmd + stdin io.WriteCloser + stdout *bufio.Reader + language *Language + capabilities lsp.ServerCapabilities +} + +type RPCMessage struct { + RPCVersion string `json:"jsonrpc"` + ID int `json:"id"` + Method string `json:"method"` + Params interface{} `json:"params"` +} + +type RPCInit struct { + RPCVersion string `json:"jsonrpc"` + ID int `json:"id"` + Result lsp.InitializeResult `json:"result"` +} + +func StartServer(l Language) (*Server, error) { + c := exec.Command(l.Command, l.Args...) + + log.Println("Running", l.Command, l.Args) + + stdin, err := c.StdinPipe() + if err != nil { + return nil, err + } + + stdout, err := c.StdoutPipe() + if err != nil { + return nil, err + } + + err = c.Start() + if err != nil { + return nil, err + } + + s := new(Server) + s.cmd = c + s.stdin = stdin + s.stdout = bufio.NewReader(stdout) + s.language = &l + + // ActiveServers[l.Command] = s + + return s, nil +} + +func (s *Server) Initialize(directory string) error { + params := lsp.InitializeParams{ + ProcessID: os.Getpid(), + RootURI: lsp.DocumentURI("file://" + directory), + ClientInfo: lsp.ClientInfo{ + Name: "micro", + Version: util.Version, + }, + Trace: "off", + Capabilities: lsp.ClientCapabilities{ + Workspace: lsp.WorkspaceClientCapabilities{ + WorkspaceEdit: struct { + DocumentChanges bool `json:"documentChanges,omitempty"` + ResourceOperations []string `json:"resourceOperations,omitempty"` + }{ + DocumentChanges: true, + ResourceOperations: []string{"create", "rename", "delete"}, + }, + ApplyEdit: true, + }, + TextDocument: lsp.TextDocumentClientCapabilities{ + Formatting: &struct { + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + }{ + DynamicRegistration: true, + }, + Completion: struct { + CompletionItem struct { + DocumentationFormat []lsp.DocumentationFormat `json:"documentationFormat,omitempty"` + SnippetSupport bool `json:"snippetSupport,omitempty"` + } `json:"completionItem,omitempty"` + + CompletionItemKind struct { + ValueSet []lsp.CompletionItemKind `json:"valueSet,omitempty"` + } `json:"completionItemKind,omitempty"` + + ContextSupport bool `json:"contextSupport,omitempty"` + }{ + CompletionItem: struct { + DocumentationFormat []lsp.DocumentationFormat `json:"documentationFormat,omitempty"` + SnippetSupport bool `json:"snippetSupport,omitempty"` + }{ + DocumentationFormat: []lsp.DocumentationFormat{lsp.DFPlainText}, + SnippetSupport: false, + }, + ContextSupport: false, + }, + }, + Window: lsp.WindowClientCapabilities{ + WorkDoneProgress: false, + }, + Experimental: nil, + }, + } + + resp, err := s.SendMessage("initialize", params) + if err != nil { + return err + } + + // todo parse capabilities + log.Println("Received", string(resp)) + + var r RPCInit + err = json.Unmarshal(resp, &r) + if err != nil { + return err + } + + fmt.Println(r) + s.capabilities = r.Result.Capabilities + fmt.Println(s.capabilities) + + _, err = s.SendMessage("initialized", struct{}{}) + if err != nil { + return err + } + + return nil +} + +func (s *Server) SendMessage(method string, params interface{}) ([]byte, error) { + m := RPCMessage{ + RPCVersion: "2.0", + ID: os.Getpid(), + Method: method, + Params: params, + } + + msg, err := json.Marshal(m) + if err != nil { + return nil, err + } + + msg = append(msg, '\r', '\n') + + header := []byte("Content-Length: " + strconv.Itoa(len(msg)) + "\r\n\r\n") + msg = append(header, msg...) + + log.Println("Sending", string(msg)) + + s.stdin.Write(msg) + + 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 = s.stdout.Read(bytes) + if err != nil { + return nil, err + } + + return bytes, nil +} diff --git a/internal/lsp/servers_toml.go b/internal/lsp/servers_toml.go new file mode 100644 index 00000000..6123d290 --- /dev/null +++ b/internal/lsp/servers_toml.go @@ -0,0 +1,79 @@ +package lsp + +var servers = `[language.rust] +command = "rls" +install = [ + ["rustup", "update"], + ["rustup", "component", "add", "rls", "rust-analysis", "rust-src"], +] + +[language.javascript] +command = "typescript-language-server" +args = ["--stdio"] +install = [["npm", "install", "-g", "typescript-language-server"]] + +[language.typescript] +command = "typescript-language-server" +args = ["--stdio"] +install = [["npm", "install", "-g", "typescript-language-server"]] + +[language.html] +command = "html-languageserver" +args = ["--stdio"] +install = [["npm", "install", "-g", "vscode-html-languageserver-bin"]] + +[language.ocaml] +command = "ocaml-language-server" +args = ["--stdio"] +install = [["npm", "install", "-g", "ocaml-language-server"]] + +[language.python] +command = "pyls" +install = [["pip", "install", "python-language-server"]] + +[language.c] +command = "clangd" +args = ["--log=verbose"] + +[language.cpp] +command = "clangd" +args = [] + +[language.haskell] +command = "hie" +args = ["--lsp"] + +[language.go] +command = "gopls" +args = ["serve"] +install = [["go", "get", "-u", "golang.org/x/tools/gopls"]] + +[language.dart] +command = "dart_language_server" +install = [["pub", "global", "activate", "dart_language_server"]] + +[language.ruby] +command = "solargraph" +args = ["stdio"] +install = [["gem", "install", "solargraph"]] + +[language.css] +command = "css-languageserver" +args = ["--stdio"] +install = [["npm", "install", "-g", "vscode-css-languageserver-bin"]] + +[language.scss] +command = "css-languageserver" +args = ["--stdio"] +install = [["npm", "install", "-g", "vscode-css-languageserver-bin"]] + +[language.viml] +command = "vim-language-server" +args = ["--stdio"] +install = [["npm", "install", "-g", "vim-language-server"]] + +[language.purescript] +command = "purescript-language-server" +args = ["--stdio"] +install = [["npm", "install", "-g", "purescript-language-server"]] +`