github.com/tiagovtristao/plz@v13.4.0+incompatible/tools/build_langserver/langserver/handler.go (about) 1 package langserver 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "runtime/debug" 9 "sync" 10 11 "github.com/sourcegraph/jsonrpc2" 12 "gopkg.in/op/go-logging.v1" 13 14 "github.com/thought-machine/please/src/core" 15 "github.com/thought-machine/please/tools/build_langserver/lsp" 16 ) 17 18 var log = logging.MustGetLogger("lsp") 19 20 // NewHandler creates a BUILD file language server handler 21 func NewHandler() jsonrpc2.Handler { 22 h := &LsHandler{ 23 IsServerDown: false, 24 } 25 return langHandler{jsonrpc2.HandlerWithError(h.Handle)} 26 } 27 28 // handler wraps around LsHandler to correctly handler requests in the correct order 29 type langHandler struct { 30 jsonrpc2.Handler 31 } 32 33 // LsHandler is the main handler struct of the language server handler 34 type LsHandler struct { 35 init *lsp.InitializeParams 36 analyzer *Analyzer 37 mu sync.Mutex 38 conn *jsonrpc2.Conn 39 40 workspace *workspaceStore 41 42 repoRoot string 43 requestStore *requestStore 44 45 IsServerDown bool 46 supportedCompletions []lsp.CompletionItemKind 47 48 diagPublisher *diagnosticsPublisher 49 } 50 51 // Handle function takes care of all the incoming from the client, and returns the correct response 52 func (h *LsHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (result interface{}, err error) { 53 defer func() { 54 if r := recover(); r != nil { 55 log.Error("Panic in handler: %s: %s", r, debug.Stack()) 56 } 57 }() 58 if req.Method != "initialize" && h.init == nil { 59 return nil, fmt.Errorf("server must be initialized") 60 } 61 h.conn = conn 62 63 log.Info("handling method %s with params: %s", req.Method, req.Params) 64 methods := map[string]func(ctx context.Context, req *jsonrpc2.Request) (result interface{}, err error){ 65 "initialize": h.handleInit, 66 "initialzed": h.handleInitialized, 67 "shutdown": h.handleShutDown, 68 "exit": h.handleExit, 69 "$/cancelRequest": h.handleCancel, 70 "textDocument/hover": h.handleHover, 71 "textDocument/completion": h.handleCompletion, 72 "textDocument/signatureHelp": h.handleSignature, 73 "textDocument/definition": h.handleDefinition, 74 "textDocument/formatting": h.handleReformatting, 75 "textDocument/references": h.handleReferences, 76 "textDocument/rename": h.handleRename, 77 } 78 79 if req.Method != "initialize" && req.Method != "exit" && 80 req.Method != "initialzed" && req.Method != "shutdown" { 81 ctx = h.requestStore.Store(ctx, req) 82 defer h.requestStore.Cancel(req.ID) 83 } 84 85 if method, ok := methods[req.Method]; ok { 86 result, err := method(ctx, req) 87 if err != nil { 88 log.Error("Error handling %s: %s", req.Method, err) 89 } 90 return result, err 91 } 92 93 return h.handleTDRequests(ctx, req) 94 } 95 96 func (h *LsHandler) handleInit(ctx context.Context, req *jsonrpc2.Request) (result interface{}, err error) { 97 if h.init != nil { 98 return nil, errors.New("language server is already initialized") 99 } 100 if req.Params == nil { 101 return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams} 102 } 103 104 var params lsp.InitializeParams 105 if err := json.Unmarshal(*req.Params, ¶ms); err != nil { 106 return nil, err 107 } 108 109 // Set the Init state of the handler 110 h.mu.Lock() 111 defer h.mu.Unlock() 112 113 // TODO(bnmetrics): Ideas: this could essentially be a bit fragile. 114 // maybe we can defer until user send a request with first file URL 115 core.FindRepoRoot() 116 117 // TODO(bnm): remove stuff with reporoot 118 params.EnsureRoot() 119 h.repoRoot = string(params.RootURI) 120 h.workspace = newWorkspaceStore(params.RootURI) 121 h.diagPublisher = newDiagnosticsPublisher() 122 123 h.supportedCompletions = params.Capabilities.TextDocument.Completion.CompletionItemKind.ValueSet 124 h.init = ¶ms 125 126 h.analyzer, err = newAnalyzer() 127 if err != nil { 128 return nil, &jsonrpc2.Error{ 129 Code: jsonrpc2.CodeParseError, 130 Message: fmt.Sprintf("error in parsing .plzconfig file: %s", err), 131 } 132 } 133 134 // Reset the requestStore, and get sub-context based on request ID 135 reqStore := newRequestStore() 136 h.requestStore = reqStore 137 ctx = h.requestStore.Store(ctx, req) 138 139 defer h.requestStore.Cancel(req.ID) 140 141 // start the goroutine for publishing diagnostics 142 go func() { 143 for { 144 h.publishDiagnostics(h.conn) 145 } 146 }() 147 148 // Fill in the response results 149 TDsync := lsp.SyncIncremental 150 completeOps := &lsp.CompletionOptions{ 151 ResolveProvider: false, 152 TriggerCharacters: []string{".", ":"}, 153 } 154 155 sigHelpOps := &lsp.SignatureHelpOptions{ 156 TriggerCharacters: []string{"(", ","}, 157 } 158 159 log.Info("Initializing plz build file language server..") 160 return lsp.InitializeResult{ 161 162 Capabilities: lsp.ServerCapabilities{ 163 TextDocumentSync: &TDsync, 164 HoverProvider: true, 165 RenameProvider: true, 166 CompletionProvider: completeOps, 167 SignatureHelpProvider: sigHelpOps, 168 DefinitionProvider: true, 169 TypeDefinitionProvider: true, 170 ImplementationProvider: true, 171 ReferencesProvider: true, 172 DocumentFormattingProvider: true, 173 DocumentHighlightProvider: true, 174 DocumentSymbolProvider: true, 175 }, 176 }, nil 177 } 178 179 func (h *LsHandler) handleInitialized(ctx context.Context, req *jsonrpc2.Request) (result interface{}, err error) { 180 return nil, nil 181 } 182 183 func (h *LsHandler) handleShutDown(ctx context.Context, req *jsonrpc2.Request) (result interface{}, err error) { 184 h.mu.Lock() 185 if h.IsServerDown { 186 log.Warning("Server is already down!") 187 } 188 h.IsServerDown = true 189 defer h.mu.Unlock() 190 return nil, nil 191 } 192 193 func (h *LsHandler) handleExit(ctx context.Context, req *jsonrpc2.Request) (result interface{}, err error) { 194 h.handleShutDown(ctx, req) 195 h.conn.Close() 196 return nil, nil 197 } 198 199 func (h *LsHandler) handleCancel(ctx context.Context, req *jsonrpc2.Request) (result interface{}, err error) { 200 // Is there is no param with Id, or if there is no requests stored currently, return nothing 201 if req.Params == nil || h.requestStore.IsEmpty() { 202 return nil, nil 203 } 204 205 var params lsp.CancelParams 206 if err := json.Unmarshal(*req.Params, ¶ms); err != nil { 207 return nil, &jsonrpc2.Error{ 208 Code: lsp.RequestCancelled, 209 Message: fmt.Sprintf("Cancellation of request(id: %s) failed", req.ID), 210 } 211 } 212 213 defer h.requestStore.Cancel(params.ID) 214 215 return nil, nil 216 } 217 218 // getParamFromTDPositionReq gets the lsp.TextDocumentPositionParams struct 219 // if the method sends a TextDocumentPositionParams json object, e.g. "textDocument/definition", "textDocument/hover" 220 func (h *LsHandler) getParamFromTDPositionReq(req *jsonrpc2.Request, methodName string) (*lsp.TextDocumentPositionParams, error) { 221 if req.Params == nil { 222 return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams} 223 } 224 225 log.Info("%s with params %s", methodName, req.Params) 226 var params *lsp.TextDocumentPositionParams 227 if err := json.Unmarshal(*req.Params, ¶ms); err != nil { 228 return nil, err 229 } 230 231 documentURI, err := getURIAndHandleErrors(params.TextDocument.URI, methodName) 232 if err != nil { 233 return nil, err 234 } 235 236 params.TextDocument.URI = documentURI 237 238 return params, nil 239 } 240 241 // ensureLineContent handle cases when the completion pos happens on the last line of the file, without any newline char 242 func (h *LsHandler) ensureLineContent(uri lsp.DocumentURI, pos lsp.Position) string { 243 fileContent := h.workspace.documents[uri].textInEdit 244 // so we don't run into the problem of 'index out of range' 245 if len(fileContent)-1 < pos.Line { 246 return "" 247 } 248 249 lineContent := fileContent[pos.Line] 250 251 if len(lineContent)+1 == pos.Character && len(fileContent) == pos.Line+1 { 252 lineContent += "\n" 253 } 254 255 return lineContent 256 } 257 258 func getURIAndHandleErrors(uri lsp.DocumentURI, method string) (lsp.DocumentURI, error) { 259 documentURI, err := EnsureURL(uri, "file") 260 if err != nil { 261 message := fmt.Sprintf("invalid documentURI '%s' for method %s", documentURI, method) 262 log.Error(message) 263 return "", &jsonrpc2.Error{ 264 Code: jsonrpc2.CodeInvalidParams, 265 Message: message, 266 } 267 } 268 return documentURI, err 269 } 270 271 func isVisible(buildDef *BuildDef, currentPkg string) bool { 272 for _, i := range buildDef.Visibility { 273 if i == "PUBLIC" { 274 return true 275 } 276 277 label := core.ParseBuildLabel(i, currentPkg) 278 currentPkgLabel := core.ParseBuildLabel(currentPkg, currentPkg) 279 if label.Includes(currentPkgLabel) { 280 return true 281 } 282 } 283 return false 284 }