github.com/tiagovtristao/plz@v13.4.0+incompatible/tools/build_langserver/langserver/completion.go (about) 1 package langserver 2 3 import ( 4 "context" 5 "github.com/thought-machine/please/src/core" 6 "encoding/json" 7 "fmt" 8 "github.com/thought-machine/please/src/query" 9 "strings" 10 11 "github.com/thought-machine/please/tools/build_langserver/lsp" 12 13 "github.com/sourcegraph/jsonrpc2" 14 ) 15 16 const completionMethod = "textDocument/completion" 17 18 // TODO(bnmetrics): Consider adding ‘completionItem/resolve’ method handle as well, 19 // TODO(bnmetrics): If computing full completion items is expensive, servers can additionally provide a handler for the completion item resolve request 20 21 func (h *LsHandler) handleCompletion(ctx context.Context, req *jsonrpc2.Request) (result interface{}, err error) { 22 if req.Params == nil { 23 return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams} 24 } 25 var params lsp.CompletionParams 26 if err := json.Unmarshal(*req.Params, ¶ms); err != nil { 27 return nil, err 28 } 29 30 documentURI, err := getURIAndHandleErrors(params.TextDocument.URI, completionMethod) 31 if err != nil { 32 return nil, err 33 } 34 35 h.mu.Lock() 36 defer h.mu.Unlock() 37 38 itemList, err := h.getCompletionItemsList(ctx, documentURI, params.Position) 39 if err != nil { 40 return nil, err 41 } 42 43 log.Info("completion item list %s", itemList) 44 return &lsp.CompletionList{ 45 IsIncomplete: false, 46 Items: itemList, 47 }, nil 48 } 49 50 func (h *LsHandler) getCompletionItemsList(ctx context.Context, 51 uri lsp.DocumentURI, pos lsp.Position) ([]*lsp.CompletionItem, error) { 52 53 fileContent := h.workspace.documents[uri].textInEdit 54 fileContentStr := JoinLines(fileContent, true) 55 lineContent := h.ensureLineContent(uri, pos) 56 57 log.Info("Completion lineContent: %s", lineContent) 58 59 var completionList []*lsp.CompletionItem 60 var completionErr error 61 62 if isEmpty(lineContent, pos) { 63 return completionList, nil 64 } 65 66 contentToPos := lineContent[:pos.Character] 67 if len(lineContent) > pos.Character+1 && lineContent[pos.Character] == '"' { 68 contentToPos = lineContent[:pos.Character+1] 69 } 70 71 // get all the existing variable assignments in the current File 72 contentVars := h.analyzer.VariablesFromContent(fileContentStr, &pos) 73 74 stmts := h.analyzer.AspStatementFromContent(JoinLines(fileContent, true)) 75 subincludes := h.analyzer.GetSubinclude(ctx, stmts, uri) 76 77 call := h.analyzer.CallFromAST(stmts, pos) 78 79 if LooksLikeAttribute(contentToPos) { 80 completionList = itemsFromAttributes(h.analyzer, 81 contentVars, contentToPos) 82 } else if label := ExtractBuildLabel(contentToPos); label != "" { 83 completionList, completionErr = itemsFromBuildLabel(ctx, h.analyzer, 84 label, uri) 85 } else if strVal := ExtractStrTail(contentToPos); strVal != "" { 86 completionList = itemsFromLocalSrcs(call, strVal, uri, pos) 87 } else { 88 literal := ExtractLiteral(contentToPos) 89 90 // Check if we are inside of a call, if so we get the completion for args 91 if call != nil && !strings.Contains(contentToPos, "=") { 92 ruleDef := h.analyzer.GetBuildRuleByName(call.Name, subincludes) 93 completionList = itemsFromFuncArgsName(ruleDef, call, literal) 94 } else { 95 completionList = itemsFromliterals(h.analyzer, subincludes, 96 contentVars, literal) 97 } 98 } 99 100 if completionErr != nil { 101 message := fmt.Sprintf("fail to get content for completion, file path: %s", uri) 102 log.Error(message) 103 return nil, &jsonrpc2.Error{ 104 Code: jsonrpc2.CodeParseError, 105 Message: fmt.Sprintf("fail to get content for completion, file path: %s", uri), 106 } 107 } 108 109 return completionList, nil 110 } 111 112 func itemsFromFuncArgsName(ruleDef *RuleDef, call *Call, matchingStr string) []*lsp.CompletionItem { 113 var items []*lsp.CompletionItem 114 115 if ruleDef == nil || matchingStr == "" { 116 return nil 117 } 118 119 for name, info := range ruleDef.ArgMap { 120 if info.IsPrivate { 121 continue 122 } 123 if strings.Contains(name, matchingStr) && !argExist(call, name) { 124 details := strings.Replace(info.Definition, name, "", -1) 125 items = append(items, getCompletionItem(lsp.Variable, name+"=", details)) 126 } 127 } 128 129 return items 130 } 131 132 func argExist(call *Call, argName string) bool { 133 for _, arg := range call.Arguments { 134 if arg.Name == argName { 135 return true 136 } 137 } 138 return false 139 } 140 141 func itemsFromLocalSrcs(call *Call, text string, uri lsp.DocumentURI, pos lsp.Position) []*lsp.CompletionItem { 142 if !withinLocalSrcArg(call, pos) { 143 return nil 144 } 145 146 files, err := LocalFilesFromURI(uri) 147 if err != nil { 148 log.Warning("Error occurred when trying to find local files: %s", err) 149 return nil 150 } 151 152 var items []*lsp.CompletionItem 153 for _, file := range files { 154 if strings.Contains(file, text) { 155 items = append(items, getCompletionItem(lsp.Value, file, "")) 156 } 157 } 158 159 return items 160 } 161 162 // withinLocalSrcArg checks if the current position is part of the arguments that takes local srcs, 163 // such as: src, srcs, data 164 func withinLocalSrcArg(call *Call, pos lsp.Position) bool { 165 if call == nil { 166 return false 167 } 168 169 for _, arg := range call.Arguments { 170 if withInRange(arg.Value.Pos, arg.Value.EndPos, pos) && StringInSlice(LocalSrcsArgs, arg.Name) { 171 return true 172 } 173 } 174 return false 175 } 176 177 func itemsFromBuildLabel(ctx context.Context, analyzer *Analyzer, labelString string, 178 uri lsp.DocumentURI) (completionList []*lsp.CompletionItem, err error) { 179 180 labelString = TrimQuotes(labelString) 181 182 if strings.ContainsRune(labelString, ':') { 183 labelParts := strings.Split(labelString, ":") 184 var pkgLabel string 185 186 // Get the package label based on whether the labelString is relative 187 if strings.HasPrefix(labelString, ":") { 188 // relative package label 189 pkgLabel, err = PackageLabelFromURI(uri) 190 if err != nil { 191 return nil, err 192 } 193 } else if strings.HasPrefix(labelString, "//") { 194 // none relative package label 195 pkgLabel = labelParts[0] 196 } 197 198 return buildLabelItemsFromPackage(ctx, analyzer, pkgLabel, uri, labelParts[1], true) 199 } 200 201 pkgs := query.GetAllPackages(analyzer.State.Config, labelString[2:], core.RepoRoot) 202 for _, pkg := range pkgs { 203 labelItems, err := buildLabelItemsFromPackage(ctx, analyzer, "/"+pkg, uri, "", false) 204 if err != nil { 205 return nil, err 206 } 207 208 completionList = append(completionList, labelItems...) 209 } 210 211 // check if '/' is present, and only gets the next part of the label, 212 // so auto completion doesn't write out the whole label including the existing part 213 // e.g. //src/q -> query, query:query 214 if strings.ContainsRune(labelString[2:], '/') { 215 ind := strings.LastIndex(labelString[2:], "/") 216 217 for i := range completionList { 218 completionList[i].Label = completionList[i].Label[ind+1:] 219 } 220 } 221 222 return completionList, nil 223 } 224 225 func buildLabelItemsFromPackage(ctx context.Context, analyzer *Analyzer, pkgLabel string, 226 currentURI lsp.DocumentURI, partials string, nameOnly bool) (completionList []*lsp.CompletionItem, err error) { 227 228 pkgURI := analyzer.BuildFileURIFromPackage(pkgLabel) 229 buildDefs, err := analyzer.BuildDefsFromURI(ctx, pkgURI) 230 if err != nil { 231 return nil, err 232 } 233 234 currentPkg, err := PackageLabelFromURI(currentURI) 235 if err != nil { 236 return nil, err 237 } 238 239 for name, buildDef := range buildDefs { 240 if isVisible(buildDef, currentPkg) && strings.Contains(name, partials) { 241 targetPkg, err := PackageLabelFromURI(pkgURI) 242 if err != nil { 243 return nil, err 244 } 245 246 fullLabel := targetPkg + ":" + name 247 detail := fmt.Sprintf(" BUILD Label: %s", fullLabel) 248 249 completionItemLabel := name 250 if !nameOnly { 251 completionItemLabel = strings.TrimPrefix(fullLabel, "//") 252 } 253 254 item := getCompletionItem(lsp.Value, completionItemLabel, detail) 255 completionList = append(completionList, item) 256 } 257 } 258 259 // also append the package label if there are visible labels in the package 260 if !nameOnly && len(completionList) != 0 { 261 detail := fmt.Sprintf(" BUILD Label: %s", pkgLabel) 262 263 item := getCompletionItem(lsp.Value, strings.TrimPrefix(pkgLabel, "//"), detail) 264 completionList = append(completionList, item) 265 } 266 267 return completionList, err 268 } 269 270 func itemsFromliterals(analyzer *Analyzer, subincludes map[string]*RuleDef, 271 contentVars map[string]Variable, literal string) []*lsp.CompletionItem { 272 273 if literal == "" { 274 return nil 275 } 276 277 var completionList []*lsp.CompletionItem 278 279 for key, val := range analyzer.BuiltIns { 280 if strings.Contains(key, literal) { 281 // ensure it's not part of an object, as it's already been taken care of in itemsFromAttributes 282 if val.Object == "" { 283 completionList = append(completionList, itemFromRuleDef(val)) 284 } 285 } 286 } 287 288 for k, v := range contentVars { 289 if strings.Contains(k, literal) { 290 completionList = append(completionList, getCompletionItem(lsp.Variable, k, " variable type: "+v.Type)) 291 } 292 } 293 294 for k, v := range subincludes { 295 if strings.Contains(k, literal) { 296 completionList = append(completionList, itemFromRuleDef(v)) 297 } 298 } 299 300 return completionList 301 } 302 303 func itemsFromAttributes(analyzer *Analyzer, contentVars map[string]Variable, lineContent string) []*lsp.CompletionItem { 304 305 contentSlice := strings.Split(lineContent, ".") 306 partial := contentSlice[len(contentSlice)-1] 307 308 literalSlice := strings.Split(ExtractLiteral(lineContent), ".") 309 varName := literalSlice[0] 310 variable, present := contentVars[varName] 311 312 if LooksLikeStringAttr(lineContent) || (present && variable.Type == "str") { 313 return itemsFromMethods(analyzer.Attributes["str"], partial) 314 } else if LooksLikeDictAttr(lineContent) || (present && variable.Type == "dict") { 315 return itemsFromMethods(analyzer.Attributes["dict"], partial) 316 } else if LooksLikeCONFIGAttr(lineContent) { 317 var completionList []*lsp.CompletionItem 318 319 for tag, field := range analyzer.State.Config.TagsToFields() { 320 if !strings.Contains(tag, strings.ToUpper(partial)) { 321 continue 322 } 323 item := getCompletionItem(lsp.Property, tag, field.Tag.Get("help")) 324 325 completionList = append(completionList, item) 326 } 327 return completionList 328 } 329 return nil 330 } 331 332 // partial: the string partial of the Attribute 333 func itemsFromMethods(attributes []*RuleDef, partial string) []*lsp.CompletionItem { 334 335 var completionList []*lsp.CompletionItem 336 for _, attr := range attributes { 337 // Continue if the the name is not part of partial 338 if !strings.Contains(attr.Name, partial) { 339 continue 340 } 341 item := itemFromRuleDef(attr) 342 completionList = append(completionList, item) 343 } 344 345 return completionList 346 } 347 348 // Gets all completion items from function or method calls 349 func itemFromRuleDef(ruleDef *RuleDef) *lsp.CompletionItem { 350 351 // Get the first line of docString as lsp.CompletionItem.detail 352 docStringList := strings.Split(ruleDef.Docstring, "\n") 353 var doc string 354 if len(docStringList) > 0 { 355 doc = docStringList[0] 356 } 357 358 detail := ruleDef.Header[strings.Index(ruleDef.Header, ruleDef.Name)+len(ruleDef.Name):] 359 360 item := getCompletionItem(lsp.Function, ruleDef.Name, detail) 361 item.Documentation = doc 362 363 return item 364 } 365 366 func getCompletionItem(kind lsp.CompletionItemKind, label string, detail string) *lsp.CompletionItem { 367 return &lsp.CompletionItem{ 368 Label: label, 369 Kind: kind, 370 Detail: detail, 371 InsertTextFormat: lsp.ITFPlainText, 372 SortText: label, 373 } 374 }