internal/langserver/handlers/tfschema/body_candidates.go (269 lines of code) (raw):

package tfschema import ( "fmt" "strings" "github.com/Azure/azapi-lsp/internal/azure" "github.com/Azure/azapi-lsp/internal/azure/types" "github.com/Azure/azapi-lsp/internal/langserver/schema" ilsp "github.com/Azure/azapi-lsp/internal/lsp" "github.com/Azure/azapi-lsp/internal/parser" lsp "github.com/Azure/azapi-lsp/internal/protocol" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" ) func bodyCandidates(data []byte, filename string, block *hclsyntax.Block, attribute *hclsyntax.Attribute, pos hcl.Pos, property *Property) []lsp.CompletionItem { if attribute.Expr != nil { if _, ok := attribute.Expr.(*hclsyntax.LiteralValueExpr); ok && parser.ToLiteral(attribute.Expr) == nil { if property != nil { return property.ValueCandidatesFunc(nil, editRangeFromExprRange(attribute.Expr, pos)) } } } bodyDef := BodyDefinitionFromBlock(block) if bodyDef == nil { return nil } hclNode := parser.JsonEncodeExpressionToHclNode(data, attribute.Expr) if hclNode == nil { tokens, _ := hclsyntax.LexExpression(data[attribute.Expr.Range().Start.Byte:attribute.Expr.Range().End.Byte], filename, attribute.Expr.Range().Start) hclNode = parser.BuildHclNode(tokens) } if hclNode == nil { return nil } return buildCandidates(hclNode, filename, pos, bodyDef) } func BodyDefinitionFromBlock(block *hclsyntax.Block) types.TypeBase { typeValue := parser.ExtractAzureResourceType(block) if typeValue == nil { return nil } var bodyDef types.TypeBase def, err := azure.GetResourceDefinitionByResourceType(*typeValue) if err != nil || def == nil { return nil } bodyDef = def if len(block.Labels) >= 2 && block.Labels[0] == "azapi_resource_action" { parts := strings.Split(*typeValue, "@") if len(parts) != 2 { return nil } actionName := parser.ExtractAction(block) if actionName != nil && len(*actionName) != 0 { resourceFuncDef, err := azure.GetResourceFunction(parts[0], parts[1], *actionName) if err != nil || resourceFuncDef == nil { return nil } bodyDef = resourceFuncDef } } return bodyDef } func buildCandidates(hclNode *parser.HclNode, filename string, pos hcl.Pos, def types.TypeBase) []lsp.CompletionItem { candidateList := make([]lsp.CompletionItem, 0) hclNodes := parser.HclNodeArraysOfPos(hclNode, pos) if len(hclNodes) == 0 { return nil } lastHclNode := hclNodes[len(hclNodes)-1] switch { case parser.ContainsPos(lastHclNode.KeyRange, pos): // input a property with a prefix hclNodes := hclNodes[0 : len(hclNodes)-1] defs := schema.GetDef(def.AsTypeBase(), hclNodes, 0) keys := make([]schema.Property, 0) for _, def := range defs { keys = append(keys, schema.GetAllowedProperties(def)...) } if len(hclNodes) == 1 { keys = ignorePulledOutProperties(keys) } editRange := ilsp.HCLRangeToLSP(lastHclNode.KeyRange) candidateList = keyCandidates(keys, editRange, lastHclNode) case !lastHclNode.KeyRange.Empty() && !lastHclNode.EqualRange.Empty() && lastHclNode.Children == nil: // input property = defs := schema.GetDef(def.AsTypeBase(), hclNodes, 0) values := make([]string, 0) for _, def := range defs { values = append(values, schema.GetAllowedValues(def)...) } editRange := lastHclNode.ValueRange if lastHclNode.Value == nil { editRange.End = pos } candidateList = valueCandidates(values, ilsp.HCLRangeToLSP(editRange), false) case parser.ContainsPos(lastHclNode.ValueRange, pos): // input a property defs := schema.GetDef(def.AsTypeBase(), hclNodes, 0) keys := make([]schema.Property, 0) for _, def := range defs { keys = append(keys, schema.GetAllowedProperties(def)...) } if len(hclNodes) == 1 { keys = ignorePulledOutProperties(keys) } editRange := ilsp.HCLRangeToLSP(hcl.Range{Start: pos, End: pos, Filename: filename}) candidateList = keyCandidates(keys, editRange, lastHclNode) if len(lastHclNode.Children) == 0 { propertySets := make([]schema.PropertySet, 0) for _, def := range defs { propertySets = append(propertySets, schema.GetRequiredPropertySet(def)...) } if len(hclNodes) == 1 { for i, ps := range propertySets { propertySets[i].Name = "" propertySets[i].Properties = ignorePulledOutPropertiesFromPropertySet(ps.Properties) } } candidateList = append(candidateList, requiredPropertiesCandidates(propertySets, editRange, lastHclNode)...) } } return candidateList } func editRangeFromExprRange(expression hclsyntax.Expression, pos hcl.Pos) lsp.Range { expRange := expression.Range() if expRange.Start.Line != expRange.End.Line && expRange.End.Column == 1 && expRange.End.Line-1 == pos.Line { expRange.End = pos } return ilsp.HCLRangeToLSP(expRange) } func ignorePulledOutProperties(input []schema.Property) []schema.Property { res := make([]schema.Property, 0) // ignore properties pulled out from body for _, p := range input { if !isPropertyPulledOut(p) { res = append(res, p) } } return res } func ignorePulledOutPropertiesFromPropertySet(properties map[string]schema.Property) map[string]schema.Property { res := make(map[string]schema.Property) // ignore properties pulled out from body for _, p := range properties { if !isPropertyPulledOut(p) { res[p.Name] = p } } return res } func isPropertyPulledOut(p schema.Property) bool { return p.Name == "name" || p.Name == "location" || p.Name == "identity" || p.Name == "tags" } func keyCandidates(props []schema.Property, r lsp.Range, parentNode *parser.HclNode) []lsp.CompletionItem { candidates := make([]lsp.CompletionItem, 0) propSet := make(map[string]bool) for _, prop := range props { if propSet[prop.Name] { continue } propSet[prop.Name] = true content := prop.Name newText := "" sortText := fmt.Sprintf("1%s", content) if prop.Modifier == schema.Required { sortText = fmt.Sprintf("0%s", content) } keyPart := fmt.Sprintf(`%s =`, content) if parentNode.KeyValueFormat == parser.QuotedKeyEqualValue { keyPart = fmt.Sprintf(`"%s" =`, content) } else if parentNode.KeyValueFormat == parser.QuotedKeyColonValue { keyPart = fmt.Sprintf(`"%s":`, content) } switch prop.Type { case "string": newText = fmt.Sprintf(`%s "$0"`, keyPart) case "array": newText = fmt.Sprintf(`%s [$0]`, keyPart) case "object": newText = fmt.Sprintf("%s {\n\t$0\n}", keyPart) default: newText = fmt.Sprintf(`%s $0`, keyPart) } candidates = append(candidates, lsp.CompletionItem{ Label: content, Kind: lsp.PropertyCompletion, Detail: fmt.Sprintf("%s (%s)", prop.Name, prop.Modifier), Documentation: lsp.MarkupContent{ Kind: "markdown", Value: fmt.Sprintf("Type: `%s` \n%s\n", prop.Type, prop.Description), }, SortText: sortText, InsertTextFormat: lsp.SnippetTextFormat, InsertTextMode: lsp.AdjustIndentation, TextEdit: &lsp.TextEdit{ Range: r, NewText: newText, }, Command: constTriggerSuggestCommand(), }) } return candidates } func requiredPropertiesCandidates(propertySets []schema.PropertySet, r lsp.Range, parentNode *parser.HclNode) []lsp.CompletionItem { candidates := make([]lsp.CompletionItem, 0) for _, ps := range propertySets { if len(ps.Properties) == 0 { continue } props := make([]schema.Property, 0) for _, prop := range ps.Properties { props = append(props, prop) } for range props { for i := 0; i < len(props)-1; i++ { if props[i].Name > props[i+1].Name { props[i], props[i+1] = props[i+1], props[i] } } } newText := "" index := 1 for _, prop := range props { keyPart := fmt.Sprintf(`%s =`, prop.Name) if parentNode.KeyValueFormat == parser.QuotedKeyEqualValue { keyPart = fmt.Sprintf(`"%s" =`, prop.Name) } else if parentNode.KeyValueFormat == parser.QuotedKeyColonValue { keyPart = fmt.Sprintf(`"%s":`, prop.Name) } if len(prop.Value) != 0 { newText += fmt.Sprintf("%s \"%s\"\n", keyPart, prop.Value) } else { switch prop.Type { case "string": newText += fmt.Sprintf(`%s "$%d"`, keyPart, index) case "array": newText += fmt.Sprintf(`%s [$%d]`, keyPart, index) case "object": newText += fmt.Sprintf("%s {\n\t$%d\n}", keyPart, index) default: newText += fmt.Sprintf(`%s $%d`, keyPart, index) } newText += "\n" index++ } } label := "required-properties" if len(ps.Name) != 0 { label = fmt.Sprintf("required-properties-%s", ps.Name) } detail := "Required properties" if len(ps.Name) != 0 { detail = fmt.Sprintf("Required properties - %s", ps.Name) } candidates = append(candidates, lsp.CompletionItem{ Label: label, Kind: lsp.SnippetCompletion, Detail: detail, Documentation: lsp.MarkupContent{ Kind: "markdown", Value: fmt.Sprintf("Type: `%s` \n```\n%s\n```\n", ps.Name, newText), }, SortText: "0", InsertTextFormat: lsp.SnippetTextFormat, InsertTextMode: lsp.AdjustIndentation, TextEdit: &lsp.TextEdit{ Range: r, NewText: newText, }, Command: constTriggerSuggestCommand(), }) } return candidates }