From 053134af1cfec92023967d91b3c70f424eda6aa8 Mon Sep 17 00:00:00 2001 From: Zachary Yedidia Date: Mon, 10 Aug 2020 18:19:13 -0400 Subject: [PATCH] Basic non-compliant autocompletion via LSP --- internal/action/actions.go | 2 +- internal/buffer/autocomplete.go | 32 +++++++ internal/buffer/buffer.go | 10 +- internal/lsp/requests.go | 44 ++++++++- internal/lsp/server.go | 163 ++++++++++++++++++++++---------- 5 files changed, 193 insertions(+), 58 deletions(-) diff --git a/internal/action/actions.go b/internal/action/actions.go index b80ff67b..ce816a15 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -678,7 +678,7 @@ func (h *BufPane) Autocomplete() bool { b.CycleAutocomplete(true) return true } - return b.Autocomplete(buffer.BufferComplete) + return b.Autocomplete(buffer.LSPComplete) } // CycleAutocompleteBack cycles back in the autocomplete suggestion list diff --git a/internal/buffer/autocomplete.go b/internal/buffer/autocomplete.go index 7af3f0b7..d660c0b2 100644 --- a/internal/buffer/autocomplete.go +++ b/internal/buffer/autocomplete.go @@ -7,6 +7,7 @@ import ( "sort" "strings" + lspt "github.com/sourcegraph/go-lsp" "github.com/zyedidia/micro/v2/internal/util" ) @@ -203,3 +204,34 @@ func BufferComplete(b *Buffer) ([]string, []string) { return completions, suggestions } + +func LSPComplete(b *Buffer) ([]string, []string) { + c := b.GetActiveCursor() + _, argstart := GetWord(b) + + if argstart == -1 { + return []string{}, []string{} + } + + pos := lspt.Position{ + Line: c.Y, + Character: c.X, + } + items, err := b.server.Completion(b.AbsPath, pos) + if err != nil { + return []string{}, []string{} + } + + suggestions := make([]string, len(items)) + + for i, item := range items { + suggestions[i] = item.Label + } + + completions := make([]string, len(suggestions)) + for i := range suggestions { + completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart) + } + + return completions, suggestions +} diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index 4eba4cc8..3ffac2e5 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -138,7 +138,9 @@ func (b *SharedBuffer) insert(pos Loc, value []byte) { inslines := bytes.Count(value, []byte{'\n'}) b.MarkModified(pos.Y, pos.Y+inslines) - b.lspDidChange(pos, pos.MoveLA(util.CharacterCount(value), b.LineArray), string(value)) + p := pos + p.X += util.CharacterCount(value) + b.lspDidChange(pos, p, string(value)) } func (b *SharedBuffer) remove(start, end Loc) []byte { b.isModified = true @@ -405,7 +407,11 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT 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) + bytes := b.Bytes() + if len(bytes) == 0 { + bytes = []byte{'\n'} + } + b.server.DidOpen(b.AbsPath, ft, string(bytes), b.version) } } } diff --git a/internal/lsp/requests.go b/internal/lsp/requests.go index 4aacbd7b..3f756b61 100644 --- a/internal/lsp/requests.go +++ b/internal/lsp/requests.go @@ -1,9 +1,17 @@ package lsp import ( + "encoding/json" + "github.com/sourcegraph/go-lsp" ) +type RPCCompletion struct { + RPCVersion string `json:"jsonrpc"` + ID int `json:"id"` + Result lsp.CompletionList `json:"result"` +} + func (s *Server) DidOpen(filename, language, text string, version int) { doc := lsp.TextDocumentItem{ URI: lsp.DocumentURI("file://" + filename), @@ -16,7 +24,7 @@ func (s *Server) DidOpen(filename, language, text string, version int) { TextDocument: doc, } - go s.SendMessage("textDocument/didOpen", params) + s.SendMessage("textDocument/didOpen", params) } func (s *Server) DidSave(filename string) { @@ -27,7 +35,7 @@ func (s *Server) DidSave(filename string) { params := lsp.DidSaveTextDocumentParams{ TextDocument: doc, } - go s.SendMessage("textDocument/didSave", params) + s.SendMessage("textDocument/didSave", params) } func (s *Server) DidChange(filename string, version int, changes []lsp.TextDocumentContentChangeEvent) { @@ -42,7 +50,7 @@ func (s *Server) DidChange(filename string, version int, changes []lsp.TextDocum TextDocument: doc, ContentChanges: changes, } - go s.SendMessage("textDocument/didChange", params) + s.SendMessage("textDocument/didChange", params) } func (s *Server) DidClose(filename string) { @@ -53,7 +61,7 @@ func (s *Server) DidClose(filename string) { params := lsp.DidCloseTextDocumentParams{ TextDocument: doc, } - go s.SendMessage("textDocument/didClose", params) + s.SendMessage("textDocument/didClose", params) } func (s *Server) DocumentFormat() { @@ -64,8 +72,34 @@ func (s *Server) DocumentRangeFormat() { } -func (s *Server) Completion() { +func (s *Server) Completion(filename string, pos lsp.Position) ([]lsp.CompletionItem, error) { + cc := lsp.CompletionContext{ + TriggerKind: lsp.CTKInvoked, + } + docpos := lsp.TextDocumentPositionParams{ + TextDocument: lsp.TextDocumentIdentifier{ + URI: lsp.DocumentURI("file://" + filename), + }, + Position: pos, + } + + params := lsp.CompletionParams{ + TextDocumentPositionParams: docpos, + Context: cc, + } + resp, err := s.SendMessageGetResponse("textDocument/completion", params) + if err != nil { + return nil, err + } + + var r RPCCompletion + err = json.Unmarshal(resp, &r) + if err != nil { + return nil, err + } + + return r.Result.Items, nil } func (s *Server) CompletionResolve() { diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 459e210b..f3719023 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -30,6 +30,9 @@ type Server struct { capabilities lsp.ServerCapabilities root string lock sync.Mutex + active bool + requestID int + responses map[int]chan ([]byte) } type RPCMessage struct { @@ -45,6 +48,12 @@ type RPCInit struct { 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) { c := exec.Command(l.Command, l.Args...) @@ -70,6 +79,7 @@ func StartServer(l Language) (*Server, error) { s.stdin = stdin s.stdout = bufio.NewReader(stdout) s.language = &l + s.responses = make(map[int]chan []byte) // activeServers[l.Command] = s @@ -133,67 +143,83 @@ func (s *Server) Initialize(directory string) { }, } - go func() { - resp, err := s.SendMessage("initialize", params) - if err != nil { - return - } - - // todo parse capabilities - log.Println("Received", string(resp)) - - var r RPCInit - err = json.Unmarshal(resp, &r) - if err != nil { - return - } - - _, err = s.SendMessage("initialized", struct{}{}) - if err != nil { - return - } - - slock.Lock() - activeServers[s.language.Command+"-"+directory] = s - slock.Unlock() - - s.lock.Lock() - s.capabilities = r.Result.Capabilities - s.root = directory - s.lock.Unlock() - }() -} - -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) + err := s.SendMessage("initialize", params) if err != nil { - return nil, err + return } - msg = append(msg, '\r', '\n') + resp, err := s.receiveMessage() + if err != nil { + return + } - header := []byte("Content-Length: " + strconv.Itoa(len(msg)) + "\r\n\r\n") - msg = append(header, msg...) + // todo parse capabilities + log.Println("Received", string(resp)) - log.Println("Sending", string(msg)) + var r RPCInit + err = json.Unmarshal(resp, &r) + if err != nil { + return + } + + err = s.SendMessage("initialized", struct{}{}) + if err != nil { + return + } + + slock.Lock() + activeServers[s.language.Command+"-"+directory] = s + slock.Unlock() s.lock.Lock() - s.stdin.Write(msg) + s.capabilities = r.Result.Capabilities + s.root = directory + s.active = true + s.lock.Unlock() + go s.receive() +} + +func (s *Server) receive() { + for s.active { + resp, err := s.receiveMessage() + if err != nil { + log.Println(err) + continue + } + log.Println("Received", string(resp)) + + var r RPCResult + err = json.Unmarshal(resp, &r) + if err != nil { + log.Println(err) + continue + } + + switch r.Method { + case "window/logMessage": + // TODO + case "textDocument/publishDiagnostics": + // TODO + case "": + // Response + if _, ok := s.responses[r.ID]; ok { + s.responses[r.ID] <- resp + } + } + } +} + +func (s *Server) receiveMessage() ([]byte, error) { n := -1 for { + log.Println("waiting for header") b, err := s.stdout.ReadBytes('\n') if err != nil { return nil, err } headerline := strings.TrimSpace(string(b)) + log.Println("Read header", headerline) if len(headerline) == 0 { break } @@ -213,15 +239,52 @@ func (s *Server) SendMessage(method string, params interface{}) ([]byte, error) return []byte{}, nil } + log.Println("CONTENT-LENGTH:", n) + bytes := make([]byte, n) - _, err = s.stdout.Read(bytes) + _, err := io.ReadFull(s.stdout, bytes) + if err != nil { + log.Println("ERROR:", err) + } + return bytes, err +} + +func (s *Server) SendMessageGetResponse(method string, params interface{}) ([]byte, error) { + id := s.requestID + r := make(chan []byte) + s.responses[id] = r + err := s.SendMessage(method, params) if err != nil { return nil, err } - log.Println("Received", string(bytes)) - - s.lock.Unlock() + bytes := <-r + delete(s.responses, id) return bytes, nil } + +func (s *Server) SendMessage(method string, params interface{}) error { + m := RPCMessage{ + RPCVersion: "2.0", + ID: s.requestID, + Method: method, + Params: params, + } + s.requestID++ + + msg, err := json.Marshal(m) + if err != nil { + return err + } + + // 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...) + + log.Println("Sending", string(msg)) + + s.stdin.Write(msg) + return nil +}