internal/langserver/handlers/hover/hover.go (182 lines of code) (raw):

package hover import ( "context" "fmt" "log" "strings" "github.com/Azure/azapi-lsp/internal/azure" "github.com/Azure/azapi-lsp/internal/azure/types" "github.com/Azure/azapi-lsp/internal/langserver/handlers/tfschema" "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/Azure/azapi-lsp/internal/telemetry" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" ) func HoverAtPos(ctx context.Context, data []byte, filename string, pos hcl.Pos, logger *log.Logger, sender telemetry.Sender) *lsp.Hover { file, _ := hclsyntax.ParseConfig(data, filename, hcl.InitialPos) body, isHcl := file.Body.(*hclsyntax.Body) if !isHcl { logger.Printf("file is not hcl") return nil } block := parser.BlockAtPos(body, pos) if block != nil && len(block.Labels) != 0 && strings.HasPrefix(block.Labels[0], "azapi") { resourceName := fmt.Sprintf("%s.%s", block.Type, block.Labels[0]) resource := tfschema.GetResourceSchema(resourceName) if resource == nil { return nil } // hover on an attribute if attribute := parser.AttributeAtPos(block, pos); attribute != nil { property := resource.GetProperty(attribute.Name) if property == nil { return nil } switch attribute.Name { case "parent_id": if parser.ContainsPos(attribute.NameRange, pos) { typeAttribute := parser.AttributeWithName(block, "type") if typeAttribute != nil { if typeValue := parser.ToLiteral(typeAttribute.Expr); typeValue != nil && len(*typeValue) != 0 { parentType := GetParentType(*typeValue) return Hover(property.Name, property.Modifier, property.Type, fmt.Sprintf("The ID of `%s` which is the parent resource in which this resource is created.", parentType), attribute.NameRange) } } } case "body": if parser.ContainsPos(attribute.NameRange, pos) { return Hover(property.Name, property.Modifier, property.Type, property.Description, attribute.NameRange) } bodyDef := tfschema.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 { break } return hoverOnBody(hclNode, pos, bodyDef) default: if !parser.ContainsPos(attribute.NameRange, pos) { return nil } return Hover(property.Name, property.Modifier, property.Type, property.Description, attribute.NameRange) } } // hover on a block if nestedBlock := parser.BlockAtPos(block.Body, pos); nestedBlock != nil { property := resource.GetProperty(nestedBlock.Type) if property == nil { return nil } if attribute := parser.AttributeAtPos(nestedBlock, pos); attribute != nil { for _, p := range property.NestedProperties { if p.Name == attribute.Name { return Hover(p.Name, p.Modifier, p.Type, p.Description, attribute.NameRange) } } } else { return Hover(property.Name, property.Modifier, property.Type, property.Description, nestedBlock.TypeRange) } } // hover on the resource definition if block.DefRange().ContainsPos(pos) { typeValue := "" if typeAttribute := parser.AttributeWithName(block, "type"); typeAttribute != nil { if v := parser.ToLiteral(typeAttribute.Expr); v != nil && len(*v) != 0 { typeValue = *v } } azureResourceType := "" if parts := strings.Split(typeValue, "@"); len(parts) >= 2 { azureResourceType = parts[0] } if azureResourceType == "" { return nil } sender.SendEvent(ctx, "textDocument/hover", map[string]interface{}{ "kind": "resource-definition", "type": typeValue, }) return &lsp.Hover{ Range: ilsp.HCLRangeToLSP(block.DefRange()), Contents: lsp.MarkupContent{ Kind: lsp.Markdown, Value: fmt.Sprintf(`%s %s '%s' [View Documentation](https://learn.microsoft.com/en-us/azure/templates/%s?pivots=deployment-language-terraform)`, block.Type, strings.Join(block.Labels, "."), typeValue, strings.ToLower(azureResourceType)), }, } } } return nil } func hoverOnBody(hclNode *parser.HclNode, pos hcl.Pos, bodyDef types.TypeBase) *lsp.Hover { hclNodes := parser.HclNodeArraysOfPos(hclNode, pos) if len(hclNodes) == 0 { return nil } lastHclNode := hclNodes[len(hclNodes)-1] if parser.ContainsPos(lastHclNode.KeyRange, pos) { defs := schema.GetDef(bodyDef.AsTypeBase(), hclNodes[0:len(hclNodes)-1], 0) props := make([]schema.Property, 0) for _, def := range defs { props = append(props, schema.GetAllowedProperties(def)...) } if len(props) != 0 { index := -1 for i := range props { if props[i].Name == lastHclNode.Key { index = i break } } if index == -1 { return nil } return Hover(props[index].Name, string(props[index].Modifier), props[index].Type, props[index].Description, lastHclNode.KeyRange) } } return nil } func GetParentType(resourceType string) string { parts := strings.Split(resourceType, "/") if len(parts) <= 2 { def, err := azure.GetResourceDefinitionByResourceType(resourceType) if err != nil || def == nil || len(def.ScopeTypes) == 0 { return "Azure Resource" } res := make([]string, 0) for _, scope := range def.ScopeTypes { switch scope { case types.Unknown: return "Azure Resource(Unknown scope)" case types.Tenant: res = append(res, "Tenant") case types.Subscription: res = append(res, "Subscription") case types.ManagementGroup: res = append(res, "Microsoft.Management/managementGroups") case types.ResourceGroup: res = append(res, "Microsoft.Resources/resourceGroups") case types.Extension: res = append(res, "Azure Resource(Extension scope)") } } return strings.Join(res, ", ") } return strings.Join(parts[0:len(parts)-1], "/") } func Hover(name string, modifier string, propType string, description string, r hcl.Range) *lsp.Hover { return &lsp.Hover{ Range: ilsp.HCLRangeToLSP(r), Contents: lsp.MarkupContent{ Kind: lsp.Markdown, Value: fmt.Sprintf("```\n%s: %s(%s)\n```\n%s", name, modifier, propType, description), }, } }