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, &params); 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  }